实战教程 · 内容整理自沉默王二《把 RAG 升级成 Agent》一文,以派聪明(PaiSmart)知识库和 PaiCLI 的设计思路为例,用自己的话重新组织,并补充了通用代码骨架。

学习目标

学完本教程,你将能够:

  • 说清楚 RAG 与 Agent 的本质区别;

  • 把已有的检索能力包装成可被大模型调用的 Tool;

  • 实现一个带预算控制的 ReAct 决策循环;

  • 设计「短期 + 长期」两层记忆;

  • 对改造结果做 Review 和集成测试,并能在简历里讲清楚这个项目。

适合人群:已有 Java / Spring Boot 基础,做过或看过 RAG 项目的开发者。

前置准备

项目

说明

起点项目

一个能跑通的 RAG 知识库(示例:Spring Boot 3.4 + Elasticsearch 8.10 + DeepSeek API)

编码助手

Claude Code(Opus 级模型)、Codex 或其他能改代码仓库的 AI 编码工具

模型能力

所用大模型必须支持 function calling(tool_calls)

辅助组件

Redis(存反馈与长期记忆),可选 MySQL(统计文档数量)

一、先搞清楚:RAG 与 Agent 差在哪

示例项目的纯 RAG 流程只有四步:

  1. 用户提问;

  2. 混合检索(向量 KNN + BM25 重打分,并做权限过滤);

  3. 把检索结果拼成上下文;

  4. 调大模型生成回答(WebSocket 流式返回)。

这条链路是固定的、一次性的。当用户说「把知识库里关于 Spring AI 的文档整理成摘要」,系统只能检索一次、把片段原样返回,无法判断「找到了几篇?要不要合并?要不要再查一次?」。

一句话区分:RAG 是被动问答;Agent 是自己做决策、自己动手执行,并根据结果决定下一步。

要从 RAG 走到 Agent,需要三层递进的改造:工具调用 → 决策循环 → 记忆管理。

二、第一步:给系统加上工具调用(Tool Calling)

2.1 原理

给大模型一份「工具清单」。每个工具包含:

  • name:唯一名称;

  • description:什么时候该用、能做什么;

  • parameters:参数名、类型、是否必填;

  • 执行逻辑:真正干活的 Java 方法。

模型读完用户问题后,自行判断「直接回答」还是「调用某个工具」。

关键经验:模型是靠 description 来决定调不调用工具的,描述质量直接决定 Agent 的决策准确率。描述含糊,就会选错工具或乱调用。好的描述要写明「什么情况下调用」,例如「仅在用户明确需要查询知识库内容时调用」。

2.2 为知识库设计四个工具

工具

作用

参数

实现要点

search_knowledge

检索相关文档片段

query(必填)、topK(默认 5)

直接复用现有混合检索服务,注意带上当前用户的权限过滤

generate_summary

对某主题生成结构化摘要

topic(必填)、maxDocs(默认 5)

内部先检索、再二次调用大模型总结(这是「工具里再调模型」,要和外层循环区分)

submit_feedback

记录用户满意/不满意

rating(good/bad)、reason(可选)

写入 Redis Hash,后续可用于调整检索权重或 Prompt

knowledge_stats

返回文档数、片段数、最近更新时间

无

查 ES 的 index stats + 数据库的文档计数

2.3 通用代码骨架

下面是一个与具体框架无关的最小实现思路(可按自己项目调整):

// 工具定义:模型看到的是前三项,后面的 handler 是本地执行逻辑
record ToolDef(String name,
               String description,
               Map<String, Object> jsonSchema,
               Function<Map<String, Object>, String> handler) {}

@Component
public class AgentToolRegistry {
    private final Map<String, ToolDef> tools = new LinkedHashMap<>();

    public AgentToolRegistry(HybridSearchService search) {
        register(new ToolDef(
            "search_knowledge",
            "在知识库中检索与问题相关的文档片段。仅当用户明确要查询知识库内容时调用。",
            schema(prop("query", "string", true), prop("topK", "integer", false)),
            args -> format(search.searchWithPermission(
                (String) args.get("query"),
                CurrentUser.id(),
                (int) args.getOrDefault("topK", 5)))
        ));
        // 其余三个工具以同样方式注册……
    }

    public List<ToolDef> definitions() { return List.copyOf(tools.values()); }
    public String execute(String name, Map<String, Object> args) {
        return tools.get(name).handler().apply(args);
    }
    private void register(ToolDef t) { tools.put(t.name(), t); }
}

2.3 让 AI 编码助手帮你落地

给 Claude Code / Codex 的提示词,建议包含四个要素:要建的类、每个工具的描述、参数、执行逻辑,再加上「描述要站在大模型理解的角度写」「generate_summary 内部会二次调用模型,注意与外层循环区分」这类约束。

完成后你应该能验证到:

  • 说「整理一下知识库里 RAG 的内容,最多 3 个片段」→ 触发 generate_summary;

  • 说「刚才的检索结果我很满意」→ 触发 submit_feedback。

三、第二步:加上 ReAct 决策循环

3.1 什么是 ReAct

ReAct = Reasoning(推理)+ Acting(行动)。让 Agent 进入循环:

思考该做什么 → 调用工具 → 观察结果 → 再思考……直到认为任务完成。

关键判断点在于模型返回里有没有 tool_calls:

  • 有 → 执行工具,把结果作为 tool 消息追加进对话历史,继续下一轮;

  • 没有 → 说明模型认为可以作答,输出最终回答并退出循环。

3.2 循环骨架

