1. 为什么是 Spring AI,而不是直接调用 OpenAI SDK?

我第一次在团队内部技术分享会上提出“用 Spring AI 写第一个 AI 应用”时,后座一位写了八年 Java 的老哥直接举手:“不就是发个 HTTP 请求?Spring Boot 自带 WebClient,三行代码搞定,为啥要多套一层?”——这问题太真实了。它精准戳中了绝大多数 Java 开发者面对新框架时的第一反应: 怕重复造轮子,更怕引入新坑

但三个月后,他主动找我,说正在把项目里原来手写的 OpenAI 调用模块,一整块替换成 Spring AI。不是因为“时髦”,而是因为他在给一个金融风控服务加多模型 fallback 逻辑时,被自己写的那套“if-else 切模型 + 手动序列化 + 线程池管理 + 异常重试 + token 统计”代码逼疯了。他当时贴给我一段截图:一个 ChatClient 的封装类,光构造函数参数就传了 7 个——API Key、Base URL、超时时间、重试策略、日志开关、响应拦截器、自定义 ObjectMapper……而这些,全是为了让同一个 sendMessage() 方法,在对接 Qwen、DeepSeek 和本地 Ollama 时,能勉强跑通。

这就是 Spring AI 的真实价值锚点: 它不解决“能不能调通大模型”这个最底层问题,而是系统性地解决“在企业级 Java 工程中,如何可持续、可维护、可观测、可灰度地使用大模型”这个工程问题 。它不是另一个 SDK,而是一套面向生产环境的 AI 基础设施抽象层。

它的核心设计哲学,和 Spring Data JPA 一脉相承:你写业务逻辑时,只关心“我要查什么数据”或“我要问什么问题”,至于底层是 MySQL 还是 PostgreSQL,是 OpenAI 还是 Ollama,是同步调用还是流式响应,是 JSON 还是 SSE 协议——这些都由 ChatModel EmbeddingModel AudioModel 等接口背后的实现类去适配。你只需要在 application.yml 里改一行 spring.ai.ollama.chat.model: qwen2:7b ,整个应用的对话模型就无声无息地切换了,连测试用例都不用动。

这背后是 JDK17+ 的强类型泛型、Spring Boot 的自动配置(Auto-Configuration)机制、以及 Project Reactor 的非阻塞流支持共同构建的护城河。比如,当你声明一个 @Bean ChatClient chatClient(ChatModel model) ,Spring AI 会自动注入一个预置了重试、熔断、指标埋点、请求/响应日志(可开关)的客户端实例。你拿到的 ChatClient ,本质上是一个经过 Spring 生态深度武装的“AI 通信兵”,而不是裸奔的 HttpClient

所以,别再纠结“它比原生 SDK 多几行代码”。真正该问的是:当你的 Spring Boot 项目明天要接入阿里云百炼、后天要切到私有部署的 Llama 3、大后天要给客服机器人加上语音转文字能力时,你希望是改 3 行配置,还是重构 300 行网络调用代码?Spring AI 的答案很朴素: 让 AI 调用,回归到 Spring 开发者最熟悉的“声明式编程”范式里来

提示:Spring AI 并非取代 OpenAI 官方 Java SDK。官方 SDK 更适合做 POC 或极简集成;而 Spring AI 是为中大型 Java 项目准备的“AI 中间件”。就像你不会在 Spring Boot 项目里手写 JDBC 连接池一样,你也不该在需要长期维护的项目里,手写大模型的调用胶水代码。

2. 环境筑基:JDK17、Spring Boot 3.x 与 Ollama 的三位一体

跑通第一个 Spring AI 应用,90% 的失败案例,都卡在环境这一环。不是代码写错了,而是环境没对齐。我见过太多人对着 UnsupportedClassVersionError 抓耳挠腮两小时,最后发现只是 JDK 版本没切到 17。所以,我们得像搭乐高一样,严丝合缝地拼好这三块基石:JDK17、Spring Boot 3.x、Ollama。

2.1 JDK17:不是“推荐”,而是硬性门槛

Spring AI 1.0.x 系列(当前稳定版)明确要求 JDK 17 或更高版本 。这不是一个“建议兼容”的软性要求,而是由其底层依赖决定的硬性约束。关键原因有两个:

