1. 环境准备与项目搭建

大家好,我是老张,在AI和智能硬件这块摸爬滚打了十来年。今天咱们不聊那些高大上的概念,就实实在在地聊聊怎么把你本地跑起来的那个Ollama大模型,跟咱们最熟悉的SpringBoot项目给“焊”在一起,做成一个能对外提供服务的Web应用。我知道很多朋友自己把Ollama在本地部署好了,玩得挺嗨,但一到要把它集成到项目里,就感觉有点无从下手。别急,跟着我的步骤走,保证你能从零开始,一步步搞定。

首先,咱们得把“地基”打好。这个地基就是你的开发环境。我踩过的第一个坑就是版本问题。SpringBoot整合Ollama,对JDK和SpringBoot的版本是有硬性要求的。你必须使用JDK 17或更高的版本,SpringBoot也得是3.x系列。我一开始用的JDK 11,折腾了半天,各种依赖冲突,最后才发现是版本不匹配,白白浪费了一个下午。所以,第一步,先去检查你的Java版本,用 java -version 命令看一眼,别嫌麻烦。

项目创建我强烈推荐直接用官方的 Spring Initializr,网址就是 https://start.spring.io/。这个工具用起来特别省心,图形化界面点点选选就行了。打开页面后,你需要关注几个关键选项:

  • Project:选 Maven 或者 Gradle 都行,看你的习惯,我习惯用 Maven。
  • Language:Java。
  • Spring Boot:选一个3.x的最新稳定版,比如 3.2.x。
  • Project Metadata:按照你的包名和项目名填好。
  • Dependencies:这是重中之重!点击“Add Dependencies”按钮,搜索并添加两个依赖:
    1. Spring Web:这是构建Web应用的基础。
    2. Spring AI Ollama:这是Spring官方提供的与Ollama集成的“桥梁”,有了它,我们就不用自己去写复杂的HTTP客户端调用了。

选好之后,点击页面下方的“GENERATE”按钮,它会自动下载一个压缩包。解压后用你喜欢的IDE(比如IntelliJ IDEA或VS Code)打开,一个基础的项目骨架就准备好了。我实测下来,用这个方式创建的项目,依赖是最干净、冲突最少的。

2. 核心配置与模型连接测试

项目创建好了,咱们先别急着写业务代码。我的经验是,先打通从SpringBoot到本地Ollama服务的“任督二脉”,确保能正常对话,后面的一切才好展开。这一步,我们用单元测试来做最合适,独立于Web环境,快速验证。

2.1 配置文件是关键

首先,打开 src/main/resources/application.yml 文件(如果你喜欢用 .properties 文件也行,但我个人觉得YAML层次更清晰)。在这里,我们需要告诉Spring AI,我们的Ollama模型在哪里,叫什么名字。

spring:
  ai:
    ollama:
      # 这是你本地Ollama服务的地址,默认就是本机的11434端口
      base-url: http://localhost:11434
      # 这里填写你通过 `ollama pull` 拉取并运行的模型名称,比如我常用的是 `qwen2.5:7b`
      chat:
        options:
          model: qwen2.5:7b

这个配置非常简单,就两行。base-url 指向你本地运行的Ollama服务,model 指定你要使用哪个模型。保存文件后,Spring AI会自动读取这些配置,并为我们准备好一个可用的 OllamaChatModel Bean。

2.2 编写第一个单元测试

接下来,我们写个测试类来验证连接是否成功。在 src/test/java 下对应的包路径里,创建一个新的测试类,比如叫 OllamaConnectionTest

import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class OllamaConnectionTest {

    @Autowired
    private OllamaChatModel chatModel; // 直接注入配置好的模型

    @Test
    void testSimpleChat() {
        // 构建一个简单的提示
        Prompt prompt = new Prompt(new UserMessage("请用中文介绍一下你自己。"));
        // 调用模型获取响应
        ChatResponse response = chatModel.call(prompt);
        // 提取并打印回复内容
        String content = response.getResult().getOutput().getContent();
        System.out.println("模型回复: " + content);
        // 断言回复不为空
        assertThat(content).isNotBlank();
    }
}

运行这个测试。如果一切顺利,你会在控制台看到模型用中文做的自我介绍。这标志着你的SpringBoot项目已经成功连接上了本地的Ollama大模型!我第一次跑通的时候,感觉就像修好了一条通往新世界的数据管道,特别有成就感。

2.3 深入理解模型参数调优

仅仅能对话还不够,我们得能让模型按照我们的需求来工作。Ollama提供了非常丰富的参数来控制生成文本的行为,比如创造性、重复惩罚等。这些参数既可以在 application.yml 里全局配置,也可以在代码里针对每次请求动态设置。

