1. 这九种模式,为什么值得花时间搞懂
我一直觉得,Agent 开发最尴尬的阶段不是“做不出来”,而是“能做出来但不知道怎么做才稳”。你让大模型去调用工具、读文档、生成报告,它好像都能跑,但换个场景就翻车,同样的需求换一种写法效果又不一样。折腾几次之后我才意识到,问题不在模型强弱,而在于我们把它放进了一个什么样的结构里。结构对了,普通模型也能干复杂活;结构不对,再强的模型也像没头苍蝇。
这九种设计模式,本质上就是 Agent 的九种“组织架构”。它们决定了 Agent 怎么思考、怎么行动、怎么纠错、怎么协作。不夸张地讲,你选一种模式,就基本选定了这个 Agent 的天花板和下限。理解它们,比记住某个框架的 API 重要得多。
我先说清楚这文章适合谁。如果你自己用代码搭过 Agent,但觉得效果不稳定、扩展困难,那这九种模式的拆解正好能帮你找到问题所在。如果你是刚接触 Agent 开发,这篇文章涉及的原理和示例也能帮你建立一个全局认知,至少看完之后不会再觉得 Agent 是个玄学东西。我会把每个模式的适用场景、踩坑点和你可能忽视的细节都讲透,代码方面则尽量给可运行的示例。文章会有点长,但这块内容确实值得一个字一个字读完。
2. 先把 Agent 想成一个外包团队,问题就好懂了
在拆解九种模式之前,我得先说一个理解 Agent 的诀窍:把 Agent 当成一个团队,而不是一个聪明的人。你雇一个全能超人,什么都会,但没有人给他派活、没有时间表、没有验收标准,他照样产出垃圾。大模型本质上也是这样,它能说会道,但它不知道什么时候该停下来,不知道哪些外部信息是必要的,也不知道自己算错了要回头检查。如果我们把 Agent 拆成“规划、调用、记忆、反思、协作”这些不同的角色,问题就会清晰很多。
这其实也解释了为什么设计模式在传统软件开发里好用,换到 Agent 开发里照样好用。传统软件架构解决的是“模块怎么搭、数据怎么流”,Agent 设计模式解决的是“思考怎么组织、行动怎么触发、错误怎么恢复”。两者关注的层次不一样,但思维方式非常相通:不靠偶然,靠结构。
我在实际项目里最常用的一句话是,不要试图让模型一次做对所有事,给它一条足够清晰的路径,让它每一步只需做一个小决策。九种模式核心就是换着花样设计这条路径。下面我就从最经典的 ReAct 开始,一个一个拆。
3. ReAct 模式:让思考和行动交替前进
3.1 原理:不再一口气回答,而是一步步“想一步,做一步”
ReAct 的全称是 Reason and Act,意思是模型在回答过程中交替进行“推理”和“行动”。这个模式的核心思路是,模型不直接生成最终答案,而是生成一个思考过程,再根据思考过程决定调用什么工具,拿到工具的结果后再继续思考,直到认为可以给出答案为止。
你可能觉得这没什么稀奇,让模型一步步思考不是常见做法么。但 ReAct 的关键在于,它把“思考”和“行动”明确地交替编织在一起。普通 CoT 只是让模型在回答前把思路写出来,但 ReAct 要求模型在每轮思考之后,要么调用一个工具,要么给出最终答案。这样每一步都有外部反馈作为依据,而不是凭空推理。
一个典型的 ReAct 循环长这样:
Thought: 我需要知道用户所在地的天气,但我的训练数据里没有实时天气信息,所以需要调用天气查询工具。 Action: 调用 get_weather(city="北京") Observation: 北京当前天气为晴,气温 5°C Thought: 好,天气信息已经拿到了,用户问的是是否需要带伞,当前晴天且没有降雨,所以不需要。 Final Answer: 北京今天晴天,气温 5°C,不需要带伞。看到区别了吗?模型不是直接说“不需要带伞”,而是先承认自己缺少实时信息,然后主动调用工具,最后基于工具返回的事实下结论。这个“随时承认不够、随时补信息”的过程,就是 ReAct 的精髓。
3.2 适用场景:什么时候应该用 ReAct
ReAct 模式最适合那些需要多步推理、并且中间步骤需要外部信息或工具支持的任务。比如:
- 用户问“帮我对比两款手机,看看哪一款性价比更高”,需要实时价格和配置信息。
- 数据分析场景中,Agent 需要先查询数据库里的数据,再做聚合计算。
- 客服场景中,Agent 需要先查到订单状态,再回答用户“我的快递到哪了”。
与之相对的,如果任务只需要模型依据自身知识直接回答,那 ReAct 就有点杀鸡用牛刀,反而增加了延迟和调用失败的风险。比如你问“李白是哪个朝代的诗人”,直接让模型回答就好,不需要走一遍思考和调用的循环。
我用 ReAct 时最看重的一点是“每一步都有依据”。它让模型不再是凭记忆瞎编,而是每一步都踩在事实上。对于一个对准确率有要求的场景,这几乎是必须的。
3.3 落地示例:用 LangChain 实现一个最小 ReAct Agent
现在我用代码给你演示一个简化版的 ReAct Agent。这里我不依赖某个重量级库,而是直接展示核心逻辑,帮助你理解它内部发生了什么。
import json # 模拟一个天气查询工具 def get_weather(city): weather_data = { "北京": {"weather": "晴", "temp": 5}, "上海": {"weather": "小雨", "temp": 12}, "广州": {"weather": "多云", "temp": 18}, } info = weather_data.get(city) if info: return json.dumps(info, ensure_ascii=False) return "未找到该城市数据" # 模拟大模型的思考逻辑(实际请替换为 LLM 调用) def llm_with_react(user_query, tools, max_steps=5): current_query = user_query for step in range(max_steps): # 这里替换成真实 LLM 调用,例如 openai.ChatCompletion.create # 我们用一个硬编码的模拟来演示结构 thought = input("[模拟思考] 请决定下一步动作(输入 'call:get_weather:北京' 或 'final:答案'):") if thought.startswith("call:"): _, tool_name, param = thought.split(":") if tool_name == "get_weather": result = get_weather(param) print(f"[观察结果] {result}") # 将 observation 追加到上下文,进入下一轮 current_query += f"\n工具返回:{result}" elif thought.startswith("final:"): print(f"[最终回答] {thought.split(':', 1)[1]}") break else: print("达到最大步数,停止循环。") if __name__ == "__main__": tools = {"get_weather": get_weather} llm_with_react("北京今天需要带伞吗?", tools)注意上面这个简化代码里,我用input()代替了真实模型,目的是让你直观看到 ReAct 是一个“思考 → 行动 → 观察 → 再思考”的循环。真实项目中,模型输出的文本会被解析成Action和Action Input,然后由代码负责执行对应的工具,再把Observation拼接回上下文。这就是所有 Agent 框架底层都在做的事情。
3.4 高频踩坑点:循环不终止
ReAct 最大的坑是模型容易陷入死循环。它可能会反复调用同一个工具,或者想了半天还是决定继续想。解决手段有两个:一个是在系统提示词里明确“如果可以回答了,必须直接输出最终答案”,另一个是硬性设置最大轮数。我一般会同时设置两个防线,把max_steps设为 5 到 8,并在提示词里强调“要尽快收敛”。极限情况下,只要 Action 输出格式不合法,就直接报错终止,不能让 Agent 无限制地跑下去。
ReAct 的另一个坑是上下文膨胀。每执行一步,Observation 都会拼接到对话里,步骤多了以后上下文越来越长,模型反而被大量中间信息干扰。我通常会在每一轮只保留与当前目标最相关的上一步结果,或者在工具返回特别长时先做一次摘要再拼接。
4. Plan-and-Execute 与 Plan-and-Solve:先定计划,再动手干活
4.1 原理:把“全局规划”和“局部执行”拆开
ReAct 模式的问题在于,它走一步看一步,没有一个全局视角。比如你让 Agent 做一个“从零开始写一篇市场调研报告”的任务,如果走 ReAct,模型很容易在第一步就纠结于找数据,结果写了一部分才意识到漏了行业背景分析。为了解决这个问题,Plan-and-Execute 模式出现了一个明显的分工:先让模型生成一个完整的任务计划,再按计划一步一步执行。Plan-and-Solve 则是它的加强版,要求模型在生成计划时不仅写出步骤,还要说明每一步为什么这么做。
这两种模式解决的核心问题是一致的:让 Agent 具备“先规划、后执行”的能力,避免深陷局部细节而失去全局方向。如果说 ReAct 是一个即兴发挥的话唠员工,那 Plan-and-Execute 就是一个先写好周计划再逐项打勾的项目经理。项目经理的产出不一定比话唠员工快,但更可控、更不容易跑偏。
它们的执行流程大概是这样的:
- 规划器(Planner)根据用户请求生成多步计划。
- 执行器(Executor)按计划执行每一步,每一步都是一个子任务,可以由同一个模型完成,也可以调用不同工具或子 Agent。
- 检查点(Optional):每一步执行后,规划器评估结果是否满足预期,然后决定是继续下一步、修正计划还是结束任务。
4.2 对比表格:三种“思考类”模式的取舍
| 模式 | 是否提前规划 | 是否交替行动 | 是否需要外部反馈 | 典型问题 |
|---|---|---|---|---|
| CoT | 否 | 否 | 否 | 一次想太多容易出错 |
| ReAct | 否 | 是 | 是 | 容易陷入局部、反复试探 |
| Plan-and-Execute | 是 | 是(按计划) | 是 | 计划可能在执行中失效 |
从表格可以看出,Plan-and-Execute 比 ReAct 多了一个“计划层”,但这个计划层也带来了新问题:计划一旦制定,执行时如果环境发生变化,计划很可能不现实。所以我通常会在计划之后加一个“定期重规划”机制,比如每执行三步就让规划器检查一次当前进展,并决定是否调整剩余计划。这也是我在生产环境中最推荐的变体。
4.3 落地示例:两步式代码框架
我用一个简化 Python 示例演示 Plan-and-Execute 的基本骨架。这里假设规划器已经通过一次 LLM 调用生成了计划,代码负责按计划调用工具并收集结果。
def generate_plan(task): # 真实项目中对应一次 LLM 调用,要求模型输出 JSON 数组,如: # [{"step": 1, "task": "搜索市场数据", "tool": "search", "param": "..."}] return [ {"step": 1, "task": "收集行业规模", "tool": "search", "param": "2024年国内智能家居市场规模"}, {"step": 2, "task": "收集主要玩家", "tool": "search", "param": "智能家居主要品牌市场份额"}, {"step": 3, "task": "整合报告", "tool": "writer", "param": "基于前两步数据撰写报告"}, ] def execute_step(step): if step["tool"] == "search": # 调用真实搜索引擎 API return f"模拟搜索结果:{step['param']} 的相关信息" elif step["tool"] == "writer": return f"模拟报告内容:基于数据撰写..." return "工具不存在" def run_plan_and_execute(task): plan = generate_plan(task) results = [] for step in plan: print(f"执行步骤 {step['step']}: {step['task']}") result = execute_step(step) results.append(result) # 这里可以增加一个评估器:判断结果是否符合预期,如不符合则重规划 return results从实现角度来看,Plan-and-Execute 本身并不复杂,它甚至比 ReAct 更容易落地,因为每一步都是固定顺序执行的。真正的复杂度在重规划逻辑:什么时候判定“当前计划已经不适用了”?我一般通过两个信号判断:一是某一步执行结果与计划预期明显不符;二是连续几步都出现低质量输出。一旦触发,就调用规划器重新生成“剩余步骤”,而不是整个任务从头开始。
4.4 我的个人体会:简单任务别用重模式
Plan-and-Execute 并不是银弹。对于两三个步骤就能完成的小任务,比如“把这句话翻译成英文再润色一下”,没必要先生成一个计划,直接一步到位更高效。我见过很多团队在小任务上强行套用 Plan-and-Execute,结果就是多了一次模型调用、多了一秒延迟,收益却微乎其微。好钢要用在刀刃上,超过五个步骤、涉及多个外部依赖的任务,才是这种模式的舒适区。
还有一点,计划本身的表达方式很重要。如果计划是一大段文字,模型执行每一步时还要从长文中找自己该做什么,既费 token 又容易出错。我建议让规划器输出结构化的 JSON 数组,每个步骤包含明确的动作、工具和参数,这样执行器只需要按图索骥,不需要二次解析。
5. Reflection 模式:让 Agent 学会自我批评
5.1 原理:输出之后,再跳出来审一遍
Reflection 模式听起来很玄,其实特别朴素:让 Agent 在生成答案后,再调用同一个模型(或另一个模型)对答案进行审查和批评,再根据批评意见优化答案。这个过程就像让你写完一段代码后,再装成另一个程序员去 code review。角色变了,视角就变了,问题也更容易暴露。
标准 Reflection 流程如下:
- Actor(演员)角色生成初始答案。
- Critic(评论家)角色审查答案,找出问题,如逻辑漏洞、事实错误、遗漏要点等。
- Actor 根据批评意见修改答案。
- 可以重复多轮,直到评论家认为答案合格或达到设定轮数。
这个模式最大的价值是给 Agent 增加了一个“跳出当前思维”的机制,非常适合创造力型任务、内容生成任务和代码生成任务。比如写文章时,模型第一遍写出来的东西往往平庸,但经过一轮“批评+重写”之后,质量会有明显提升。GitHub Copilot 之类的工具在生成代码后给出一段 explanation 并让模型自查,本质上也是 Reflection 的简化版。
5.2 什么时候能靠一个模型完成反思
很多人第一反应是,让模型自己审自己?它能发现自己错在哪吗?这个问题确实存在,而且它跟模型能力强相关。能力强的模型往往具有较好的自我审视能力,能识别出自己先前输出中的逻辑问题;能力弱的模型则倾向于自我肯定,审完跟没审一样。我实测过的体会是,模型越强,Reflection 的收益越明显,如果你的模型本身质量一般,反思步骤可能只是白白消耗 token。
要让反思更有效,有几个小技巧:
- 让 Critic 角色使用与 Actor 不同的提示词,甚至指定一个对立的立场,比如“你是一个极其严格的评审专家”。
- 在批评阶段要求 Critic 必须输出具体的修改建议,而不是泛泛地说“内容不够好”。
- 限制反思轮数,否则模型可能越改越偏,尤其对于主观性强的任务,改两三轮之后甚至可能脱离原始需求。
5.3 一个轻量级 Reflection 伪代码
def generate_answer(query): # 第一次生成 return "北京是中国的首都,人口约 2000 万。" def critique_answer(query, answer): # 审查生成结果,返回批评意见 return ["缺少数据来源", "建议补充城市气候信息"] def refine_answer(query, answer, critique): # 根据批评意见重写 return "北京是中国的首都,截至2023年常住人口约2100万,属于温带季风气候,夏季炎热多雨,冬季寒冷干燥。" def reflection_loop(query, max_rounds=2): ans = generate_answer(query) for i in range(max_rounds): critique = critique_answer(query, ans) if not critique: return ans print(f"第 {i+1} 轮批评:{critique}") ans = refine_answer(query, ans, critique) return ans这段代码是 Reflection 最核心的骨架。实际项目里,通常让模型以 JSON 格式输出{"issues": [...], "suggestions": [...]},这样更便于程序判断“没有问题”和“继续修改”的分支。这里要注意,critique 和 refine 可以是同一个模型,但要通过不同的 system prompt 来模拟不同角色,否则模型很容易陷入“自说自话”的状态。
5.4 我踩过的坑:反思方向错了,越改越糟
Reflection 模式有一个很隐蔽的问题,就是批评本身可能也是错的。模型可能推翻一个本来正确的答案,或者把简单的事实改成复杂但错误的表述。我在一个知识问答场景里就遇到过这种情况,第一轮生成的答案本来是对的,模型自我批评后反而加了一段“推测性内容”,结果正确答案变成了错误答案。
解决这个问题的办法有两个方向。一个是在 Critic 提示词中专门强调“不要无中生有,只基于已有事实审查”,另一个是给 Actor 和 Critic 设定不同的信息来源。比如 Actor 可以自由发挥,但 Critic 必须基于给定的资料来验证答案是否准确,而不是凭模型内部知识乱批评。如果任务对准确率要求极高,还可以引入外部校验环节,比如用检索到的资料与模型答案做对比。
6. Multi-Agent 协作模式:让一群角色分工配合
6.1 原理:多 Agent 不是人多力量大,而是专业分工
Multi-Agent 模式不像其他模式那样只调一个模型循环,而是同时运行多个 Agent,让它们分别扮演不同角色、执行不同任务、互相传递信息。这个模式的灵感来自现实团队协作:有人负责调研,有人负责写方案,还有人负责审核把关。Agent 世界里也一样,把复杂任务拆解给多个专业化 Agent,往往比让一个 Agent 从头干到尾更高效。
常见的 Multi-Agent 拓扑有三种:
- 组长-组员(Orchestrator-Workers):一个中央 Agent 负责拆解任务、分发子任务、汇总结果,其他 Worker Agent 各司其职。
- 辩论/竞争模式:两个或多个 Agent 针对同一问题给出不同观点,互相辩驳,最后由裁判 Agent 或投票机制决定最终结论。
- 流水线模式:Agent A 的输出作为 Agent B 的输入,形成一条处理链,比如“分析 Agent → 设计 Agent → 代码 Agent → 测试 Agent”。
6.2 设计要点:什么时候配一个 Router
Multi-Agent 模式的实现并不难,难的是决定“谁干什么活”。我最常用的策略是加一个路由器(Router)层。路由器 Agent 的任务是分析用户输入,决定交给哪个 Worker 处理。比如用户问“今天有什么新闻”,路由器判断应该转发给“新闻聚合 Agent”;用户上传一份合同问“有没有风险”,路由器转发给“法律审查 Agent”。角色分离得越清楚,每个 Agent 的提示词就可以越专注,效果自然越好。
不要把每个 Worker 的能力边界设定得太宽。如果你让一个 Agent 既能写代码,又能查资料,又能做设计,它就会退化成一个普通模型,失去了分而治之的意义。我一般会在提示词里明确每个 Worker 的“职责范围”和“禁止事项”,比如“你是数据分析 Agent,只负责处理与分析数据相关的事务,其他问题一律不回答”。
6.3 举例:一个调研报告生成系统
假设我们要搭建一个生成行业调研报告的系统,可以拆成这样几个 Agent:
| Agent 角色 | 职责 | 输入 | 输出 |
|---|---|---|---|
| 规划 Agent | 拆解任务、生成提纲 | 用户需求 | 调研提纲 |
| 搜索 Agent | 收集资料、检索数据 | 调研提纲中的关键词 | 资料摘要集合 |
| 分析 Agent | 数据分析和趋势判断 | 资料摘要 | 分析结论 |
| 撰写 Agent | 组织语言、生成报告 | 分析结论 | 报告初稿 |
| 审核 Agent | 检查逻辑、事实和格式 | 报告初稿 | 修订意见或终稿 |
实现时,规划 Agent 先输出提纲,搜索 Agent 按提纲去检索,分析 Agent 对搜索到的内容做提炼,撰写 Agent 最终成文。审核 Agent 如果发现问题,可以把报告退回给撰写 Agent 修改。整个流程是一个有反馈的流水线,而不是一次性的接力。
代码结构上,可以使用 Python 的asyncio来让多个并行 Agent 同时工作,比如搜索 Agent 可以并发执行多个关键词的检索。我在实际项目里会把并行度和结果汇总逻辑分离,比如搜索 Agent 并行执行 5 个关键词,每个返回一个结果,再统一交给分析 Agent。这样做带来的效率提升很明显,但代价是必须处理“部分搜索失败”的情况,不能因为一个失败就整个任务崩溃。
6.4 协作的代价:上下文和通信开销
Multi-Agent 模式最需要警惕的问题是上下文爆炸和通信开销。如果 A Agent 把一个超长文档传给 B Agent,B 再传给 C,上下文里充斥着大量冗余信息,token 消耗会非常惊人。我的解决办法是,在 Agent 之间传递信息之前先做摘要和结构化。比如搜索 Agent 不直接传原始网页内容,而是传“标题、来源、关键数据点、结论摘要”这样的结构化 JSON。
另一个问题是协调成本。多 Agent 系统看起来高效,但一旦某个 Agent 输出格式不标准,下游 Agent 就听不懂了。所以我会给每个 Agent 的输出定义严格的 JSON Schema,并在路由层做校验,不合格的重新生成一次。没有校验的 Multi-Agent 系统,运行时间越长,出错概率越高。
7. Tool Calling 模式:连接外部世界的关键通道
7.1 原理:让模型学会“请求别人帮它做事”
大模型本质上是一个统计语言模型,它无法真正执行代码、查询数据库、调用外部 API。Tool Calling(工具调用,也称 Function Calling)模式就是给模型开一个口子,让它在回答问题时可以请求执行某个函数,然后把函数的返回值作为进一步推理的依据。
在 OpenAI 和各大模型厂商的 API 中,Function Calling 的实现方法是:开发者把工具的描述和参数 Schema 传给模型,模型在需要时返回一个 JSON 格式的工具调用请求,而不是直接生成答案。开发者收到这个请求后执行相应工具,把结果返回给模型,模型再继续生成最终的回复。这里面最关键的是“工具的描述”和“参数 Schema”,它们决定了模型能不能正确理解和调用工具。
一个工具定义的 JSON Schema 类似这样:
{ "name": "get_weather", "description": "获取指定城市当前天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称" } }, "required": ["city"] } }工具名称和描述看似简单,实际上直接影响调用准确率。测试下来,工具描述写得越具体,模型选择工具的准确率就越高。比如"获取指定城市当前天气情况"比"天气查询"要更能消除歧义。
7.2 工具数量的边界:不是越多越好
很多团队一上来就接十几个工具,结果发现模型经常选错工具,或者明明有合适的工具却编造参数。这是因为模型在选择工具时,要在所有工具的描述之间做语义匹配,工具越多,干扰就越大。我个人的习惯是,单次对话中暴露给模型的工具尽量控制在 5 到 10 个以内,如果工具太多,就做一层工具分组或先用路由器缩小候选范围。
另一个容易被忽视的细节是,工具调用后的结果往往非常长。比如一个查询订单状态的 API 返回了几百行 JSON,如果把全部内容塞给模型,既浪费 token 又容易让模型抓不到重点。这时候我们需要对工具结果做后处理,只保留关键字段,或者让模型先做摘要再决定下一步。我在项目里通常会写一个简单的后处理器,把工具返回的 JSON 过滤成“用户关心的字段 + 状态字段”,这比直接原样传给模型效果好得多。
7.3 兼容性和降级策略
工具调用可能会失败,原因包括参数格式错误、API 超时、结果为空等。一个成熟的 Agent 系统必须有降级策略。我在代码中会用 try-catch 包裹工具调用,如果调用失败,可以把错误信息作为 Observation 返回给模型,让模型决定是换一种参数重新调用,还是向用户说明暂时无法获得该信息。
更极端的情况是,模型可能输出了不存在的工具名称,或者参数格式完全不符合 Schema。这里我建议加一层“严格校验”,不合法就直接反馈错误,并附带正确调用示例,让模型在下一次尝试时修正。不要指望模型自行纠正,很多时候它会在同样的问题上反复犯错,必须由代码层及时拦截。
8. RAG 模式:给 Agent 装一个外部知识库
8.1 原理:用检索结果增强生成
RAG 的全称是 Retrieval-Augmented Generation,检索增强生成。它是目前让模型“知道更多”的主要手段。大模型训练数据有截止日期,无法覆盖私有文档和最新资料,但 RAG 允许我们在推理时,先从外部知识库检索出与用户问题最相关的文档片段,再把它们作为上下文提供给模型,让模型基于这些片段生成回答。
RAG 和微调经常被放在一起比较。我的体会是,微调适合让模型学习某种风格或格式,而 RAG 适合让模型获取事实性内容。如果你的需求是“回答内容必须来自公司内部文档”,RAG 远比微调可靠,因为它是检索真实片段,而不是依赖模型记忆。微调之后模型可能记错信息,RAG 则能把答案“钉”在给定资料上。
一个标准 RAG 链路包含四步:
- 离线索引:把文档切分成 chunk,用 Embedding 模型转成向量,存入向量数据库。
- 在线检索:把用户问题转成向量,在数据库中做相似度检索,取 top-k 相关片段。
- 上下文组装:把检索到的片段与用户问题组装成 prompt。
- 答案生成:模型基于组装好的 prompt 生成最终回答。
8.2 实际项目中的 RAG 注意事项
RAG 看似简单,落地时坑却不少。第一个坑是切分粒度。切得太长,检索到的内容可能包含大量无关噪音;切得太短,单个片段可能缺少必要语境。我常用的是按语义段落切分,固定 300 到 500 字左右,并且保留 20% 的重叠度,确保跨段边缘的信息不会丢失。
第二个坑是检索质量不等于生成质量,但检索质量直接决定生成质量。如果 top-k 检索结果与问题无关,再怎么强的大模型也答不对。所以一定要花心思在 embedding 模型的选型和检索策略上。对于问答场景,我通常会同时使用“向量检索”和“关键词检索”,然后把两种结果做一个融合排序。这个方案比纯向量检索更稳。
第三个坑是引用溯源。在需要严谨回答的场景,比如医疗、法律、财务,我强烈建议在 prompt 中要求模型必须标注答案来源。具体方法是把每个 chunk 的文档 ID 作为元数据传给模型,并规定格式:“(来源:[文档ID],第N段)”。这样不仅用户可以看到依据,也让 Agent 在资料不完善时敢于说“资料中未找到相关信息”,而不是强行编造。
8.3 RAG 在 Agent 中扮演的角色
在 Agent 语境下,RAG 往往不是单独运行的,而是作为 Tool Calling 中的一个工具来使用。比如一个“法律顾问 Agent”在回答用户问题时,可以先调用“内部法规检索”工具,工具内部执行 RAG 链路,然后把检索结果返回给 Agent,Agent 再据此回答。这种做法把 RAG 的复杂度封装在工具内部,Agent 无需关心向量数据库和 chunk 逻辑,只需要决定“什么时候需要搜索、以什么关键词搜索”即可。
要注意的是,检索工具的参数设计很关键。如果工具只接受一个query参数,模型有时不知道怎么把复杂问题简化成关键词。我一般会增加几个可选参数,比如top_k、date_range、doc_type,让模型有更多控制空间。参数越多,模型的理解负担越重,所以也要拿捏好度。
9. Memory 模式:从“阅后即焚”到长期记忆
9.1 原理:三种记忆层次
人和 Agent 的一个重要区别是记忆。大模型本身没有记忆能力,每次对话都是“阅后即焚”的。但在很多场景下,Agent 需要记住用户偏好、历史记录、业务上下文等信息,才能提供连贯的服务。Memory 模式的本质就是为 Agent 建立一套记忆机制。
我把 Agent 的记忆分为三个层次:
- 会话级记忆(短期):当前对话的上下文,通常直接放在 prompt 中,受 token 限制。
- 长期记忆:跨会话存储的用户偏好、事实信息,使用向量库或键值库存储,在需要时检索注入。
- 内部记忆(工作记忆):Agent 当前任务执行过程中的中间状态,比如已经完成哪些步骤、有哪些结论。
会话级记忆最容易被忽视。很多人直接把多轮对话全部塞进 prompt,造成上下文膨胀。我习惯的做法是,超过一定轮次后,对早期对话做摘要,保留最近几轮完整对话。这个“摘要 + 最近轮次”的组合,既节省 token,又不会让模型完全忘记前面的内容。
9.2 长期记忆落地:存储什么、怎么检索
长期记忆的设计要回答两个问题:存什么、怎么取出来。存的时候,不要一股脑把用户说过的所有话都存起来,而要筛选出真正具有长期价值的事实。比如用户说“我目前负责公司的新媒体运营”,这个信息应该存;用户说“今天天气真热”,这个信息不需要存。我会在对话结束后,专门安排一次“记忆提取”的模型调用,把对话内容转成一条条结构化记忆,例如{"type": "user_info", "content": "用户负责新媒体运营"}。
检索的时候,记忆不能全盘注入,否则和上下文膨胀没有区别。我常用的做法是在用户发起新请求时,先用用户 ID 去记忆库中做相关性检索,只召回最相关的 3 到 5 条记忆。比如用户问“我这周的任务安排怎么样”,系统会优先召回与任务安排相关的记忆,而不是把用户一年前的聊天记录全部翻出来。
长期记忆还可以进一步分成“事实记忆”和“事件记忆”。事实记忆描述用户属性,比如姓名、偏好、身份;事件记忆描述发生过的事情,比如“上周完成了项目立项”。不同类型记忆的检索权重应该不同,回答事实型问题时优先用事实记忆,回答提醒类问题时优先用事件记忆。
9.3 一个轻量级记忆模块示例
import sqlite3 import json class MemoryManager: def __init__(self, db_path="memory.db"): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, memory_type TEXT, content TEXT, embedding BLOB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) self.conn.commit() def add_memory(self, user_id, memory_type, content, embedding=None): self.conn.execute( "INSERT INTO memories (user_id, memory_type, content, embedding) VALUES (?,?,?,?)", (user_id, memory_type, content, embedding) ) self.conn.commit() def retrieve_memories(self, user_id, query_embedding, top_k=3): # 真实项目中此处用向量相似度检索 # 简化实现:直接按 user_id 取出,再交给 LLM 筛选 cursor = self.conn.execute( "SELECT content, memory_type FROM memories WHERE user_id=? ORDER BY created_at DESC LIMIT ?", (user_id, top_k) ) return cursor.fetchall()这个小实现展示了记忆管理的核心要求:存储介质、用户标识、记忆类型、时间戳。真实项目可以把 SQLite 换成更强大的向量数据库,但概念是相通的。别忘了在记忆管理中加入“忘记”机制,比如定期清理过时记忆或让用户主动清除记忆,这是产品合规和体验上不可回避的问题。
10. Human-in-the-loop 模式:关键时刻让人拍板
10.1 原理:不是所有审批都该自动化
Human-in-the-loop(人在回路中)是指在 Agent 工作流的某些节点上,插入人工确认或审批环节。这个模式不是 Agent 无能,而是面对高风险操作时,必须让人来承担最终责任。比如自动给客户发邮件、自动扣款、自动删除数据库记录,这些操作一旦执行就无法撤回,全部交给 Agent 自主决定风险太高。
这个模式与其他模式不是互斥关系,而是一种叠加机制。你可以在 ReAct 流程中的关键工具调用前插入人工确认,也可以在 Multi-Agent 的审核 Agent 上报疑点后转人工处理。关键问题有三个:在哪个环节插入人、以什么方式通知人、等待多久没响应怎么办。
插入环节的选择取决于风险的严重程度。一个金融客服 Agent 中,“查询余额”不需要人工介入,“转账超过一万元”则需要人工确认。从实现上看,可以给工具打上“需要审批”的标签,框架在调用这类工具前暂停流程,推送给管理员审批。
10.2 实现方式:异步审批 + 超时策略
我在实际项目中实现人工审批时,通常不会让 Agent 流程“卡住”等待,而是采用异步方式:Agent 发起一个审批请求,把当前状态保存起来,然后结束这一轮;管理员在后台处理审批,通过后系统自动触发 Agent 继续执行。这种方式避免长时间占用调用线程,也让管理员有充足的时间去处理。
超时策略是必须提前规定的。管理员可能不在线,或者忘记审批。我一般设置三个分支:同意、拒绝、超时未响应。超时后默认拒绝,并且给用户明确提示“该操作因未获人工确认而取消”。这比无限等待更安全。
在实际产品中,人工审批界面要做到三件事:展示操作上下文、展示 Agent 判断依据、让管理员一键同意/拒绝。如果只给管理员一个冷冰冰的按钮,没有上下文信息,管理员很难做出决策。我现在会给管理员展示 Agent 的思考过程摘要和工具返回的关键数据,确保审批者不用去翻原始日志。
10.3 什么场景必须上 Human-in-the-loop
我总结了几类必须保留人工介入的场景:
- 资金操作:转账、支付、退款等。
- 账号操作:修改密码、注销账号、改动权限。
- 对外沟通:自动发送邮件、群发营销消息。
- 数据删除:批量删除用户数据或生产环境数据。
- 法律合同:自动签约、承诺某种法律责任。
在这些场景里,就算 Agent 的准确率已经达到 99%,那 1% 的风险可能也是不可接受的。用人工介入换确定性,在成本上绝对划算。而且从用户信任角度看,有人工确认环节反而让用户更放心。
11. 模式组合与选型:没有银弹,只有权衡
11.1 模式选型决策表
单一模式解决不了所有问题,九种模式之间也不是互斥关系。实际上,成熟项目几乎都是多种模式的组合。比如一个客服系统可能是“Tool Calling + ReAct + RAG + Memory + Human-in-the-loop”的混合体。
我整理了一个选型决策表,你可以根据自己的场景去匹配:
| 需求特征 | 推荐核心模式 | 可选叠加模式 |
|---|---|---|
| 需要精确回答私域知识问题 | RAG | Tool Calling |
| 需要多步推理,逐步验证 | ReAct | Reflection |
| 需要长任务、多阶段交付 | Plan-and-Execute | Human-in-the-loop |
| 需要多个角色分工协作 | Multi-Agent | Memory |
| 需要工具调用访问外部系统 | Tool Calling | Human-in-the-loop |
| 需要个性化长期服务 | Memory | RAG |
| 需要降低错误风险、提高答案质量 | Reflection | Human-in-the-loop |
这个表是经验性的,不是标准答案。我的建议是,从最核心的一个模式入手,跑通之后再叠加其他模式。不要第一天就所有模式一起上,否则出了问题你根本定位不到是哪一环的锅。
11.2 我从项目中学到的组合顺序
如果让我给一个从零开始的 Agent 项目选路,我会按这样的顺序演进:
- 先做 Tool Calling,保证 Agent 能访问外部工具。
- 加 ReAct 循环,让它能够在工具调用和推理之间来回切换。
- 如果任务步骤多、容易跑偏,引入 Plan-and-Execute。
- 如果希望答案质量更高,加 Reflection。
- 如果涉及内部专属知识,把 RAG 封装成一个检索工具注入。
- 如果用户需要跨会话记忆,加 Memory 模块。
- 如果存在高风险操作,在关键节点插入 Human-in-the-loop。
- 多角色需求出现时,再拆 Multi-Agent。
这个顺序有一个核心逻辑:先确保“能做”,再追求“做得好”,最后解决“敢做”。每一步都是在前一步的基础上叠加的,不会产生架构上的大返工。
11.3 组合时最常见的问题:职责重叠
组合模式最容易出的问题,是各个模块的职责重叠。比如你既用了 Plan-and-Execute 又用了 ReAct,那“谁来思考下一步”就变得模糊;你既用了 Memory 又用了 RAG,那“资料到底来自记忆还是知识库”也需要区分。我在设计系统时会明确每层模块的边界,并在提示词里约定:规划器只负责写计划,执行器只负责调用工具,反思器只负责审内容,互不越权。一旦越权,输出质量必然下降。
另一个经验是,每一层都应该有独立的可观测性。我要能在日志中单独看到某个工具被调用了几次、某次反思改了什么、哪条记忆被召回。一个没有可观测性的组合型 Agent,出问题等于大海捞针。至少要把每个关键节点的输入输出记录下来,方便回溯。
12. 常见问题与排查技巧实录
12.1 Agent 陷入循环怎么办
这是最常见的问题。现象是 Agent 反复调用同一个工具,或者不断“思考”却不给出最终答案。排查手段有三个:
- 检查工具调用是否有副作用。比如重复创建订单、重复扣费等,这类工具必须加幂等控制。
- 在 prompt 中加入终止条件,例如“如果你已经获得所需信息,不要再调用工具,直接回答”。
- 在代码层强制限制最大轮次,比如 5 到 8 轮,超限直接终止并返回当前内容。
我在项目中通常三个手段一起用。代码层限制是保底,prompt 约束是常规手段,工具幂等是安全兜底,三者缺一不可。
12.2 工具返回内容过长
工具返回一个大 JSON,直接把模型给“冲晕”了,生成的答案就会偏离主题。我的处理方式是在工具层就做截断和摘要。比如对于查询 API,只返回与当前问题相关的字段;对于搜索 API,只返回标题、摘要和 URL。如果确实需要完整长文本,就先把文本切块,只让 Agent 读取当前步骤需要的那一部分。
12.3 反思把正确答案改错了
这个问题我在前面提到过,解决方案是:只让反思器基于事实纠错,禁止凭模型记忆“丰富”答案。具体操作时,我会在反思器的 prompt 里加一句“只能依据给定资料和已经获得的事实进行修改,不要补充资料中不存在的信息”。如果项目条件允许,还可以引入一个独立的验证器,比如用规则引擎检查格式,用外部 API 核对数据。
12.4 多 Agent 之间格式不一致
每个 Agent 都应该有独立的输出格式约束。我通常采用 JSON Schema 输出,并在各 Agent 之间加一个“协议层”。协议层负责将上一个 Agent 的输出转为下一个 Agent 可用的标准格式。这有点像传统开发中的 API 网关,能做校验、转换和错误处理。有了协议层,即使某个 Agent 的输出格式变了,也只需要修改协议转换逻辑,不必重写所有下游 Agent。
12.5 检索不到相关文档
RAG 场景下检索不到相关内容,先别急着换向量数据库。多数情况是切分粒度不对,或者 embedding 模型不适合这个领域。我建议先做一次简单的调试:把几个典型问题拿出来,直接对文档片段做关键词匹配,看看片段内容是否包含答案。如果不包含,说明切分有问题;如果包含但检索不到,再考虑换 embedding 或调检索策略。这个排查顺序能帮你快速定位瓶颈,而不是盲目换方案。
13. 最后再分享一点我的个人心得
九种模式拆开讲完,我想说说这些模式背后共通的东西。它们本质上都是在做同一件事:把不可控的大模型行为,装进一个可控的框架里。RAG 用事实约束幻觉,ReAct 用行动约束推理,Reflection 用批评约束自满,Human-in-the-loop 用人工约束风险。模式不是银弹,但它能把模型的不可靠压缩到一个可接受的范围。
我在实际项目中最大的体会是,不要追求一次做到完美,而是先跑通一个最小闭环,再加一个轮子,再改一处细节。九种模式完全可以一步一步引入,每次引入前想清楚一个问题:它帮我解决了哪个具体的痛点?如果回答不出来,就先不要加。
模式越多,系统越复杂,调试成本和 token 成本都会上升。好的 Agent 设计,应该是“用最少的模式,解决最多的问题”。希望这篇解析能让你在设计自己的 Agent 时少走一些弯路。如果你在实际项目中碰到某个模式失效的情况,不妨回头看看是不是“组合方式”出了问题——很多时候不是模式错了,而是模式之间的衔接错了。