ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent演进全解析:从ReAct到多Agent协作的工程实践与避坑指南

AI Agent演进全解析:从ReAct到多Agent协作的工程实践与避坑指南

1. 从"能聊天"到"能干活":AI Agent到底在演进什么

很多人第一次接触AI Agent这个概念,脑子里浮现的画面大概是"一个更聪明的聊天机器人"。这个理解不算错,但远远不够。聊天机器人解决的是"你问我答",而Agent要解决的是"你给个目标,我自己想办法把它办成"。这两者之间的差距,不是模型参数从70亿涨到700亿那么简单,而是整个系统架构从"单次推理"变成了"感知—规划—行动—记忆—反思"的闭环。

我最早接触Agent是在做一些自动化流程的时候,当时的需求很朴素:让模型帮我读一批文档,提取关键信息,然后按规则填进表格。用纯Prompt的方式试了几轮,发现一个致命问题——只要任务步骤超过三步,模型就开始"忘事",前面读过的内容到后面就丢了,或者干脆自己编一个看起来合理但完全错误的结论。这就是没有Memory、没有Tool调用、没有规划能力的"裸模型"在复杂任务面前的典型表现。

后来我开始系统性地研究Agent的演进路径,从最早的ReAct范式,到后来的Plan-and-Execute,再到带长期记忆的Memory-Augmented Agent,以及现在越来越多团队在做的多Agent协作。每一步演进都不是为了炫技,而是为了解决上一代方案在实际落地中暴露出来的具体问题。这篇文章我会把这条演进链路拆开讲清楚,包括每个阶段的核心机制、它解决了什么问题、又引入了什么新问题,以及在实际搭建Agent时那些文档里不会写的坑。

如果你正在做AI Agent相关的开发,或者正在评估要不要把某个业务流程交给Agent来处理,又或者你只是好奇"这东西到底能不能真的干活",那接下来的内容应该能给你一个比较完整的认知框架。我会尽量少讲空泛的概念,多讲机制、多讲取舍、多讲我踩过的坑。

2. 第一代范式:ReAct循环与"边想边做"的原始逻辑

2.1 ReAct的核心机制:Thought-Action-Observation三段式

ReAct(Reasoning + Acting)是Agent演进史上绕不开的一个节点。它的核心思想非常朴素:让模型在每一步都先"想一下"(Thought),然后决定"做什么"(Action),执行完拿到"结果"(Observation),再基于结果继续想下一步。这个循环一直持续到模型认为任务完成为止。

用伪代码表示大概是这样:

while not done: thought = llm.think(context, history) action = llm.decide_action(thought, available_tools) observation = execute(action) history.append((thought, action, observation)) done = llm.check_completion(history)

这个结构看起来简单,但它解决了一个非常关键的问题:模型不再需要一次性把所有步骤都想清楚。它可以根据每一步的实际反馈来调整下一步的动作。比如你让它"查一下北京今天的天气,如果下雨就提醒我带伞",ReAct模式下它会先调用天气查询工具,拿到"下雨"这个Observation之后,再决定要不要执行"发送提醒"这个Action。而纯Prompt模式下,模型可能会直接编一个天气结果然后给你一个提醒,因为它没有"先查再判断"的能力。

2.2 ReAct在实际落地中的三个硬伤

ReAct虽然开创了Agent的基本范式,但在真实项目里用起来,问题很快就暴露了。

第一个硬伤是循环失控。模型有时候会陷入"想—做—想—做"的死循环,比如反复调用同一个搜索工具,每次拿到相似的结果,但就是不肯给出最终答案。我遇到过最夸张的一次,一个简单的信息提取任务,模型循环了23次才停下来,token消耗直接爆表。后来我的做法是在循环里加一个硬性上限,比如最多15步,超过就强制让它输出当前最优结果,同时记录日志方便排查。

