从循环到图:Agent 系统的编排工程 · 第 5 / 6 节

第 5 课:评审回路,以及把模式组合成图

学习目标:

  • 实现评审-优化回路(生成一版、评一版、按意见再改一版),并用两个判断标志决定这个回路值不值得建
  • 写出比「最多 N 轮」更聪明的停止条件,并把确定性检查排在裁判前面
  • 把五个模式组合成本课所称的「图」,并在自己的文档里说清这套画法是自创隐喻、锚在哪句一手依据上

前置要求:读完第 1–4 课(谁持有计划、链式与路由、并行化、编排者-工人),能手写一个以 stop_reason 驱动的 harness 循环 | 上一课 第 4 课 << | 下一课 第 6 课 >>

一版产出总差口气

你让一个 agent 写一份数据库迁移方案。第一版交上来,读着像那么回事:背景、步骤、时间窗都有。但你扫一眼就发现两个洞——回滚步骤只写了一句「必要时回滚」,风险等级压根没排。你在聊天框里回了两行字把这两点说了,第二版交上来,两个洞补上了,整份东西的可用度上了一个台阶。

这个过程你已经重复过几十次了。每次都是:产出差口气,人提两句意见,产出明显变好。

问题不在于模型写不好,而在于你的那两句意见并不难产生。「回滚步骤要具体到命令」「每一步要标风险等级」——这是一份检查清单就能覆盖的东西。既然你能把它说清楚,模型多半也能把它说清楚。那这两句话为什么要每次都由你来说?

这个形状值得写成一个回路。

评审-优化:把「再改一版」写进控制流

一手材料对它的定义只有一句:一个 LLM 调用生成响应,另一个在回路里提供评估与反馈1

前四课的四个模式各有各的拓扑:链式是把任务拆成一串步骤、每步处理上一步的输出1;路由是分类后分发到专门的后续任务1;并行化是同时跑、结果由代码聚合1;编排者-工人是让一个中心 LLM 动态拆解任务、派给工人、再合成结果1。它们的共同点是数据一路向前流。评审回路是第一个带回边的模式——产出会绕回去,重新进入生成节点。

它在产品里长什么样,Claude Code 的动态工作流文档给了一句大白话:跑一个检查器、修掉没过的、重复直到通过或不再有进展2。另一句描述的是同一分工的另一种用法:让独立的代理互相对抗式地评审对方的发现,之后再上报2。它是上报前的一次交叉互评,没有回边、不迭代——把它并进评审回路是本课的归类,不是原文说的同一个拓扑。

一个词汇的三种叫法

这里要显式搭一座桥,不然你会以为自己在学三样东西。

本系列第 6 门课教多 Agent 协作时,把「一个产出、一个挑刺」这种分工叫生产者-评审者。那是我们自己的教学叫法。一手材料里,Claude Code 的工作流文档用一句话描述了它在产品里的用法:让独立的代理对抗式地互审彼此的发现(「对抗式互审」是本课给这句起的简称)。同一个形状,一手材料里有两个说法:Anthropic 的模式原典叫它 evaluator-optimizer(评审-优化)1;Claude Code 的工作流文档不给名字,只描述用法——就是上面那句「对抗式地互审彼此的发现」2

三个名字,一个形状。区别在你从哪个角度看它:讲协作分工时看到的是两个角色,讲编排模式时看到的是一条回边,讲产品能力时看到的是一种可复用的质量手法。

还有一层分工要说清楚。本系列第 10 门课花了整整一门的篇幅教你怎么当好一个裁判:怎么写量表、怎么收紧裁判的输出格式、为什么裁判要有自己独立的上下文、为什么干活的那个不能当裁判。那门课教的是裁判本身的质量。本课不重复那些,本课教的是怎么把裁判接进控制流——它在回路的哪个位置、什么时候跑、跑几轮、什么时候停。

什么时候值得建这个回路

一手材料给的适用判据是:评估标准清晰、并且迭代打磨能带来可测量的价值时,这个工作流特别有效;两个好匹配的标志是,第一,当人把反馈说清楚时,LLM 的响应确实能被明显改进;第二,LLM 自己也能给出这种反馈1

这段引文你见过。本系列第 10 门课引的就是这同一句,当时用它回答的问题是「评审—修改回路值不值得建」。同一句判据,换到编排语境下问的是同一件事,只是这次你要把答案落成控制流里的一个环。

拆开用,这两个标志各自在挡不同的坑:

第一个标志挡的是「改也白改」。 有些任务,人把意见说得再清楚,第二版也不会更好——因为问题出在输入数据缺失、或者任务本身定义模糊,不在产出的措辞上。这种情况下建回路,你只是在花两倍的钱得到两版同样不可用的东西。验证方法很土但有效:你自己先手动做三次。三次里有几次是「人一提就明显变好」?如果三次里有两次是「提了也没用」,别建。

第二个标志挡的是「裁判给不出那种反馈」。 就算人一提就好使,也得问:模型能不能自己提出同类的意见?如果你的意见依赖只有你知道的东西(这个客户上季度投诉过什么、法务上周口头交代了什么),模型没有这些信息,它给出的反馈就是另一种东西了。这时候要么把那些信息喂进裁判的提示词里、把它变成模型能判的标准,要么承认这个环节需要人。

还有一个前置条件比这两个标志更早生效:评估标准清晰。 标准不清晰的时候,回路会稳定地产出一种特定的失败——裁判每轮给出方向不同甚至互相矛盾的意见,产出在两个版本之间来回横跳,轮数烧完,最后一版还不如第一版。这不是回路的问题,是标准还没定下来。

确定性的检查排在裁判前面

一手材料对确定性系统与非确定性系统的定义式区分是:确定性系统给定相同输入每次产出相同输出,而像 Agent 这样的非确定性系统,即使起始条件相同也可能生成不同的响应3

裁判是非确定性的。任何一条「代码能判死」的规则交给裁判,你都是拿一个每次结果可能不同的东西去判一件本来每次结果都该相同的事——同时还多付一次模型调用的钱。

本系列第 10 门课把这条纪律叫分层判分:能用代码判的用代码判,代码判不了的才交给模型。本课把它照搬进回路的节点顺序里。第 2 课引过的那句在这里照样管用——你可以在任意中间步骤上加程序化检查,确保流程还在轨道上1。回路里的每一版草稿都是一个中间步骤。

具体到迁移方案那个例子:「每一步是否都有对应的回滚命令」可以用正则或结构化解析判死,属于关卡;「回滚命令写得是否可信」得靠裁判。前者没过就直接把缺哪几步告诉写作者,连裁判都不用惊动。

回路的代码骨架

几个细节值得单说。

runAgent 是一个完整的 harness 循环。 这一点从第 2 课起就没变过:这段脚本里的每个 await runAgent(...) 背后,都是本系列第 7 门课那个以 stop_reason 驱动的循环在跑。这里只是在循环外面又套了一层代码写的控制流。

这几个 reason 是不同的结局,别把它们混成一个布尔值(真实系统里常常还要再分,比如关卡连续失败单列一档)。 passed 可以直接交付;max-rounds 意味着轮数烧完了但还没过,多半要转人工;no-progress 意味着模型卡住了,继续烧钱也不会更好。这三种结局在观测数据里应该是三条能分开数的线——本系列第 11 门课教的那套记录方式,在这里要落到 reason 这个字段上。

关卡失败也算一轮。 continue 之前 rounds 已经加过了。这是有意的:关卡反复不过说明写作者的提示词有问题,让它无限重试只会把钱烧在同一个坑里。

两种停止条件都要有。 一手材料谈 Agent 循环时说过:任务常常在完成时终止,但加上停止条件(比如最大迭代数)来维持控制也很常见1。这是「最多 N 轮」的出处,它是一根保险丝——保证这段代码在任何情况下都会停。而「不再有进展」这个停法来自另一处:跑一个检查器、修掉没过的、重复直到通过或不再有进展2。它比保险丝聪明,因为它盯的是这一轮有没有比上一轮好,而不是跑了几轮。

分数没涨就退出是最省事的实现,但不是唯一的实现。如果你的裁判不输出分数,可以改成盯未通过项的数量有没有减少;如果任务本身波动大,可以改成「连续两轮没涨才退出」,用一个 stalled 计数器。挑哪一种取决于你的裁判有多稳,不取决于哪种听起来高级。(注意骨架里那个 bestDraft:分数没涨就退出的前提,是你手上一直留着分数最高的那一版;只跟 bestScore 不留 bestDraftno-progressmax-rounds 两个出口就会把更差的当前版交出去。)

把模式组合起来:本课把它叫「图」

五个模式到这里就齐了。接下来的问题是怎么把它们摆在一起。

先给官方立场:这些构件不是硬性规定,它们是开发者可以塑形和组合、以适配不同用例的常见模式;成功的关键,和任何 LLM 功能一样,是测量表现并对实现做迭代1