第一, Records 类型的深度使用 。Spring AI 的核心数据结构,如 ChatResponse EmbeddingResponse AudioResponse ,大量采用 record 关键字定义不可变数据载体。 record 是 JDK14 引入、JDK17 正式成为标准特性的语法糖。它带来的不仅是代码简洁,更是语义安全——编译器强制保证这些对象的不可变性,这对 AI 响应这种“一次生成、多次消费”的场景至关重要。如果你强行用 JDK11 运行,编译器会直接报错 error: records are not supported in -source 11

第二, java.util.concurrent.StructuredTaskScope 的依赖 。Spring AI 在实现并行调用(如同时调用多个 Embedding 模型)时,采用了 JDK19 引入、但在 JDK17 的 jdk.incubator.concurrent 模块中已提供预览版的结构化并发 API。虽然 Spring AI 当前版本主要用 CompletableFuture ,但其未来演进路径已锁定在此。这意味着,选择 JDK17,你不仅满足了当下需求,更买到了未来升级的“入场券”。

安装建议:直接去 Oracle 官网 Adoptium 下载 JDK17 LTS 版本。Windows 用户务必在安装后检查 JAVA_HOME 环境变量是否指向 jdk-17.x.x 目录,并在 PATH 中加入 %JAVA_HOME%\bin 。验证命令:

java -version
# 输出应为:java version "17.x.x" ...

2.2 Spring Boot 3.x:WebFlux 与 Jakarta EE 9 的必然选择

Spring AI 是 Spring 官方项目,其所有 Starter 都基于 Spring Boot 3.x 构建。这带来两个不可绕过的事实:

  • 必须使用 Spring Boot 3.0+ 。Spring Boot 2.7 已于 2023 年 11 月停止维护,且其底层的 Spring Framework 5.x 不支持 Spring AI 所需的响应式流(Reactive Streams)规范。
  • 默认 Web 容器是 Netty(WebFlux),而非 Tomcat(MVC) 。这是为了原生支持大模型的流式响应(Streaming)。当你调用 chatClient.stream() 时,Spring AI 会通过 Flux<ChatResponse> 将每个 token 的增量响应推送到前端,而不是等整个回答生成完毕才返回。这要求你的 Controller 必须是 @RestController + Flux 返回类型,而非传统的 @ResponseBody

创建项目最稳妥的方式,是访问 start.spring.io ,选择:

  • Project:Maven
  • Language:Java
  • Spring Boot:3.3.x(最新稳定版)
  • Dependencies:Spring Web、Spring AI Ollama(注意:不是 “Spring AI Core”,而是具体实现)

生成的 pom.xml 会自动包含:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

这个 Starter 不仅引入了核心依赖,还内置了针对 Ollama 的自动配置类 OllamaAutoConfiguration ,它会扫描 application.yml 中的 spring.ai.ollama.* 配置,并为你准备好 OllamaChatModel OllamaEmbeddingModel 等 Bean。

2.3 Ollama:本地大模型的“即插即用”运行时

Ollama 是目前 Java 开发者入门 AI 最友好的本地模型运行时。它把复杂的模型加载、GPU 显存管理、HTTP API 封装成一个简单的命令行工具。你不需要懂 CUDA、不需要配 Docker、甚至不需要手动下载几十 GB 的模型文件——一条命令,模型就跑起来了。

安装步骤极其简单:

  • Mac brew install ollama ,然后 ollama run qwen2:7b
  • Windows :下载 Ollama Windows 安装包 ,双击安装,然后在 PowerShell 中执行 ollama run qwen2:7b
  • Linux curl -fsSL https://ollama.com/install.sh | sh ,然后 ollama run qwen2:7b

注意: qwen2:7b 是通义千问 2 的 7B 版本,对显存要求低(8GB RAM 即可),中文理解优秀,是入门首选。如果你的机器显存充足(≥16GB),可以换 qwen2:14b llama3:8b

安装完成后,Ollama 会在本地启动一个 HTTP 服务,默认地址是 http://localhost:11434 。你可以用 curl 验证:

curl http://localhost:11434/api/tags
# 返回所有已下载模型的列表

