几乎每一个多步骤 Agent,打开来都是一条队列:第一步、第二步、第三步……每一个都礼貌地等着上一个做完。
仔细一看你会发现——大约一半的步骤,根本没有东西需要等。
它们不路由、不拆分、不并行。它们只是站成一排:一个脑子、一个上下文、一次只做一件事,直到窗口被塞满,Agent 悄悄忘记自己一开始在干什么。
有个没人明说的真相,这篇文章要把它讲透:
这就是「Graph Engineering(图工程)」。这堂课带你一步步,把一条「排队」的 Agent,改成一张能「扇出、自检、收敛」的图。
一张图只有两个零件,把它们搞清楚,大部分困惑就消失了:
一个 Agent、一个有边界的任务、一个输入进、一个输出出。
它说:这个节点的输出,喂给那个节点的输入。就这么简单。
「总结这个文件,然后告诉我天气」——这句话里根本没有边。天气不消费总结。这是两个完全不相连的节点,被线性脚本毫无理由地串在了一起。
对 Agent 里每一个「然后」,问一句:下一步真的读了上一步的输出吗?
如果没有——那里就没有边,那个等待就是纯粹的浪费。
「查一下今天的天气,然后写一封给客户的邮件」
「先抓取 10 个来源,然后去重、合并成一份摘要」
当你写「先 A,再 B,再 C,再 D」,你已经画了一张图:一条没有分叉的链,每个节点恰好一条边进、一条边出。
链没有冗余。如果 C 卡住,D 永远不会发生,A 产出的所有东西都堵在上游无处可去。
这是最差的一张图:无分叉、无冗余、一个节点死全链死。
拿起你的线性 Agent,走一遍每一条箭头,问第 1 课那个问题。实际你会找到 2-3 条「不传递任何数据」的箭头——它们只存在于那里,因为你刚好是按那个顺序打字的。
砍掉那些空箭头,链就「侧向塌缩」成更宽的形状:
几个独立的节点可以同时跑,一起喂给那个「需要它们全部」的节点。
一个你无法「推理」的节点,是一个你无法「并行」的节点。解法是契约:
在 workflow 里,用 schema 强制:给 agent() 挂一个 JSON schema,它 spawn 的子 Agent 就被迫返回校验过的结构化数据。校验发生在工具调用层,不匹配时 Claude 会重试,而不是丢给你一堆要「祈祷着解析」的自由文本。
边不是「B 在 A 后面」,而是一个承诺:A 产出这个形状,B 就是被设计来消费这个形状的。
按「数据」给边命名,而不是按「顺序」,两件事会容易得多:
这是「回本所有其他东西」的一招。当你有 N 个独立节点——N 个来源要查、N 个文件要审、N 条路线要审计——不要串行。
在 workflow 里,parallel() 接受一个 thunk 数组,每个 thunk spawn 一个子 Agent,并发运行,然后交回结果数组。
↑ 同时跑,而不是排队
parallel() 是一个屏障——等每一个 thunk 都跑完才返回,下一阶段永远看到完整集合。null,而不是拖垮整批——一个「抽风」的 Agent 不会让整轮翻车。出结果时记得 .filter(Boolean)。并发度被核心数封顶,多的会排队——你交给它 100 个 thunk 也都能跑完,只是一小撮一小撮地跑。
扇出活在 Claude 写的代码里,不是模型对话里。Claude 自己的上下文从不同时装 9 个来源——每个子 Agent 各背各的,只有最终答案回来。
这就是为什么一个 workflow 能 scale 到几十上百个子 Agent 而不把会话淹死。编排层不花钱,因为它不是 Claude 的又一轮思考。
扇出只有在「有东西把它聚起来」时才有价值。收拢(fan in)是边汇合的那个节点——一个 Agent 或一段代码,同时看到所有上游结果,做一件真正需要「全部集合」的事:跨来源去重、按影响排序、如果全空就提前退出。
一个扇出 + 一个收拢,就是每张严肃 Agent 图的「主力拓扑」——钻石(diamond)。
一个节点拆任务 → 多个节点并行做 → 一个节点合并。市场扫描、依赖审计、代码审查、研究报告,都是这个形状。换掉来源和 prompt,同一个骨架全都适用。
barrier 有真实的墙钟时间成本。规则是:只在「这一阶段真正需要所有先前结果」时才用。
「把所有来源去重,然后按影响排序」
「只是把一个列表 flatten 一下,中间那步没有跨项依赖」
不是每张图都是固定的。有时走哪条边,取决于某个节点「发现了什么」。路由节点检查结果,决定哪条下游路径点火:给工单分类 → 分支到对应处理器;看 diff 大小 → 快速审查 or 全量审计。
在 workflow 里,这就是一个 JavaScript 的 if 或 switch——因为你的控制流活在代码里。
路由的「判断」可以是 Claude 驱动的(子 Agent 分类),但「路由」本身是 Claude 写的代码——同一个分类,每次都以同样的方式跑。
你在节点上得到 Claude 的判断,在边上得到脚本的可靠。不会有「Claude 今天跳过了审计」这种突发情况——因为跳过必须被写进图里,而它没被写进去。
图的真正杠杆不是「更多 Agent」,而是你能围绕它们包上一层产出『置信度』的结构。验证器节点坐在边上,在结果被放行下游之前,唯一任务就是试图证伪它。活下来就通过,活不下来就到不了你的答案。
正是这个模式,让一个真实团队把 Bun 运行时移植进了对抗式代码审查。
链里,失败会级联:C 死了,D 不跑,全链停。图里,失败应该被「关在」它的节点里。
一部分已经天然成立:parallel() 里抛异常的 thunk resolve 成 null,8 个好 Agent 照样返回,那个坏的掉队。你的 .filter(Boolean) 就是「隔离」。设计每个收拢时,要「容忍缺失输入」,而不是假设全集。
更隐蔽的是「节点互相踩」——当多个 Agent 并行写文件时,它们会撞车。解法是隔离:worktree。每个 Agent 在自己的 git worktree 里、在沙箱里干活,最后干净地 merge。
只在节点「真正并行写」时才用 worktree——它是某种特定拓扑的安全带,不是每次运行都要交的默认税。
有时你进入任务前,根本不知道活有多大——「未知规模的发现」。一个 bug 扫描,找到一个又冒出三个。这需要「循环」:一条受控的、回到更早节点的边。
危险很明显:不收敛的循环 = 无限循环,一直 spawn Agent 直到预算烧光。
收敛的模式是 loop until dry:持续 spawn「发现者」,直到连续 K 轮没有新东西,就停。
成败的关键——几乎所有人第一次都搞错——是:你针对什么去重?
✅ 对「所有见过的东西」去重,而不是只对「已确认的结果」去重。否则被否的发现每轮都回来,循环永远跑不干,你就造了一台「花钱反复重新发现同一个死胡同」的机器。
不是每个节点都需要你最好的模型。图让这件事一目了然:有些节点是「有界且重复」的——提取这个字段、分类这个工单;另一些才承载真正的判断——综合报告、裁决发现。
无聊的节点跑便宜模型,把贵的 token 花在「判断真正存在的地方」。
在 workflow 里,Claude spawn 的每个子 Agent 默认继承你的会话模型,除非脚本覆盖——所以一次大跑默认按你的会话档位全价计费。
单个 agent() 调用上的 model 选项,就是让 Claude 只把「那一个」节点路由到别处。大跑之前先 /model 看一眼,然后让 Claude 把扇出的重复节点降到便宜模型,同时把 merge 节点留在高位。
这就是把「吃 token 的图」变成「经济的图」的杠杆——完全不动形状。
图的形状不是装饰,是你在墙钟时间上最大的一根杠杆。几乎所有人都栽在 parallel() vs pipeline() 的选择上。
让一切等「最慢的节点」,下一阶段才能开始。
每项独立流经所有阶段,无屏障——A 在阶段 3 时,B 还在阶段 1。快的先完成,不等慢的。
默认用 pipeline()。只在「阶段真正需要全部结果」时才用屏障:跨集合去重、基于总量的提前退出、把一个发现和所有其他发现做比较。
「代码更干净」「阶段感觉更分开」都不是理由。屏障延迟是真实、可测、被浪费的时间。分开 ≠ 同步。
Q1. 「节点」和「边」分别是什么?
Q2. 怎么判断一个「然后」是不是真的依赖?
Q3. 循环里「几乎所有人都搞错」的去重细节是?
Q4. 默认该用 pipeline() 还是 parallel() 屏障?
你的 Agent 的天花板,几乎从来不是模型——是你交给它的「工作的形状」。
1️⃣ 砍掉不传数据的箭头——「然后」不是依赖。
2️⃣ 默认 pipeline(),而不是 barrier。
3️⃣ 对「所有见过的东西」去重,而不是「已确认的」。
这三条,比「往队列里再加一步」更能让你的 Agent 更快、更便宜、更难被弄坏。
「提问者问一个问题。架构师画一张图。你的 Agent 现在是一条线,还是一张图?」
原文:《Graph Engineering: Stop Chaining Your Agents》by seeco(@seeconvm)