第二个硬伤是工具选择的随机性。当你有十几个工具可选时,模型经常选错。比如你同时提供了"网页搜索"和"本地知识库检索"两个工具,模型有时候明明应该查本地知识库,却非要去搜网页。这个问题的根源在于,ReAct的Thought阶段对工具的"适用场景"理解不够精确。我的经验是,在工具描述里不要只写"这个工具能做什么",还要写"什么时候应该用这个工具,什么时候不应该用",把边界条件写清楚,选择准确率能提升不少。

第三个硬伤是上下文膨胀。每一轮的Thought、Action、Observation都要塞进上下文,几轮下来token就爆了。尤其是Observation里如果包含大段网页内容或者长文档,上下文长度会迅速逼近模型上限。我一般的处理方式是,对Observation做摘要压缩,只保留和当前任务相关的关键信息,而不是把原始结果全塞进去。

2.3 什么时候该用ReAct,什么时候不该用

ReAct适合那种步骤不确定、需要根据中间结果动态调整的任务。比如"帮我调研一下某个行业的竞争格局",你事先不知道要查多少资料、要对比哪些维度,这种就适合ReAct。

但如果任务步骤是固定且可枚举的,比如"读邮件—提取关键信息—填表—发送确认",那用ReAct反而是浪费。这种场景更适合后面要讲的Plan-and-Execute模式,先把计划列好,再逐步执行,效率更高,也更可控。

3. 第二代进化:Plan-and-Execute与"先谋后动"的工程化尝试

3.1 为什么要从"边想边做"转向"先计划再执行"

ReAct的"边想边做"在处理简单任务时很灵活,但一旦任务复杂度上来,问题就很明显了。模型在每一步都要重新思考"我接下来该干嘛",这导致两个后果:一是效率低,每一步都要调用一次模型做决策;二是容易跑偏,因为模型没有全局视角,走着走着就忘了最终目标是什么。

Plan-and-Execute的思路正好反过来:先让模型制定一个完整的执行计划,然后按计划逐步执行,执行过程中可以根据实际情况微调,但大方向不变。这就像你装修房子,ReAct是"走一步看一步,今天先刷墙明天再说",Plan-and-Execute是"先出设计图,然后按图施工,中间遇到问题再调整"。

具体实现上,通常分两个阶段:

# 阶段一:制定计划 plan = planner_llm.generate_plan(task_description, available_tools) # 阶段二:逐步执行 for step in plan.steps: result = executor_llm.execute(step, context) context.update(result) if need_replan(result): plan = planner_llm.replan(plan, context)

3.2 计划粒度的取舍:太粗会翻车,太细会僵化

Plan-and-Execute落地时最难的决策是计划要拆到多细。我试过两种极端:

一种是拆得非常粗,比如"第一步:收集资料;第二步:分析数据;第三步:生成报告"。这种计划的问题是执行阶段模型还是不知道该具体怎么做,相当于把ReAct的问题推迟到了执行阶段。

另一种是拆得非常细,比如"第一步:打开浏览器;第二步:输入关键词;第三步:点击搜索按钮……"。这种计划的问题是太僵化,一旦中间某个步骤的实际结果和预期不符,整个计划就废了,而且重新规划的成本很高。

我后来摸索出来的经验是:计划的粒度应该以"可验证的中间产出"为单位。比如"收集至少5个竞品的定价信息"就是一个好的计划步骤,因为它有明确的完成标准,执行阶段模型可以自己决定用什么工具、怎么收集,但目标是很清晰的。

3.3 重规划(Replan)的触发条件设计

Plan-and-Execute不是制定完计划就一成不变了,执行过程中需要根据实际情况触发重规划。但重规划不能太频繁,否则就退化成ReAct了;也不能太迟钝,否则计划已经明显不可行还在硬执行。

我一般会设置这几个重规划触发条件:

  • 步骤执行失败超过2次:说明当前方法行不通,需要换思路
  • 实际结果与预期偏差超过阈值:比如计划里假设能拿到结构化数据,实际拿到的是一堆非结构化文本
  • 发现新的关键信息:比如执行过程中发现了一个计划里没考虑到的重要因素
  • 完成进度严重滞后:比如计划10步完成,执行了15步还没到一半

