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 机器人的时候,会踩一堆坑:

  1. 记忆、工具、RAG 分别写在哪一层?写错位置:改完代码不生效、必须频繁重启、多接口互相干扰。
  2. Spring‑AI 实现 RAG 有两种方式:QuestionAnswerAdvisor内置检索,或者把向量检索封装成@Tool工具调用。两者底层逻辑完全不一样,很多人不知道该怎么选。

本文结合可运行代码 + 实测现象,讲清楚三件套挂载规则、两种 RAG 模式差异、记忆‑工具‑RAG 联动逻辑。读完你可以直接搭建具备会话记忆、工具调用、知识库问答的客服机器人。

适合人群:使用 Spring‑AI 1.x 开发 Agent 的 Java 后端。

读完收益:
✅ 完整可运行客服 Demo 代码(两套 RAG 模式共存对照)
✅ 记忆 / 工具 / RAG 组件挂载层级梳理,搞懂哪些改完需要重启
✅ 两套 RAG 接入方案 A/B 对比,明确业务选型标准
✅ 生产开发避坑清单

🎯业务场景:三句对话,暴露 Agent 三大核心能力

模拟电商客服机器人,三段用户对话覆盖三大能力:

  1. 用户:我叫老王,订单号是10086 → 会话记忆,需要记住用户提供的信息
  2. 用户:我的订单到哪了? → 工具调用,结合记忆中保存的订单号调用物流查询工具,用户不再重复传入订单号
  3. 用户:你们的退货政策是什么? → RAG 知识库检索,查询内部售后知识库,不能大模型凭空编造规则

单独跑记忆、工具、向量检索都不难。真正复杂的是组合装配,两个核心问题:

  1. 记忆、工具、RAG 应该挂载在哪一层?Bean 初始化阶段还是每次请求阶段?挂载错误会出现配置修改无效,多会话互相污染。
  2. 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 全局默认

两条关键推论

  1. Bean 级别装配 = 启动固化
    ChatClient是 Spring 单例 Bean,defaultAdvisors、defaultSystem都是容器初始化的时候一次性构建完成。如果你修改 Config 里面的 Bean 配置,必须重启应用才能生效。

适合:几乎不会变动的全局能力,例如会话记忆。

  1. 请求级配置覆盖全局 Bean 配置,实现多接口差异化
    同一个ChatClientBean,可以在不同接口使用.system()覆盖全局提示词,不同接口挂载不同工具集。

适合:接口差异化逻辑,不同接口需要不同工具、不同人设。

小知识点:多次调用.advisors()是叠加效果,不会覆盖。示例代码中,第一句 advisors 传递 cid 会话参数;第二句 advisors 挂载 QAA 检索 Advisor,两者同时生效。

📚核心机制 2:两种 RAG 接入模式对比:义务检索 VS 选择性检索

核心本质区别:谁来决定要不要做向量检索

模式 A QuestionAnswerAdvisor(义务检索)

Advisor 会在向大模型发送请求的before钩子阶段,不管用户问什么,无条件拿用户输入做一次向量检索,把检索出来的文档直接塞进 Prompt 上下文,再提交给大模型。
👉 大模型没有选择权,无论问题是否和知识库相关,都带上检索到的文档片段。

优点:简单稳定,开箱即用,适合固定 FAQ 问答机器人。
缺点:闲聊场景也会执行向量检索,消耗向量库调用、消耗 token;直接拿用户原始 query 检索,没有 query 改写能力。

模式 B:向量检索封装成 @Tool 工具(选择性检索)

向量检索只是一个普通工具函数。是否调用知识库,完全交给大模型自己决策。
只有大模型判断当前问题需要内部业务知识,才会主动调用searchKnowledge工具执行向量检索。

实测得到两个额外红利:

  1. Query 自动改写优化:不是直接拿用户原话检索,大模型会把用户简短问题扩展、重写成更适合向量库搜索的 query,检索召回效果更好。

用户输入:退货政策是什么,模型生成检索 query 会自动扩展为退货政策是什么?包含退货条件、流程、时限。

  1. 大模型可以评估检索结果有效性:当知识库没有匹配内容的时候,模型读取工具返回结果,自己识别 “检索内容不相关”,诚实告诉用户找不到资料,不会强行基于无关文档编造答案。

两种模式完整对比

表格

对比维度模式 A QuestionAnswerAdvisor模式 B Tool 工具化检索
检索触发时机每一次请求强制检索大模型自主判断按需触发
Token 消耗恒定,闲聊也带入知识库片段按需消耗,闲聊完全不走向量检索
闲聊体验无关文档混入上下文,存在轻微干扰干净,不加载知识库内容
风险点检索质量差直接污染 Prompt 上下文大模型可能忘记主动调用知识库工具
检索使用的 query用户原始输入大模型改写、扩展后的 query
适用定位简单固定 FAQ 问答机Agent 智能体,复杂业务机器人

✅选型建议:

  • 如果你的业务是简单知识库问答,问题域固定,追求稳定,选模式 A QAA
  • 如果要做 Agent 客服,有记忆 + 多工具调用组合,优先选模式 B 工具检索,拿到 query 改写、结果评估的能力。

📚核心机制 3:记忆 × 工具 如何联动完成业务闭环

看模式 B 接口完整执行链路,cid=u1 会话:

  1. 用户输入:我叫老王,订单号是10086。消息存入会话记忆。
  2. 用户接着提问:我的订单到哪了?,本次输入没有携带订单号。
  3. 大模型读取会话历史记忆,从历史对话提取出订单号10086,把这个记忆中拿到的值作为参数传入工具函数。
  4. 调用getOrderStatus("10086")获取物流结果,整理之后回复用户。

✨关键点:工具的入参不仅仅来自当前用户消息,会话记忆里面的历史消息,同样可以抽取出来当做工具调用参数,这才是 Agent 客服的核心价值。

双层提示引导技巧

// 1. @Tool注解内部description:告诉模型这个工具是干什么,什么场景调用(第一优先级)
// 2. prompt.system()全局指令:整体策略引导,第二层保险
  • Tool 的 description 是大模型认知工具的第一来源,描述一定要写清楚触发条件;
  • System 提示词补充整体业务策略,两层配合,降低大模型忘记调用工具的概率。

✅开发四原则(生产写代码可以直接遵守)

  1. 按照「会不会变化」选择挂载层级
    几乎不变的全局能力放在 Bean 初始化(记忆);每个接口存在差异的工具、人设、检索策略,放到请求级别。防止出现改代码不生效,频繁重启。
  2. Agent 类业务优先选择「检索工具化模式 B」;简单固定 FAQ 使用 QAA 模式 A。
  3. 工具触发条件写两份:@Tool#description写每个工具单独的触发条件,System 提示词写整体业务策略,双层引导。
  4. 做 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 难度不高。真正的难点是装配架构决策:

  1. 分清 Bean 初始化挂载、请求级别挂载,搞懂哪些修改需要重启;
  2. RAG 两种模式的本质差异:检索是管道义务,还是交给 Agent 的可执行动作。

把向量检索从前置管道,变成大模型可以主动执行的工具动作,会额外拿到 query 改写、检索结果评估两大收益,这也是 Agent 对比传统静态 RAG 的优势。

下一篇预告:LangChain4j 对照实战,对比两套框架记忆、工具调用底层设计,看看 Agent 组件设计的行业共性。
————————————————
本文是「Java 后端 AI 转型实录」系列第 8篇。项目已开源在 Gitcode,实时更新。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