1. 为什么我决定认真啃一遍 Spring AI
第一次听说 Spring AI 是在一个做企业级 SaaS 的朋友群里,有人丢了一句“Spring Boot 项目里直接调大模型现在不用自己写 HTTP 客户端了”,当时我没太在意。后来陆续看到 RAG、Tool Calling、Advisor 这些词频繁出现在 Java 圈子的讨论里,加上 Spring AI Alibaba 也开始冒头,我才意识到这事不是玩票——它解决的是 Java 后端开发者接入 AI 能力时最痛的那一层:把模型调用、提示词管理、向量检索、工具编排这些脏活累活,收敛成 Spring 风格的 Bean 和注解。
说白了,以前你在 Spring Boot 里接一个大模型,得自己封装 HTTP 请求、处理流式响应、管理对话上下文、拼 RAG 的检索结果,代码写出来又臭又长,换个模型厂商还得重写一遍。Spring AI 干的事情就是把这些抽象成统一的接口,让你像注入 JdbcTemplate 一样注入一个 ChatClient,然后该干嘛干嘛。它适合谁?适合已经有 Spring Boot 基础、想把 AI 能力嵌进现有业务系统的后端开发,也适合想理解 RAG 和 Agent 到底怎么落地、而不是停留在调 API 层面的工程师。
我这段时间从零开始把 Spring AI 的核心模块过了一遍,踩了不少坑,也总结了一些文档里不会写的细节。下面按我自己的学习路径,把整体设计思路、核心机制、实操过程和排查经验完整拆开讲。
2. Spring AI 的整体设计与选型思路
2.1 它到底抽象了哪几层
Spring AI 的定位不是“又一个 LangChain”,它的设计哲学是面向 Spring 生态的 AI 能力适配层。我把它拆成四层来理解:
- 模型抽象层:ChatModel、EmbeddingModel、ImageModel 这些接口统一了不同厂商的调用方式。你写
chatClient.prompt().user("...").call(),底层是 OpenAI 还是智谱还是通义,对上层代码几乎透明。 - 提示词层:Prompt、PromptTemplate、Message 这些类把提示词从字符串拼接升级成可管理的对象,支持系统消息、用户消息、助手消息的角色区分。
- 增强层:Advisor 机制是 Spring AI 比较有特色的设计,它类似 Servlet 的 Filter 链,可以在请求前后插入逻辑,RAG 检索、对话记忆、内容审核都能挂在这条链上。
- 工具层:Tool Calling 让模型能反过来调用你定义的 Java 方法,这是做 Agent 的基础。
为什么这么分层?因为企业项目里最怕的就是“模型绑定”。今天用某家,明天老板说要换,如果代码里到处是厂商 SDK 的调用,迁移成本极高。Spring AI 用接口隔离了这层变化,这是它相比直接调 SDK 最大的价值。
2.2 版本与依赖选型:别一上来就追新
我一开始图新鲜用了比较激进的版本组合,结果遇到依赖冲突。后来稳定下来的组合是Java 21 + Spring Boot 3.x + Spring AI 1.0.x 正式版。这里有个经验:Spring AI 在 1.0 之前 API 变动非常频繁,ChatClient的构建方式、Advisor 的接口签名都改过,所以网上很多教程是过时的,你照着抄会编译不过。
Maven 依赖的核心就两个:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-simple</artifactId> </dependency>注意 artifactId 的命名规则,1.0 之后从spring-ai-openai-spring-boot-starter改成了spring-ai-starter-model-openai,这个改动坑了不少人。如果你用智谱 AI,它兼容 OpenAI 协议,所以可以直接用 openai 的 starter,只需要把 base-url 和 model 换掉。
提示:选版本时先看 Spring AI 官方文档的 compatibility matrix,Spring Boot 3.2 和 3.3 对应的 Spring AI 版本不一样,混用会出现自动配置不生效的问题。
2.3 为什么 Advisor 是我最看重的机制
如果只能挑一个 Spring AI 里最值得深入学的点,我选 Advisor。原因很简单:RAG、对话记忆、日志追踪这些横切关注点,全靠它串起来。没有 Advisor,你的 RAG 代码会散落在 Service 层的各个角落;有了它,检索逻辑封装成一个 Advisor,挂到 ChatClient 上就完事。
它的执行模型是链式的,请求进来先过一圈 Advisor 的before逻辑,然后到模型,响应回来再过after逻辑。这个设计让“检索增强”变成了一个可插拔的组件,而不是硬编码的流程。我后面会专门讲怎么自定义一个 Advisor。
3. 核心机制拆解与实操要点
3.1 ChatClient 的构建与调用姿势
ChatClient 是日常用得最多的入口。它的构建有两种方式,我推荐用 Builder 注入的方式,因为可以统一配置默认的 Advisor 和系统提示词:
@Configuration public class AiConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultSystem("你是一个严谨的技术助手,回答要给出依据") .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); } }这里有个细节:defaultAdvisors注册的 Advisor 会对所有通过这个 Client 发起的请求生效。如果你只想对某次调用生效,可以在prompt()后面单独加。我踩过的坑是——同一个 Advisor 实例被多个 Client 共享时,如果它内部有可变状态,会出现并发问题。所以自定义 Advisor 时尽量做成无状态的,需要存上下文就用请求级别的 context 传递。
调用侧就三行:
String answer = chatClient.prompt() .user("Spring AI 的 Advisor 是什么") .call() .content();流式的话把call()换成stream(),返回Flux<String>。注意流式场景下 Advisor 的after逻辑触发时机和同步不一样,如果你在 after 里做统计,要确认它是在流结束后才执行。
3.2 Tool Calling:让模型调用你的 Java 方法
Tool Calling 是我觉得最能体现“Agent 雏形”的功能。原理是:你把一个 Java 方法用@Tool注解标记,Spring AI 会把它转成模型能理解的函数描述,模型判断需要时返回一个调用意图,框架再反射执行你的方法,把结果喂回模型。
@Component public class WeatherTools { @Tool(description = "根据城市名查询当前天气") public String getWeather(@ToolParam(description = "城市名称") String city) { return weatherService.query(city); } }注册方式是在调用时指定:
String result = chatClient.prompt() .user("北京今天天气怎么样") .tools(new WeatherTools()) .call() .content();关键点在于 description 的写法。模型靠这段描述判断该不该调这个工具,描述写得含糊,模型要么不调,要么乱调。我的经验是描述里要写清楚“什么时候用”,而不只是“这个工具做什么”。比如“查询实时天气,当用户询问某地当前天气状况时使用”,比单纯写“天气查询工具”效果好很多。
还有一个坑:工具方法的参数类型要简单。复杂对象模型理解起来容易出错,尽量用 String、int 这种基础类型,复杂查询让模型传 ID 而不是传整个对象。
3.3 RAG 的落地:从向量化到检索增强
RAG 这块是重头戏。它的核心流程是:文档切分 → 向量化 → 存入向量库 → 查询时检索相关片段 → 拼进提示词。Spring AI 把每一步都提供了抽象。
文档读取用DocumentReader,切分用TokenTextSplitter,向量化用EmbeddingModel,存储用VectorStore。我实测下来,切分策略对效果影响极大。默认的按 token 切分容易把一句话切断,我一般会设置chunkSize在 500 到 800 之间,chunkOverlap留 100 左右,保证上下文连贯。
TokenTextSplitter splitter = new TokenTextSplitter(600, 100, 5, 10000, true); List<Document> chunks = splitter.apply(documents); vectorStore.add(chunks);检索增强用QuestionAnswerAdvisor最省事,它自动把用户问题向量化、检索、拼进提示词:
ChatClient client = builder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().topK(4).similarityThreshold(0.7).build())) .build();topK和similarityThreshold这两个参数要调。topK 太大,噪声多,模型容易被无关内容带偏;太小,可能漏掉关键信息。我一般从 4 开始试,阈值 0.7 左右,具体看你的文档质量。
3.4 对话记忆:别让模型失忆
多轮对话需要记忆。Spring AI 提供了ChatMemory和对应的 Advisor。默认的InMemoryChatMemory存在内存里,重启就没了,生产环境要换成基于 Redis 或数据库的实现。
ChatMemory memory = new InMemoryChatMemory(); ChatClient client = builder .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build();调用时要传一个 conversationId,否则所有会话的记忆会混在一起:
client.prompt() .user("接着上面说") .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-123")) .call() .content();这个 conversationId 的坑我踩过:忘记传的话,默认用一个固定 key,多个用户的对话历史会串台,测试时不容易发现,上线就是事故。
4. 完整实操:搭一个带 RAG 和工具调用的问答服务
4.1 项目结构与配置
我搭的 demo 结构很朴素:一个 Controller、一个配置类、一个工具类、一个知识库加载器。配置文件里关键是模型和向量库的连接信息:
spring: ai: openai: base-url: https://open.bigmodel.cn/api/paas/v4 api-key: ${AI_API_KEY} chat: options: model: glm-4 temperature: 0.7 vectorstore: simple: initialize-schema: true这里 base-url 换成智谱的地址,model 换成 glm-4,就完成了从 OpenAI 到智谱的切换,代码一行不用改。这就是抽象层的价值。api-key 一定要走环境变量,别硬编码进仓库。
4.2 知识库加载与向量化
启动时把文档灌进向量库,我写了个ApplicationRunner:
@Bean ApplicationRunner loadDocs(VectorStore vectorStore, EmbeddingModel embeddingModel) { return args -> { List<Document> docs = List.of( new Document("Spring AI 的 Advisor 是请求拦截链..."), new Document("Tool Calling 允许模型调用 Java 方法...") ); TokenTextSplitter splitter = new TokenTextSplitter(600, 100, 5, 10000, true); vectorStore.add(splitter.apply(docs)); }; }实测下来,文档内容最好先做清洗,去掉页眉页脚、乱码,否则检索出来的片段质量很差。另外向量化是有成本的,别每次启动都重新灌,加个判断或者用持久化的向量库。
4.3 组装 ChatClient 并暴露接口
配置类里把 Advisor 和工具都挂上:
@Bean public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory memory) { return builder .defaultSystem("你是技术助手,优先基于知识库回答,不确定就说不确定") .defaultAdvisors( new MessageChatMemoryAdvisor(memory), new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().topK(4).similarityThreshold(0.7).build()) ) .build(); }Controller 层:
@PostMapping("/chat") public String chat(@RequestParam String q, @RequestParam String sessionId) { return chatClient.prompt() .user(q) .tools(new WeatherTools()) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) .call() .content(); }跑起来之后,问知识库里有的内容,它会基于检索结果回答;问天气,它会触发工具调用。整个链路是通的。
4.4 参数调优的实测记录
我拿同一批问题做了几组对比。topK 从 2 调到 8,发现 4 的时候答案最稳,8 的时候开始出现“答非所问”,因为检索进来的无关片段干扰了模型。similarityThreshold 从 0.5 提到 0.8,召回变少但准确率上升,最终定在 0.7。temperature 对 RAG 场景影响不大,我设 0.3 让回答更收敛。
这些参数没有万能值,跟你的文档质量、问题类型强相关。我的建议是准备 20 条左右的测试问题,手动标注期望答案,然后网格搜索这几个参数,比拍脑袋强。
5. 常见问题与排查技巧实录
5.1 自动配置不生效
最常见的报错是注入 ChatClient 时提示找不到 Bean。原因通常是依赖 artifactId 写错,或者 Spring Boot 版本和 Spring AI 版本不匹配。排查顺序:先看mvn dependency:tree里有没有 spring-ai 的 starter,再看启动日志里有没有OpenAiAutoConfiguration相关的加载记录。如果用了自定义 base-url,确认配置前缀是spring.ai.openai而不是别的。
5.2 流式响应中文乱码
流式接口返回Flux<String>时,如果前端收到乱码,检查响应头的 Content-Type 有没有带 charset。Spring AI 默认返回的是 UTF-8,但如果你在 Controller 上手动设置了produces,可能覆盖掉。我一般显式写produces = MediaType.TEXT_EVENT_STREAM_VALUE。
5.3 RAG 检索不到相关内容
这个问题我遇到好几次。排查思路:先确认文档真的进了向量库(打印vectorStore.similaritySearch的结果),再看相似度分数是不是都低于阈值。如果分数普遍很低,可能是 embedding 模型和查询用的模型不一致,或者文档切分太碎导致语义丢失。把 chunkSize 调大、overlap 调大通常能缓解。
5.4 工具调用不触发
模型不调工具,八成是 description 写得不好,或者工具方法的参数类型太复杂。我试过把参数从自定义对象改成 String,触发率立刻上来了。另外确认.tools()是在prompt()之后调的,顺序错了不生效。
| 问题现象 | 可能原因 | 排查动作 |
|---|---|---|
| ChatClient 注入失败 | 依赖 artifactId 错误 | 检查 dependency:tree |
| 流式乱码 | Content-Type 缺 charset | 显式设置 produces |
| 检索为空 | 阈值过高或切分过碎 | 打印相似度分数,调 chunkSize |
| 工具不触发 | description 含糊 | 改写描述,简化参数类型 |
| 记忆串台 | 未传 conversationId | 每次调用传唯一 sessionId |
5.5 几个文档里不写的经验
第一,开发阶段把模型的原始响应打出来。Spring AI 的ChatResponse里有 token 使用量、finish reason 这些元信息,排查问题时非常有用,别只看content()。
第二,Advisor 的顺序有讲究。记忆 Advisor 一般放在检索 Advisor 前面,这样检索时能带上历史上下文。顺序反了,多轮对话里的指代就解析不了。
第三,向量库别用内存版上生产。SimpleVectorStore 重启即失,而且数据量大时性能急剧下降。生产环境换 Redis、PGVector 或者 Milvus,Spring AI 都有对应的 starter。
第四,控制好成本。每次调用都带 RAG 检索和工具描述,token 消耗比裸调大不少。我一般会给检索结果设个长度上限,工具描述也尽量精简。
6. 我对 Spring AI 后续学习路径的看法
把基础跑通之后,我下一步打算深入的是 Agentic RAG 这块,也就是让模型自己决定要不要检索、检索几轮,而不是固定走一次检索。Spring AI 的 Advisor 机制其实已经为这种动态编排留了口子,你可以写一个 Advisor,在里面根据模型的第一轮输出决定是否触发二次检索。另外 Spring AI Alibaba 的 NL2SQL 能力我也在关注,它把自然语言转 SQL 和 RAG 结合起来,对做数据类产品的团队挺有参考价值。
我个人在实际操作中的体会是:Spring AI 的学习曲线不在 API 本身,而在理解它每一层抽象背后的取舍。你知道了 Advisor 为什么存在、Tool Calling 的边界在哪、RAG 的参数怎么影响效果,用起来才不会只是抄代码。最后分享一个小技巧——把每次调优的参数和对应的测试结果记在一个表格里,积累一段时间后你会发现,针对自己业务的最优配置是有规律可循的,比到处问别人“topK 设多少”靠谱得多。