这些条件不需要全部满足,命中任意一个就可以触发重规划。重规划的时候,我会把已经完成的部分和当前上下文一起传给规划模型,让它基于最新情况重新制定剩余步骤,而不是从头再来。

4. 第三代跃迁:Memory机制的引入与"长期记忆"的工程实现

4.1 为什么没有Memory的Agent永远只是"金鱼"

ReAct和Plan-and-Execute解决的是"单次任务"的执行问题,但它们都有一个共同的缺陷:任务结束,记忆清零。下次你再让Agent做类似的事情,它完全不记得上次是怎么做的、踩过什么坑、用户有什么偏好。

这就像你雇了一个员工,他每天上班都失忆,你每次都要从头交代一遍所有事情。这种Agent在实际业务里几乎没法用,因为很多场景的价值恰恰在于"越用越懂你"。

Memory机制的引入,就是为了解决这个问题。它让Agent能够跨会话保留信息,包括用户的偏好、历史任务的执行经验、领域知识等。从工程角度看,Memory一般分三层:

  • 短期记忆(Working Memory):当前会话的上下文,就是对话历史,任务结束就清空
  • 长期记忆(Long-term Memory):跨会话保留的信息,通常存在向量数据库或关系型数据库里
  • 情景记忆(Episodic Memory):具体任务的执行记录,包括用了什么工具、得到了什么结果、成功还是失败

4.2 记忆的写入、检索与遗忘:三个最容易翻车的环节

Memory机制听起来简单,但实际做起来,三个环节都容易出问题。

写入环节最大的坑是"什么都往里存"。我见过一些实现,把每一轮对话的原始内容全部塞进向量库,结果检索的时候噪音极大,真正有用的信息被淹没了。我的做法是,只存"经过提炼的、有复用价值的信息",比如用户的明确偏好、任务的成功执行路径、领域的关键事实。原始对话记录可以存,但只作为审计日志,不参与检索。

检索环节最大的坑是"检索策略太单一"。纯向量相似度检索在很多时候不够用,比如用户问"上次那个报表是怎么生成的",向量检索可能找不到,因为"上次"这个时间信息在向量空间里没有体现。我一般会做混合检索:向量相似度 + 时间衰减 + 关键词匹配,三者加权排序。时间衰减的意思是,越近的记忆权重越高,这样能保证Agent优先使用最新的信息。

遗忘环节是最容易被忽略的。Memory不能只增不减,否则检索效率会越来越低,而且过时的信息会干扰判断。我一般会设置TTL(生存时间),比如用户偏好类的记忆保留90天,任务执行记录保留30天,临时性的上下文信息保留7天。同时,对于被频繁检索到的记忆,可以延长TTL,因为这说明它确实有价值。

4.3 用向量库做Memory检索时的参数调优实战

用向量数据库做Memory检索时,有几个参数直接决定了效果好坏。

第一个是分块大小(chunk size)。分块太大,检索精度下降;分块太小,上下文不完整。我的经验是,对于对话记忆,按"语义完整的段落"来分块,一般200-500字比较合适。对于文档知识,按自然段落分块,一般500-1000字。

第二个是相似度阈值。低于阈值的检索结果不应该返回,否则会引入噪音。但这个阈值不能设太高,否则会漏掉有用信息。我一般从0.7开始试,根据实际效果调整。如果发现Agent经常"答非所问",就提高阈值;如果发现Agent"明明知道却想不起来",就降低阈值。

第三个是返回结果数量(top-k)。返回太多会撑爆上下文,返回太少可能漏掉关键信息。我一般设top-k=5,然后根据上下文长度动态调整。如果上下文预算充足,可以放宽到10;如果紧张,就压缩到3。

