Java 后端 AI 转型实录(八):客服机器人三件套|记忆、工具、RAG 装载归属
Java 后端 AI 转型实录 Day8:客服机器人三件套|记忆、工具、RAG 装载归属 & 两种 RAG 检索模式选型
公众号 / CSDN 技术博客,适配 Java 后端开发者,Spring‑AI 1.1.x,Agent 实战。
版本环境:Spring‑AI 1.1.0、JDK21、Ollama (qwen3.8:27b + bge‑m3)
💡阅读前置:建议已经跑通过 Spring‑AI 基础 Demo,了解 ChatClient、@Tool 工具调用、VectorStore 向量库基础用法。
🎁读完本文你将收获:
- 一套可直接运行的客服 Agent 完整 Demo,支持两种 RAG 模式 A/B 对照
- 搞懂记忆、Advisor、工具的挂载生命周期,解决「修改配置不生效」常见疑难
- 理清两套 RAG 实现的优缺点,掌握业务场景选型标准
- 生产环境开发避坑清单,可直接落地业务开发
📖 本文要解决什么问题
很多同学单独跑通了记忆、工具调用、RAG 知识库检索 demo,但是把三者组装成业务客服 Agent 机器人的时候,会踩一堆坑:
- 记忆、工具、RAG 分别写在哪一层?写错位置:改完代码不生效、必须频繁重启、多接口互相干扰。
- Spring‑AI 实现 RAG 有两种方式:
QuestionAnswerAdvisor内置检索,或者把向量检索封装成@Tool工具调用。两者底层逻辑完全不一样,很多人不知道该怎么选。
本文结合可运行代码 + 实测现象,讲清楚三件套挂载规则、两种 RAG 模式差异、记忆‑工具‑RAG 联动逻辑。读完你可以直接搭建具备会话记忆、工具调用、知识库问答的客服机器人。
适合人群:使用 Spring‑AI 1.x 开发 Agent 的 Java 后端。
读完收益:
✅ 完整可运行客服 Demo 代码(两套 RAG 模式共存对照)
✅ 记忆 / 工具 / RAG 组件挂载层级梳理,搞懂哪些改完需要重启
✅ 两套 RAG 接入方案 A/B 对比,明确业务选型标准
✅ 生产开发避坑清单
🎯业务场景:三句对话,暴露 Agent 三大核心能力
模拟电商客服机器人,三段用户对话覆盖三大能力:
- 用户:
我叫老王,订单号是10086→ 会话记忆,需要记住用户提供的信息 - 用户:
我的订单到哪了?→ 工具调用,结合记忆中保存的订单号调用物流查询工具,用户不再重复传入订单号 - 用户:
你们的退货政策是什么?→ RAG 知识库检索,查询内部售后知识库,不能大模型凭空编造规则
单独跑记忆、工具、向量检索都不难。真正复杂的是组合装配,两个核心问题:
- 记忆、工具、RAG 应该挂载在哪一层?Bean 初始化阶段还是每次请求阶段?挂载错误会出现配置修改无效,多会话互相污染。
- RAG 知识库,是用 Advisor 做前置强制检索?还是封装成 Tool 交给大模型自主判断要不要检索?两种实现分别适合什么业务?
🔨完整可运行 Demo 代码
两套 RAG 模式写在同一个项目,提供两个接口做对照实验。
1、ChatClient 配置类:会话记忆装配
⚠️重点:
MessageChatMemoryAdvisor在这里属于Bean 级别装配,应用启动就实例化完成,修改这里代码必须重启服务。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.ChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
/**
* 会话记忆Bean,窗口记忆,最多保留30条消息
*/
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(30)
.build();
}
/**
* 全局ChatClient单例
* defaultAdvisors挂载记忆:所有使用这个ChatClient的请求默认带上会话记忆能力
* defaultSystem是全局默认System提示词,请求级别可以覆盖
*/
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultSystem("你是一位严谨的技术助手,回答保持简洁。")
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
2、模式 B 核心:把 RAG 向量检索封装成 Tool 工具
模式 B:知识库检索变成大模型可以调用的函数,是否检索知识库由大模型自己判断。
当大模型发现问题属于内部业务知识,自己调用这个工具执行向量检索。
import org.springframework.ai.document.Document;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
@Component
public class KnowledgeTools {
private final VectorStore vectorStore;
public KnowledgeTools(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* @Tool 定义知识库查询工具
* description非常关键!这是大模型判断要不要调用本工具的依据
*/
@Tool(name = "searchKnowledge",
description = "查询公司内部知识库(客服话术、售后政策、业务规则),当用户询问公司政策、售后规则,模型自身不确定答案的时候调用该工具")
public String searchKnowledge(
@ToolParam(description = "用户原始问题,尽量完整描述查询需求") String question
) {
// 和普通手写RAG检索逻辑一模一样,只是包装成工具方法
return vectorStore.similaritySearch(
SearchRequest.builder().query(question).topK(3).build())
.stream()
.map(Document::getText)
.collect(Collectors.joining("\n---\n"));
}
}
3、Controller,两套接口 A/B 对照
/api/bot:模式 B,检索 = Tool 工具,模型自主选择是否查询知识库
/api/bot‑a:模式 A,QuestionAnswerAdvisor,每一次请求强制做向量检索
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class BotController {
private final ChatClient chatClient;
// 业务工具:订单查询、库存查询等(业务工具,自行实现)
private final ShopTools shopTools;
// RAG检索工具(模式B使用)
private final KnowledgeTools knowledgeTools;
private final VectorStore vectorStore;
public BotController(ChatClient chatClient,
ShopTools shopTools,
KnowledgeTools knowledgeTools,
VectorStore vectorStore) {
this.chatClient = chatClient;
this.shopTools = shopTools;
this.knowledgeTools = knowledgeTools;
this.vectorStore = vectorStore;
}
/**
* 模式B:检索封装为@Tool工具,大模型自主决定要不要查知识库
*/
@GetMapping("/bot")
public String bot(@RequestParam String q,
@RequestParam(defaultValue = "u1") String cid) {
return chatClient.prompt()
// 请求级System提示词,会覆盖Bean的defaultSystem,实现接口自定义人设
.system("你是电商客服小助手。优先调用工具获取实时业务信息;涉及售后政策优先查询知识库,回答简洁,禁止编造不存在信息。")
.user(q)
// 指定会话ID,隔离不同用户对话,生产环境绝对不能省略cid
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, cid))
// 请求级别注册工具:业务工具 + 知识库检索工具
.tools(shopTools, knowledgeTools)
.call()
.content();
}
/**
* 模式A:QuestionAnswerAdvisor,每次请求无条件执行向量检索
* ⚠️注意:本接口不再传入knowledgeTools,单一变量对照,避免双重检索干扰结果
*/
@GetMapping("/bot-a")
public String botModeA(@RequestParam String q,
@RequestParam(defaultValue = "u2") String cid) {
return chatClient.prompt()
.system("你是电商客服小助手。回答简洁,禁止编造。")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, cid))
// 挂载QAA Advisor:每次请求前强制向量检索
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(3).build())
.build())
// 只挂载业务工具,知识库检索交给Advisor完成
.tools(shopTools)
.user(q)
.call()
.content();
}
}
4、curl 测试命令
cid = 会话 ID,不同 cid 代表不同用户会话,用来验证会话隔离。
# 模式B 测试会话记忆、工具、知识库
curl -sG "http://localhost:8080/api/bot" --data-urlencode "q=我叫老王,订单号是10086" --data-urlencode "cid=u1"
curl -sG "http://localhost:8080/api/bot" --data-urlencode "q=我的订单到哪了" --data-urlencode "cid=u1"
curl -sG "http://localhost:8080/api/bot" --data-urlencode "q=你们的退货政策是什么" --data-urlencode "cid=u1"
# 模式A,闲聊场景,观察即使是闲聊依然强制执行向量检索
curl -sG "http://localhost:8080/api/bot-a" --data-urlencode "q=讲个笑话" --data-urlencode "cid=u2"
💡重要提示:
QuestionAnswerAdvisor不在 spring‑ai‑chat‑core 核心包!需要单独引入依赖spring‑ai‑advisors‑vector‑store,否则直接报类找不到。
📚核心机制 1:三件套到底挂载在哪一层?为什么改配置要重启?
很多新手踩坑:修改记忆相关配置,重启才生效,不重启完全没变化。根源就是挂载生命周期。
表格
| 能力 | 挂载层级 | 挂载方式 | 生命周期说明 |
|---|---|---|---|
| 会话记忆 | Bean 构建期 | defaultAdvisors(MessageChatMemoryAdvisor) | 应用启动实例化为单例,修改配置必须重启服务 |
| 业务工具 | 请求级 | prompt().tools() | 每一次接口请求重新注册工具,修改代码无需重启(devtools) |
| RAG 模式 A (QAA) | 请求级 | prompt().advisors(QuestionAnswerAdvisor) | 每次请求动态挂载 Advisor |
| RAG 模式 B (Tool 检索) | 请求级 | prompt().tools(xxx) | 交给大模型自主决定调用时机 |
| System 人设提示词 | 两层支持 | defaultSystem (Bean) /prompt.system (请求) | 请求级会覆盖 Bean 全局默认 |
两条关键推论
- Bean 级别装配 = 启动固化
ChatClient是 Spring 单例 Bean,defaultAdvisors、defaultSystem都是容器初始化的时候一次性构建完成。如果你修改 Config 里面的 Bean 配置,必须重启应用才能生效。
适合:几乎不会变动的全局能力,例如会话记忆。
- 请求级配置覆盖全局 Bean 配置,实现多接口差异化
同一个ChatClientBean,可以在不同接口使用.system()覆盖全局提示词,不同接口挂载不同工具集。
适合:接口差异化逻辑,不同接口需要不同工具、不同人设。
小知识点:多次调用
.advisors()是叠加效果,不会覆盖。示例代码中,第一句 advisors 传递 cid 会话参数;第二句 advisors 挂载 QAA 检索 Advisor,两者同时生效。
📚核心机制 2:两种 RAG 接入模式对比:义务检索 VS 选择性检索
核心本质区别:谁来决定要不要做向量检索
模式 A QuestionAnswerAdvisor(义务检索)
Advisor 会在向大模型发送请求的before钩子阶段,不管用户问什么,无条件拿用户输入做一次向量检索,把检索出来的文档直接塞进 Prompt 上下文,再提交给大模型。
👉 大模型没有选择权,无论问题是否和知识库相关,都带上检索到的文档片段。
优点:简单稳定,开箱即用,适合固定 FAQ 问答机器人。
缺点:闲聊场景也会执行向量检索,消耗向量库调用、消耗 token;直接拿用户原始 query 检索,没有 query 改写能力。
模式 B:向量检索封装成 @Tool 工具(选择性检索)
向量检索只是一个普通工具函数。是否调用知识库,完全交给大模型自己决策。
只有大模型判断当前问题需要内部业务知识,才会主动调用searchKnowledge工具执行向量检索。
实测得到两个额外红利:
- Query 自动改写优化:不是直接拿用户原话检索,大模型会把用户简短问题扩展、重写成更适合向量库搜索的 query,检索召回效果更好。
用户输入:
退货政策是什么,模型生成检索 query 会自动扩展为退货政策是什么?包含退货条件、流程、时限。
- 大模型可以评估检索结果有效性:当知识库没有匹配内容的时候,模型读取工具返回结果,自己识别 “检索内容不相关”,诚实告诉用户找不到资料,不会强行基于无关文档编造答案。
两种模式完整对比
表格
| 对比维度 | 模式 A QuestionAnswerAdvisor | 模式 B Tool 工具化检索 |
|---|---|---|
| 检索触发时机 | 每一次请求强制检索 | 大模型自主判断按需触发 |
| Token 消耗 | 恒定,闲聊也带入知识库片段 | 按需消耗,闲聊完全不走向量检索 |
| 闲聊体验 | 无关文档混入上下文,存在轻微干扰 | 干净,不加载知识库内容 |
| 风险点 | 检索质量差直接污染 Prompt 上下文 | 大模型可能忘记主动调用知识库工具 |
| 检索使用的 query | 用户原始输入 | 大模型改写、扩展后的 query |
| 适用定位 | 简单固定 FAQ 问答机 | Agent 智能体,复杂业务机器人 |
✅选型建议:
- 如果你的业务是简单知识库问答,问题域固定,追求稳定,选模式 A QAA
- 如果要做 Agent 客服,有记忆 + 多工具调用组合,优先选模式 B 工具检索,拿到 query 改写、结果评估的能力。
📚核心机制 3:记忆 × 工具 如何联动完成业务闭环
看模式 B 接口完整执行链路,cid=u1 会话:
- 用户输入:
我叫老王,订单号是10086。消息存入会话记忆。 - 用户接着提问:
我的订单到哪了?,本次输入没有携带订单号。 - 大模型读取会话历史记忆,从历史对话提取出订单号
10086,把这个记忆中拿到的值作为参数传入工具函数。 - 调用
getOrderStatus("10086")获取物流结果,整理之后回复用户。
✨关键点:工具的入参不仅仅来自当前用户消息,会话记忆里面的历史消息,同样可以抽取出来当做工具调用参数,这才是 Agent 客服的核心价值。
双层提示引导技巧
// 1. @Tool注解内部description:告诉模型这个工具是干什么,什么场景调用(第一优先级)
// 2. prompt.system()全局指令:整体策略引导,第二层保险
- Tool 的 description 是大模型认知工具的第一来源,描述一定要写清楚触发条件;
- System 提示词补充整体业务策略,两层配合,降低大模型忘记调用工具的概率。
✅开发四原则(生产写代码可以直接遵守)
- 按照「会不会变化」选择挂载层级
几乎不变的全局能力放在 Bean 初始化(记忆);每个接口存在差异的工具、人设、检索策略,放到请求级别。防止出现改代码不生效,频繁重启。 - Agent 类业务优先选择「检索工具化模式 B」;简单固定 FAQ 使用 QAA 模式 A。
- 工具触发条件写两份:
@Tool#description写每个工具单独的触发条件,System 提示词写整体业务策略,双层引导。 - 做 A/B 对照测试,保持单一变量:
测试 A 模式就不要再注入知识库 Tool,测试 B 模式就不要挂载 QAAAdvisor,否则双重检索互相干扰,测试结果失真。其他参数 topK、模型、会话 ID 全部对齐。
⚠️踩坑速查表
表格
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
QuestionAnswerAdvisor类找不到 | 该类不属于 core 核心包,独立依赖 | 引入spring‑ai‑advisors‑vector‑storeMaven 依赖 |
| 给 QAA 的 SearchRequest 设置 query 参数不生效 | QAA 内部会直接覆盖 query,强制使用用户输入问题 | 不要设置 query,只配置 topK、过滤条件 |
| 同时开启 QAA + 检索 Tool,回答效果混乱 | 发生双重检索,两次向量查询同时生效 | 对照实验保持单一变量;生产根据业务选其中一种模式 |
| 知识库没有相关文档,模型依然乱回答 | 模式 A:检索结果直接塞 prompt,模型被迫阅读无关内容;模式 B 属于模型能力问题 | 模式 B 会自主识别无效检索结果;模式 A 需要增加过滤阈值,过滤低分数文档 |
| 多用户对话记忆互相串,A 用户看到 B 用户对话 | 请求没有传入正确CONVERSATION_ID(cid) | 生产环境 cid 必传,demo 的 defaultValue 只是方便测试,禁止直接上生产 |
| 修改 ChatClientConfig 配置,运行中不生效 | 记忆 Advisor 挂载在 Bean 级别,启动固化 | Bean 层修改必须重启;可变的业务提示词放到请求级.system() |
📝小结
记忆、工具调用、RAG 三个组件,单独写 demo 难度不高。真正的难点是装配架构决策:
- 分清 Bean 初始化挂载、请求级别挂载,搞懂哪些修改需要重启;
- RAG 两种模式的本质差异:检索是管道义务,还是交给 Agent 的可执行动作。
把向量检索从前置管道,变成大模型可以主动执行的工具动作,会额外拿到 query 改写、检索结果评估两大收益,这也是 Agent 对比传统静态 RAG 的优势。
下一篇预告:LangChain4j 对照实战,对比两套框架记忆、工具调用底层设计,看看 Agent 组件设计的行业共性。
————————————————
本文是「Java 后端 AI 转型实录」系列第 8篇。项目已开源在 Gitcode,实时更新。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)