ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI 实战:从 ChatClient 到 RAG 与 Tool Calling 的 Java AI 开发指南

Spring AI 实战:从 ChatClient 到 RAG 与 Tool Calling 的 Java AI 开发指南 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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-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 matrixSpring 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()返回FluxString。注意流式场景下 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); ListDocument 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 知识库加载与向量化启动时把文档灌进向量库我写了个ApplicationRunnerBean ApplicationRunner loadDocs(VectorStore vectorStore, EmbeddingModel embeddingModel) { return args - { ListDocument 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 流式响应中文乱码流式接口返回FluxString时如果前端收到乱码检查响应头的 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每次调用传唯一 sessionId5.5 几个文档里不写的经验第一开发阶段把模型的原始响应打出来。Spring AI 的ChatResponse里有 token 使用量、finish reason 这些元信息排查问题时非常有用别只看content()。第二Advisor 的顺序有讲究。记忆 Advisor 一般放在检索 Advisor 前面这样检索时能带上历史上下文。顺序反了多轮对话里的指代就解析不了。第三向量库别用内存版上生产。SimpleVectorStore 重启即失而且数据量大时性能急剧下降。生产环境换 Redis、PGVector 或者 MilvusSpring 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 设多少”靠谱得多。
返回列表