也就是说,「怎么组合」这件事,一手材料给的是许可,不是配方。配方得你自己写。

图术语的诚实声明

本课从头到尾用的这套「图 / 节点 / 边」的画法,是本课自己的工程隐喻,不是官方术语(前面几节已经用到「节点」「回边」,用的就是这个意思)。

这句话请照抄进你自己的架构文档。「图」「节点」「边」「DAG」「状态机」这些词,在本课引用的全部一手来源里一次都没有出现过。一手材料的词汇是 workflows(工作流)、patterns(模式)、orchestrator-workers(编排者-工人)、fan out(扇出)——它讲的是模式清单,不是拓扑结构。

我们仍然要用「图」这个词,因为五个模式摆在一起时需要一种语言把它们说清楚,而「图」是最省力的那种。但这个隐喻必须有个锚点,不然它就是凭空发明的行话。锚点是一手材料里真实存在的这一句:工作流脚本自己持有循环、分支与中间结果,模型的上下文里只装最终答案2

这句话已经把图的三个要素说全了:循环(回边)、分支(分叉点)、中间结果(状态)。我们做的只是给它们各起了个名字。

画法约定

在本课的画法里:

  • 节点 = 一次 runAgent 循环,或者一段纯代码(关卡、分类、聚合、切批)。标注每个节点是哪一种,是画图时最有价值的动作——它逼你回答「这一步真的需要模型吗」。
  • = 「谁的输出喂给谁」。边不是数据结构,只是脚本里下一行代码读了上一行的变量。
  • 状态 = 脚本变量。一手锚点在这里也有一句:中间结果留在脚本变量里,而不是落进模型的上下文2没有「节点间传递的状态对象」这种官方概念,那是我们从别的领域借来的说法;本课不建这个抽象,需要什么就传什么变量。

五个模式在这套画法下的形状

text
链式        A ──> B ──> C                    一条线
路由           ┌──> B1            A ─┼──> B2                       一个分叉点               └──> B3
并行           ┌──> W1 ──┐            A ─┼──> W2 ──┼──> 汇合           一把扇子:扇出,再汇合               └──> W3 ──┘
编排者-工人    ┌──> W? ──┐                   分叉点是动态的:有几条边、            A ─┼──> W? ──┼──> 汇合           每条边干什么,由 A 看了输入才定               └──> W? ──┘
评审回路    A ──> J ──┐                      一个带回边的环            ^         │            └───否────┘

第四个形状的注解值得重读一遍。并行化和编排者-工人在形状上长得一样(原文用的词是 topographically similar),关键差异是子任务不是预定义的,而是由编排者根据具体输入决定的1。画在纸上,这个差异就是「三条边是我画的」和「三条边是 A 画的」的区别——纸面看不出来,但代码里差得很远。

一个组合示例

把路由、扇出、汇合、评审回路串在一起:

text
[分类] ─┬─ 简单 ──> [直接答] ──────────────────────> 交付        └─ 复杂 ──┬─> [工人1] ─┐                  ├─> [工人2] ─┼─> {汇合} ─> [起草] <──────┐                  └─> [工人3] ─┘                │          │                                                v          │                                             {关卡} ─否─────┤                                                │ 过        │                                                v          │                                             [评审] ─否─────┘                                                │ 是                                                v                                               交付
节点类型:[ ] = runAgent 循环    { } = 纯代码边 = 谁的输出喂给谁。{关卡} 是确定性检查,排在 [评审] 之前;关卡未过或评审否,都打回 [起草]。[分类] 这里画成模型循环而不是 {纯代码},是因为退款/技术/投诉的边界模糊——边界清楚时应换成传统分类器(这就是分层判分:能用代码判的先用代码判)。

第 6 课实现的是这张图的一个变体:那批工单的验收标准恰好都能写成规则,所以 [评审] 那层退化成一道 {关卡},扇出也从「一条复杂条目分给三个工人」换成「一批工单各派一个处理者」。哪几处变了、为什么变,第 6 课开头会逐条列。这里先看形状,代码留到下一课。

组合带来的工程红利

把控制流搬进代码,好处不只是「看得懂」。有几条是有一手依据的:

逐步留痕带来可恢复。 运行时会随着运行推进逐个追踪每个代理的结果,这正是一次运行能在同一会话内恢复的原因2。翻译成本课的画法:图的每个节点天然就是一个检查点位置——节点跑完,结果落进脚本变量,这个变量就是「跑到哪了」的记录。本系列第 9 门课教的检查点设计在这里不用另起炉灶,节点边界就是天然的落点。

