
做 Agent 开发这一年多我踩过最深的坑不是模型不够聪明而是“看起来什么都懂”的模型在真实任务里经常一本正经地跑偏。后来把所有需要多步处理的逻辑全部切到 ReAct 模式也就是让 Agent 严格走“思考→行动→观察”循环效果立刻不一样了。这篇文章先聊聊我理解的 ReAct再拆一下为什么这个看似简单的循环才是 Agent 真正可用的基石。1. 先聊一次翻车经历Agent 为什么需要“循环”1.1 一次真实翻车现场很久以前我做了一个内部问答 Agent核心功能是让同事用自然语言查项目排期、统计数据。最初的设计非常简单用户提问我把问题拼进 Prompt让模型直接输出答案。前几个 demo 例子上效果不错什么“本周还剩多少任务”“哪个模块阻塞最久”都能答上来。但一放真实数据就跑偏了而且偏得非常隐蔽。举个例子同事问“上个月渠道A和渠道B的转化率分别是多少哪个更高”。模型直接甩出一句“A 是 3.2%B 是 2.8%所以 A 更高”。听起来挺合理但实际上是错的。因为这两个渠道的数据分属两个不同的数据表需要用不同的关联键去查模型没有真正“查过”数据只是在训练记忆里做了个合理猜测。更麻烦的是它给错了答案之后完全不会发现自己哪里不对因为它根本没有反馈来源。后来我把任务换成要求模型先写 SQL、再执行、再根据执行结果回答。效果有改善但只要 SQL 报错、数据字段对不上、或者需要多步聚合模型又会原地卡住。这时候我才意识到单次“提问→回答”的结构天然有天花板。LLM 的强项是语言理解和知识联想弱项是精确计算和可靠调用而真实业务任务往往是这两者的组合。要让模型可靠地完成任务必须给它一个可以迭代修正的工作回路。1.2 ReAct 不是新魔法而是把两条老路拼在一起在 ReAct 出现之前业界主流思路基本分成两派。一派是纯推理派CoTChain-of-Thought让模型在回答前先输出一段思考过程比如“首先我需要提取用户意图然后拆成子问题……”——它能提高数学题、逻辑题的准确率但有一个硬伤模型再怎么想依赖的依然是自己的记忆不会去查实时信息也没办法在执行中间步骤时纠错。另一派是纯行动派Act-only让模型直接调用工具、把结果拿回来拼接。它能碰到外部世界但缺少规划经常做了两步就忘记整体目标行动分散得像无头苍蝇。ReAct 这个名字是 “Reasoning Acting” 的合体思路说白了就是把两派缝合起来。每一步循环里模型先输出一句 Thought思考说明当前它打算怎么推进再输出一个 Action行动指定调用哪个工具、传什么参数工具返回后外部结果作为 Observation观察重新进入模型上下文模型再基于新的观察继续思考、行动直到能给出最终答案。这个结构看起来简单但它在 LLM 身上打开了一个非常重要的可能性模型不再被要求一次性给出正确结果而是被允许在一个闭环里不断试错。每走一步模型都离真实数据更近一点每一次观察都能修正下一步的思考。它不是让模型更聪明而是让模型有了“可纠错”的机会。2. “思考→行动→观察”每一步都在解决具体问题2.1 思考把大任务拆成小步骤先定计划再动手先看“思考”这一步。很多人会忽略它的价值因为在直觉里“思考”只是模型在内心嘀咕不产生外部结果。但在 ReAct 中Thought 有两个非常实在的作用。第一个作用是任务拆解。复杂任务不可能一步完成。比如“对比三个供应商的报价、交货周期和售后条款输出一个选型建议”如果你让模型直接输出最终建议它大概率是编的。但如果你要求它先思考“这个问题需要查三家供应商的价格、周期、条款然后做两两对比最后加权给结论”它就把一个大任务切成了可执行的几个步骤。这个过程跟人做项目时先写 to-do list 是一样的。第二个作用是记录中间状态。LLM 的上下文本身不保留“网络记忆”它就是一次接收所有文本、再生成新文本的过程。多步任务里模型如果不把“我已经查到A的价格”写进 Thought下一步就很可能会再次去查A造成重复劳动。Thought 相当于一个临时工作笔记让模型在循环中时刻知道“我已经做到哪了、接下来要做什么”。但这里有一个很重要的细节Thought 不是越详细越好。我见过一些模型输出几百字思考结果主要是自言自语真正有用的信息只占一成。工程上我会在 Prompt 里明确要求思考控制在三到五句话以内直接说结论和下一步计划不做冗余推理。原因很简单Thought 写得越长占用的 token 越多留给观察结果的上下文空间就越少而且长思考还特别容易发散。2.2 行动把决定外化成工具调用让系统真正碰世界“行动”这一步是 ReAct 的灵魂。没有行动整个循环就是一个更啰嗦的 CoT模型说了一大堆计划但永远不落地。有了行动模型才真正“伸手够到世界”。行动的核心表现形式是工具调用。在设计工具时我一般把工具抽象成函数每个函数必须有清晰的名称、描述、参数格式。比如一个天气查询工具描述写“根据城市名和日期查询天气预报”参数是{city: string, date: string}。模型看到这样的描述就能在 Action 字段里输出一段结构化指令比如Action: query_weather(city北京, date2025-01-05)然后由外层代码解析并执行。工程上行动这一步最容易出问题的不是“模型不会调用”而是“模型不知道该调用什么”。如果工具描述写得含糊比如只写“查询信息”模型根本不知道怎么选。我常用的做法是给每个工具写三样东西一句话说明工具能做什么、一行参数说明、一个使用示例。这样模型在行动前就能先大致判断该选哪个工具。还有一个隐性好处行动让整个过程变得可追踪。以前模型直接输出答案错了你都不知道怎么排查。有了 Action每步做了什么、调了哪个工具、传了什么参数都是透明的。调试的时候打开日志一看Agent 是在哪一步跑偏的一目了然。这也是我觉得 ReAct 比纯 CoT 在生产环境里更友好的原因之一。2.3 观察拿到环境反馈模型才有机会修正自己第三个环节“观察”往往被最多人低估。ReAct 的闭环之所以有价值核心恰恰在 Observation 这一步——它是模型和真实世界之间的“确认信号”。举个例子模型让工具执行了一个 SQL 查询结果发现查询报错因为表名不存在。如果没有观察环节模型就不知道报错信息会一直使用错误的 SQL 逻辑编下去。有了观察环节工具把报错信息传回模型上下文模型在下一步 Thought 里就可能说“哦这张表名写错了我看看数据库里实际有哪些表改一下查询。”这就是自我修正。观察不仅仅是接收工具返回值还包括对返回值的“判断”。不是说工具返回一个结果模型就要全盘接受而是要基于结果做评估。比如工具返回一个空列表可能是没数据也可能是查询条件错了。模型需要在这个观察里判断结果是合理的吗如果可疑要不要换个方式再查一次这种判断能力虽然依赖模型本身但被 ReAct 的循环结构放大了。不过观察内容的质量直接决定了修正效果。如果工具返回的信息是乱糟糟的大段 JSON模型就很难提取关键信息。我通常会建议工具返回时做一层结构化预处理能简化的简化能去掉的字段去掉尽量给出模型可以直接使用的摘要。比如数据库查询工具返回的不是原始查询结果而是“查询到 12 个订单总金额 3.8 万最大单笔 9000 元”这样的一句话摘要。观察内容越干净模型的下一步行动就越准确。2.4 三个环节互为锁链顺序为什么不能乱可能有同学会问思考、行动、观察顺序真的这么重要吗先行动再思考行不行先观察再思考行不行我自己的理解是这个固定顺序其实是在给模型建立一个“认知节奏”。思考在行动之前是要求模型“先想后做”观察在行动之后、思考之前是保证“从反馈中再想”。如果把顺序打乱比如先行动再思考模型就是在没有计划的情况下瞎碰运气行动的成功率会大幅下降。如果先观察再思考一开始就没有任何观察对象模型只能凭空幻想。更深一层说这种固定顺序其实塑造了模型的行为模式。模型是对文本概率做预测它在 ReAct 模板里学会的不仅是“接下来该输出什么文字”而是“当前时刻我应该处于哪个阶段”。当模型看到历史轨迹里最后一条是 Observation 时它自然倾向于输出 Thought也就是准备继续推理当最后一条是 Thought 时它自然倾向于输出 Action。这种规律一旦建立Loop 就能顺畅滚动。如果顺序混乱模型就会频繁迷失不知道当前该生成什么类型的内容。3. 工程上怎么把一个空循环真正落地3.1 定义循环体从 Prompt 模板到动作解析ReAct 的核心是一个循环体工程实现上其实是一个非常简单的 while 循环模型推理 → 解析动作 → 执行工具 → 拼装观察 → 再喂回模型。注意真正难的从来不是循环本身而是循环里每一轮的消息怎么组织、怎么终止、怎么兜底。我常用的系统 Prompt 模板大致长这样你是一个能调用外部工具的智能助手你必须按下述格式循环工作 Thought: 简述你当前的思考结果与下一步计划。 Action: 你要调用的工具名以及传给该工具的参数。一次只能调用一个工具。 Observation: 工具执行后返回的结果由系统填写你不需要生成这一行。 你必须严格遵守以上格式当已经获取足够信息、可以回答问题时输出 Thought: 我已经获得足够信息。 Final Answer: 最终答案模型侧输出的内容需要开发者在工程端做解析。最简单的做法是用正则去匹配Action:和Final Answer:标记。如果一轮输出中既没有 Action 也没有 Final Answer说明模型格式错乱了需要做重试或者直接截断。很多框架现在都提供了现成的 Agent 执行器比如原生函数调用function calling或者各类 Agent 框架它们本质上也是在跑 ReAct 循环只是把解析动作交给了模型原生输出结构。但不论用哪种方式自己理解这个循环模型排查问题时会特别有帮助。3.2 工具注入让模型清楚“手边有什么能用”工具注册这一层质量直接影响模型的选择正确率。我见过不少半死不活的 Agent 项目不是模型不行而是工具描述写得太随意。一个标准工具注册表我会包含这些字段字段作用示例name工具唯一标识query_salesdescription说明何时使用该工具查询某个产品的每日销售数据支持日期范围过滤parameters参数结构说明{date_from: 2025-01-01, product_id: A100}examples一个典型调用示例query_sales(product_idA100, date_from2025-01-01)工具数量多的时候我还习惯在工具描述里加上“使用场景”和“不要使用场景”。比如一个天气查询工具我会写“不要用来查询历史气候统计那是另一个工具的能力”。这种负面约束能大幅减少模型错误调用工具的几率比单纯累加描述更管用。另外一次请求里塞几十个工具定义也不是好做法。工具太多模型容易被干扰选择困难而且挤占上下文窗口。实操上我一般会长尾工具分组只把当前任务可能涉及的候选工具注入给模型这样选型精度显著提高。3.3 上下文组织历史轨迹怎么拼才不会越走越乱ReAct 循环走到第二步以后上下文里除了最初的用户问题会堆叠大量 Thought、Action、Observation。怎么把这些内容拼接给模型是决定循环能不能继续的关键。最简单的做法是把所有历史记录按顺序拼成一个长文本每换一轮就在末尾追加新的 Thought 和 Action。这个办法在步数少的时候没问题但步数多了以后有两个致命问题。第一是上下文长度暴涨模型处理速度越来越慢成本越来越高。第二是最早期的信息在长上下文里容易被模型遗忘模型可能做着做着忘了最初的任务目标。我在实际项目里的优化方案有两个。一个是滚动窗口只保留最近 N 轮轨迹超出部分压缩成一段极简摘要放进上下文开头比如“我们已经确认了产品A的报价当前在比较物流周期”。另一个是任务目标强化用户最初的问题不要只放在最前面而是在每一轮循环的 Prompt 里都再强调一次。这样即使中间轨迹很长模型也会被反复提醒最终目标是什么。观察结果的截断也属于上下文管理的一部分。工具返回的内容如果特别长我一般在后端就做截断最多保留 1000 到 1500 个字符并且把关键信息提前不重要的字段直接删掉。这一步能立竿见影地降低上下文压力。3.4 终止条件怎么让循环正常结束而不是无限跑下去循环体写起来简单但是“什么时候停”才是工程上的重头戏。一个健康的 ReAct 循环至少要设计好下面几条终止路径。第一正常终止。模型检测到自己已有足够信息输出Final Answer。这是最理想的情况但工程上不能只依赖它因为模型可能产生幻觉明明信息不足却提前“觉得”够了。第二步数上限。这是一个绝对保险。我会在循环外层设置max_steps比如 8 步到 15 步超过就直接停止。步数上限要根据任务复杂度调太短会让模型还没完成就被砍太长又可能浪费大量 token。一般的多步任务10 步以内基本都能完成所以我常设 10。第三工具执行异常。工具抛错、返回异常值这种情况不能直接把错误信息丢给模型继续循环而要先判断是否值得重试重试超过两次就要整轮终止并给出错误报告。第四观察重复。模型连续好几轮都在调用同一个工具、传同一类参数说明它已经陷入死循环。这时哪怕没到步数上限也应该主动打断。我会记录最近三轮的 Action 特征如果发现重复度很高直接终止并告诉用户“当前任务执行进入了循环需要重新描述需求或检查工具”。这套终止机制看起来繁琐但每一层都是在给“模型失控”这件事兜底。Agent 在真实环境里跑出错不可怕怕的是出错之后不停止白白消耗算力和用户耐心。4. 跑起来之后的常见问题与排查技巧实录4.1 模型光想不动Thought 刷屏不输出 Action我在把某个数据查询 Agent 开放给团队用的时候遇到一个很头疼的现象模型输出了一大段 Thought反复分析问题却迟迟不输出 Action。日志里看到它“思考”了很多轮一轮下来全是废话一个工具都没调。这个问题的根源多半在 Prompt 的温度和模板约束两个层面。温度太高时模型生成随机性变强容易自我发散模板约束不够强时模型不觉得必须先输出 Action。我的解决方法是把温度降到 0 或接近 0同时在 Prompt 里加上一句硬性要求“每当你产生一个行动想法时必须马上在 Thought 之后的 Action 字段中指定工具调用不允许连续输出超过一轮不含 Action 的思考。”如果这样还不行我就在解析逻辑里做兜底检测到连续两轮都没有 Action就直接终止并把错误抛回给用户。宁可任务失败并明确告知也不要让模型装模作样地“思考”半天。4.2 观察结果太长直接把上下文撑爆了有一次我让 Agent 比较 20 个渠道的数据报表工具直接把 20 个渠道的完整明细全部返回单轮 Observation 就有四千多字。结果模型的注意力被淹没在大量数字里最后总结出来的结论全是错的。为了根治这个问题我把工具返回内容全面改成摘要模式。查询工具在后端先做一次聚合返回“每个渠道最近 30 天总销售额和转化率”这样的精简结果而不是原始流水。同时在前端 Loop 里我给 Observation 做了强制截断超过限制就自动切掉并提示“后续数据已省略”。关键数据尽量只保留数值和结论不带无关解释。这背后有一个原则观察内容是给模型用来推理的不是给模型用来复读的。观察越精简模型越容易抓住核心信息无关细节反而会干扰判断。4.3 重复循环模型绕圈做无用功浪费 token还有一类典型问题模型反复做同一件事。比如查询一个产品的库存第一次返回的是基础库存它想对比销售趋势又查了一次销售数据然后发现自己还是需要库存又回去查库存。几轮下来信息其实早就拿到了但模型一直没有综合起来。这类问题的排查有两个方向。一个方向是模型的归纳能力不足中间步骤没有及时把已获取的信息整合进 Thought。我会在 Prompt 里强调“在每次行动前先回顾你之前已经获得的观察结果避免重复查询”。另一个方向是工具设计过于细粒度每一步只能拿一小块数据导致模型为了拼出全貌来回查询。这种情况我会增加聚合型工具比如“一次查询某产品全周期统计信息”让模型一步到位拿到关键数据。还有一种更隐蔽的重复是模型查询了正确答案但因为观察结果里没有明确标注“这个结果已经满足需求”它不知道什么时候可以停止。我设计工具返回的时候会在结果后面附带一句“这是该查询的最终结果无更多分页”帮助模型判断信息是否完整。4.4 工具选错描述不清导致调用错误的接口工具选错的问题是所有问题里最让人哭笑不得的。某次模型要查用户余额结果调用了一个删除订单的工具幸好当时该工具还处于 mock 阶段没有造成真实影响但这给我敲了警钟。工具选错的常见原因有两个。第一是工具命名和描述过于相似模型难以区分。比如update_order和modify_order看起来都能改订单但一个是改地址、一个是改商品数量模型很容易选错。我后来统一重新设计了工具名在描述里明确区分场景。第二是工具描述太长模型读取时忽略了关键约束。我给每个工具都写了“不要用于”清单并把最关键的场景约束放在描述最前面模型一眼就能看到。另外在工程上我还会做一个保护性校验。在调用真实工具之前先把模型输出的参数做一次格式校验和取值范围检查如果参数明显不合理比如日期格式错误、ID 不存在先拦截住不让它真的打到业务系统上。这层防护的成本不高但在关键时刻能避免灾难性后果。5. 我在实际项目中沉淀下来的一些落地经验聊了这么多原理和坑最后分享几条我自己在实际项目里反复验证过的经验对想快速把 ReAct 落地的人来说应该直接能用。第一不要追求一次把所有工具都接进来。我第一版 Agent 接了十几个工具结果模型选型一团糟。后来砍到五个核心工具准确率立刻上来了。工具接得越多模型的选择成本越高。先把 80% 的高频任务用三五个工具跑通再逐步扩量。第二所有 Action 调用都要有日志。不只是记“调用了哪个工具”还要记“传了什么参数、返回了什么”。这个日志既用于事后排查也用于后续优化工具的 prompt 描述。我经常在回看日志时才发现某个工具描述有误导性。第三一定要在主流程之外单独跑评测集。ReAct 循环每次执行过程都可能不同存在随机性只靠几个 demo 根本无法评估 Agent 的真实水平。我一般准备一批有标准答案的任务跑完统计成功率同时观察平均步数和 token 消耗。评估集不需要很大三五十条覆盖典型场景就能发现大部分问题。第四如果任务简单到一步就能完成就不要强行套多步循环。ReAct 的价值在多步、需反馈、需修正的场景里最大。单轮问答该走简单 Prompt 就走简单 Prompt强行套循环只会增加延迟和成本。好的架构师知道什么时候不发明轮子。ReAct 不是 Agent 的银弹但它是 Agent 从“demo”走向“可用”的一个基本范式。理解它为什么要“思考→行动→观察”的这个顺序比记住几个框架 API 更重要。一旦想通了这一点你写出来的 Agent 就不再是模型的放大器而是一个真正会做事的系统。