对 ReAct 的复盘:失控、修正与混合架构
我最早接触 ReAct 的时候,对它的理解很简单。模型先想一下,再调一个工具,拿到结果以后继续往下走。这个循环本身不复杂,真正难的地方,看起来也很明确:提示词怎么写,工具描述怎么给,示例要不要补,输出格式怎么约束。
真把系统跑起来以后,我很快就发现自己想浅了。
最早冒出来的问题,往往不是模型不会做,而是它会在错误路径上做很久。它并没有彻底停住,日志也不是一片报错。相反,它看起来很勤奋,动作很多,理由也挺完整,就是任务始终不往前走。有时候它一直在搜;有时候明明已经拿到关键结果,下一轮又像忘了一样重新查;有时候更麻烦,每一步都说得通,最后却发现整条链从第二步开始就偏了。
我后来才慢慢把这件事想明白:ReAct 真正难的地方,不在“能不能调工具”,也不在“模型会不会想”,而在它是一个闭环。推理、执行、反馈、状态管理缠在一起,哪里失稳都会一路传下去。你在日志里看到的是“死循环”,底下可能是工具返回脏数据,可能是上下文管理出了问题,也可能是模型在一个错误前提上越走越远。
这篇文章不是想再讲一遍 ReAct 是什么。我更想把那条从失控到修正的路径摊开:问题最早长什么样,我一开始怎么误判,它们后来是怎么被拆开、被处理,最后又为什么把我推向了 ReAct 和 Plan-and-Execute 的混合设计。
ReAct 最先坏掉的地方,不是不会做,而是会一直做错
我印象很深的一个例子,看起来其实不复杂。有一次我要做一件多来源信息核对的任务,系统手里有搜索工具,也有一个能做简单结构化提取的工具。按理说,这种任务很适合 ReAct:先搜,拿到线索,提取,再决定下一步往哪里补。
结果跑起来完全不是这么回事。
第一轮搜索看着还正常,拿到几段网页摘要。第二轮模型觉得信息不够,换了个关键词继续搜。第三轮又换了表达方式。第四轮开始,query 还在变,但 Observation 已经越来越像垃圾:有的是空结果,有的是一堆清洗得不干净的网页片段,有的是跟任务只沾一点边的内容。系统没有停,它只是一直忙。最后要么跑到最大轮次被硬截断,要么拼出一段看起来还行、实际缺关键信息的回答。
我一开始的第一反应很典型:提示词写得不够好。要么是系统提示里没把任务边界讲清楚,要么是工具描述太粗,模型不知道什么时候该停,什么时候该换路。那段时间我改了不少提示词,也补过示例,确实有一点帮助,但帮助很有限。它会让一些明显的误用变少,却解释不了更核心的现象:为什么模型明明拿到了线索,还会继续在同一类动作上消耗;为什么 query 在变,整体却没有推进;为什么有时候它不是卡住,而是在稳定地做错事。
后来我开始一条条翻日志,把 Thought、Action、Observation 对着看,才发现问题根本没有我以为的那么“提示词化”。很多时候,系统其实是在错误路径上持续地产生看似合理的动作。它不是不会做,而是一直在做局部合理、全局无效的事。
这比单纯答错更麻烦。答错通常是一两步就暴露了,修正思路也比较直接;持续做错则不一样,它会吃掉很多 token,挤满上下文,还会让模型在后面的轮次里越来越难回头。
所谓“死循环”,其实不是一个问题,而是三类不同的失稳
很长一段时间里,我会把这些现象统称为“死循环”。现在回头看,这个词太粗了。它把几种不同性质的问题揉在了一起,看起来方便,实际上会把修法也带偏。
我后来更愿意把 ReAct 里的失控拆成三类:参数重复型、结果失效型、方向漂移型。它们都可能表现为“跑了很多步还没做完”,但成因不一样,检测信号不一样,修起来也不是同一个思路。
参数重复型:模型在同一方向上反复尝试
这一类最直观。Action 类型没怎么变,参数也很像。搜索型 Agent 特别容易出现这种情况:模型连续几轮都在调 search(),只是把 query 换个写法,年份改一下,中文英文来回切,或者在原句上多加两个修饰词。
如果只看单轮,它像是在认真重试;把最近几轮连起来看,就会发现它根本没离开过原来的方向。
这类问题我最早用的是很轻的启发式。参数先做规范化,转小写、去标点、去停用词,再拆成词集合,然后用 Jaccard 相似度比较最近几轮的同类型 Action。这个方案没有什么“高级”可言,优点就是够轻,接到现有执行链里非常快。对短 query 的搜索场景,它带来的收益也往往比一个更重的语义方案来得更直接。
这里有个地方我后来会更谨慎地说:不是 embedding 在短文本上一定没用,而是在我当时那个“短 query 去重”的场景里,Jaccard 这种简单方案已经能挡住最明显的重复,成本又低,所以优先级更高。参数一旦变成长文本或者复杂约束,这个判断就不一定成立了。
再往后做一点,会碰到两个具体问题。一个是阈值怎么定。阈值太高,很多换皮式重试抓不住;阈值太低,正常探索又会被误伤。另一个是比较窗口怎么定。只看相邻两轮不够,因为模型很可能第一轮搜了一个方向,第二轮插了别的尝试,第三轮又绕回来。如果只和上一轮比,很多“回到老路上”的动作会漏掉。
我后来更习惯把当前 Action 跟最近几轮都做一遍比较,但只在同类型 Action 之间比。search() 和 calculate() 这种跨工具动作,本来语义就不在一个平面上,硬比相似度意义不大。这样做也会带来新的误伤:有些任务确实需要回到同一主题上做更深一层的搜索,比如先拿到一个大概数字,后面再补口径、时间范围、GAAP 和非 GAAP 的差异。参数看着很像,任务目的却不一样。
所以参数重复型的检测,我后来一直把它当成“入口信号”,不是终局判断。它能告诉系统:这里看起来像是在原地绕圈了,你该多看一眼。但如果仅凭这一层就直接打断,误杀会很难看。
参数重复型的问题值得先做,不是因为它最严重,而是因为它最容易马上见效。刚开始做 ReAct 系统的时候,你总得先抓住一点什么,这一类正好能给你一个入口。平均步数会降一点,超最大轮次的占比也可能好看一点。
但很快就会碰到下一层问题:query 明明变了,系统为什么还是没有推进。
结果失效型:query 在变,但 Observation 一直没价值
这类问题更麻烦。它不像参数重复型那样有明显的“相似 query”可抓。模型每一步都在变,甚至变得还挺像回事:换关键词、换语言、换实体写法、换查询角度。单看 Action,你甚至会觉得系统还挺灵活。
问题出在结果上。
Observation 里回来的东西要么是空,要么是一段没清洗干净的 HTML,要么跟任务只勉强沾边,要么虽然相关,但没有带来任何新信息。系统表面上在动,实际上一直在原地踩空。
这时候,如果你只盯着 Action 层,很容易产生一种错觉:重复率不是已经降下来了么,为什么整体循环次数还没怎么变。真正的答案是,系统没有停止尝试,它只是在持续拿无效结果。日志很好看,任务却没推进。
也是从这一步开始,我不再把 Observation 只当成“喂给模型的上下文”,而是把它当成一个需要单独治理的对象。工具返回结果之后,不是原样塞回去就完了,至少要先过几道判断:
- 这是不是一个有效结果;
- 它跟当前 Action 的意图是不是相关;
- 它有没有提供新的信息;
- 它值不值得继续占用上下文窗口。
前两层相对还直观:空结果、HTTP 错误、明显脏数据、完全跑题的内容,都能先用规则抓掉一部分。但光有“相关”还不够。很多 Observation 和 query 是相关的,只是它把你已经知道的东西换了一种说法又讲了一遍。这样的结果如果连续出现,从任务推进的角度看,和空结果差别并没有想象中那么大。
这里我慢慢形成了两层处理。第一层是清洗。通用清洗先做一遍,比如剥掉 HTML 标签、压掉连续空白、处理异常编码、把明显无意义的模板段落删掉。再往下,如果某个工具本身返回格式特别不稳定,就单独给它加适配层。搜索类工具很难指望严格 schema,因为上游返回本来就很杂;结构化数据库工具就不一样,可以做更硬的字段校验。两者不能用一套标准。
第二层才是有效性判断。最容易先上的是规则:空结果、错误码、HTML 垃圾占比过高、文本长度太短,这些都好抓。再往后就要碰到语义层了:结果是不是跟当前 Action 对得上,是不是只是把历史信息重复了一遍。这里如果每轮都让大模型多判一次,成本又会上去。我更倾向于先用轻一点的方案做一层粗筛,只有在边界模糊的时候,才让小模型或者更贵一点的判断介入。
我后来越来越在意的一句话是:有返回,不等于有进展。
ReAct 很容易把“模型拿到了一些文字”错当成“系统推进了一步”。真正难的地方恰恰在这里。很多时候,系统不是完全拿不到结果,而是一直拿到那种看起来像结果、其实没有实质推进的结果。
方向漂移型:每一步都不重复,但整条链已经偏了
这类问题最隐蔽,也最让我后怕。
它既不是明显的参数重复,也不是明显的结果失效。每一步 Action 都在变化,Observation 也不是空的,Thought 看上去通常还挺合理。你单看局部,很难马上说这一步哪里错了。可把整条轨迹拉长一看,系统可能早在前几步就建立在一个错误前提上了,后面所有努力都只是沿着那个前提越走越深。
这种情况特别像“高质量地做错事”。
比如用户想要的是某个财务指标,系统早期把概念理解偏了,后面几轮都在认真补另一组数据;或者用户问的是某个时间范围,系统第一步就用了一个过期前提,之后所有比较都基于那个错的时间点展开。单独看每一步,你都能理解它为什么这么走;从任务层面看,它其实早就偏航了。
这也是我后来不太愿意把所有异常都叫成“循环”的原因。方向漂移有时根本不循环,它是在持续前进,只是前进的方向错了。
参数重复型更像原地打转,结果失效型像一直踩空,方向漂移型则像跑偏了还在加速。
这一类问题很难靠单步规则解决。你不能只问“这一步像不像前一步”,也不能只问“这一轮返回结果是不是有效”。你得开始看更高一层的东西:
- 这几步累积起来,任务目标有没有被偷偷换掉;
- 系统是在缩小“还缺什么”的范围,还是只是在堆更多材料;
- 最近几轮的信息有没有真正收敛,还是只是把错误前提包装得更完整;
- 它到底还在做原来的任务,还是已经在一个相似但错误的问题上自洽了。
我开始加阶段性检查、轨迹摘要、目标漂移检测,真正想解决的就是这一层。它们的意义不是多调一次模型,而是逼系统从单步视角里退出来,回头看看自己到底还在不在原来的轨道上。
我当时比较在意的一点,是这种阶段性检查到底该怎么触发。理想状态当然是风险触发:系统自己察觉到“这几步有点不对”再检查一次。但真做起来,风险触发本身也需要依据,而那些依据很多时候还是得靠额外判断。为了先把东西落地,我反而更能接受固定步数的检查,比如每隔几步做一次轻量回看。这个方案没那么优雅,但实现简单,行为也比较稳定。
轨迹摘要也不是把 Thought 全存一遍就完了。真正有用的不是长篇推理原文,而是几个更像状态字段的东西:当前目标是什么、这几步用了什么策略、已经拿到了什么、还缺什么。等系统跑到第五六步的时候,让它回看的不是一长串散乱日志,而是一张简化过的进度表,方向漂移会更容易暴露出来。
这三类失稳经常不是分开来的
把这三类拆开以后,再回看线上日志,会有一种很强的感觉:真实系统里很少只出一种问题。
更常见的是串联。先是工具返回结果质量不稳定,系统拿不到有效信息;拿不到信息以后,模型开始换 query,慢慢又出现参数层面的重复;几轮无效尝试之后,早期那点模糊线索被模型误当成了确定前提,接着整条轨迹开始偏航。
所以最后在日志里看到的“死循环”,通常只是几种执行失稳共同留下的外观。
如果不先把它们拆开,后面的修法就很容易失焦。你今天补一个提示词,明天加一个阈值,后天再上最大轮次,看起来忙了很多,系统还是不稳。原因不是这些东西完全没用,而是它们打到的根本不是同一层问题。
我后来把防失控拆成了三层防线
把问题拆开以后,修法反而没那么神秘。最关键的变化,是我不再指望单一手段解决全部问题,而是开始把防失控机制分层放。
第一层还是提示词和工具描述,但我对它的期待变了。它的作用是减少明显误用,让模型知道工具边界,知道什么场景该查、什么场景该停、什么场景该换路。它很重要,但它不是 runtime 的看门狗。靠提示词去盯住循环、识别无效结果、判断轨迹偏航,最后往往会变成一句软约束:写在那里,压力一大就失灵。
我基本不再把“如果结果不理想请换一种方式尝试”这种话当核心方案。它可以写,写了也不算白写,但它更像在给模型一个方向感,不是兜底逻辑。系统真正需要的是:就算模型没有自觉意识到自己在打转,执行层也得看得见。
真正有效的检测,大多还是落在执行层。
我后来把 executor 和 runtime 看得重了很多。Action dedup、Observation failure detection、阶段性检查、trajectory-level 的状态感知,这些都应该发生在模型真正执行动作的那一层,而不是期待模型自己在提示词里永远自觉。因为模型会犯错,会执拗,会被脏数据误导;执行层的职责,就是在它开始出现异常征兆的时候,给系统一个不依赖主观自觉的硬判断。
所以我更愿意把这层叫成“看门狗”,而不是“推理增强”。它的任务不是替模型想得更深,而是在模型开始无效继续的时候,把系统从边上拽一下。很多逻辑其实都很朴素:连续几轮同类 Action 且参数很像,连续几轮 Observation 都无效,最近几步“还缺什么”几乎没变化。这些判断单独看都不高级,叠起来却很有用。
这里我逐渐形成了一个很朴素的分工:
- 提示词负责告诉模型“你能做什么、边界在哪里”;
- runtime 负责盯“你现在是不是开始做无效的事了”;
- loop 层负责最后止损。
第三层也就是 loop 层,主要就是最大轮次、stop condition、finish fallback 这一套。它当然得有,没有它系统就没有最后的刹车。但我越来越不愿意把 max-step 当成解决方案。它只能止血,不能解释系统为什么会一路跑到那里。真正有价值的是在它之前就识别出:这条链还值不值得继续。
看效果的时候,我也逐渐不再追着一个模糊的“死循环率”跑。这个指标看起来直观,实际很不好定义:连续三轮相似算不算,连续两轮无效结果算不算,还是只有硬撞到最大轮次才算。最后更稳的做法,反而是盯几个更具体的代理指标:超最大轮次占比、平均步数、P95 或更长尾的步数分布。它们各有局限,但至少定义清楚,能让你知道系统到底是在变好,还是只是换了种方式把问题藏起来。
到了这一步,我对“干预”这件事也慢慢不再那么简单粗暴。以前一说打断循环,直觉就是停掉。后来我更愿意把干预分成几档:有时候只是注入一条提示,让模型换策略;有时候给出更明确的方向,比如别再搜摘要了,去找原始数据源;再极端一点,才是 finish 或硬截断。系统不一定每次都要强行把模型拉停,有时只要把它从当前轨道上拨开一点,就已经够了。
干预怎么注入,我后来也有过一点取舍。一开始最容易想到的是改系统提示,但它太全局了,这种提示往往只对当前节点有意义,不值得把整轮上下文都搅一遍。更顺手的做法通常还是把它包装成一条特殊 Observation,让系统看起来像是“刚刚又拿到了一条反馈”。这样能复用原来的循环结构,也更容易控制影响范围。
但问题也正是在这里变得更难了。
真正难的不是打断循环,而是别误杀正常探索
刚开始做防循环的时候,人会很容易有一种成就感:抓到了很多重复,超最大轮次的任务也下来了。再往前做一点,就会撞上另一面墙——正常探索和无效重复之间,边界其实没有想象中清楚。
复杂任务本来就需要多轮搜索。用户要对比两个年份、几个品牌、几组指标,或者需要在不同来源之间交叉验证,系统连续做五六轮 search 完全可能是正常的。你如果把“连续搜很多次”直接当异常,打掉的不一定是循环,可能是任务本来该有的工作量。
更难的是,有些动作看起来很像重复,实际上是在深挖同一条线索。第一轮搜一个指标的大概数字,第三轮回到同一主题去补口径差异,或者核对另一份来源,这在日志上都可能长得很像“你怎么又搜回来了”。单看参数相似度,很容易误伤。
这也是为什么我对“复杂度预估”这类东西的态度后来变得保守。它可以有,用来帮系统大致判断这个任务是不是天生就需要更高的容忍度,但它只能是软信号。预估步骤数本身经常不准,模型又天然偏乐观,你如果把它当主判据,系统迟早会被带偏。
我更愿意把它理解成“调节容忍度”的旋钮,而不是“决定生死”的闸门。任务复杂,系统可以宽一点;任务很短,系统可以收紧一点。但最后要不要打断,还是得回到更硬的执行态证据上。不然就很容易出现一种尴尬情况:模型一开始估错了,后面整条链都跟着这个错误预估在跑。
真正更稳的信号,还是那些执行态里能观察到的东西:连续无效 Observation、信息增量停滞、目标漂移、状态机持续异常。这些信号不是完美的,但比“模型说这个任务大概要几步”要可靠得多。
到最后,你会发现这其实是个很典型的工程取舍问题。你把循环率压得很低,往往就会提高误杀率;你尽量不误杀,系统又会放过一部分无效继续。这个平衡没有一个永远正确的阈值,它更像是你对任务成本、用户等待时间、误杀代价的综合判断。
我当时比较信的一种验证方式也很土,就是把被提前打断的任务单独捞出来回看。别只看总数下降了多少,要看里面有多少其实再走两步就能成功。如果这一层不做,系统很容易表面指标好看,真实体验反而变差。后来有些阈值调整,我就是靠这种回看慢慢收出来的:不是为了追一个漂亮数字,而是让系统更少在该探索的时候过早放弃。
我后来反而觉得,这一层最能区分“写了点规则”和“真的做过系统”。会打断并不难,难的是知道什么时候不该打断。
很多问题看起来像提示词问题,其实是 tool 和 state 管理出了错
我有一段时间特别容易把锅甩给提示词。系统最后坏在模型输出上,第一反应自然是:是不是提示词不够清楚,是不是工具描述写得太粗,是不是示例还不够多。
直到我开始更细地看上下文,我才意识到很多问题根本不在推理本身,而在状态管理。
最典型的一类情况是 Observation 太长。工具返回一大段网页摘要,系统原样塞进历史里,前几轮看着还正常,到了后面上下文越来越满,早期真正关键的 Observation 被挤掉了。模型不是突然变笨了,它只是忘了自己已经查到什么,于是又回头去查一遍。
这种问题特别容易伪装成“模型怎么老犯同一个错”。其实不是同一个错,它只是没有把前面的状态稳定地保留下来。
我挺少再把这种情况叫成“模型失忆”,因为听起来像在拟人化。更准确一点说,是系统没有把哪些信息值得长期保留这件事设计好。原始网页摘要、长段工具输出、重复 Observation 全都平铺进上下文,最后真正关键的状态反而被埋了。模型当然会回头查,因为它当前看到的历史里,前一次查到的结果已经不在显眼的位置了。
也是从这里开始,我对 Observation 的态度彻底变了。它不是一个临时塞给模型看的材料块,而是整个系统状态的一部分。Observation 进上下文之前要不要清洗,要不要压缩,要不要做摘要,要不要只保留结构化关键信息,这些都不是优化细节,它们决定的是系统后面还能不能记住自己刚刚做过什么。
再往后看,摘要也不是越多越好。太激进地压缩,会把真正需要回溯的细节一起抹掉;完全不压缩,窗口又会很快被拖满。所以它更像预算管理,而不是单纯节省 token。最近几轮保留原文,早一点的 Observation 压成结构化摘要,关键事实单独维护成一份稳定状态,这种分层通常比“全文保留”或者“全文摘要”都更靠谱。
回头看,很多像“模型没想明白”的问题,底下其实是 tool adaptation、context architecture、state retention 这几层没有立住。
工具返回脏数据,系统没清洗;Observation 太长,历史没压缩;上下文预算快见底了,摘要和裁剪策略还没上线。到最后,错误表现全压到了模型那一层,看起来像 reasoning 问题,根因却不在 reasoning。
这也是为什么我后来越来越不愿意把 ReAct 写成一个“提示词框架”。它在工程里本质上是一个有状态的执行系统。工具怎么接、结果怎么落、历史怎么存、预算怎么控,这些东西一旦没处理好,模型再聪明也会被拖进重复、遗忘和偏航里。
做完这些以后,我对 Thought 的理解也变了
我一开始把 Thought 想得很像 CoT 在 Agent 里的翻版:模型把中间推理写出来,再决定下一步做什么。这个理解不算错,但太薄了。
真正做过一段时间系统以后,你会发现 Thought 在 ReAct 里扮演的角色复杂得多。它当然有推理的成分,但它还在消化 Observation,在决定下一步 Action,在给系统留下一段可调试的轨迹。有时候它的价值不在于“看起来会想”,而在于给了系统一个纠错入口。
这也是它跟普通 CoT 不太一样的地方。CoT 更多是封闭推理,模型把中间步骤写出来,目标是把最终答案做对。Thought 则长在一个带反馈的循环里:上一轮 Observation 改变了它下一轮该做什么,它不是单纯为了“推得更完整”,而是在参与行动决策。这个差别在工程上很重要,因为你调 Thought,不只是调一段文本风格,你是在调系统怎么承接反馈、怎么改路。
我越来越看重这一点。
ReAct 相比线性执行或者纯规划式执行,一个很实际的优势就是它允许系统每拿到一轮反馈,就停下来重新判断一下方向。这个“再判断一下”的位置,很多时候就是 Thought 在系统里真正值钱的地方。没有它,模型也许还能把简单任务跑完,但一旦 Observation 出现异常、工具结果不稳定、或者任务中途需要换策略,恢复能力通常会先掉下来。
但与此同时,我也越来越不敢把 Thought 直接当成“真实推理过程”。
日志看多了以后,会明显发现一种现象:Thought 看起来很完整,理由也说得通,可它未必真的是在驱动后面的 Action。有时候更像是模型先有了一个动作倾向,再回头补一段解释,让整个轨迹看起来更连贯。合并生成的时候,这个问题尤其容易被看见;分开生成也没有从根上解决,它只是把这种不一致藏得更深一点。
所以我后来对 Thought 的态度很矛盾,但也更真实:它的价值是真的,尤其是在纠错和调试上;可见的 Thought 文本又不能被天真地等同成内部决策因果链。它里面会混有真实推理、模板化套话,也会混有事后合理化。
这件事对工程和产品都有影响。
工程上,你不能因为模型“写出了一段好看的 Thought”就默认系统真的理解了问题;产品上,你也不能轻易把原始 Thought 全量暴露给用户,因为那里面既可能有内部实现细节,也可能有并不那么可靠的解释。
前阵子我又顺手去翻了一遍 Codex 的开源代码,这个判断反而更坐实了。它给我的感觉不是“去证明 reasoning 真的驱动了 action”,而是从系统设计上尽量别让 reasoning 文本变成执行依据。执行链路里真正落地的是结构化的工具调用、审批和沙箱这一类约束,以及工具结果再写回上下文。像 FunctionCallOutput 这种回填项,本质上就是把“工具到底返回了什么”重新变成下一轮能消费的输入;而 reasoning 在界面和事件里更像单独的展示或记录通道。这样一来,就算模型内部已经有了很强的动作倾向,真正会不会产生副作用,还是要过独立的控制层。这个思路我很认同:现实系统没必要去信任 Thought 的真实性,而是应该去信任可验证的动作轨迹和工具返回。
我更能接受一种折中:开发阶段把 Thought 留得比较完整,方便查问题;用户侧如果真要展示,也最好是翻译过的行动说明,而不是原始 Thought 原样端出来。开发者看的是调试轨迹,用户要的是系统现在在做什么、为什么还没结束,这两件事不是一个展示层级。
到最后,我对 Thought 的看法比以前克制了很多。我不会轻易拿掉它,因为很多纠错能力确实依赖这层;但我也不会神化它,把它当成某种可以直接解释模型内部因果的窗口。
当问题从“不确定”变成“可规划”,我会让 ReAct 退后,Planning 进来
ReAct 这一套机制修得越多,你越容易意识到:有些问题继续在 ReAct 里硬顶,性价比并不高。
这里我说的 Planning,更多是一个上位概念,指的是“先规划,再执行,必要时重规划”这一类思路。真落到工程实现上,我后面讨论的大部分东西,其实更接近常说的 Plan-and-Execute:先有一个 planner 产出计划,再有一个 executor 按计划推进,执行过程中再按需触发重规划。
如果任务的不确定性主要来自信息本身——不知道该去哪里找,不知道哪条线索更有用,中途还经常要根据反馈换方向——ReAct 的确很好用。它的长处就是边走边看,边拿反馈边调整。
但如果任务的路径已经比较清楚,只是步骤比较长,或者有明显的分阶段结构,那继续把全部决策都留在在线循环里,往往会显得有点笨。每一步都重新想一轮,token 成本高,执行节奏也松。这个时候,让 Planning 先把大框架铺出来,或者更具体一点,让一套 Plan-and-Execute 结构先把计划和执行拆开,通常会更稳。
我判断要不要从 ReAct 往 Planning 推一步,主要看两件事。第一件是信息不确定性到底有多大。如果最核心的问题是“不知道该去哪找、找到以后还得临场判断下一步”,那 ReAct 的边走边看很有价值。第二件是任务路径是不是已经大体清楚。比如先收集几类信息、再做整理、最后产出结论,这种结构一旦稳定下来,继续把所有决策都放进在线循环里,很多时候只是在重复支付推理成本。这种时候,Plan-and-Execute 往往会比纯 ReAct 更顺手。
我越来越不喜欢把 Planning 描述成 ReAct 的升级版。它没有更高级,只是把问题换了个位置。
ReAct 的麻烦主要在执行中暴露:重复、失效、偏航、失忆。Planning 这一大类思路会把一部分压力前置到计划质量上;如果说得再具体一点,Plan-and-Execute 这类实现会把麻烦集中到计划粒度、依赖管理和 Replan 上。你从 ReAct 切到 Planning,不是问题消失了,而是失稳结构变了。
这一点我体会挺深。ReAct 的问题是你在执行中不断发现自己走歪了,所以需要 runtime 盯得很紧;Plan-and-Execute 的问题则是你一开始就可能把计划写粗了、写细了,或者漏掉关键依赖。计划粗,执行时又要不断临场补子计划;计划细,任何一个前提错了,后面整串都要重算。到了这一步,系统的麻烦不再是“这一轮 Action 对不对”,而是“这份计划本身是不是还值得继续执行”。
token 成本也是同样。以前很容易粗算成“ ReAct 要 N 轮,Planning 只规划一次”,真正跑过系统以后就知道不能这么算。ReAct 的成本很大一部分来自历史上下文递增,Observation 越长越贵;而在 Plan-and-Execute 这类实现里,计划本身的维护、变量传递、Replan、执行阶段重复带 plan,也都是实打实的开销。
我会更愿意把两者的成本拆开看。ReAct 的开销更像是边际递增的:每多走一步,除了当步的 Thought 和 Action,你还要带上越来越长的历史。Observation 如果不压,后面每一轮都会更贵。Plan-and-Execute 的开销则更像前置集中,再叠加维护成本:先花一笔把计划写出来,后面每步执行都要带着它,计划一旦改动还会带来重排和重注入。单看调用次数,Plan-and-Execute 常常显得便宜;一旦计划又长又经常改,它并不会比 ReAct 轻松多少。
所以我不太会回答“ReAct 和 Planning 哪个更省 token”这种抽象问题。我更关心的是:这个任务的信息结构、上下文长度、结果形态、Replan 概率分别是什么。只要这些前提不一样,成本结论就会跟着变。真落到具体实现上,很多时候比较的也不是抽象的 Planning,而是某一种 Plan-and-Execute 到底有没有比当前 ReAct 链路更划算。
所以最后真正可落地的,通常不是二选一,而是混合。粗规划交给 Planning,很多时候就是一层 Plan-and-Execute;局部探索留给 ReAct,底下再用统一的 runtime 做结果清洗、状态管理和异常检测。这种结构看起来不如“单一范式”干净,工程上却通常更稳。
我更喜欢把这种混合理解成职责分配,而不是范式妥协。Planning 负责把任务的大框架搭出来,避免系统在明显可规划的部分反复想;如果继续往实现层走,Plan-and-Execute 就是把这部分职责拆给 planner 和 executor;ReAct 则负责处理那些真的要根据反馈随时改路的局部区域;runtime 负责盯住不管你上层怎么切范式,底下都一样会出现的执行问题,比如脏 Observation、上下文膨胀、重复动作和异常停滞。这样分下来,系统虽然不像某些论文图里那么整齐,工程上反而更好维护。
回头看,我现在更认可的是一种分层的 Agent 设计
如果现在让我重新搭一个类似系统,我最先会下手的,不会再是提示词本身。
我会先把几件事立住:工具输出进来之前先清洗;Observation 进入历史前要有压缩和摘要策略;执行层要能感知重复、失效和偏航;最大轮次只是最后一道刹车;任务一开始就要大致判断,这件事更适合 ReAct、Planning,还是更具体的一种 Plan-and-Execute 落地方式。
这几年做下来,我越来越认同一种分层的设计方式。
提示词负责定义边界和角色,不负责兜所有底;runtime 负责硬保障,监控系统是不是开始在做无效的事;executor 负责具体执行策略,决定什么时候宽一点,什么时候收紧一点;state 和 context 则是整个系统的基础设施,别等出问题了才想起来补。
这套分层对我最大的帮助,不是让架构图变好看,而是出了问题以后终于知道该去哪一层看。模型总在乱搜,不一定是提示词;可能是 runtime 没拦住重复。系统明明查过还在重查,不一定是 Thought 太弱;可能是 state 没把关键结果保住。计划执行到一半开始全部返工,也不一定是执行器不行;可能是 planner 把依赖关系想得太乐观。能把这些问题分层拆开,排查成本会低很多。
我也越来越反感那种“全靠一个万能 Agent”式的想象。只要系统真的进了生产,你迟早要面对工具质量不稳定、上下文预算有限、结果格式不统一、用户问题本身很乱这些现实约束。到那个时候,真正让系统站住的,往往不是某一个特别聪明的提示词,而是底下这些看起来不那么显眼的基础设施有没有先搭好。
Thought 我会留着,但会把它更多看成一种有用但不完全可信的中间层;ReAct 和 Planning 我也不会再拿来当立场问题,更多是看任务结构。信息很不确定,就让系统边走边修;路径大体清楚,就让规划先把骨架搭出来;如果需要把规划和执行明确拆开,那就直接上 Plan-and-Execute。
回头看,真正让我改观的,不是哪一个提示词技巧,也不是某个更花哨的推理范式,而是我终于把 Agent 当成了一个执行闭环在看。只要它还是闭环,问题就不会只出在模型那一层。工具、结果、状态、预算、轨迹,哪一层松了,系统都会往失控那边滑。
这也是我现在重新理解 ReAct 的方式:它不是一个会不会调工具的范式问题,而是一套推理、执行和状态管理怎么一起工作的系统问题。
附录 1 :Planning 和 Plan-and-Execute 到底差在哪
很多时候这两个词会被混着用,但严格说并不是一回事。
Planning 更像一种思路。
它强调的是:系统不要每一步都临场决定下一步,而是先形成一个相对稳定的任务分解,再按这个分解推进,必要时再重规划。这个说法本身比较宽,可以包含很多不同实现方式,比如先出粗计划、执行中再细化,也可以是阶段性重规划,或者带依赖关系的结构化计划。
Plan-and-Execute 则更像一种具体架构。
它把“规划”和“执行”明确拆成两个阶段,甚至两个模块:先由 planner 生成计划,再由 executor 逐步执行;如果执行中发现计划不成立,再触发 Replan。只要开始讨论 planner 和 executor 怎么分工、计划粒度怎么定、步骤依赖怎么传、Replan 何时触发,这些问题其实就已经进入 Plan-and-Execute 的语境了。
所以更准确地说:
- Planning 是上位概念;
- Plan-and-Execute 是其中一种很典型的实现。
在这篇文章里,凡是讲“什么时候适合先规划再做”,我更愿意用 Planning 这个上位词;凡是讲计划质量、粒度、依赖、Replan、planner 和 executor 的分工时,说的其实已经更接近 Plan-and-Execute 了。
换句话说,Plan-and-Execute 属于 Planning,但 Planning 不只等于 Plan-and-Execute。前者更像范式层的说法,后者更像工程落地时常见的一种架构形态。
附录 2 :ReAct 之外,我怎么看其他几种 Agent 推理方式
把 ReAct 真正跑进系统以后,我对很多“新范式”的看法也变得实际了不少。以前看这些名字,会很容易按论文脉络去分;真做工程以后,我更关心的是:它到底在替 ReAct 解决哪类问题,又会顺手带来什么新麻烦。
ReWOO
ReWOO 很容易被粗暴地理解成“把 Thought 去掉了”,但我觉得这样说不准。更贴切的理解是,它把原来循环里大量在线发生的思考,前置成了一次规划,并且用变量引用把后续步骤串起来。
它解决的问题很直接:如果任务的大部分步骤本来就是可预判的,那每拿到一次 Observation 都重新想一轮,确实有点浪费。ReWOO 这种做法会让执行节奏更紧,推理成本也更可控。
但它的问题也同样直接。前面规划得越多,后面就越依赖前面的判断别出大错。一旦早期规划里某个变量依赖写偏了,后面不是一两步走歪,而是整串都跟着歪。它本质上还是在用“前置思考”换“在线灵活性”。
所以我会把 ReWOO 放在“规划式做法”的谱系里看,而不是把它看成一个和 ReAct 完全平行的新物种。它适合那些任务骨架已经比较稳定、局部反馈又没那么频繁的场景。
Reflexion
Reflexion 这条线我一直觉得挺有意思,因为它盯住的不是“当下这一步怎么走”,而是“上一轮到底为什么失败了”。
它适合那种重试成本比较高的任务。比如一条链已经跑了不少步,最后发现失败了,直接原样重来往往意义不大。更有效的做法是先抽一层反思,把“刚才错在什么地方”沉下来,再带着这个反思去跑下一轮。
这个思路对工程是有吸引力的,因为它比盲目重试更像积累经验。但它也不是没有代价。最大的问题是,反思文本本身未必可靠。系统完全可能写出一段看起来很深刻的复盘,实际上并没有抓到真正的失败原因。这样一来,它不是在纠错,而是在给下一轮灌入新的误导。
所以如果真要用 Reflexion,我会更愿意把它放在那些失败代价高、但能接受多一轮总结的任务里,而不是默认每个任务都来一遍“复盘再试”。
搜索式推理
像 Tree of Thought 这类搜索式推理,我会把它理解成:系统不再满足于只走一条链,而是同时保留多条候选路径,在中间做筛选、回溯、分支。
它解决的是 ReAct 一个很明显的短板:ReAct 默认是一条轨迹往下走,一旦早期方向错了,后面再修就会越来越贵。搜索式推理则试图在更早的地方保留岔路,让系统别太快把自己锁死在单一路径上。
问题也很明显:贵,而且难控。分支一旦多起来,成本和状态管理压力都会迅速上去。你不只是多走了几步,而是同时在维护几条还没确定值不值得走的路径。论文里这当然很吸引人,工程里就得老老实实算账:这条任务到底值不值得你把这么多预算花在“先别急着选路”上。
所以这类方法我不会把它当默认配置。它更像一种在高价值、高不确定性任务里才值得开的重模式。
弱化 Thought,甚至直接 Act-only
还有一类做法看上去没那么“新”,但其实很实用:不是增加更多推理结构,而是主动把 Thought 压薄,甚至干脆去掉,直接让系统动作起来。
这种做法适合什么场景,我前面正文已经讲过:工具少、路径清楚、延迟敏感、错误恢复压力不大。到了这种任务里,保留一大段 Thought 未必是帮助,反而可能只是多一层开销。
所以我现在不太会把“推理更复杂”直接等同成“系统更高级”。很多时候,真正成熟的地方反而是知道什么时候该把结构做厚,什么时候该收薄。
回头看,这几种路子并不是谁替代谁,而是各自对 ReAct 的某个短板做了不同方向的修正。有的在前面加规划,有的在后面加反思,有的改成分支搜索,有的干脆减少显式思考。真正落到工程里,还是那句话:先看任务结构,再决定你到底要补哪一种能力。
附录 3 :我现在怎么判断 ReAct 的未来位置
如果只看这两年 Agent 的讨论热度,很容易产生一种感觉:ReAct 是不是快过时了,未来会不会被更复杂的规划式系统、搜索式系统或者多 Agent 协作彻底替掉。
我现在不太这么看。
ReAct 不会消失
原因很简单。现实里一直会有大量任务,本质上就是信息不确定、反馈频繁、需要边做边改。你不知道该去哪找,找到了以后也不知道是不是对的,下一步很可能取决于刚拿到的 Observation。这类任务天然适合 ReAct,不是因为它优雅,而是因为它顺手。
只要这类任务还在,ReAct 就不会消失。它可能不再以“纯 ReAct”的形态单独存在,但它那种短反馈闭环、在线调整路线的能力,始终会留在很多系统里。
纯 ReAct 会越来越少
但如果说未来还是一条 Thought、一条 Action、一条 Observation 这么一路跑到底,我觉得会越来越少。
原因也不复杂。纯 ReAct 对上下文管理太敏感,对工具质量太敏感,对执行层看门狗的依赖也很重。系统稍微复杂一点,你就会很自然地往里补别的结构:前面补一层规划,后面补一层反思,中间再加状态摘要和预算控制。等这些东西都补进来以后,它已经不是“纯 ReAct”了。
所以我现在更愿意说,ReAct 不太会消失,但会越来越多地被吸收到混合架构里。
长上下文会缓解一部分问题,但不会替你把系统设计好
还有一个很容易乐观的判断,是觉得上下文窗口越来越长以后,很多问题自然就会消失。这个结论我一直不太敢下。
长上下文当然有帮助。很多原来要急着做摘要和裁剪的地方,会没那么紧张;有些历史信息也不用太早丢掉;系统在较长轨迹上维持连续性的能力,理论上也会变强。
但这不等于状态管理就不重要了。噪音不会因为窗口变长就自动消失,预算问题也不会消失。更现实的一点是:上下文再长,系统也还是要面对工具输出不稳定、Observation 价值参差不齐、历史里哪些信息值得长期保留这些判断。窗口变长,很多时候只是让你晚一点撞墙,不是墙没了。
多模型协作会越来越常见
我反而更愿意相信的一个方向,是多模型协作会越来越常见。不是所有环节都值得用同一个模型做,也不是所有步骤都值得用同样深的推理强度。
更自然的做法是:大模型负责规划、分解、判断方向,小模型负责摘要、清洗、分类、轻量执行,必要时再把关键节点抬回大模型。这样一来,系统的“思考”不再是一整段统一的东西,而是被拆成不同成本、不同职责的几层。
这种分工一多,ReAct 的形态也会跟着变。它不再一定是“一个模型自己边想边做”,而更像是系统里某些局部区域仍然保留了 ReAct 式的短闭环。
最后留下来的,可能不是单一范式,而是几种能力的组合
所以如果要我现在给一个判断,我大概会这么说:ReAct 不会被简单替代,但它会越来越少地以纯粹形式出现。未来更常见的,不会是“全系统只信一种范式”,而是规划、执行、反思、摘要、状态管理几种能力被拼在一起,各自处理自己最擅长的那一段。
从这个角度看,ReAct 的未来位置也许没有以前那么居中,但它那种“拿到反馈以后立刻改路”的能力,还是会一直留在很多系统的核心环节里。