public String run(String userMessage, List<Message> history, AgentBudget budget) {
    history.add(Message.user(userMessage));

    while (budget.allowNextRound()) {            // 预算控制:轮数 + token
        ChatResponse resp = llm.chat(history, registry.definitions());
        history.add(resp.assistantMessage());     // 含 tool_calls 的 assistant 消息也要入历史

        if (resp.toolCalls().isEmpty()) {
            return resp.text();                   // 没有工具调用 => 最终答案
        }
        for (ToolCall call : resp.toolCalls()) {
            pushToFrontend(call.name(), "executing");
            String result;
            try {
                result = registry.execute(call.name(), call.args());
            } catch (Exception e) {
                result = "工具执行失败:" + e.getMessage();   // 异常也回传给模型,而不是中断
            }
            history.add(Message.tool(call.id(), result));
        }
    }
    return "已达到最大执行轮数,请缩小问题范围后重试。";
}

消息格式要符合模型的多轮规范:system / user / assistant(含 tool_calls)/ tool(含 tool_call_id + content)。

3.3 一个多步任务是怎么跑完的

用户:「搜一下知识库里有没有 RAG 相关文档,有的话整理成摘要。」

  1. 第 1 轮:模型判断需要先查 → 调用 search_knowledge(query="RAG");

  2. 第 2 轮:观察到返回了多篇文档,用户要摘要 → 调用 generate_summary(topic="RAG");

  3. 第 3 轮:摘要已生成,无需更多工具 → 直接输出最终回答。

三轮、零人工干预,完成「搜索 → 判断 → 总结」。

3.4 必须做的四个保险

  1. 预算控制:限制最大循环次数(如 10 轮)和 token 上限,超限强制退出,防止死循环烧钱。

  2. 「不确定就问」:在 System Prompt 中明确要求——拿不准时先问用户,不要猜。这能避免误调用破坏性工具(例如误删文档)。

  3. 异常回传:工具报错不要中断循环,把错误文本作为 tool 消息交给模型,让它自己换思路。

  4. 并行与超时:同一轮模型可能同时请求多个工具,用线程池并行执行(示例中最多并行 4 个,单个超时 90 秒),避免排队拖慢响应。

3.5 保留流式体验

沿用原有 WebSocket 通道,把每次工具调用的状态也推给前端,例如:

{"type": "tool_call", "tool": "search_knowledge", "status": "executing"}

这样用户能看到 Agent 在「思考—调用工具」,体验更透明。

四、第三步:加上记忆管理

只靠「把最近几轮对话拼进 Prompt」有两个问题:上下文窗口有限、历史一长就得截断;以及换一次会话就全部忘光。

4.1 短期记忆:当前任务的上下文

  • 直接复用 ReAct 的 history;

  • 每轮循环前统计 token 总量;

  • 超过阈值时压缩而不是硬删:保留最近 3 轮完整对话,更早的内容让模型总结成摘要;

  • 摘要作为一条 system 消息放到历史开头。

这样既不会忘掉关键信息,也不会撑爆上下文窗口。

4.2 长期记忆:跨会话的用户画像

以 Redis 为例:

  1. 会话结束或用户明确说「记住……」时,提取:问了哪些类型的问题、调用了哪些工具、最终结果、用户要求记住的内容;

  2. 以 ltm:{userId}:{timestamp} 为 key 存储,设置 30 天过期;

  3. 新会话开始时取该用户最近 5 条,注入 System Prompt(「以下是该用户的历史交互摘要……」)。

进阶玩法:用向量库保存历史交互摘要,按语义检索最相关的记忆;比如发现某用户常查「产品手册」,下次可优先在该分类下检索,提高命中率。

五、Review 与测试

5.1 让 AI 做一轮 Code Review

重点盯四处:

  1. ReAct 循环有没有死循环风险;

  2. 工具执行有没有异常处理;

  3. WebSocket 推送有没有线程安全问题;

  4. 记忆持久化有没有数据一致性问题。

5.2 四个集成测试场景

场景

预期

「知识库里有多少文档?」

调用 knowledge_stats 并返回统计

「搜 RAG 文档并整理成摘要」

先 search_knowledge,再 generate_summary

「刚才那个回答不太准确」

调用 submit_feedback 记录负面反馈

连续三个问题,第三个依赖第一个的结果

能从短期记忆中取回第一个问题的上下文

六、常见坑速查

  • 工具描述太笼统 → 模型乱调用;写清「何时调用、何时不要调用」。

  • 没有预算上限 → 死循环、费用失控。

  • 工具异常直接抛出 → 整个会话中断;应转成 tool 消息回传。

  • 只截断历史 → 丢失关键信息;改用「摘要 + 最近几轮」。

  • 漏了权限过滤 → Agent 可能越权检索;工具内部必须带用户身份。

  • 工具里再调模型 → 日志和计费要与外层循环区分开,便于排查。

七、把项目写进简历

可以围绕「架构升级」来讲,突出这几点(数据请用你项目的真实指标,不要照搬):

  1. 混合检索引擎(KNN 向量 + BM25 重打分);

  2. 自研 Tool Calling 注册框架,落地若干个业务工具;

  3. ReAct 决策引擎,支持多步复合任务,带预算与超时控制;

  4. 双层记忆:token 预算压缩的短期记忆 + Redis 持久化的长期记忆;

  5. WebSocket 实时推送 Agent 的思考与工具调用状态。

小结与练习

小结:RAG → Agent 的升级,本质是把「固定流水线」变成「模型驱动的循环」——工具让它能动手,ReAct 让它会决策,记忆让它不健忘,预算与「不确定就问」让它不失控。

练习:

  1. 再设计一个工具(如 delete_document),思考它的 description 与保护措施(二次确认)应怎么写;

  2. 给 AgentBudget 增加「同一工具连续调用 N 次相同参数即中止」的防重复逻辑;

  3. 把长期记忆从 Redis 换成向量检索,对比命中效果。