在单元测试里,我们可以这样进行更精细的控制:

@Test
void testChatWithOptions() {
    // 通过 Builder 模式构建请求选项,这里会覆盖配置文件中的默认值
    var options = OllamaOptions.builder()
            .model(“qwen2.5:7b”) // 明确指定模型,虽然配置文件有,这里可以覆盖
            .temperature(0.7) // 温度值,控制随机性。0.1更确定和保守,1.0更有创造性
            .topP(0.9) // 核采样,与top-K配合,影响输出的多样性
            .numPredict(100) // 最大生成token数,控制回答长度
            .repeatPenalty(1.1) // 重复惩罚系数,越高越避免重复
            .build();

    Prompt prompt = new Prompt(
            new UserMessage(“写一首关于春天的五言绝句。”),
            options // 将选项传入Prompt
    );

    ChatResponse response = chatModel.call(prompt);
    System.out.println(“生成的诗歌:” + response.getResult().getOutput().getContent());
}

我整理了一个最常用参数的速查表,方便你随时参考:

参数 描述 常用值范围 实战影响
temperature 创造性/随机性。值越低,输出越确定、保守;值越高,输出越多样、有创意。 0.1 - 1.0 写代码、总结事实用低值(0.1-0.3);写故事、诗歌用高值(0.7-0.9)
topP 核采样。与top-K协同,决定从哪些候选词中采样。 0.5 - 1.0 通常设0.7-0.9,平衡质量和多样性。设为1.0等于禁用此过滤。
numPredict 最大生成长度。控制模型回答的最大token数。 正整数 根据你的场景设置,对话可以设512,长文生成可以设2048。注意别设太小导致回答被截断。
repeatPenalty 重复惩罚。惩罚已出现过的文本,避免车轱辘话。 1.0 - 1.5 对于摘要、对话,设为1.05-1.2能有效减少无意义重复。
seed 随机种子。设为固定值可使相同输入产生确定性的输出。 任意整数 在需要可复现结果的场景(如测试、演示)下非常有用。

通过单元测试灵活调整这些参数,你能很快摸清手上这个模型的“脾气”,为后续的Web应用集成打下坚实基础。记住,没有一套参数适合所有场景,多试几次才能找到最佳配置。

3. 构建Web API控制器

单元测试跑通了,证明我们的核心链路是健康的。接下来,就要把这些能力通过HTTP接口暴露出去,做成一个真正的Web服务。这里我们会重点设计Controller层,并且解决一个非常常见的问题:流式输出时的中文乱码

3.1 基础聊天接口实现

首先,我们创建一个 ChatController。这里我直接注入之前在配置文件里定义好的 OllamaChatModel,这样最省事。

import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;

@RestController
public class ChatController {

    @Resource
    private OllamaChatModel chatModel;

    // 使用一个内存中的ChatMemory来模拟会话记忆,实际项目请用数据库
    private final ChatMemory chatMemory = new InMemoryChatMemory();

    @GetMapping(“/chat”)
    public String chat(@RequestParam String message) {
        // 为每次请求生成一个随机会话ID,模拟多用户场景
        String sessionId = UUID.randomUUID().toString();
        // 构建ChatClient,并添加消息记忆顾问,保留最近10轮对话
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 10))
                .build();
        // 发起同步调用,获取完整回复
        return chatClient.prompt(message).call().content();
    }
}

这个 /chat 接口是同步的,也就是说,客户端发来请求后,会一直等待模型生成完整的回答,然后一次性返回。这种方式简单直接,适合回答较短、生成速度较快的场景。启动应用,用浏览器访问 http://localhost:8080/chat?message=你好,你应该就能收到模型的回复了。

3.2 实现流式输出与乱码解决

但是,对于大模型来说,生成一段较长的文本可能需要好几秒甚至十几秒。让用户对着空白页面干等,体验非常糟糕。所以,流式输出(Server-Sent Events, SSE)几乎是现代AI应用的标配。它能让答案像打字一样,一个字一个字地“流”到前端,体验感瞬间提升。

然而,这里有个大坑等着我们:中文乱码。很多朋友在实现流式接口时,会发现前端收到的中文是一堆问号或者乱码。这个问题困扰了我半天,最后发现是响应头(Content-Type)的编码设置问题。

