开篇 · 一个普遍的错误

你的 Agent,其实在「排队」

几乎每一个多步骤 Agent,打开来都是一条队列:第一步、第二步、第三步……每一个都礼貌地等着上一个做完。

仔细一看你会发现——大约一半的步骤,根本没有东西需要等。

它们不路由、不拆分、不并行。它们只是站成一排:一个脑子、一个上下文、一次只做一件事,直到窗口被塞满,Agent 悄悄忘记自己一开始在干什么。

有个没人明说的真相,这篇文章要把它讲透:

「Prompt 是一句话。Loop 是一个循环。Harness 是 Agent 脚下踩的地板。
但『工作本身的形状』——什么先跑、什么能同时跑、什么真正必须等别人——那个形状,是一张图(graph)
节点负责思考,边负责传递结果。

这就是「Graph Engineering(图工程)」。这堂课带你一步步,把一条「排队」的 Agent,改成一张能「扇出、自检、收敛」的图。

第 1 课 · 节点和边

「然后」≠ 依赖

一张图只有两个零件,把它们搞清楚,大部分困惑就消失了:

🔵 节点(Node)= 一个工作单元

一个 Agent、一个有边界的任务、一个输入进、一个输出出。

🔗 边(Edge)= 一个依赖

它说:这个节点的输出,喂给那个节点的输入。就这么简单。

🎯 大多数人犯的错:把「然后」当成「边」

「总结这个文件,然后告诉我天气」——这句话里根本没有边。天气不消费总结。这是两个完全不相连的节点,被线性脚本毫无理由地串在了一起。

👆 点我看「要问自己的那个问题」

🎯 判断:下面哪对是「真正的边」?

「查一下今天的天气,然后写一封给客户的邮件」

对。邮件没读天气的输出,这两个节点没有数据流动,纯排队浪费。

「先抓取 10 个来源,然后去重、合并成一份摘要」

对。合并这一步必须读前面所有来源,这是真实的数据依赖。
第 2 课 · 线性链是退化图

你的线性脚本,已经是一张图——
只是最烂的那种

当你写「先 A,再 B,再 C,再 D」,你已经画了一张图:一条没有分叉的链,每个节点恰好一条边进、一条边出。

它「能跑」,但跑得慢、还坏得惨

链没有冗余。如果 C 卡住,D 永远不会发生,A 产出的所有东西都堵在上游无处可去。

A B C D

这是最差的一张图:无分叉、无冗余、一个节点死全链死。

第一个真功夫:重新画链

拿起你的线性 Agent,走一遍每一条箭头,问第 1 课那个问题。实际你会找到 2-3 条「不传递任何数据」的箭头——它们只存在于那里,因为你刚好是按那个顺序打字的。

👆 点我看「砍掉箭头之后」
第 3 课 · 契约

每个节点、每条边,都要有「契约」

节点契约:有界输入、有界输出、只干一件事

一个你无法「推理」的节点,是一个你无法「并行」的节点。解法是契约:

  • 输入:显式传入,而不是从「它恰好所在的某个共享窗口」里猜。
  • 输出:一个定义好的结构(最好经过校验),让下一个节点不用猜就能消费。

在 workflow 里,用 schema 强制:给 agent() 挂一个 JSON schema,它 spawn 的子 Agent 就被迫返回校验过的结构化数据。校验发生在工具调用层,不匹配时 Claude 会重试,而不是丢给你一堆要「祈祷着解析」的自由文本。

边也是数据契约

边不是「B 在 A 后面」,而是一个承诺:A 产出这个形状,B 就是被设计来消费这个形状的。

按「数据」给边命名,而不是按「顺序」,两件事会容易得多:

  • 你能立刻看出这条边「是不是真的」——数据真的流过吗?
  • 只要形状不变,你可以替换任一端节点,不动图的其余部分。
👆 点我看「一个安静的红利」
第 4 课 · 扇出

parallel():让 N 个节点同时跑

这是「回本所有其他东西」的一招。当你有 N 个独立节点——N 个来源要查、N 个文件要审、N 条路线要审计——不要串行。

parallel() 是什么

在 workflow 里,parallel() 接受一个 thunk 数组,每个 thunk spawn 一个子 Agent,并发运行,然后交回结果数组。

来源 1 来源 2 来源 3 … 来源 N

↑ 同时跑,而不是排队

两个让它健壮的细节

barrierparallel() 是一个屏障——等每一个 thunk 都跑完才返回,下一阶段永远看到完整集合。
null 兜底一个 thunk 抛异常会 resolve 成 null,而不是拖垮整批——一个「抽风」的 Agent 不会让整轮翻车。

出结果时记得 .filter(Boolean)。并发度被核心数封顶,多的会排队——你交给它 100 个 thunk 也都能跑完,只是一小撮一小撮地跑。

👆 点我看「为什么这能规模化」
第 5 课 · 收敛 & 钻石

扇出要有意义,就必须「收拢」

扇出只有在「有东西把它聚起来」时才有价值。收拢(fan in)是边汇合的那个节点——一个 Agent 或一段代码,同时看到所有上游结果,做一件真正需要「全部集合」的事:跨来源去重、按影响排序、如果全空就提前退出。

钻石拓扑:拆、做、合

一个扇出 + 一个收拢,就是每张严肃 Agent 图的「主力拓扑」——钻石(diamond)

拆分(split)
做 A做 B做 C
合并(merge)

一个节点拆任务 → 多个节点并行做 → 一个节点合并。市场扫描、依赖审计、代码审查、研究报告,都是这个形状。换掉来源和 prompt,同一个骨架全都适用。

