控制流的承载:推理范式对 Harness 的能力要求
ReAct、Reflexion 这些范式被报告的收益,大多依赖一个廉价的外部验证信号。基准自带它,生产环境往往没有。
这是《Agent Harness 工程》系列的第二章。上一章界定了 harness 与 agent、workflow 的分工, 这一章处理控制流:ReAct、Reflexion 这些推理范式分别要求承载层提供什么能力, 以及它们被报告的收益究竟来自哪里。
结论先放在这里:推理范式不是你实现的,是你支撑的。 决定一个范式在你的场景里有没有用的,不是它的结构本身, 而是承载层能不能提供它暗中依赖的那个信号。
一、范式对承载层提出的要求
四类主流范式,各自需要 harness 提供的能力并不相同。
ReAct(arXiv:2210.03629,2022-10)让模型在同一条轨迹里交替生成思考与动作, 思考用于追踪计划与处理异常,动作用于调用外部接口获取事实。 它对承载层的要求最低:只需要把思考与工具调用放进同一个消息序列,并完整保留到下一轮。 今天绝大多数 agent 框架的默认循环就是它的实现。
Plan-and-Solve(ACL 2023)与 Tree of Thoughts(arXiv:2305.10601,2023-05) 要求的是状态管理。前者先产出计划再逐子任务执行,承载层要能持有这份计划并在执行中比对; 后者把中间推理显式化为可搜索的节点,要求状态可分支、可回溯—— 这意味着承载层需要真正的检查点与分叉能力,而不只是一条线性历史。
Self-Refine(arXiv:2303.17651)与 Reflexion(arXiv:2303.11366)要求的是反馈通道。 Reflexion 把失败轨迹转化为自然语言反思,写入情节记忆,在下一次尝试时作为上下文影响决策。 承载层要能留存失败轨迹、并把反思结果带进下一轮。
CodeAct(ICML 2024)用可执行代码取代 JSON 动作,动作天然具备组合与控制流。 它对承载层的要求最重:需要沙箱、资源限额、以及对副作用的幂等处理—— 错误形态从「格式不对」变成了「跑了一半留下了脏数据」。
到这里为止,这些都是教科书式的对应关系。真正的问题在下一节。
二、收益归因:被报告的效果来自哪里
过去两年有一批工作专门检验这些范式的收益来源,结论比原始论文谨慎得多。
ReAct 的收益可能不来自交替结构。一组消融实验(arXiv:2405.13966,2024-05)发现, 替换或扰动推理轨迹的内容对结果影响甚微,而示例与查询的相似度才是主要变量—— 作者的判断是,所谓推理更接近一次近似检索。
Tree of Thoughts 的收益集中在生成端。后续分析(arXiv:2410.17820,2024-10)显示, 放大生成组件能显著提升效果,放大判别器只有边际提升;并且 ToT 并非在所有模型上都优于 简单方法。搜索结构本身不是收益的主要来源。
Reflexion 最关键的那条批评来自自纠错本身。Huang 等人的工作(ICLR 2024,arXiv:2310.01798) 证明,在没有外部反馈的「内在自纠错」设定下,模型往往无法识别自身错误, 性能甚至在自纠错之后下降。Kambhampati 组在推理与规划任务上得到一致结论。
把这三条放在一起看,有一个共同的模式:这些范式在基准上的增益, 高度依赖基准自带的外部验证信号。Reflexion 在 HumanEval 上的成绩依赖单元测试, Voyager 的技能库依赖 Minecraft 的即时环境反馈,ToT 在 Game of 24 上依赖可判定的算术答案。 这些信号廉价、即时、客观——而生产环境里的任务,大多数一样都不占。
Kambhampati 的立场论文(arXiv:2402.01817)把这件事推到了结论上: 自回归模型无法完成可靠的自验证,可行的路径是 LLM-Modulo——把外部验证器放进回路, 而不是指望模型自己判断自己做对了没有。
对承载层来说,这条结论非常具体:与其实现反思机制,不如先回答一个问题—— 这个任务有没有一个廉价的、客观的成功判据。有,就把它接进循环;没有, 反思结构只会让模型用更多 token 确认自己是对的。
三、我们的选择:为没有判据的领域造一个判据
上一节的结论指向一个具体的工程动作:与其让模型自我验证,不如把外部验证器接进回路。 难点在于,很多任务看起来根本没有现成的判据可接——这时候真正该做的是把判据造出来。
跨设备同步是我们遇到的最典型的一个。两台设备各自离线修改、重新连上之后应该收敛成什么状态, 人眼看不出对错:它涉及并发顺序、时钟偏移、删除与重命名的因果关系。模型更判断不了—— 它既无法穷举并发交错,也没有立场判定哪一边的时间戳可信。
我们的做法是造一个验证装置:一个参考服务端,加两个跑真实同步栈的子进程, 按固定剧本与种子化的随机序列执行,全部通过时退出码为 0。 失败时报告落盘,并且可以用同一个种子原样复现。 另有一条冒烟路径走同一份客户端代码打开发环境,覆盖注册、租约、fencing、 幂等与推送时延——这些同样是靠读代码无法确认的行为。
造完之后,「这次改动有没有破坏同步」从一个需要人工判断的问题,变成了一个退出码。
这就是把验证器放进回路的工程形态:不是让模型反思,而是让正确性变成可执行的断言。 它对 agent 的意义在于,一个能被调用、返回明确成败的程序, 才是模型可以据以纠错的信号;「看起来对」不是。
判据造出来之后还有一步:它必须能被 agent 直接调用,否则模型会用模糊手段自己猜。 我们在插件开发工具链上验证过这件事的反面。当时技能文档里没有列全可用的验证工具, 也没有写明判据,结果是:agent 验证页面渲染时截了图、做了五次文字识别, 而能直接返回结构化内容的读页面工具一次都没有被调用。 它不是不会验证,是在它看得见的工具里,截图加识别就是最接近验证的手段。 修法不是在提示词里加约束,而是把工具按用途列全,并直接写明判据—— 文本与结构走读页面断言,截图只保留给需要人眼判断视觉效果的场合。
至于规划,我们保留了它,但保留的形态是一个工具,不是一个节点。 模型可以调用待办工具写下计划、勾掉完成项,也可以完全不用。 工具把规划变成模型可选的表达,节点则用架构强制它必须经过某个步骤—— 上一节的那些消融实验说明,后者假设的「模型需要被结构纠正」通常不成立。
四、代价:验证程序会悄悄失效
造判据不是一次性投入。验证程序与被验证对象是耦合的,后者演进之后前者会失效, 而且失效是静默的——它不报警,只是不再检验任何东西。
我们另一个仿真装置就处在这个状态。它用一个假宿主按剧本喂画面,检验一套循环逻辑 能否自愈、会不会打转、认不出目标时如何退出。被测的那一侧后来新增了一条接管能力, 假宿主没有跟上,于是整套用例在调用不存在的接口时集体报错。 从那一刻起它检验的不再是循环逻辑,而是自己的装配是否完整—— 但它不会告诉任何人,直到有人真的跑一次,看到满屏的失败。
这引出验证程序的第一条纪律:它必须被定期执行。 一个从不运行的验证程序,与不存在的区别只在于它给了你一种错觉。
五、另一项代价:错误必须送达
除了判据本身,还有一件事决定模型能不能纠错:失败要变成它读得到的一条消息。
这件事比它听起来难。我们踩到的是三层:
第一层,异常根本不产生消息。框架把工具抛出的异常包成中间件错误直冒到图的顶层, 而编排库默认的工具节点错误处理拦不住它——结果是不产生任何工具回执, 那条工具调用永远停在「运行中」,整轮静默结束。模型没有收到任何失败信号, 因为压根没有信号被生成。
第二层,工具自己兜不住全部错误。工具内部的异常捕获只能覆盖执行期。 而参数校验发生在工具函数被调用之前——按 schema 校验入参失败时, 工具函数根本没有执行,内部的捕获逻辑无从生效。典型的表现是 「接收到的工具输入与预期 schema 不符」这类错误完全逃逸。
我们的处理是在工具调用的最外层包一道守卫,把任何异常统一转成一条错误状态的工具回执写回去, 让本轮循环继续、模型据此纠错,而不是让整个任务终止。
第三层,有两类信号必须原样上抛,不能被兜底。 一是用户主动中止——那不是「工具失败可重试」,而是「停止本轮」, 兜成一条工具回执会让 agent 在用户已经点了停止之后继续往下跑。 二是编排库的控制流信号,比如向用户提问所触发的中断: 它必须原样上抛去暂停整张图,否则提问会被吞成一次「工具调用失败」, 用户永远收不到问题,而 agent 拿着一条假错误去重试。
这三层加起来才是「让模型能自我纠正」的真实工程量。它和反思机制没有关系, 和提示词也没有关系——它只是确保出错的时候,模型确实收到了一条说明出错的消息。 在我们的实践里,这一层带来的纠错效果,远大于任何显式的反思结构。
六、一个可以直接用的判据
面对某个「推理增强」的提案时,我们现在只问三个问题:
- 这个任务有没有廉价、客观、即时的成功判据?有(编译通过、测试通过、文件确实生成、 接口返回预期状态码),就把它接进循环。没有的话,不要先去实现反思结构, 而应当先估算造一个判据的成本——多数时候它比反思结构更便宜,也更管用。
- 这个判据能不能被 agent 直接调用?只存在于流水线里、或者只有人能跑起来的检查, 对模型等于不存在。模型看得见什么工具,就只会用什么工具去验证。
- 失败现在能不能送达模型?如果答案是否定的,那么在补上这条之前, 任何范式层面的改造都不会生效——模型连自己失败了都不知道。
后两个问题最容易被跳过,也最值得先回答。
七、本章的结构,哪些会过时
这个系列的每一章都以同一个问题收尾。Anthropic 在 Managed Agents 里有一句值得反复引用的话: 「Harnesses encode assumptions that go stale as models improve.」 Harness 编码的是关于模型能力的假设,而这些假设会随模型进步而失效。
本章的内容里:
会过时的是第一个问题里「廉价」的阈值。今天需要专门造一套仿真装置才能判定的领域, 随着模型对复杂状态的理解变强,可能只需要一份结构化的现场快照就足以判断。 判据本身不会过时,但「必须造到多重才够用」会持续下移, 所以这笔投入值得每隔一段时间重新估算一次,而不是一次决定、长期照搬。
不会过时的是后两个问题。判据能否被调用、失败能否送达,都是纯工程约束, 与模型能力无关:模型再强,也无法调用一个它看不见的程序, 更无法对一条从未生成的消息作出反应。
本文的实践背景是 Solazah —— 一个 AI 驱动的启动器,无限的工具:solazah.com
