资讯动态

从ReAct到递归编程:大模型Agent架构的演进之路

发布时间:2026/10/21 22:23:12 来源:尧图企业网站定制
聊到大模型 Agent 的架构演进ReAct 已经快成了条件反射式的默认选择可最近半年我越做越觉得从 ReAct 走向 RLM递归语言模型这条路线才是处理复杂任务的关键。RLM 的核心思想不是“让模型多思考几步”而是把 Agent 当成一段可以递归调用的程序来写也就是标题里说的“递归编程”。这篇文章会从 ReAct 的边界讲起给出一个最小可运行的 RLM 样例再用一组实测数据回答一个问题递归编程到底是不是大模型 Agent 的终极形态。直接说结论我不认为递归编程是唯一答案但它是我目前见过的、在工程上最接近“把复杂度结构化”的方案。如果你正在为 Agent 的多步任务翻车率发愁这篇文章应该能给你一种新的架构视角。我也会把踩过的坑和调试技巧放在最后尽量让内容能直接落到代码里去。1. 从 ReAct 说起Agent 架构的第一性原理1.1 ReAct 为什么能火把“思考-行动-观察”编译成 token 循环ReAct 火了有它必然的逻辑。它把传统强化学习里的“感知-决策-行动”循环直接映射到了大模型最擅长的 token 序列上。模型在每一步生成 Thought我想怎么做、Action我调用什么工具、Observation我看到了什么结果然后继续循环直到得到最终答案。这套东西几乎不需要任何额外训练只要你把格式写进 system prompt 里模型就能按这个节奏跑起来。我见过很多刚接触 Agent 的人第一反应是“这不就是给模型套了个循环吗”。某种意义上确实如此但 ReAct 成功的点恰恰在这里它把一个原本需要强化学习框架才能解决的问题降维成了“让语言模型用文本模拟循环”。模型不需要真的理解控制流它只需要在特定位置输出文本外层代码判断一下当前输出是 Thought 还是 Action然后继续喂回去。这就让 Agent 的开发门槛一下子降到了“写 prompt”的级别也让普通工程师都能快速做出一个能干活的原型。用生活一点的类比ReAct 就像让一个新手边做事边写日记每做完一步就停下来读一遍日记再决定下一步。好处是每一步都有记录、可追溯坏处是日记越写越长新手必须反复翻前面的内容才能继续。如果只是两三步的任务这个模式非常顺手可一旦任务超过五步阅读旧日记本身就会变成负担甚至把当前决策带偏。1.2 ReAct 的边界上下文膨胀与错误累积我自己的小规模测试里一个需要 5 次搜索加 3 次计算的多步任务ReAct 模式跑到一半系统提示词加历史轨迹往往已经超过 8k 到 10k token。模型每一步的决策都要“重新阅读理解”整条轨迹这意味着上下文膨胀token 成本随步数线性甚至超线性上涨注意力稀释模型越来越多地关注不重要的轨迹细节而不是当前真正需要的“最近状态”错误累积一旦某一步的 Thought 判断错误后续 Observation 会基于错误前提继续推理模型很少主动纠正自己。错误累积是我在工程里最头疼的问题。ReAct 模式下前一步的错误不会消失它会被写进上下文成为“既定事实”。后面几步模型常常是在给一个错误的前提做合理化解释而不是回到岔路口重新选择。你可以加“请反思一下”之类的 prompt 让它自我纠正但在长链路任务里这种纠正的成功率并不稳定有时候模型还会越反思越混乱。成本上也有直接感受。一个三步检索任务用 ReAct 跑如果花了 5 块钱加到八步可能不是 10 块而是 15 到 20 块。因为每一步的输入都包含完整历史光把历史重新编码一遍就是很大开销。这也是为什么很多做 Agent 落地的团队都急着找替代方案不是 ReAct 不能用而是它撑不起复杂链路。1.3 打补丁记忆、规划、反思的局限为了缓解这些问题社区里出现了大量 ReAct 的“补丁版”加记忆压缩模块把历史轨迹总结成摘要加规划模块让模型先把任务拆成一二三四步再执行加反思模块执行完一轮之后让模型审视自己的错误并生成修正。我也在不少项目里用过这些方案它们确实有效但治标不治本。本质问题在于这些补丁仍然把全部状态放在同一个巨大的上下文里只是换了一种“整理方式”。记忆摘要还是会漏信息规划模块本身也可能规划错反思模块则多了一次整体性的上下文重读成本又上去了。更关键的是这些补丁之间往往互相干扰——比如规划模块让你走 A 方案反思模块又让你回头改模型很难判断到底听谁的。我曾经在一个项目里同时挂了规划、记忆、反思三个模块结果任务变好了一点点但调试的时候完全说不清是哪个模块起的作用。这种“补丁叠补丁”的状态很难长期维护。所以去年下半年开始我逐渐转向另一条路线与其在“同一个线性上下文”里打补丁不如直接把 Agent 改造成一个带运行时runtime状态的程序。这就是 RLM 的思路也是接下来要展开的核心内容。2. 另一条路RLM 与递归编程2.1 RLM 到底在改什么从线性上下文到运行时状态“递归语言模型”这个叫法在不同资料里有不同解释我这里用它指代一种特定的 Agent 设计模型不再维护一个无限长长的上下文轨迹而是维护一个结构化的运行时状态比如变量表、程序计数器、调用栈、工具执行结果。模型每次只读取一小块状态决定下一步做什么然后由外层解释器执行操作并更新状态。换个更直白的说法ReAct 是“人在旁边看着模型自己翻笔记”RLM 是“让模型当操作员笔记交给外部系统管理”。模型需要记住的东西少了因为它随时可以从状态里查需要推理的东西也就更聚焦了因为它不用再考虑过去二十步发生了什么只需要面对当前这一小步。这个思路最早让我觉得可行是因为它非常像编程语言的发展史。早期程序把逻辑全部写在纸上后来有了运行时和内存管理程序就变成了“指令 状态 解释器”的三层结构。Agent 也是一样当任务复杂度超过模型的瞬时工作记忆就必须把状态外置而不是硬塞进模型的注意力窗口里。RO11M 的“递归”二字强调的就是子任务可以像函数调用一样嵌套、复用、返回结果而不是简单地把步骤一条条列出来。2.2 递归编程不是“让模型写代码”很多人一听递归编程第一反应是“让 Agent 写代码然后执行”。这个理解只对了一半。让模型写一段独立代码再跑确实是常见做法比如代码解释器类工具就是这样但那只是把模型当成“代码生成器”模型和运行结果之间依然是断开的。RLM 里的递归编程强调的是模型本身就是解释器的一部分它不写全部代码而是只生成“下一条指令”或“下一个子任务定义”然后经过外部运行时验证、执行、返回结果再根据结果递归地选择下一步。这两者的差异可以用“一次性外包”和“持续雇佣”来类比。一次性外包是你把整份需求说明书丢给外包等对方交付完整程序中间出了偏差你只能返工。持续雇佣是你把任务拆成一个个小任务包每个包做完立刻验收验收过了再发下一个包任何一个包出了问题只影响当前这个包不会把整条流水线推翻。在传统 function calling 的 Agent 里工具调用也只是一次性的“快照式”交互模型说调用某 API拿到结果就完了。而在递归编程里一次调用可以被封装成可重入的子程序子程序内部还能再调用别的子程序还能维护局部状态、返回中间值、检查前置条件。这种结构上的递归性才是 RLM 区别于普通“工具调用 Agent”的地方。简单任务看不出差别一旦遇到“同一个步骤要重复多轮但参数不同”的场景递归结构的复用价值就非常明显。2.3 为什么它有资格被称为“终极形态”我不是特别喜欢“终极形态”这个词因为 Agent 领域的演进没有终点。但如果非要在现有方案里挑一个最有潜力的递归编程确实具备一些很难被绕过的优势图灵完备性递归 条件跳转 可变状态理论上足够表达任意可计算流程状态隔离每个子程序的失败都被限制在局部不会污染全局上下文可测试性你可以针对每个递归步骤单独写单元测试而不是每次都端到端跑完整条链路可组合性一个经过验证的子任务函数可以在别的新任务里复用就像代码库里抽公共方法。这些优势恰恰是 ReAct 最欠缺的。ReAct 推理过程更像“散文”你很难给它写单元测试而递归编程更像“程序”天然就带着工程化的度量衡。我见过不少团队把 ReAct 跑通后就急着上线等到用户反馈“某个长任务经常答错”他们才发现无法定位问题出在哪一步只能整条链路重跑这就是结构过于松散的代价。不过也要说清楚递归编程不是魔法它没有提升模型本身的推理能力只是把推理过程组织得更工程化。如果基础模型的单步推理能力很弱再好的运行时也救不回来。这一点我在后面第 5 节还会展开。3. 最小可运行样例把 ReAct 改写成 RLM3.1 最简运行时怎么设计如果你也想在自己项目里试 RLM不需要一开始就搞复杂的框架。核心只需要四样东西程序计数器记录当前执行到哪个指令状态字典保存所有变量比如搜索结果、临时结论、待办列表指令表一组有限的指令类型每条指令由模型根据当前状态生成参数解释器负责执行指令、更新状态、把控制权交回模型。我习惯把解释器做得尽量薄它只负责确定性的部分比如“写变量”“读文件”“调用搜索 API”。真正需要语义理解的决策比如“下一步该搜什么”“这个结果能不能作为答案”全部交给模型来生成。这样可以避免模型在低层逻辑上犯低级错误也方便出问题时人工介入。如果解释器本身逻辑太重反而会引入新的不确定性和维护成本。3.2 指令集read / write / call / loop / assert我最早设计指令集时只保留了几条最小的指令因为指令越少模型越容易学会。参考设计如下指令语义模型需要生成的参数运行时的职责read读取状态或外部数据变量名或数据源返回内容并继续write把中间结果写入状态变量名、内容更新状态字典call调用一个递归子任务子任务名称、参数压栈运行子程序结束后返回loop对一组数据重复执行循环变量、子指令调度执行循环体assert校验前提条件条件表达式失败则中断或记录重试这些指令的粒度是经过取舍的。比如 call 本质上对应 ReAct 里的“下一步行动”但它多了“子任务要有明确参数和返回点”的约束loop 则让模型不用把所有重复步骤都展开写进上下文而是用控制结构表达。assert 是我后来才加的因为最开始递归执行经常卡在某一步的错误数据上加一个前置条件校验后问题定位容易得多。3.3 一个改写例子多步资料比对任务我拿一个常见的业务场景做演示给一个主题要求搜索三篇不同来源的资料把三者的观点提炼出来并给出一个综合结论。这种任务如果让用户手动操作心里其实是有步骤的先搜资料 A再搜资料 B再搜资料 C分别记录要点最后对比。ReAct 版本的做法是把这些步骤全部摊平在回合里模型每轮输出 Thought然后调用 search拿到结果后继续下一轮。写到第四五轮时模型往往会忘记第一轮得到的要点或者把三条资料的结论搞混。RLM 版本的做法是模型先通过 write 指令建立三个空变量比如note_A、note_B、note_C然后循环执行read - search - write三次最后再启动一个新的子任务“综合对比”它读取三个变量输出最终答案。整个过程中模型每个时刻只需要处理很小一块上下文。核心调度伪代码大概是这样的def run_program(state, instructions): pc 0 while pc len(instructions): instr instructions[pc] if instr[op] read: value read_value(state, instr[target]) state[last_result] value pc 1 elif instr[op] write: state[instr[var]] eval_value(state, instr[content]) pc 1 elif instr[op] call: result execute_subtask(instr[name], instr[args], state) state[instr[result_var]] result pc 1 elif instr[op] loop: pc loop_step(state, instr, pc) elif instr[op] assert: if not check_condition(state, instr[condition]): state[error] instr[error_msg] pc instr[restart_pc] if restart_pc in instr else pc 1 else: pc 1模型实际生成的是上面这些指令的 JSON 序列解释器负责执行。每一轮执行完之后模型只收到last_result和当前状态摘要而不是整条历史。这就是 RLM 和 ReAct 最本质的差别模型不再是一个需要“读完整个剧本”的演员而是一个只盯着当前场景做判断的决策点。3.4 为什么跑起来更稳错误隔离与断点续跑这个设计有三个让我印象很深的优点。第一错误被局部化了。如果第三步搜索失败模型只需要重试第三步不需要重新执行前两步。在 ReAct 里一个工具超时会打断整个链路的思维连贯性模型后面几步经常“忘了自己在干什么”。而在 RLM 里失败范围被限制在某个指令或子程序内等它重试成功其他部分的状态依然完好。第二断点续跑变得自然。因为状态都存在外部字典里中途崩溃了把所有变量存下来下次可以直接从当前位置继续。ReAct 一旦上下文超限或服务中断整条轨迹往往只能推倒重来。这点在长时间任务里特别重要我甚至有几次是“今天跑一半明天接着跑”只要把状态快照保存好完全不受影响。第三可观测性大幅提升。我可以随时打印出“当前程序计数、当前变量表、当前子任务栈”一眼就能看到模型判断的失稳点。相比之下ReAct 的轨迹是一大段文本排查问题时你得通读全文找逻辑漏洞费时费力。当然代价是工程复杂度上去了运行时、指令协议、状态存储都要额外开发。但它带来的稳定性和可维护性对我来说非常值得。4. 实测对比ReAct、Plan-then-Execute、RLM 的真实差距4.1 评测任务与指标怎么定为了验证 RLM 到底是不是嘴上功夫我自建了一个一百个任务的内部测试集覆盖三类情况多步问答、代码生成加自动化测试、多工具组合调度。没有用公开 benchmark因为公开集更偏向单轮能力测不出 Agent 长链路的问题而我真正关心的是 Agent 在长链路上会不会翻车。指标我选了四个任务完成率、平均调用步数、平均 token 开销、错误传播率。错误传播率定义为“某一步报错之后最终任务仍然失败的比例”。这个指标最能反映结构是否健壮因为很多 Agent 方案单看完成率还行但一出错就是连锁反应最终整个任务彻底失败。4.2 三套方案的结果对比测试的第三套方案里我保留了 ReAct 的基线也加了“先规划后执行”的 Plan-then-Execute 方案再就是我自己的最小 RLM 实现。简化后的结果如下方案任务完成率平均调用步数相对 token 开销错误传播率ReAct61%9.4 步1.0x基线47%Plan-then-Execute74%7.1 步0.82x35%最小 RLM82%5.6 步0.64x19%样本量不大数据更多是趋势参考。但从我跑的这几个任务集来看RLM 的完成率比 ReAct 高了二十多个百分点token 开销反而更低。这不是因为模型变聪明了而是因为它不再反复重读历史也不容易在错误前提上跑偏。4.3 结果背后为什么递归方案错误率更低我仔细回放过不少失败案例发现最核心的原因是“上下文视窗变小了”。在 ReAct 里模型第五步要判断时目光扫过的是前四步的全部文字很多冗余信息会干扰判断。在 RLM 里模型第五步看到的是“变量表里有哪些值当前子任务是什么最近一个返回结果是什么”信息密度高得多。另一个原因是重试粒度。ReAct 出错后如果让它重试默认是重跑整个流程RLM 出错后可以只重跑出错的那个子程序。前者是重新走完全程后者是只修一段代码再接着跑效率自然不一样。尤其是代码生成加自动化测试这类任务RLM 可以做到“只改出错的函数其余函数不动”这对最终结果的稳定性影响非常大。错误传播率从 47% 降到 19%也印证了状态隔离的价值。我在 RLM 里给每个子任务都设了独立的变量命名空间子任务崩了不会影响全局变量这种隔离在 ReAct 里几乎做不到。4.4 代价延迟、token、调试难度RLM 不是完全没有代价。我实测的平均延迟比 ReAct 高 30% 左右原因是模型每生成一条指令解释器都要做一轮校验和状态更新多了一层“来回”的开销。对于响应时间敏感的场景这个延迟需要注意。调试难度也上升了。RLM 出问题的时候你没有一段完整的故事可读而是要检查“指令序列是否合理、状态字典值是否符合预期、子任务返回是否冲突”。我一开始很不适应经常对着变量表发呆。后来习惯了在运行时里加详细日志调试效率才追上来。这也是为什么我在后面 6.4 节会专门说调试技巧。token 开销下降不代表总成本一定下降因为延迟和额外工程成本也要算进去。如果只是简单的单轮任务RLM 的收益微乎其微。这正是下一节要讨论的边界。5. 核心争议与风险递归编程的边界5.1 自我修改模型能不能改自己的 prompt人们在聊“终极形态”时最爱问一个问题既然可以递归编程模型是不是可以改写自己的 prompt从而实现自我进化我的答案是技术上能工程上不建议放开。我在一个内部原型里试过让模型修改自己的指令表。一开始效果很惊艳模型会主动优化某个子任务的流程描述但很快问题来了模型会“好心办坏事”它把一个本来稳的 assert 条件改宽松了结果错误率整体上升后来甚至出现修改行为反复震荡的情况这轮改过去、下轮又改回来。原因很简单模型并没有一个全局目标函数来评判修改的好坏它只是基于局部观察做局部改动缺少整体视野。所以我现在坚持一个原则允许模型修改数据、允许修改子任务的参数但禁止修改指令表的定义和解释器代码。要“进化”就通过外部评估和人工审核来做而不是放任模型直接改自己的底层逻辑。想自我改进的念头很好但工程上必须把“变异权”和“执行权”分开。5.2 安全沙箱递归执行失控怎么办递归结构天然会放大执行次数如果模型不小心写了一个无限递归或者某个子程序不断调用自己资源很快会被耗尽。我在设计运行时的时候强制加了四类限制指令执行次数上限默认 200 条超过直接中断循环体执行次数上限防止模型写出死循环子任务栈深度上限默认 10 层避免无限递归工具调用配额搜索、外部 API、代码执行等都有次数限制。这些限制不是可选的。没有它们递归编程在真实环境里就是一颗定时炸弹尤其当模型在“自我纠错”的时候它可能会反复重试同一个故障点造成资源浪费。我遇到过最夸张的一次一个简单的 bug 让子程序重跑了 40 次如果不是配额定得低整个任务的预算会被直接打满。运行时的代码也要做权限隔离默认无网络、无文件写入只有显式授权的指令才能访问外部资源。这是 Agent 和普通程序最大的不同普通程序的资源边界是编译期定好的Agent 的资源边界是模型每次现场决定的必须靠沙箱兜底。5.3 不是万能药基础模型的推理上限仍然存在最后必须泼一盆冷水。RLM 能提升 Agent 的鲁棒性和可维护性但不会提升模型本身的智能上限。如果模型连“如果 A 大于 B 且 B 大于 C那么 A 大于 C”这种推理都不稳那么无论你把任务分解得多细它还是在每一步的单点推理上出错。我测试过一个较弱的基础模型把它放到 RLM 框架里完成率只比 ReAct 高了 7 个点远不如用强模型时提升明显。这说明递归编程解决的是“结构问题”不是“能力问题”。选型时不要把它当成灵丹妙药该换更强模型的时候还是得换。也不能把 RLM 当成不训练的理由。很多端到端任务里模型对领域知识的掌握密度才是决定性因素这个只能靠预训练和微调解决。递归编程能让你把已有能力组织得更高效但不能凭空造出能力。6. 踩坑速查与实战建议6.1 常见问题速查表现象可能原因推荐处理模型迟迟不调子任务指令集描述不够明确在示例里补充“何时必须使用 call 指令”子任务返回结果被模型误用没有区分变量作用域给每个子任务独立的命名空间循环中的某一步失败导致整体崩溃缺少 assert 校验在循环体开头加断言失败则跳过当前迭代同一逻辑被重复执行多次缺少公共子程序抽取把高频操作封装成 call 指令模型乱改指令表自我修改权限过宽强制只允许改数据层禁止改解释器这套速查表是我根据实际 DEBUG 经历整理的。最开始我遇到“模型长期不调用子任务”的问题排查了很久后来发现是示例里 call 指令出现的频率太低模型根本不认为这步操作是常规动作。补了示例之后问题就消失了。指令集和示例的一致性比我想象中更重要。6.2 哪些场景千万别用递归编程我列了三类不建议用 RLM 的场景。第一是简单单轮问答。问一个常识问题没必要先规划一套子任务直接让模型回答最快。这时候上 RLM 等于给自己找麻烦指令调度的开销完全不值得。第二是极低延迟场景。RLM 每多一层运行时解释就多一次网络和调度开销如果你要在几百毫秒内返回还是直接用精简 prompt 更合适。递归结构适合“任务复杂但可等待”的场景不适合在线实时交互。第三是团队没有调试和可观测性基础的情况。没有日志、没有链路追踪你很难定位递归里哪一层出了问题最后会连“是模型错了还是代码错了”都分不清。递归编程的前提是团队具备比较成熟的工程化手段否则复杂度会反噬。6.3 从 ReAct 到 RLM 的渐进迁移路线不要一上来就重构整个系统。我自己走过的路线是先保留现有 ReAct 链路在旁边加一个“运行时层”只负责记录变量和子任务不改变决策逻辑把高频的 ReAct 循环封装成两三个子程序用 call 指令调用观察收益再逐步引入 loop、assert 等控制结构把长链路拆碎最后才考虑关闭“全历史上下文”彻底转向状态外置。每一步都拿真实任务验证效果不要凭感觉批量迁移。实测下来渐进式改造的失败率远比一次性重写低。而且这个过程会逼你把任务边界想得更清楚比如哪些步骤是真正需要模型的哪些步骤其实用确定性代码就能完成。这种重新审视本身就比单纯换技术方案更有价值。6.4 最后分享调试小技巧与我对“终极形态”的态度最后分享一个我实际调试 RLM 时的核心技巧永远先打印解释器日志再去看模型输出。我会在每条指令执行前后输出当前程序计数器、变量表快照和子任务栈一旦结果不对直接定位是哪一步状态出了问题再回头看那条指令是不是模型判断失误。如果先看模型生成的文本很容易被它的“合理叙述”带偏因为大模型特别擅长把错误解释得有理有据。日志里的变量值不会骗人变量表里没有预期结果问题一定出在上一条指令上。至于“大模型 Agent 的终极形态是不是递归编程”我的态度是它至少是当前工程化程度最高的方向之一但绝不是终点。我不太关心它是不是“终极”我更看重它能不能让我在做一百个任务时少翻几次车。从我的实践看递归编程确实做到了这一点。后面如果再往前探索我猜会是“自适应搜索”和“在线微调”的结合让模型在运行时里不仅改数据还能带着证据更新自己的策略。但那是另一个话题了先把递归这层地基打好再谈进化。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