🎯 判断:什么时候「才」该用 barrier(屏障)?

barrier 有真实的墙钟时间成本。规则是:只在「这一阶段真正需要所有先前结果」时才用。

「把所有来源去重,然后按影响排序」

对。跨来源去重、按影响排序,需要「全部」结果同时在场,barrier 在这里花得值。

「只是把一个列表 flatten 一下,中间那步没有跨项依赖」

对。只是 flatten 不是 barrier,是边。嗅觉测试很简单:如果「parallel → transform → parallel」中间那步没有跨项依赖,你该用 pipeline,彻底跳过 barrier。
第 6 课 · 路由 & 验证

边可以「运行时决定」,也可以「把关」

条件路由(conditional routing)

不是每张图都是固定的。有时走哪条边,取决于某个节点「发现了什么」。路由节点检查结果,决定哪条下游路径点火:给工单分类 → 分支到对应处理器;看 diff 大小 → 快速审查 or 全量审计。

在 workflow 里,这就是一个 JavaScript 的 ifswitch——因为你的控制流活在代码里。

👆 点我看「确定性的价值」

验证器(verifier):放在边上,先「试图杀死」这个发现

图的真正杠杆不是「更多 Agent」,而是你能围绕它们包上一层产出『置信度』的结构。验证器节点坐在边上,在结果被放行下游之前,唯一任务就是试图证伪它。活下来就通过,活不下来就到不了你的答案。

1
对抗式验证对每个发现,spawn N 个独立的「怀疑者」,让他们反驳它,多数存活才保留。
2
多元视角验证给每个验证器不同的透镜——正确性、安全性、能否复现。多元性能抓住 N 个相同检查永远抓不到的失败模式。
3
评审团从不同角度生成 N 个尝试,用并行裁判打分,从胜者综合、同时嫁接亚军的精华。

正是这个模式,让一个真实团队把 Bun 运行时移植进了对抗式代码审查。

第 7 课 · 隔离 & 循环

让失败停在本地,让循环一定收敛

隔离节点:一个失败只留在本地

链里,失败会级联:C 死了,D 不跑,全链停。图里,失败应该被「关在」它的节点里。

一部分已经天然成立:parallel() 里抛异常的 thunk resolve 成 null,8 个好 Agent 照样返回,那个坏的掉队。你的 .filter(Boolean) 就是「隔离」。设计每个收拢时,要「容忍缺失输入」,而不是假设全集。

👆 点我看「更隐蔽的失败」

加一个循环,但让它收敛

有时你进入任务前,根本不知道活有多大——「未知规模的发现」。一个 bug 扫描,找到一个又冒出三个。这需要「循环」:一条受控的、回到更早节点的边。

危险很明显:不收敛的循环 = 无限循环,一直 spawn Agent 直到预算烧光。

👆 点我看「几乎所有人都搞错的那个细节」
第 8 课 · 分层 & 拓扑

给不同节点分不同的模型

不是每个节点都需要你最好的模型。图让这件事一目了然:有些节点是「有界且重复」的——提取这个字段、分类这个工单;另一些才承载真正的判断——综合报告、裁决发现。

无聊的节点跑便宜模型,把贵的 token 花在「判断真正存在的地方」。

👆 点我看「省钱杠杆」

拓扑 = 你的成本和延迟

图的形状不是装饰,是你在墙钟时间上最大的一根杠杆。几乎所有人都栽在 parallel() vs pipeline() 的选择上。

parallel() 屏障

让一切等「最慢的节点」,下一阶段才能开始。

pipeline() 流水线

每项独立流经所有阶段,无屏障——A 在阶段 3 时,B 还在阶段 1。快的先完成,不等慢的。

默认用 pipeline()。只在「阶段真正需要全部结果」时才用屏障:跨集合去重、基于总量的提前退出、把一个发现和所有其他发现做比较。

「代码更干净」「阶段感觉更分开」都不是理由。屏障延迟是真实、可测、被浪费的时间。分开 ≠ 同步。

结业 · 测一测

你真的懂了吗?

Q1. 「节点」和「边」分别是什么?

对。一个节点 = 一个 Agent、一个有界任务。一条边 = 一个数据依赖。

Q2. 怎么判断一个「然后」是不是真的依赖?

对。不读 = 没有边 = 那个等待纯浪费。

Q3. 循环里「几乎所有人都搞错」的去重细节是?

对。dedupe against everything seen, not just confirmed。否则就是「花钱反复重新发现同一个死胡同」。

Q4. 默认该用 pipeline() 还是 parallel() 屏障?

对。屏障有真实的延迟成本,分开 ≠ 同步。只在跨集合去重、总量早退、跨项比较时才值得。
恭喜
🎓

你抓住了这篇文章的核心

你的 Agent 的天花板,几乎从来不是模型——是你交给它的「工作的形状」。

如果只带走三件事

1️⃣ 砍掉不传数据的箭头——「然后」不是依赖。

2️⃣ 默认 pipeline(),而不是 barrier。

3️⃣ 对「所有见过的东西」去重,而不是「已确认的」。

这三条,比「往队列里再加一步」更能让你的 Agent 更快、更便宜、更难被弄坏。

「提问者问一个问题。架构师画一张图。你的 Agent 现在是一条线,还是一张图?」

原文:《Graph Engineering: Stop Chaining Your Agents》by seeco(@seeconvm)

本页为互动教学演示,基于 seeco (@seeconvm) 的原文 提炼整理,仅供学习。