细粒度扇出保住更多进度。 一手材料的原话是:把工作扇出给许多小代理的工作流,比一条长代理保住更多进度2。一条跑了四十分钟的长代理挂掉,四十分钟全没了;四十个各跑一分钟的小节点挂掉一个,你只丢一分钟,而且知道丢的是哪一分钟。

可重复的质量手法能复用。 把计划搬进代码,还让工作流可以套用一种可重复的质量模式,而不只是多跑几个代理:它可以让独立的代理对抗式地互审彼此的发现之后再上报,或者从几个角度各起草一份方案再互相权衡,从而得到比单次通过更可信的结果2。这里的关键词是「可重复」——手动做一次互审是操作,写进脚本才是能力。

确定性护栏包着非确定性的代理。 Anthropic 在多 Agent 研究系统的复盘里写道:他们把基于 Claude 的 AI 代理的适应性,与重试逻辑、定期检查点这类确定性护栏结合起来4。翻译成本课的画法:图的骨架是确定性的(谁调用谁、什么时候停、失败了走哪条边),节点内部是非确定性的。这个分层不是审美偏好,是让系统可运维的前提。

组合的纪律:每加一层都要过一关

红利说完了,说约束。

每加一层都要过「可测量的改进」这一关。 一手材料在两处叮嘱同一件事,第二处还专门加了「重申一次」:只有当复杂度能明显改善结果时,你才该考虑增加它1。这条对本课尤其要命——五个模式摆在你面前,最容易犯的错是全都用上。多加一个节点,就多一次模型调用、多一处可能失败的地方、多一个要排查的对象。加之前先问:去掉它,指标会掉吗?答不上来就说明还没测。

节点级的重试与超时是工程实践,不是官方设计。 一手材料关于这件事只有一个从句(重试逻辑与定期检查点这类确定性护栏4)。所以下面这些是以工程实践的口径写的,你不会在任何一手文档里找到它们的背书:给每个 runAgent 节点包一层超时,超时后要么重试要么把这个节点标为失败继续走;重试次数按节点的性质定(只读的检索节点可以多试几次,有副作用的写入节点最好一次都别自动重试);节点失败时要区分「这条边可以跳过」和「整张图必须停」,别让一个可选节点的失败拖垮整次运行。这些都是普通的分布式系统常识,只是搬到了 Agent 上,不要把它们当成什么新东西。

深度是有界的。 产品级的参照摆在那里:默认情况下,一个子代理可以再派生自己的子代理,最多到主对话之下三层5。三层这个数字不是本课发明的门槛,但它传达的意思很清楚——真实产品里的嵌套深度不是无限的,有人认真想过在哪停。你的图也该有个类似的答案。如果你画出来的图有五层嵌套,先怀疑是任务拆得太碎,而不是先去想怎么支持更深。

图不是目标,是任务形状的描述

有一种失败模式在这门课的最后特别值得防:先选一个酷炫的拓扑,再找任务往里塞。

顺序应该反过来。先把任务本身的依赖形状画出来——哪几步必须排队(前一步的输出是后一步的输入),哪几步互不相干(谁先跑都一样),哪一步需要看了输入才知道要拆成几份,哪一步的产出需要有人挑刺才靠得住。这张图画完,用哪几个模式基本上已经定了:排队的地方是链,互不相干的地方是扇子,看了才知道的地方是编排者,需要挑刺的地方是回路。

模式是任务形状的名字,不是可以任意挑选的菜单。

而这条纪律之上还有一条更早生效的:找到可能的最简单方案,只在需要时才增加复杂度1。这句话在第 1 课就出现过,在本课结尾还是同一句。五个模式学完之后,「一次 LLM 调用就够了」仍然是一个完全合法的答案——一手材料自己也说,对很多应用来说,用检索和上下文示例把单次 LLM 调用优化好通常就足够了1

完整的、能跑的组合代码在第 6 课。本课到此为止,你手上有的是五个模式、一套画法,和一份关于什么时候不该用它们的清单。

💻 练习