国内用户常遇到的问题是 ollama run qwen2:7b 下载极慢。这不是网络问题,而是 Ollama 默认从 Hugging Face 下载,而 HF 在国内没有 CDN 加速。解决方案是配置国内镜像源。在 ~/.ollama/modelfile (Windows 是 %USERPROFILE%\.ollama\modelfile )中添加:

FROM https://mirrors.tuna.tsinghua.edu.cn/hugging-face/models/Qwen/Qwen2-7B-Instruct/gguf/qwen2-7b-instruct.Q4_K_M.gguf

或者,更通用的方法是设置环境变量(推荐):

# Linux/macOS
export OLLAMA_HOST=0.0.0.0:11434
export OLLAMA_ORIGINS="http://localhost:*"
# Windows PowerShell
$env:OLLAMA_HOST="0.0.0.0:11434"
$env:OLLAMA_ORIGINS="http://localhost:*"

然后重启 Ollama 服务。这样,后续所有 ollama run 命令都会优先走清华源。

这三者的关系,可以类比为一辆汽车:JDK17 是发动机(提供动力),Spring Boot 3.x 是整车底盘和操作系统(提供运行框架),Ollama 则是油箱和燃油(提供 AI 能力)。少任何一个,车都开不起来。而 Spring AI,就是那个把发动机、底盘、油箱完美耦合在一起的总装厂。

3. 从零开始:一个可运行的 Spring AI + Ollama 对话应用

现在,所有基石都已铺好。我们来亲手搭建第一个真正可用的 Spring AI 应用。目标很明确:一个 Spring Boot Web 服务,接收用户输入的文本问题,调用本地 Ollama 的 Qwen2 模型,返回结构化的 AI 回答,并支持流式输出。整个过程,我们将严格遵循 Spring 的“约定优于配置”原则,尽量减少样板代码。

3.1 创建项目与依赖配置

打开 start.spring.io ,按如下配置生成项目:

  • Project:Maven Project
  • Spring Boot:3.3.3(或最新稳定版)
  • Dependencies:Spring Web、Spring AI Ollama、Lombok(简化 POJO)

下载 ZIP 包,解压后导入 IDE。打开 pom.xml ,确认 spring-ai-ollama-spring-boot-starter 依赖已存在。接着,打开 src/main/resources/application.yml ,进行核心配置:

spring:
  ai:
    ollama:
      # Ollama 服务地址,如果在本机运行,保持默认即可
      base-url: http://localhost:11434
      # 指定默认使用的聊天模型
      chat:
        model: qwen2:7b
      # 可选:配置嵌入模型,用于后续向量检索
      embedding:
        model: nomic-embed-text
  # 启用 Spring AI 的调试日志,方便排查问题
  logging:
    level:
      org.springframework.ai: DEBUG

# 为 WebFlux 配置响应式超时,避免流式请求挂起
server:
  reactive:
    max-http-header-size: 65536

这个配置文件,就是 Spring AI 的“魔法开关”。 spring.ai.ollama.base-url 告诉 Spring AI 去哪里找 Ollama; spring.ai.ollama.chat.model 指定了默认模型;而 logging.level.org.springframework.ai: DEBUG 则会在控制台打印出每一次请求的完整 Request/Response,是调试阶段的救命稻草。

3.2 编写核心业务逻辑:ChatService

src/main/java/com/example/demo 下,创建 service/ChatService.java

package com.example.demo.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.List;

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 同步调用:发送单条消息,获取完整响应
     */
    public String ask(String userQuery) {
        // 构建用户消息
        UserMessage userMessage = new UserMessage(userQuery);
        // 构建 Prompt(可选:添加系统指令,让模型更“听话”)
        Prompt prompt = new Prompt(List.of(userMessage));
        // 调用模型
        ChatResponse response = chatClient.call(prompt);
        // 提取并返回内容
        return response.getResult().getOutput().getContent();
    }

    /**
     * 流式调用:发送单条消息,逐 token 返回响应
     */
    public Flux<String> askStream(String userQuery) {
        UserMessage userMessage = new UserMessage(userQuery);
        Prompt prompt = new Prompt(List.of(userMessage));
        // 返回 Flux<String>,每个元素是一个 token 的文本片段
        return chatClient.stream(prompt)
                .map(chatResponse -> chatResponse.getResult().getOutput().getContent());
    }
}