# 一个典型的Memory检索配置 memory_config = { "chunk_size": 300, "similarity_threshold": 0.72, "top_k": 5, "time_decay_factor": 0.95, # 每过一天,权重乘以0.95 "ttl_days": { "user_preference": 90, "task_record": 30, "temp_context": 7 } }

5. 第四代融合:Tool、Context与Prompt的三位一体调优

5.1 Tool设计:不是功能越多越好,而是边界越清晰越好

很多团队做Agent时,恨不得把公司所有API都封装成Tool塞给模型。结果就是模型面对几十个工具,选择困难,经常选错。我踩过这个坑之后,总结出一个原则:Tool的数量控制在7个以内,每个Tool的职责边界必须清晰。

什么叫边界清晰?举个例子,"搜索工具"和"数据库查询工具"如果都能查信息,模型就很容易混淆。正确的做法是,在Tool的描述里明确写清楚:搜索工具用于"查找公开的、非结构化的信息",数据库查询工具用于"查询内部的、结构化的业务数据"。同时,在参数设计上也要有区分度,比如搜索工具的参数是自然语言查询词,数据库工具的参数是SQL或结构化查询条件。

另外,Tool的返回值格式也很重要。我一般要求Tool返回结构化的JSON,包含status、data、error_message三个字段。这样模型能清楚地知道调用是成功了还是失败了,失败了原因是什么,而不是拿到一堆乱七八糟的文本自己去猜。

5.2 Context管理:上下文不是越多越好,而是越精准越好

Context管理的核心矛盾是:模型需要足够的信息来做决策,但上下文窗口是有限的。我见过两种极端做法,一种是拼命往上下文里塞东西,结果模型被噪音干扰;另一种是过度精简,结果模型信息不足,瞎猜。

我的做法是分层管理Context:

  • 系统层:Agent的角色定义、能力边界、行为准则,这部分固定不变,放在最前面
  • 任务层:当前任务的目标、约束条件、已完成的步骤,这部分随任务进展更新
  • 记忆层:从长期记忆中检索到的相关信息,按相关度排序,只保留top-k
  • 工具层:当前可用的工具列表及描述,如果工具很多,可以按任务动态筛选

每一层都有token预算,加起来不能超过模型上下文窗口的80%,留20%给模型的输出。如果超了,就按优先级裁剪,优先保证系统层和任务层,记忆层和工具层可以压缩。

5.3 Prompt工程在Agent场景下的特殊考量

Agent场景下的Prompt工程和普通对话场景有很大不同。普通对话场景,Prompt主要解决"怎么回答得更好";Agent场景,Prompt还要解决"怎么决策得更准"。

我在Agent的Prompt设计上,有几个固定的套路:

第一,角色定义要具体到行为。不要只写"你是一个助手",而要写"你是一个任务执行Agent,你的职责是分解任务、调用工具、验证结果,你不应该编造信息,如果工具返回失败,你应该尝试替代方案或报告失败"。

第二,决策逻辑要显式化。把"什么时候该调用工具、什么时候该直接回答、什么时候该请求用户澄清"这些决策规则明确写进Prompt,而不是指望模型自己悟。

第三,输出格式要严格约束。Agent的输出通常需要被程序解析,所以格式必须稳定。我一般用JSON Schema来约束输出,并在Prompt里给出正例和反例。

agent_system_prompt = """ 你是一个任务执行Agent。你的工作流程是: 1. 理解用户目标,判断是否需要分解为子任务 2. 对于每个子任务,判断是否需要调用工具 3. 如果需要调用工具,选择合适的工具并构造参数 4. 拿到工具结果后,验证是否满足子任务目标 5. 如果满足,继续下一个子任务;如果不满足,尝试替代方案 6. 所有子任务完成后,汇总结果并输出 约束: - 不要编造工具返回结果中不存在的信息 - 如果连续两次调用同一工具都失败,停止重试并报告失败 - 输出必须是合法的JSON格式,包含status和result字段 """

