从 RAG 到 Agent:给知识库加上工具调用、ReAct 与记忆
实战教程 · 内容整理自沉默王二《把 RAG 升级成 Agent》一文,以派聪明(PaiSmart)知识库和 PaiCLI 的设计思路为例,用自己的话重新组织,并补充了通用代码骨架。
学习目标
学完本教程,你将能够:
说清楚 RAG 与 Agent 的本质区别;
把已有的检索能力包装成可被大模型调用的 Tool;
实现一个带预算控制的 ReAct 决策循环;
设计「短期 + 长期」两层记忆;
对改造结果做 Review 和集成测试,并能在简历里讲清楚这个项目。
适合人群:已有 Java / Spring Boot 基础,做过或看过 RAG 项目的开发者。
前置准备
一、先搞清楚:RAG 与 Agent 差在哪
示例项目的纯 RAG 流程只有四步:
用户提问;
混合检索(向量 KNN + BM25 重打分,并做权限过滤);
把检索结果拼成上下文;
调大模型生成回答(WebSocket 流式返回)。
这条链路是固定的、一次性的。当用户说「把知识库里关于 Spring AI 的文档整理成摘要」,系统只能检索一次、把片段原样返回,无法判断「找到了几篇?要不要合并?要不要再查一次?」。
一句话区分:RAG 是被动问答;Agent 是自己做决策、自己动手执行,并根据结果决定下一步。
要从 RAG 走到 Agent,需要三层递进的改造:工具调用 → 决策循环 → 记忆管理。
二、第一步:给系统加上工具调用(Tool Calling)
2.1 原理
给大模型一份「工具清单」。每个工具包含:
name:唯一名称;
description:什么时候该用、能做什么;
parameters:参数名、类型、是否必填;
执行逻辑:真正干活的 Java 方法。
模型读完用户问题后,自行判断「直接回答」还是「调用某个工具」。
关键经验:模型是靠 description 来决定调不调用工具的,描述质量直接决定 Agent 的决策准确率。描述含糊,就会选错工具或乱调用。好的描述要写明「什么情况下调用」,例如「仅在用户明确需要查询知识库内容时调用」。
2.2 为知识库设计四个工具
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 轮:模型判断需要先查 → 调用
search_knowledge(query="RAG");第 2 轮:观察到返回了多篇文档,用户要摘要 → 调用
generate_summary(topic="RAG");第 3 轮:摘要已生成,无需更多工具 → 直接输出最终回答。
三轮、零人工干预,完成「搜索 → 判断 → 总结」。
3.4 必须做的四个保险
预算控制:限制最大循环次数(如 10 轮)和 token 上限,超限强制退出,防止死循环烧钱。
「不确定就问」:在 System Prompt 中明确要求——拿不准时先问用户,不要猜。这能避免误调用破坏性工具(例如误删文档)。
异常回传:工具报错不要中断循环,把错误文本作为 tool 消息交给模型,让它自己换思路。
并行与超时:同一轮模型可能同时请求多个工具,用线程池并行执行(示例中最多并行 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 为例:
会话结束或用户明确说「记住……」时,提取:问了哪些类型的问题、调用了哪些工具、最终结果、用户要求记住的内容;
以
ltm:{userId}:{timestamp}为 key 存储,设置 30 天过期;新会话开始时取该用户最近 5 条,注入 System Prompt(「以下是该用户的历史交互摘要……」)。
进阶玩法:用向量库保存历史交互摘要,按语义检索最相关的记忆;比如发现某用户常查「产品手册」,下次可优先在该分类下检索,提高命中率。
五、Review 与测试
5.1 让 AI 做一轮 Code Review
重点盯四处:
ReAct 循环有没有死循环风险;
工具执行有没有异常处理;
WebSocket 推送有没有线程安全问题;
记忆持久化有没有数据一致性问题。
5.2 四个集成测试场景
六、常见坑速查
工具描述太笼统 → 模型乱调用;写清「何时调用、何时不要调用」。
没有预算上限 → 死循环、费用失控。
工具异常直接抛出 → 整个会话中断;应转成 tool 消息回传。
只截断历史 → 丢失关键信息;改用「摘要 + 最近几轮」。
漏了权限过滤 → Agent 可能越权检索;工具内部必须带用户身份。
工具里再调模型 → 日志和计费要与外层循环区分开,便于排查。
七、把项目写进简历
可以围绕「架构升级」来讲,突出这几点(数据请用你项目的真实指标,不要照搬):
混合检索引擎(KNN 向量 + BM25 重打分);
自研 Tool Calling 注册框架,落地若干个业务工具;
ReAct 决策引擎,支持多步复合任务,带预算与超时控制;
双层记忆:token 预算压缩的短期记忆 + Redis 持久化的长期记忆;
WebSocket 实时推送 Agent 的思考与工具调用状态。
小结与练习
小结:RAG → Agent 的升级,本质是把「固定流水线」变成「模型驱动的循环」——工具让它能动手,ReAct 让它会决策,记忆让它不健忘,预算与「不确定就问」让它不失控。
练习:
再设计一个工具(如
delete_document),思考它的 description 与保护措施(二次确认)应怎么写;给 AgentBudget 增加「同一工具连续调用 N 次相同参数即中止」的防重复逻辑;
把长期记忆从 Redis 换成向量检索,对比命中效果。
从 RAG 到 Agent:给知识库加上工具调用、ReAct 与记忆
https://dhc.ink/archives/wei-ming-ming-wen-zhang-pjKjiLIz
评论