这段代码展示了 Spring AI 最核心的两种调用模式。 ask() 方法返回一个 String ,适用于需要完整回答的场景(如生成报告摘要); askStream() 方法返回一个 Flux<String> ,适用于需要实时反馈的场景(如聊天界面的打字效果)。关键在于,你完全不用关心底层是 HTTP 还是 WebSocket,是 JSON 解析还是 SSE 解码—— ChatClient 已经为你封装好了所有细节。

3.3 构建 RESTful API 接口

创建 controller/ChatController.java

package com.example.demo.controller;

import com.example.demo.service.ChatService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    /**
     * 同步问答接口
     * POST /api/chat/sync
     * Body: {"query": "你好,今天天气怎么样?"}
     */
    @PostMapping(value = "/sync", produces = MediaType.TEXT_PLAIN_VALUE)
    public String syncAsk(@RequestBody SyncRequest request) {
        return chatService.ask(request.getQuery());
    }

    /**
     * 流式问答接口
     * GET /api/chat/stream?query=你好
     * 响应类型:text/event-stream
     */
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamAsk(@RequestParam String query) {
        return chatService.askStream(query);
    }

    // 内部 DTO,用于接收 JSON 请求体
    public static class SyncRequest {
        private String query;

        public String getQuery() { return query; }
        public void setQuery(String query) { this.query = query; }
    }
}

这里有两个关键点:

  • /sync 接口使用 @PostMapping ,返回 MediaType.TEXT_PLAIN_VALUE ,即纯文本。这是最简单的交互方式,适合 Postman 测试。
  • /stream 接口使用 @GetMapping ,返回 MediaType.TEXT_EVENT_STREAM_VALUE (SSE)。浏览器或前端框架(如 Vue 的 EventSource )可以监听这个流,实时接收每一个 data: xxx 的事件。

3.4 启动与验证:见证第一个 AI 响应

一切就绪,启动应用:

mvn spring-boot:run

确保此时 Ollama 已在后台运行: ollama run qwen2:7b (首次运行会自动下载模型,耐心等待几分钟)。

测试同步接口

curl -X POST http://localhost:8080/api/chat/sync \
  -H "Content-Type: application/json" \
  -d '{"query":"用一句话解释什么是 Spring AI?"}'

预期返回:

Spring AI 是一个由 Spring 官方推出的 Java 框架,旨在为 Spring Boot 应用提供统一、可扩展、生产就绪的大模型集成能力。

测试流式接口

curl -N http://localhost:8080/api/chat/stream?query=请用三个词形容Java语言的特点

你会看到类似这样的实时输出:

data: 稳定

data: 面向对象

data: 生态丰富

每行 data: 后面的内容,就是一个独立的 token。这就是流式响应的魅力——无需等待,所见即所得。

实操心得:如果 curl -N 没有返回任何内容,请首先检查 Ollama 是否真的在运行( ollama list 查看模型状态),其次检查 application.yml 中的 base-url 是否正确(特别是 Windows 用户,有时 localhost 会被解析为 IPv6 地址,可尝试改为 127.0.0.1 )。另外, -N 参数是 curl 的关键,它禁用缓冲,确保你能实时看到流式输出。

4. 深度解构:Spring AI 的核心组件与工作原理

仅仅会“跑起来”是远远不够的。一个资深 Java 开发者,必须理解其内部是如何运转的,才能在复杂场景下驾驭它。Spring AI 的架构并非黑盒,它由几个清晰、正交的核心组件构成,每一层都解决了特定的工程问题。

4.1 ChatModel 接口:模型能力的抽象契约

ChatModel 是 Spring AI 的核心接口,定义了所有聊天模型必须实现的最小行为集:

public interface ChatModel {
    ChatResponse call(Prompt prompt);
    Flux<ChatResponse> stream(Prompt prompt);
}

这个接口的设计,体现了典型的“面向接口编程”思想。 ChatResponse 是一个标准化的响应容器,它内部封装了:

  • output : ChatResponse.Output ,包含 content (文本内容)、 role (assistant/user/system)、 tokenUsage (本次调用消耗的 token 数)、 finishReason (停止原因,如 stop length )。
  • metadata : Map<String, Object> ,存放模型特有的元信息,如 Ollama 的 model 字段、OpenAI 的 system_fingerprint

