Spring AI从对话到干活:多轮记忆、结构化输出、工具调用与RAG实践

发布时间:2026/10/22 3:42:33

Spring AI从对话到干活:多轮记忆、结构化输出、工具调用与RAG实践 Spring AI 这个系列的第二篇我想直接从“能回话”讲到“能干活”。如果你上一期已经把 Spring AI 和模型服务接通并且用 ChatClient 发过一条普通消息那这篇文章基本就是你接下来几天的路线图多轮记忆、提示词模板、结构化输出、Function Calling以及最容易被业务同事追问的 RAG 知识库问答。先说清楚这篇补什么。很多教程喜欢一上来就甩一个带数据库、带向量库、带工具调用的完整 Demo看完很爽但搬到自己的项目里往往崩得莫名其妙。因为完整 Demo 把链路一次性拉满以后你根本不知道哪一步是命中的、哪一步是恰好凑出来的。所以这篇会保持每块都能单独跑通的方式来讲每段代码都是我在本地实际执行过的最小形态。我本地环境是 Java 17 Spring Boot 3.3 Spring AI 1.0.x下面的代码不会依赖某个冷门内部 API你可以直接复制到自己的工程里改。1. 先把上一期的“聊天闭环”补全1.1 多轮对话不能靠手拼数组很多刚上手的同学第一次写完 ChatClient 就会遇到一个很尴尬的问题模型每次都是“失忆”的。因为默认情况下你发什么模型就只看什么服务端并不会保存历史。解决这个问题的正统做法是用 Spring AI 的 ChatMemory而不是自己在代码里拼一个 List 然后不断 set 进去。自己拼不是不行但你会很快遇到多轮角色错乱、token 越滚越长、以及同一段记忆被重复塞进去的问题。Spring AI 里做多轮对话的常规姿势是 ChatMemory 加 ChatClient 的会话机制。ChatClient 每次请求时会把你的新消息和历史消息一起打包给模型模型本身不会记住上一次你说了什么。Bean ChatMemory chatMemory() { return new InMemoryChatMemory(); }InMemoryChatMemory 只是把消息存在当前进程的内存里适合开发和单机演示。真正上线时可以换成基于 Redis 的实现或者你自己实现 ChatMemory 接口把消息落到数据库。重点是不管用什么实现第一步先把“会话上下文由框架统一管理”这个思路固定下来。请求时带上同一个会话 IDString first chatClient.prompt() .user(我叫小张喜欢简洁的回答风格。) .call() .content(); String second chatClient.prompt() .memory(session-001) .user(我是谁来着) .call() .content();第二个问题能答上来是因为使用了同一个会话 ID。如果你不想每个请求都传 sessionId也可以在 ChatClient.Builder 上配 defaultMemory(...)这样每次请求会自动带默认会话要区分用户时仍然用 .memory(sessionId)。另外内存实现也支持只保留最近 N 条消息一般在构造 ChatMemory 时指定一个窗口大小不同小版本的 API 叫法略有差异我这边用的是 chatMemoryRetrieverSize(10)。这个窗口别开太大否则上下文长了对响应速度和成本都不友好。1.2 从字符串拼接到 PromptTemplate如果你到现在还是靠字符串拼接组织 user 内容我建议趁早改掉。字符串拼接的问题很快会出现一是转义地狱二是模板稍微一变代码和字符串都乱三是用户输入里如果有花括号或特殊字符容易污染模板。Spring AI 的 PromptTemplate 用起来就是模板渲染和写一个带占位符的文本文件没有本质区别MapString, Object params Map.of( product, 某型号机械键盘, audience, 程序员, tone, 口语化 ); PromptTemplate template new PromptTemplate( 你是电商文案。请你围绕商品“{product}”写一段面向{audience}的简介 语气要{tone}最多 40 字不要出现夸张承诺。 , params); String answer chatClient.prompt(template.create()).call().content();我更推荐把模板放到 resources/prompts 目录下比如 product.st然后用 ClassPathResource 读取。这样文案同事可以直接改模板文件不用翻代码。我实际项目里基本都走这条路因为 prompt 迭代速度远超代码发布速度把模板和代码分开省掉一次次重新部署的烦恼。2. 结构化输出让模型返回能直接用的对象2.1 用 entity() 收下对象大多数业务系统真正需要的不是一段漂亮的散文而是订单状态、客户姓名、金额、日期这些能被程序处理的字段。Spring AI 的 BeanOutputConverter 就是干这个的它会在发给模型的 prompt 里追加一段 JSON Schema 说明让模型按格式返回然后在客户端用 Jackson 反序列化成目标类型。最简单的方式是直接用 ChatClient 的 entity()public record OrderSummary( String orderNo, String status, ListLineItem items ) { public record LineItem(String name, int quantity, double price) {} } OrderSummary order chatClient.prompt() .user(把这句话整理成结构化订单信息用户购买了键盘2个和鼠标垫1个订单号20240615001状态已发货。) .call() .entity(OrderSummary.class);如果你要返回一个列表可以这样写ListOrderSummary orders chatClient.prompt() .user(把下面的订单都整理成结构化结果……) .call() .entity(new ParameterizedTypeReferenceListOrderSummary() {});我实测下来嵌套对象和集合在大多数通用模型上都能稳定输出但前提是你在字段名上不要反人性。比如 Java 里习惯 orderNo模型输出也常见 order_no建议直接在字段上加 JsonProperty(order_no)或者在提示词里给出一个字段映射说明。字段注释越明确模型越不会自由发挥。2.2 失败兜底和解析清洗结构化输出最典型的表现是模型返回了json 开头的代码块或者夹杂着“以下是 JSON”这样的提示语。Spring AI 的 converter 会在很多模型上正常工作但对于一些小模型输出格式很难严格遵循。我的兜底做法分三层第一优先开启模型服务商的 JSON mode 或 response_format第二加一层清洗把首尾的json 和 去掉后再反序列化第三如果还失败把错误信息回传给模型明确告诉它“上次输出不是合法 JSON请只输出 JSON”。另一个坑是枚举字段。模型输出“已发货”而你代码里枚举是 SHIPPED反序列化直接报错。这种情况要么在提示词里做映射要么先收成 String 再翻译成业务对象。结构化输出的本质是“让模型按契约交付”所以契约本身要简单、明确不要指望模型自己猜你的内部约定。3. Function Calling给模型开一条“执行通道”3.1 Tool 是外壳真正的活还是你自己干先纠正一个常见误解Function Calling 不是让模型在服务端执行代码而是模型在生成回复时决定“现在该调用哪个工具、传什么参数”框架收到这个请求后执行真实方法再把结果喂回模型让模型基于结果组织语言。所以工具方法里才是数据查询、权限校验这些实质逻辑。在 Spring AI 里一个最省事的方式是用 Tool 注解直接标记业务方法Service public class OrderToolService { Tool(description 根据订单号查询订单的当前状态用于回答物流和订单进度问题) public String queryOrderStatus(String orderNo) { // 这里可以调用真实的订单服务、数据库或外部接口 return 订单 orderNo 已发货当前位于转运中心预计后天送达。; } }然后把工具注册成 ToolCallbackProviderBean ToolCallbackProvider orderTools(OrderToolService orderToolService) { return MethodToolCallbackProvider.builder() .toolObjects(orderToolService) .build(); }请求时挂载给 ChatClientString answer chatClient.prompt() .user(帮我查一下 20240615001 这个订单到哪了) .tools(orderTools) .call() .content();如果你用的是 ChatClient.Builder 的 defaultTools也可以直接把 ToolCallbackProvider 注册进去这样所有请求都会挂载这份工具列表本次请求临时挂载用 .tools(...) 更清晰。要注意的是Tool 方法所在类必须被 Spring 扫描到否则工具根本不会出现在模型面前。3.2 工具描述、参数和权限控制工具方法会被序列化成 JSON Schema 给模型方法名、参数名、注释和 Tool 里的 description 全部会出现在模型面前。模型没有读你的代码它只靠这份描述决定何时调用。描述写得越具体模型就越不容易在无关问题上随机调用工具。实际操作中我总结了四条经验参数尽量用简单类型。复杂嵌套对象模型构造失败率明显升高能传字符串就传字符串能传整数就传整数。返回值建议直接返回给模型看的文本而不是内部对象。比如返回一段精简文本“订单已发货预计后天到达”比返回一个 20 字段的 POJO 更稳模型组织回复也更容易。工具方法内部一定要做权限和参数校验。模型的调用只是“建议”代码执行前你必须确认这个方法可以被当前用户调用。多个工具互相依赖时不要让模型自己串联。框架一般会把一次回复里的所有 tool call 执行完后再统一给模型所以工具方法最好设计成幂等的重复执行不能有副作用。4. 从单轮到知识库用 RAG 把文档变成回答依据4.1 很多人理解错了 RAG 的角色RAG 不是让模型去读整个知识库而是在回答前帮你把最相关的几个片段捞出来塞进 prompt让模型照着这几段内容回答。打个比方开卷考试RAG 负责提前帮你翻到正确的几页模型负责根据这几页组织答案而不是闭卷背整本教材。Spring AI 里做 RAG 的核心组件包括 Document、切分器、EmbeddingModel、VectorStore 和 QuestionAnswerAdvisor。流程看着多实际拆开就四步文档切分、向量化存储、相似度检索、检索结果拼进 prompt。把这四步拆开理解后面看任何复杂架构都不会懵。4.2 用 SimpleVectorStore 跑通最小链路第一版不要一上来就上 PGVector 或者 Milvus先用 SimpleVectorStore 这种进程内实现验证流程跑通了再换基础设施。依赖加上 vector store 之后就有一个最小骨架dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-vector-store/artifactId /dependency配置一个 VectorStoreBean VectorStore vectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel).build(); }写入文档时先切分再存储Service public class DocIngestionService { private final VectorStore vectorStore; public DocIngestionService(VectorStore vectorStore) { this.vectorStore vectorStore; } public void load(String content) { Document document new Document(content); ListDocument chunks new TokenTextSplitter().apply(List.of(document)); vectorStore.add(chunks); } }查询时用 QuestionAnswerAdvisor 把检索和问答接起来var advisor new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(3) .similarityThreshold(0.5) .build()); String answer chatClient.prompt() .advisors(advisor) .user(根据下面的资料这款产品的保修期是多久) .call() .content();这里有个容易出错的点QuestionAnswerAdvisor 默认会往用户 prompt 里注入检索到的上下文所以你的提问里带上“根据下面的资料”或“只根据资料回答”这类限定可以帮助模型把回答范围锁在文档内减少自由发挥。SimpleVectorStore 不需要预先建 collection向量维度由 EmbeddingModel 决定。你一旦切换了 embedding 模型之前写入的向量大概率因为维度不匹配而检索不到这就是很多人遇到的“换模型之后什么都查不到”的原因。4.3 topK、阈值和上下文长度如何取舍topK 越大召回越多模型能看到更全的信息但也更容易被不相关片段干扰prompt 也会变长。我一般起步设 3 左右。similarityThreshold 如果设成 0.7 以上在很多文档上会出现召回为空的情况0.4 到 0.6 是相对高频的区间具体要看你用的 EmbeddingModel 的分数分布。另一个实用技巧是控制切分大小。按 300 到 500 token 一段比较通用太短会把一句话拆成两半太长了又会把多个主题混在一起。生产环境如果要接外部知识库除了切分策略还需要处理文档更新、源数据标注、重复文档去重这些问题但那是另一个话题了。先把检索链路跑通你才有机会看到那些问题否则一切都停留在想象里。5. 让“大模型应用”可测试、可观察5.1 集成测试别断言“原文”大模型输出不是确定性纯函数prompt 一样结果也可能措辞不同。所有指望输出完整等于某段原文的测试都会在 CI 里浪费你的周末。正确姿势是断言关键信息是否出现或者先让模型输出结构化结果再断言字段。我自己常写的集成测试长这样SpringBootTest class ChatClientIT { Autowired ChatClient chatClient; Test void answerShouldContainOrderStatus() { String answer chatClient.prompt() .user(订单 20240615001 到什么状态了) .call() .content(); assertThat(answer).contains(发货); } }如果整个测试需要更稳定我会让模型先结构化输出再对字段做精确比较。比如让回答变成 entity(OrderStatusResult.class)然后断言 status 字段等于 SHIPPED这比 substring 可靠得多。测试里一定要记得给模型调用留出足够的超时时间别用默认的几秒去请求一个大模型。5.2 观察是调优的底裤prompt 改一版感觉没问题过几天又复现不了最常见的原因就是你根本没记录当时的模型版本、参数和输入输出。我建议所有 ChatClient 调用都打日志至少记录耗时、模型名称、temperature、token 数、输入 prompt 和输出内容。生产环境出于隐私考虑可以不落盘明文但开发环境一定要留。最简单的观察方式就是自己包一层调用long start System.currentTimeMillis(); String answer chatClient.prompt().user(question).call().content(); log.info(question{} answer{} cost{}ms, question, answer, System.currentTimeMillis() - start);在调参时这几个最常见的生成参数可以提前记一下参数作用我常用的起步值temperature控制随机性越低越稳定0.2maxOutputTokens单次最大输出长度500~1500 按场景topP核采样阈值0.9frequencyPenalty抑制重复措辞0presencePenalty鼓励新话题0注意这些参数在不同模型服务商的接口里名称不完全一样Spring AI 的 ChatOptions 会做一层映射但取值范围和效果仍然由模型服务商决定。凡是遇到“我改了 temperature 怎么没反应”的问题先去查服务商文档不要怀疑框架坏了。6. 常见报错、排查思路与经验速查6.1 高频异常速查表这部分直接整理成表格方便你以后当速查工具用。症状可能原因排查/解决启动报 No qualifying bean of type ChatClient.Builder缺少 Spring AI 自动装配检查是否引入了 spring-ai-starter 相关依赖并配置了模型 API别只放了 web starter调用返回 401 / AuthenticationErrorAPI Key 缺失、过期或服务未开通检查配置项和模型服务商控制台JSON 解析失败 / Error while extracting response模型没有输出合法 JSON开启 json mode清洗 markdown 围栏或把错误回传重试Tool 方法没有被调用工具没有注册或模型不支持 function calling确认方法所在类被 Spring 扫描注册了 ToolCallbackProvider并在请求或 builder 上挂载向量检索返回空topK 太小或 similarityThreshold 太高降低阈值调大 topK如果换过 embedding 模型清空并重建向量集合上下文太长超出窗口memory 窗口大 topK 大 chunk 长限制 ChatMemory 保留条数降低 topK控制 document chunk 长度6.2 贯穿项目的三个习惯第一按“普通问答 - PromptTemplate - 结构化输出 - 工具 - RAG”的序列渐进式落地不要第一天就挂满功能。每一步先确认输入输出再继续下一层。第二把模型当作一个笨但博学的实习生来管理提示词就是任务说明工具就是它能操作的按钮每次输出都要验收。模型说“我查了订单”不代表真的查了你要看它有没有触发工具调用模型说“根据文档”不代表真的引用了相关文档你要看 RAG 的检索结果到底有没有被注入。第三记录比记忆可靠。每一条 prompt、每一组参数、每一个输出样本都值得留下快照否则后期你很难判断一个“看起来变好了”的变化到底是 prompt 带来的还是随机性带来的。最后再分享一个我坚持了很久的小习惯每次调参后我会把输入样例、输出、模型名称、temperature、时间戳、prompt 全文加上一句“这次改了什么”粘贴到笔记里。坚持几十条之后你对模型脾气会有一种说不清的直觉之后别人跟你讨论方案你也能直接说出“那次输出飘了是 temperature 改成 0.7 之后才出现的”。这期先记到这里。
延伸阅读

更多相关文章

2026/10/22 3:42:33

从零搭建纯Servlet工程:深入理解Web请求的完整处理链路

如果你翻过当前流行的Web开发脚手架自动生成的工程,你会发现一个很现实的问题:当一个请求从浏览器发出来,到你写的业务代码之间,中间到底发生了什么,很多人其实说不清楚。依赖太多、封装太深,反而让人忘了W…

2026/10/22 3:37:33

端到端自动驾驶算法全解析:从架构原理到工程落地实践

1. 端到端算法到底在解决什么问题1.1 从模块化堆叠到单一网络:一场架构范式的迁移过去做自动驾驶感知决策,主流思路是模块化流水线:感知模块负责检测障碍物、车道线、交通标志,预测模块推断其他交通参与者的未来轨迹,规…

2026/10/22 3:37:33

SSVEP-BCI脑机接口实战:从神经机制到CCA解码与系统搭建

简介:围绕稳态视觉诱发电位(SSVEP)的脑机接口(BCI)项目,提供了一套可运行的完整实现。内容涵盖EEG信号处理、CCA(典型相关分析)、基于幅度谱/复杂谱特征的CNN分类算法,适…

2026/10/22 4:42:42

YOLOv8自定义数据集训练全流程:数据准备、参数调优与部署实践

做目标检测开发这几年,YOLO系列我从v3一路用到v8,说实话,以前给别人讲训练流程,我都是直接甩一段命令让他自己跑。但后来发现,真正卡住新手的往往不是训练那一下,而是训练之前的数据准备和训练之后的结果分…

2026/10/22 4:42:42

Agent Infra入门:从后端工程师到智能体基础设施搭建

Agent Infra这个说法,最近在圈子里被反复提起,但大多数讨论都停留在"巨头们在抢什么"的层面。今天换个角度,想聊聊它到底是什么、为什么各家都把它当秘密武器,以及最关键的一点——一个普通工程师,怎么拿到这…

2026/10/22 4:42:42

注意力残差机制:解决注意力坍缩与退化问题的工程指南

注意力机制的演进已经过了堆砌结构的阶段,真正有价值的改动都藏在“最朴素的直觉”里。Kimi 团队这篇关于 Attention Residuals 的论文,标题看起来平淡,但把“残差”和“注意力”这两个关键词放在一起,懂行的人马上会精神起来。它…

2026/10/22 4:42:42

模型量化实战:精度与延迟平衡的调优方法论

模型量化这事儿,最折磨人的不是把模型压小,也不是让它跑得更快,而是每次一量化就觉得精度要崩了,但指标没掉多少,心里那块石头又放不下。这几年我做AI模型部署,量化从一开始的“敢不敢上”变成了“必备流程…

2026/10/22 4:37:41

语义分割上下文建模:PSP模块的PyTorch与TensorFlow实现要点

做语义分割的都知道,一张图里真正让你头疼的往往不是局部的边缘,而是“全局关系”。比如一个路灯杆,近看是细长条,模型完全能识别;可一旦路灯和远处的白色墙面连在一起,特征图上几个像素共享同一块感受野&a…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