下面是我们改造后的流式接口,特别注意 produces 属性

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class ChatController {
    // ... 之前的注入和属性不变

    @GetMapping(value = “/chat/stream”, produces = “text/event-stream;charset=UTF-8”)
    public Flux<String> streamChat(@RequestParam String message) {
        String sessionId = UUID.randomUUID().toString();
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 10))
                .build();

        // 关键在这里:调用 `.stream()` 方法返回一个 Flux<String>
        return chatClient.prompt(message)
                .stream() // 启用流式输出
                .content(); // 直接返回内容流
    }
}

看第7行,produces = “text/event-stream;charset=UTF-8” 是解决乱码的关键。它明确告诉浏览器,这是一个SSE流,并且字符编码是UTF-8。光在 application.yml 里设置全局编码有时对SSE流不生效,必须在注解里显式指定。

为了确保万无一失,我还会在 application.yml 里加上全局的HTTP编码配置:

server:
  servlet:
    encoding:
      charset: UTF-8
      force: true

这样双管齐下,无论是同步接口还是流式接口,中文显示就都正常了。你可以用Postman或者写一个简单的前端HTML页面来测试这个流式接口,看着文字逐个出现,感觉完全不一样。

4. 进阶功能与生产级考量

基础功能跑通后,我们可以考虑一些更贴近实际生产需求的增强功能。一个没有记忆的聊天机器人就像金鱼,只有7秒记忆,用户体验会很割裂。

4.1 实现会话记忆与管理

上面的例子我们用 InMemoryChatMemory 简单模拟了记忆,但它存在应用重启后数据丢失、无法分布式部署等问题。在实际项目中,我们需要把会话和消息记录到数据库里。

我们可以设计两张简单的表:

  1. ChatSession:记录会话ID、创建时间、用户标识等。
  2. ChatMessage:记录每条消息的会话ID、角色(用户/助手)、内容、时间。

然后,实现一个基于数据库的 ChatMemory 接口。Spring AI的 ChatMemory 接口设计得很清晰,主要就是 add()get() 消息。我们可以根据 sessionId 从数据库中拉取最近的N条对话记录,在每次请求时作为上下文传入给模型。这样,模型就能“记住”之前聊过什么,实现连贯的多轮对话。这个实现稍微复杂点,但思路就是定制一个 PersistentChatMemory 类,替换掉之前的 InMemoryChatMemory

4.2 异常处理与服务降级

本地部署的Ollama服务并不像云服务那么稳定,可能会因为资源不足、模型加载失败等原因挂掉。我们的Web应用必须要有良好的容错能力。

首先,全局异常处理是必须的。我们可以用 @ControllerAdvice 来捕获调用Ollama时可能抛出的各种异常(比如连接超时、模型不存在),然后返回给前端一个友好的错误信息,而不是一堆Java异常栈。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceAccessException.class)
    public ResponseEntity<String> handleOllamaConnectionException(ResourceAccessException e) {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                             .body(“AI服务暂时不可用,请稍后再试。”);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception e) {
        // 记录日志
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body(“系统内部错误,请联系管理员。”);
    }
}

其次,考虑服务降级。当检测到Ollama服务连续失败时,可以暂时将请求切换到一个简单的规则引擎或者返回一个预设的提示,比如“AI助手正在升级,请稍后”。Spring Cloud CircuitBreaker(如Resilience4j)可以很方便地实现这个功能。

4.3 性能优化与监控

当你的应用开始有真实用户使用时,性能和监控就变得重要了。

  • 连接池:虽然Spring AI的客户端可能有内置管理,但要注意HTTP客户端的配置,避免频繁创建连接。
  • 超时设置:一定要为模型调用设置合理的超时时间(比如30秒或60秒),防止一个慢请求拖死整个线程池。这可以在配置文件中设置。
  • 异步处理:对于同步的 /chat 接口,如果处理时间较长,可以考虑使用Spring的 @Async 将其改为异步任务,立即返回一个任务ID,让客户端轮询结果,避免HTTP连接长时间挂起。
  • 监控指标:利用Spring Boot Actuator暴露端点,监控每个聊天接口的请求量、平均响应时间、错误率。更重要的是,监控每次调用消耗的Token数,这直接关系到你的硬件资源(特别是显存)的使用情况。你可以通过拦截器或AOP,在每次调用后解析模型的输出,估算Token消耗并记录下来。

把这些进阶功能都考虑进去,你的SpringBoot + Ollama应用就不再是一个玩具,而是一个可以应对一定规模用户访问的、健壮的服务了。整个过程从环境准备到生产级优化,每一步我都踩过坑,也总结出了最实用的解法。希望这份详细的实战指南能帮你少走弯路,顺利地把本地大模型的能力释放到你的Web应用中去。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