当你在 application.yml 中配置 spring.ai.ollama.chat.model: qwen2:7b 时,Spring AI 的自动配置类 OllamaAutoConfiguration 就会创建一个 OllamaChatModel 的 Bean,并将其注入到所有需要 ChatModel 的地方。这个过程,和你配置 spring.datasource.url 后,Spring 自动创建 DataSource Bean 完全一致。

为什么需要这个抽象? 想象一下,你的项目初期用 Ollama 做 PoC,上线后要切换到阿里云百炼。如果代码里到处都是 new OllamaChatModel(...) ,那将是灾难性的。而有了 ChatModel 接口,你只需:

  1. 引入 spring-ai-alibaba-spring-boot-starter 依赖;
  2. application.yml 中添加 spring.ai.alibaba.chat.endpoint: https://dashscope.aliyuncs.com/...
  3. 修改 spring.ai.alibaba.chat.model: qwen-max

其余所有业务代码,包括 ChatService ,完全无需改动。这就是抽象的价值——它隔离了变化。

4.2 Prompt Message :提示工程的 Java 化表达

在 Spring AI 中,你永远不会直接拼接字符串来构造请求体。所有的输入,都必须通过 Prompt Message 对象来表达。这是一个革命性的转变,它将提示工程(Prompt Engineering)从“字符串艺术”提升为“类型安全的编程实践”。

Message 是一个接口,有三个实现类:

  • UserMessage : 用户输入,对应 role: user
  • AiMessage : AI 的回复,对应 role: assistant
  • SystemMessage : 系统指令,对应 role: system ,用于设定模型的行为准则(如“你是一个严谨的法律助手”)。

Prompt 则是一个容器,持有一个 List<Message> 。它还有一个重要的子类 ChatPrompt ,专门用于多轮对话场景。

例如,要实现一个“角色扮演”对话,你可以这样写:

// 构建系统指令
SystemMessage systemMessage = new SystemMessage("你是一位精通 Spring Boot 的资深架构师,回答要简洁、准确、有代码示例。");
// 构建用户历史消息(模拟多轮)
UserMessage history1 = new UserMessage("Spring Boot 如何配置多环境?");
AiMessage answer1 = new AiMessage("使用 application-{profile}.yml,通过 spring.profiles.active 激活。");
UserMessage currentQuery = new UserMessage("那如何在 Docker 中指定 profile?");

// 构建完整 Prompt
Prompt prompt = new Prompt(List.of(systemMessage, history1, answer1, currentQuery));
String response = chatClient.call(prompt).getResult().getOutput().getContent();

这种写法的好处是: 可读性、可测试性、可复用性 。你可以把 systemMessage 提取为常量,把历史对话保存在数据库中,然后动态组装 Prompt 。这比在 Controller 里写 String.format("system: %s\nuser: %s\n...", system, user) 要健壮得多。

4.3 ChatClient :面向开发者的“终极 API”

如果说 ChatModel 是面向框架的,那么 ChatClient 就是面向开发者的。它是 Spring AI 提供给业务代码的“门面(Facade)”,隐藏了所有底层复杂性。

ChatClient 的核心方法是:

ChatResponse call(Prompt prompt);
Flux<ChatResponse> stream(Prompt prompt);

但它背后,却整合了:

  • 重试机制 :当网络抖动导致请求失败时,自动重试(可配置次数和间隔)。
  • 熔断器(Circuit Breaker) :当模型服务连续失败,自动进入“熔断”状态,快速失败,避免雪崩。
  • 指标监控(Micrometer) :自动上报 spring.ai.chat.calls.count spring.ai.chat.calls.duration 等指标,可接入 Prometheus/Grafana。
  • 请求/响应日志 :在 DEBUG 级别下,自动打印完整的 HTTP 请求头、请求体、响应头、响应体。
  • Token 统计 :自动计算并记录每次调用的 inputTokens outputTokens ,为成本核算提供依据。

这一切,都无需你写一行额外代码。你只需要在 application.yml 中开启:

spring:
  ai:
    client:
      retry:
        max-attempts: 3
      circuit-breaker:
        enabled: true
        failure-threshold: 0.5

ChatClient 的设计理念,就是让开发者能像调用一个本地方法一样,去调用一个远程的、可能不稳定的、昂贵的 AI 服务。它把分布式系统的经典难题(容错、可观测、限流),变成了一个 YAML 配置项。