小结

  • 评审-优化是一个 LLM 调用生成响应、另一个在回路里提供评估与反馈1;它在产品里的形态是「跑一个检查器、修掉没过的、重复直到通过或不再有进展」2,对抗式互审是同一分工的另一种用法(单次交叉互评,不含回边),把它并进评审回路是本课的归类2。本系列第 6 门课叫它生产者-评审者,那是我们的教学词汇,一手词汇是 evaluator-optimizer1
  • 值不值得建看两个标志:人把反馈说清楚时 LLM 的响应确实能被明显改进,以及 LLM 自己也能给出这种反馈;评估标准清晰、迭代打磨有可测量价值时它特别有效1。标准不清晰的时候,先定标准,别先建回路。
  • 停止条件不止一种:最大迭代数这类停止条件是用来维持控制的1,「不再有进展」是另一种更省钱的停法2;三种结局(通过 / 烧完轮数 / 没进展)对应三种不同的下游动作,别混成一个布尔值。确定性的关卡排在裁判前面。
  • 五个模式可以组合:这些构件不是硬性规定,是开发者可以塑形与组合以适配不同用例的常见模式,成功的关键是测量表现并对实现做迭代1
  • 「图 / 节点 / 边」是本课自己的画法,不是官方术语;它的一手锚点只有一句——工作流脚本自己持有循环、分支与中间结果,模型的上下文只装最终答案2,加上中间结果留在脚本变量里这一条2。在自己的文档里用这套词汇时,把这段声明一起写上。
  • 组合的红利有据可查:运行时逐个追踪每个代理的结果,这是一次运行能在同一会话内恢复的原因2;把工作扇给许多小代理比一条长代理保住更多进度2;把计划搬进代码还能套用可重复的质量手法(对抗式互审、多角度起草再权衡)2;确定性护栏(重试逻辑与定期检查点)包着非确定性的代理4
  • 约束同样清楚:只有当复杂度能明显改善结果时才该考虑增加它1;节点级的重试与超时是普通工程实践,一手只有一个从句的背书4;深度也有界,产品级的参照是子代理最多嵌套到主对话之下三层5;最简单的方案优先1

>> 第 6 课:实战:把你的 harness 升级成一张小图

Footnotes

  1. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

  2. Orchestrate subagents at scale with dynamic workflows — Claude Code 官方文档 — https://code.claude.com/docs/en/workflows 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  3. Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents

  4. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3 4

  5. Create custom subagents — Claude Code 官方文档 — https://code.claude.com/docs/en/sub-agents 2

练习

01

不写代码。用本课的画法给下面两个任务各画一张 ASCII 图(用 text 围栏),每个节点都要标注它是 runAgent 循环还是纯代码。

Level 1:画两张图,并给回路设计停止条件

任务一 · 客服工单:工单进来先分类(退款 / 技术 / 投诉),按类走不同的处理流程;处理完做风险分级,高风险的那些要经过一个评审回路才能发出,低风险的直接发。

任务二 · 40 个模块的代码评估:一次要评估 40 个模块,分批扇出、并行评,跑完汇合,再由一个节点写汇总报告;报告要过一道 gate(40 个模块是否都有结论、分数是否在合法区间、引用是否可解析),没过就退回重写。

画完之后,为这两张图里各自的那个回路(任务一的评审回路、任务二的报告 gate 回环)分别设计停止条件:最大轮数是必备的保险丝,不参与挑选;在「通过」与「不再有进展」之间定出哪个是主要停法,并说明你为什么留下或放弃另一个。

完成标准 · 本地勾选
02

不写代码。用本课的两个判断标志——(1) 人把反馈说清楚时,产出确实能被明显改进;(2) LLM 自己也能给出这种反馈——逐个裁决下面三个场景,并给出你的处理建议。

Level 2:三个场景,判这个回路值不值得建

场景一 · 文案打磨:一个营销落地页的首屏文案生成器。产品经理说,现在生成的文案「能用但没劲」,她每次都会提两三条具体意见(卖点没落在第一句、行动号召太软、长度超了移动端两行),提完之后的版本明显更好。团队问要不要上一个评审回路。

场景二 · 财务合计校验:一个从原始凭证生成月度财务摘要的 agent。摘要里的各项合计经常对不上,有人提议加一个「审计 agent」,让它读摘要、挑出算错的地方、退回去重算,直到审计 agent 认可为止。

场景三 · 组件命名风格:一个给新组件起名并写文档的 agent。三个前端同事对产出的评价长期不一致:A 说命名太啰嗦,B 说不够描述性,C 觉得都行但文档语气太正式。有人提议上评审回路,让一个「风格评审 agent」把关。

完成标准 · 本地勾选

我的笔记

记下想法、痛点、没懂的地方。只写进这门课的附录,正课文件不动。