6. 多Agent协作:当单个Agent扛不住复杂任务时

6.1 什么时候需要多Agent,什么时候一个就够了

多Agent协作是这两年的热门方向,但我必须说一句实话:大部分场景下,单个Agent加上好的Tool和Memory就够了,不需要多Agent。多Agent引入的通信开销、协调复杂度、调试难度,很容易让项目失控。

那什么时候真的需要多Agent?我总结了几种情况:

  • 任务需要不同领域的专业知识:比如一个Agent负责法律合规审查,一个Agent负责财务分析,一个Agent负责技术可行性评估,它们的知识体系和判断标准完全不同,硬塞进一个Agent里会互相干扰。
  • 任务可以并行执行:比如同时调研5个竞品,每个竞品一个Agent,并行跑完再汇总,比串行快得多。
  • 需要对抗性验证:比如一个Agent负责生成方案,另一个Agent负责挑刺,通过对抗来提高质量。

如果不符合这些情况,我建议先用单Agent方案,把Tool和Memory做好,往往比上多Agent更有效。

6.2 多Agent的通信协议与协调机制

多Agent协作的核心问题是:Agent之间怎么通信、怎么协调、怎么避免死锁和无限循环。

通信协议上,我一般用两种模式:

一种是黑板模式(Blackboard),所有Agent共享一个公共状态空间,每个Agent可以读取和写入。这种模式适合松耦合的协作,比如多个Agent共同完善一份文档。

另一种是消息传递模式(Message Passing),Agent之间直接发消息。这种模式适合紧耦合的协作,比如一个Agent需要另一个Agent的输出才能继续。

协调机制上,我一般设一个协调者Agent(Orchestrator),它负责分配任务、监控进度、处理冲突。协调者不直接干活,只做调度。这样能避免多个Agent各自为政、互相等待的死锁问题。

# 一个简化的多Agent协调逻辑 class Orchestrator: def __init__(self, agents): self.agents = agents self.task_queue = [] self.results = {} def dispatch(self, task): subtasks = self.decompose(task) for subtask in subtasks: agent = self.select_agent(subtask) result = agent.execute(subtask) self.results[subtask.id] = result if not self.check_dependencies(subtask): self.handle_conflict(subtask) return self.aggregate(self.results)

6.3 多Agent系统的调试与可观测性建设

多Agent系统最难的不是搭建,而是调试。当系统出问题时,你很难定位是哪个Agent的哪一步出了错。所以可观测性建设必须从第一天就做。

我一般会记录这些信息:

  • 每个Agent的每一次输入和输出
  • Agent之间的每一次消息传递
  • 每个子任务的开始时间、结束时间、状态
  • 关键决策点的决策依据

这些信息汇总到一个Trace系统里,可以按任务ID串联起来,出问题时能快速定位。我用的比较多的是OpenTelemetry这套标准,配合Jaeger或Zipkin做可视化。

另外,我强烈建议在多Agent系统里加一个**"熔断机制"**:如果某个Agent连续失败超过阈值,或者整个系统的token消耗超过预算,就强制停止并报告,而不是让它无限跑下去。我见过太多因为没设熔断,一晚上烧掉几百刀token的案例。

7. 实战避坑:那些文档里不会写的Agent搭建经验

7.1 Prompt被拦截、Token超限、模型加载失败:常见报错的排查思路

做Agent开发,有几类报错几乎每个人都会遇到。

Prompt被拦截(类似"invalid prompt: your prompt was flagged as potentially violating our usage policy"),这个通常是因为Prompt里包含了敏感词或者触发了模型的安全策略。排查方法是把Prompt逐段注释掉,定位到具体是哪一段触发的。常见触发点包括:涉及个人信息的描述、涉及争议性话题的表述、过于激进的指令(比如"你必须不惜一切代价完成")。解决方法是改写措辞,用更中性、更具体的表达。