4.4 自动配置(Auto-Configuration):Spring Boot 的魔法之源

Spring AI 的所有“开箱即用”体验,都源于其精妙的自动配置机制。以 OllamaAutoConfiguration 为例,它的源码逻辑大致如下:

@Configuration
@ConditionalOnClass({OllamaChatModel.class, OllamaEmbeddingModel.class})
@ConditionalOnProperty(prefix = "spring.ai.ollama", name = "base-url")
public class OllamaAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public ChatModel ollamaChatModel(OllamaOptions options) {
        return new OllamaChatModel(options);
    }

    @Bean
    @ConditionalOnMissingBean
    public ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .build();
    }
}

这段代码的意思是:

  • 如果类路径下有 OllamaChatModel 类(即 spring-ai-ollama-spring-boot-starter 依赖已引入);
  • 并且 application.yml 中配置了 spring.ai.ollama.base-url
  • 那么,就自动创建一个 ChatModel Bean 和一个 ChatClient Bean。

@ConditionalOnMissingBean 是关键,它保证了你随时可以自己定义一个 ChatModel Bean 来覆盖默认实现,实现了完美的可扩展性。

理解了自动配置,你就明白了 Spring AI 的“灵魂”:它不是一个孤立的框架,而是 Spring Boot 生态的有机组成部分。它的所有能力,都建立在 Spring Boot 的约定之上。你学到的,不仅是如何调用 AI,更是如何在一个现代化的 Java 工程中,优雅地集成任何一种外部服务。

5. 从入门到进阶:常见问题排查与性能调优实战

跑通 Demo 只是万里长征第一步。在真实项目中,你会立刻撞上一系列“意料之外,情理之中”的问题。这些问题,往往不会出现在官方文档的 Hello World 里,但却是每个 Spring AI 使用者必经的“成人礼”。下面,我将结合自己踩过的坑,为你梳理一套完整的排错与调优指南。

5.1 问题排查链路:从“404 Not Found”到“模型未响应”

现象 :启动应用后,调用 /api/chat/sync 接口,返回 404 Not Found 500 Internal Server Error

排查链路 (严格按照此顺序):

  1. 确认 Ollama 服务状态 :在终端执行 ollama list 。如果没有任何输出,说明 Ollama 进程未启动。执行 ollama serve 启动服务守护进程。
  2. 确认模型已下载 ollama list 的输出中, qwen2:7b STATUS 列必须是 ok 。如果是 pulling ,说明还在下载中,需等待;如果是 error ,则可能是网络问题,需检查镜像源配置。
  3. 确认 Spring Boot 日志 :查看控制台启动日志,搜索关键词 OllamaAutoConfiguration 。如果看到 Skipping auto-configuration ,说明 spring.ai.ollama.base-url 配置有误或缺失。
  4. 验证网络连通性 :在 Spring Boot 应用所在机器上,执行 curl -v http://localhost:11434/api/tags 。如果返回 Connection refused ,说明 Ollama 没有监听 localhost:11434 ,检查 Ollama 的启动日志,或尝试 ollama serve --host 0.0.0.0:11434
  5. 启用 DEBUG 日志 :在 application.yml 中添加 logging.level.org.springframework.ai: DEBUG ,然后再次调用接口。日志中会清晰打印出 Spring AI 发出的 HTTP 请求 URL、Headers 和 Body,以及收到的响应。这是定位问题的“黄金日志”。

注意:Windows 用户常遇到 localhost 解析问题。如果 curl http://localhost:11434 失败,但 curl http://127.0.0.1:11434 成功,请将 application.yml 中的 base-url 改为 http://127.0.0.1:11434

5.2 性能瓶颈分析:为什么响应慢?Token 消耗高?

现象 :AI 响应时间长达 10 秒以上,或 inputTokens 消耗远超预期。

根因分析与优化

  • 模型本身性能 :Qwen2:7b 在 CPU 上推理速度较慢。优化方案: ollama run qwen2:7b-q4_k_m (量化版,速度提升 2-3 倍);或 ollama run qwen2:7b-cuda (如果你有 NVIDIA GPU,需先安装 CUDA 驱动)。
  • Prompt 过长 inputTokens 消耗高,通常是因为你把大量无关的上下文(如整个 HTML 页面、冗长的日志)塞进了 UserMessage 。优化方案:在 ChatService 中,对 userQuery 进行预处理,用正则或 NLP 库提取核心问题,丢弃噪声。
  • 网络延迟 :Spring Boot 应用与 Ollama 不在同一台机器。优化方案:将两者部署在同一宿主机,或使用 Docker Compose 统一编排,通过 host.docker.internal 访问。

实测对比数据(Mac M1 Pro)

模型 首 Token 延迟 完整响应时间 inputTokens (100字查询)
qwen2:7b 2.1s 8.7s 142
qwen2:7b-q4_k_m 0.8s 3.2s 142
llama3:8b-q4_k_m 1.2s 4.5s 138

可以看到,量化对性能提升巨大,且几乎不损失精度。这是生产环境的必备选项。

5.3 流式响应失效:为什么前端收不到 data: 事件?

现象 curl -N http://localhost:8080/api/chat/stream?query=xxx 没有输出,或只有最后一行。

根本原因与修复

  • Spring WebFlux 配置缺失 application.yml 中必须有 server.reactive.max-http-header-size: 65536 。否则,Ollama 的 SSE 响应头(如 Content-Type: text/event-stream )可能被截断。
  • 前端未正确处理 SSE :浏览器原生 EventSource 对跨域敏感。如果你的前端在 http://localhost:3000 ,而后端在 http://localhost:8080 ,需要在 ChatController 上添加 @CrossOrigin 注解:
    @CrossOrigin(origins = "http://localhost:3000")
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamAsk(@RequestParam String query) {
        return chatService.askStream(query);
    }
    
  • IDEA/VSCode 内置终端缓冲 :某些 IDE 的终端会缓冲流式输出。务必使用系统原生终端(Terminal.app, PowerShell, bash)进行测试。

5.4 生产就绪 Checklist:从 Demo 到上线

一个能跑通的 Demo,离生产环境还有十万八千里。以下是我在多个项目中总结的上线前必检清单:

  • [ ] 模型版本固化 application.yml 中的 spring.ai.ollama.chat.model 必须指定完整标签,如 qwen2:7b-q4_k_m ,而非 qwen2:latest 。后者会导致不同环境拉取不同模型,结果不可控。
  • [ ] 错误处理兜底 :在 ChatService ask() 方法中,必须用 try-catch 捕获 RuntimeException (Spring AI 的所有异常都继承于此),并返回友好的错误信息,而非让整个服务崩溃。
  • [ ] Token 用量监控告警 :通过 Micrometer 将 spring.ai.chat.calls.input-tokens.total 指标接入 Grafana,并设置阈值告警(如单次调用 > 5000 tokens)。
  • [ ] 请求限流 :使用 Spring Cloud Gateway 或 Resilience4j,对 /api/chat/* 接口添加 QPS 限流,防止恶意刷量耗尽模型资源。
  • [ ] 敏感信息脱敏 :在 DEBUG 日志中, UserMessage 的内容会被完整打印。上线前,必须在日志配置中,对 UserMessage.getContent() 进行脱敏(如 *** 替换中间字符)。

最后一个经验:永远不要相信“它应该能工作”。在交付给 QA 之前,自己用 curl 、Postman、浏览器三种方式,分别测试同步、流式、错误场景(如空 query、超长 query、非法字符),并记录下每一步的响应时间和返回内容。这份《冒烟测试报告》,是你对自己代码最大的尊重。

6. 超越 Hello World:Spring AI 的真实应用场景与演进路径

当你的第一个 qwen2:7b 对话应用稳定运行一周后,真正的挑战才刚刚开始。Spring AI 的价值,绝不仅限于“让 Java 程序员也能调用大模型”。它的设计初衷,是成为企业级 AI 应用的“操作系统内核”。下面,我将基于真实项目经验,为你勾勒出一条从入门到专家的演进路径。

6.1 场景一:智能客服知识库(RAG)

这是 Spring AI 最成熟、落地最快的场景。传统客服系统,知识库更新滞后,FAQ 覆盖率低。而 RAG(Retrieval-Augmented Generation)能让你的客服机器人,实时“阅读”最新的产品文档、工单记录、会议纪要,然后给出精准回答。

核心技术栈

  • EmbeddingModel

更多推荐