Token超限(类似"maximum context length is 1048576 tokens"),这个前面讲过,核心是做好上下文管理。但有一个容易被忽略的点:工具返回的结果也要算进上下文。我遇到过好几次,Prompt本身不长,但某个工具返回了一大段HTML,直接把上下文撑爆了。解决方法是在工具层面做截断和摘要,不要让原始的大段内容直接进入上下文。

模型加载失败(类似"failed to initialize the context: failed to allocate buffer"),这个通常是资源问题,比如显存不够、内存不足。排查方法是看资源监控,确认是哪个资源到了瓶颈。如果是显存问题,可以考虑量化模型、减小batch size、或者用CPU推理(虽然慢但能跑)。

7.2 Agent"胡说八道"的根因分析与抑制手段

Agent胡说八道(Hallucination)是最让人头疼的问题。根因通常有三个:

一是工具返回了错误信息,但Agent没有识别出来。比如搜索工具返回了一个过时的结果,Agent直接当成事实用了。抑制手段是在Prompt里明确要求Agent对工具结果做交叉验证,如果多个来源冲突,要报告冲突而不是随便选一个。

二是Agent在信息不足时强行编造。抑制手段是给Agent一个"我不知道"的选项,并且在Prompt里明确说"如果信息不足,应该报告信息不足,而不是猜测"。

三是上下文里的噪音干扰了判断。抑制手段是做好上下文管理,只保留高相关度的信息,把噪音清理掉。

我一般还会加一个**"自检"步骤**:让Agent在输出最终结果前,自己检查一遍"我的结论是否有依据、依据是否来自可靠的工具返回、有没有逻辑跳跃"。这个自检步骤能拦掉不少低级错误。

7.3 并发场景下Agent的状态管理与资源隔离

当Agent要扛并发时,状态管理就成了大问题。我踩过的坑包括:多个请求共享了同一个Memory实例,导致A用户的数据被B用户检索到;多个Agent同时写同一个向量库,导致数据覆盖。

解决方案是按会话隔离状态。每个会话有独立的Working Memory,长期Memory按用户ID隔离,向量库的写入要加锁或者用支持并发写的数据库。另外,工具调用也要做资源隔离,比如数据库连接池要按会话分配,避免一个慢查询拖垮所有请求。

# 按会话隔离的Memory管理 class SessionMemoryManager: def __init__(self): self.sessions = {} def get_memory(self, session_id, user_id): key = f"{user_id}:{session_id}" if key not in self.sessions: self.sessions[key] = { "working": WorkingMemory(), "long_term": LongTermMemory(user_id=user_id), "created_at": time.time() } return self.sessions[key] def cleanup_expired(self, ttl_seconds=3600): now = time.time() expired = [k for k, v in self.sessions.items() if now - v["created_at"] > ttl_seconds] for k in expired: del self.sessions[k]

7.4 从Demo到生产:Agent上线前必须检查的清单

最后分享一个我自己的上线检查清单,每次Agent要上生产前都会过一遍:

  • 边界测试:空输入、超长输入、恶意输入,Agent是否能优雅处理
  • 工具容错:所有工具都模拟失败一次,看Agent是否能正确降级
  • Memory隔离:多用户并发时,Memory是否串了
  • Token预算:单次任务的最坏情况token消耗是否在预算内
  • 熔断机制:是否有硬性停止条件,避免无限循环
  • 日志完备:出问题时能否通过日志复现和定位
  • 回滚方案:新版本出问题时,能否快速切回旧版本

这份清单看起来简单,但每一条背后都是真金白银的教训。我见过太多Demo跑得很漂亮、一上生产就各种问题的Agent项目,核心原因就是这些"脏活"没做。

Agent这个方向变化很快,新的范式、新的工具、新的最佳实践几乎每个月都在更新。但底层的那套逻辑——怎么规划、怎么记忆、怎么调用工具、怎么保证可靠性——是不太会变的。把这几块吃透,不管上层怎么变,你都能快速跟上。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进