
大多数尝试构建多步智能体(agent)的人最终都会得到一条直线。第一步、第二步、第三步——每一步都礼貌地等待上一步完成才开始。
十分之九的人会发现,其中一半的步骤根本不需要等待。
它们不路由(route)。它们不分支(branch)。它们不并行(parallelize)。它们只是排队——一个头脑,一个上下文(context),一次做一件事,直到窗口填满,智能体忘记了它在做什么。
关注我的 Substack 获取最新的 AI 洞察(alpha):movez.substack.com

这是 14 步路线图,它将那条单行线变成一个图(graph):一个在整个集群中扇出(fans out)、验证自身发现,并收敛到一个孤立智能体永远无法掌握的结果的图。
The temptation is to spawn an agent to “combine the results.” Resist
it. If combining means flatten-and-dedupe, that’s results.flatMap(...)
and a Set — deterministic, instant, zero tokens. Save agents for
judgment, not for plumbing. A graph where every edge is an agent is a
graph paying rent on its own wiring.
这是一个没人明说的转变。提示词(prompt)是一个句子。循环(loop)是一个周期。测试工具(harness)是智能体站立的地板。
但是工作本身的形状——什么在什么之前运行,什么可以同时运行,什么必须等待其他一切——这个形状是一个图。节点(Nodes)负责思考。边(Edges)承载结果。
Claude Code 提供了直接构建这些图的工具:动态工作流(dynamic workflows)。
Claude 编写一个纯 JavaScript 编排脚本,然后生成一个协调的子智能体(subagents)集群来执行它——而协调本身消耗零个模型 token,因为它是代码,而不是对话。

01. 节点是任务。边是流动的东西。
一个图只有两样东西,理清它们就能解决大部分困惑。节点(node)是一个工作单元——一个智能体,一个有边界的任务,一个输入和一个输出。
边(edge)是一种依赖关系:它表示这个节点的输出作为那个节点的输入。仅此而已。
错误在于将“然后(and then)”视为一条边。“总结文件然后告诉我天气”这两者之间没有边——天气并不消耗总结。
这是两个不相关的节点,被线性脚本毫无必要地链接在一起。只有当数据实际穿过它时,边才存在。
学会针对智能体中的每一个“然后”提问:下一步是否读取上一步的输出?如果没有,就没有边,等待就是浪费。
// Router node: an agent classifies, code picks the edge.
const { severity } = await agent(
`Classify this diff's risk:\n${diff}`,
{ schema: { type: 'object',
properties: { severity: { enum: ['low', 'high'] } },
required: ['severity'] } },
);
let review;
if (severity === 'high') {
// heavy path: full parallel audit
review = await parallel(FILES.map((f) => () => agent(`Audit ${f}`)));
} else {
// light path: one quick pass
review = await agent(`Quick review of ${diff}`);
}
Draw it as boxes and arrows. A box is an agent() call.
An arrow is a variable passed from one call’s return into another’s
prompt. If you can’t draw the arrow - if no variable crosses - the two
boxes are independent, and independence is the thing you’ll exploit
for the rest of this course.
02. 你的线性脚本是一个退化的图
当你把一个智能体写成“做 A,然后 B,然后 C,然后 D”时,你已经画了一个图——一条没有分支的单链。每个节点都恰好有一条输入边和一条输出边。
它运行正确。但它也运行缓慢且脆弱,因为链条没有冗余:如果 C 停滞,D 就永远不会发生,而 A 的工作被困在上游无处可去。
图工程(graph engineering)的第一个真正技能是重绘链条。拿出你的线性智能体,对于每一个箭头,问一下第 1 步的问题。
大多数链条有两三个不承载数据的箭头——它们只是你碰巧输入事物的顺序。
切断这些箭头,链条就会坍缩成更宽的东西:几个可以同时运行的独立节点,为一个需要它们全部的单一节点提供数据。
phase('Research');
// Nine sources, nine agents, all at once.
const raw = await parallel(
SOURCES.map((s) => () =>
agent(s.prompt, {
label: `research:${s.key}`,
phase: 'Research',
schema: ITEM_SCHEMA, // each node returns validated JSON
agentType: 'general-purpose',
}),
),
);
const collected = raw.filter(Boolean); // drop the nulls from failed agents
03. 给每个节点一个契约
一个你无法推理的节点,就是一个你无法并行的节点。解决方法是契约(contract):有边界的输入,有边界的输出,恰好一项任务。
输入是节点读取的任何内容——显式传入,绝不从共享窗口中假设。输出是一个定义的形状,最好经过验证,这样下一个节点就可以在不猜测的情况下使用它。

在工作流中,这个契约由模式(schema)强制执行。当你交给 Claude 一个带有 JSON 模式的 agent() 调用时,Claude 生成的子智能体被迫返回经过验证的结构化数据——验证发生在工具调用(tool-call)层,因此 Claude 会在不匹配时重试,而不是交给你必须解析并祈祷其正确的自由文本。
这就是 Claude 可以连接到图中的节点与只有在人类读取其输出时才起作用的节点之间的区别。
// A node with a real contract: bounded in, validated out, one job.
const ITEM = {
type: 'object', additionalProperties: false,
properties: {
title: { type: 'string' },
url: { type: 'string' },
impact: { type: 'string', enum: ['high', 'medium', 'low'] },
},
required: ['title', 'url', 'impact'],
};
const result = await agent(source.prompt, {
label: `research:${source.key}`,
schema: ITEM, // forces validated structured output
agentType: 'general-purpose',
});
// result is now a shape the next node can trust — not free text.
04. 将边视为数据契约
边不仅仅是“B 在 A 之后”。它是关于什么穿过它的承诺:A 产生这种形状,而 B 被构建为消耗这种形状。当你用数据——而不是顺序——来命名边时,有两件事变得更容易了。

你可以立即看出边是否真实(数据真的在移动吗?),并且只要形状保持不变,你就可以交换两端的节点而不会破坏图。
在实践中,边存在于纯 JavaScript 中。扇出(fan-out)和合成(synthesis)之间的归约(reduce)步骤——展平(flatten)、去重(dedupe)、过滤(filter)——只是对节点返回的形状进行操作的代码。
不需要智能体。图思维(graph thinking)的一个默默无闻的胜利是:人们消耗模型 token 的大量工作实际上是一条边,而边是免费的。
› Run a workflow to audit every route under src/routes/ for missing
auth. Spawn one agent per route file, then verify each finding before
reporting. ● Claude wrote an orchestration script · launching in
background… /workflows — auth-audit · running ✓ Scope 1/1 2.1k tok ·
4s ✓ Fan-out 18/18 one agent per route file ◯ Verify 11/18 3-vote
skeptics per finding… ○ Synthesize 0/1 waiting on verify session stays
responsive — keep working while the fleet runs
05. 使用 parallel() 扇出
这是物超所值的一步。当你有 N 个独立节点——N 个要检查的来源,N 个要审查的文件,N 个要审计的路由——你不要将它们链接起来。
你告诉 Claude 将它们扇出并同时运行。在工作流中,这就是 parallel():Claude 接收一个 thunk 数组,并为每个 thunk 生成一个子智能体,所有子智能体并发执行,然后将结果数组交还给你。
有两个细节使其变得健壮。首先,parallel() 是一个屏障(barrier)——它在返回之前等待每个 thunk,因此下一阶段会看到完整的集合。其次,抛出异常的 thunk 会解析为 null,而不是拒绝整个批次,因此一个不稳定的智能体不会搞砸整个运行。
始终对结果使用 .filter(Boolean)。并发性限制在你的核心数量和多余的队列周围,因此你可以传递一百个 thunk,它们都会完成——只是一次处理几个。
扇出存在于 Claude 编写的代码中,而不是在模型对话中。Claude 自己的上下文永远不会同时容纳九个来源——每个子智能体都携带自己的来源,只有最终答案会返回。
这就是让 Claude 能够将工作流扩展到几十或几百个子智能体而不会淹没会话的原因。编排层消耗零个 token,因为它不是 Claude 思考的又一个轮次。
06. 在屏障处扇入
只有当有东西收集它时,扇出才有用。扇入(fan-in)是边汇聚的节点——在这里,一个智能体(或一段代码)一次性看到所有上游结果,并执行需要整个集合的操作:跨来源去重,按影响排序,如果总数为空则提前退出。这是屏障赚回其挂钟时间(wall-clock cost)的唯一地方。
保持图快速运行的规则:仅当一个阶段真正需要所有先前的结果在一起时才使用屏障。跨所有来源去重?屏障——正确。
只是展平一个列表?那是一条边,内联执行即可。气味测试(smell test)残酷而简单:如果你写了 parallel → transform → parallel,并且中间的 transform 没有跨项依赖,你应该使用管道(pipeline)并完全跳过屏障。
07. 菱形:拆分 → 工作 → 合并
将扇出和扇入结合起来,你就得到了每个严肃智能体图的主力拓扑结构:菱形(diamond)。
一个节点拆分任务,许多节点并行工作,一个节点合并。这是市场扫描、依赖审计、代码审查、研究报告背后的形状——交换来源和提示词,相同的骨架依然适用。
规范形式有一个值得记住的名字:扇出 → 归约 → 合成(fan out → reduce → synthesize)。扇出以收集广度,用纯代码归约以压缩它,用最终的智能体合成以写出答案。
一旦你看到了菱形,你就不再问“我如何让我的智能体做更多步骤”,而是开始问“拆分在哪里,合并在哪里”——这才是真正能够扩展的问题。

08. 使用条件在运行时路由边
并非每个图都是固定的。有时要走的边取决于节点发现了什么。路由节点(router node)检查结果并决定触发哪条下游路径——对工单进行分类,然后分支到正确的处理程序;检查差异(diff)大小,然后要么进行快速审查,要么启动全面审计。
在工作流中,这只是对节点验证输出的 JavaScript if 或 switch,因为控制流(control flow)存在于代码中。

这就是确定性成为一个特性而不是限制的地方。路由器的决定可以由 Claude 驱动(子智能体进行分类),但路由是 Claude 编写的代码——因此对于相同的分类,它每次都以相同的方式运行。
你在节点处获得了 Claude 的判断力,在边处获得了脚本的可靠性。不会出现突发的“Claude 决定跳过审计”的惊喜——因为跳过必须被写入图中,而它并没有。


09. 在边上放置一个验证器
图的真正杠杆作用不是更多的智能体——而是你可以围绕它们构建以产生信心的结构。
验证器节点(verifier node)位于结果被允许进入下游之前的边上,它唯一的工作就是试图扼杀该发现。如果它幸存下来,它就通过了。如果没有,它永远不会到达答案。

有三种模式值得掌握。
-
对抗性验证(Adversarial verify):对于每一个发现,生成 N 个独立的怀疑者,提示他们反驳它;只有当多数幸存时才保留它。
-
视角多样化验证(Perspective-diverse verify):给每个验证器一个不同的视角——正确性、安全性、是否可复现——因为多样性能够捕捉到 N 个相同检查永远无法捕捉到的故障模式。
-
评委团(Judge panel):从不同角度生成 N 次尝试,用并行评委对它们进行评分,从获胜者中合成,同时嫁接亚军中最好的部分。
这正是让一个真实团队在循环中内置对抗性代码审查来移植 Bun 运行时的模式。

10. 隔离节点,使一个故障不能毒害图
在链条中,故障会级联——C 死了,D 永远不会运行,整个过程停止。在图中,故障应该被限制在其节点内。
这在某种程度上已经是真实的:在 parallel() 内部抛出异常的 thunk 会解析为 null,因此八个好的智能体仍然返回,而一个坏的智能体退出。你的 .filter(Boolean) 就是这种限制。
设计每个扇入以容忍缺失的输入,而不是假设一个完整的集合。

更微妙的故障是节点互相踩踏。当智能体并行写入文件时,它们可能会发生冲突。
解决方法是隔离:“工作树(worktree)”——每个智能体在自己的 git 工作树中运行,在沙盒中完成工作,并干净地合并。
只有当节点实际并行写入时才使用它。它是需要它的那种拓扑结构的安全带,而不是每次运行的默认税。

11. 添加一个循环——但让它收敛
有时直到你身处其中,你才知道工作有多大:未知大小的发现,一次错误扫描中发现一个错误会揭示另外三个错误。这需要一个循环(cycle)——一条受控的边回到早期的节点。
危险是显而易见的:一个不收敛的循环是一个无限循环,它会不断生成智能体,直到你的预算耗尽。
收敛的模式是循环直到枯竭(loop-until-dry):不断生成发现者,直到连续 K 轮没有出现新东西,然后停止。决定成败的一个细节——也是几乎每个人第一次都会犯的错误——是你针对什么进行去重。
针对看到的所有内容进行去重,而不仅仅是针对确认的结果。否则,被拒绝的发现会在每一轮重新出现,循环永远不会枯竭,你就建造了一台花钱永远重新发现相同死胡同的机器。

12. 在节点之间对模型进行分层
并非每个节点都需要你最好的模型。图以一种单一智能体永远无法做到的方式使这一点变得明显:一些节点是有边界和重复的(提取这个字段,分类这个工单),而一些节点承载着真正的判断(合成报告,裁定发现)。
在更便宜的模型上运行无聊的节点,并将昂贵的 token 花在真正需要判断力的地方。

在工作流中,Claude 生成的每个子智能体都会继承你的会话模型,除非脚本覆盖它——因此默认情况下,一次大型运行完全按照你的会话层级计费。单个 agent() 调用上的模型选项告诉 Claude 仅将该节点路由到其他地方。
在大型运行之前检查 /model,然后让 Claude 将扇出的重复节点路由到更便宜的模型,并保持合并节点的高级模型。这是一个杠杆,可以在不触及图形状的情况下,将消耗大量 token 的图从昂贵变为经济。
13. 拓扑结构就是你的成本和延迟
图的形状不是表面的——它是挂钟时间上最大的单一杠杆。让每个人都绊倒的选择:parallel() 与 pipeline()。一个 parallel() 屏障使一切都等待最慢的节点,然后下一阶段才开始。
一个 pipeline() 使每个项目独立地流经所有阶段,没有屏障——项目 A 可以在第 3 阶段,而项目 B 仍在第 1 阶段。快速的项目会尽早完成,而不是在慢速项目后面空转。
默认使用 pipeline()。只有当一个阶段真正需要一次性获得所有先前的结果时才使用屏障——跨集合去重,总数的提前退出,与“其他发现”进行比较的提示词。“代码更干净”和“阶段感觉是分开的”不是理由;屏障延迟是真实的、可测量的、浪费的时间。分开并不等于同步。
// The edge: plain JS, no agent, zero tokens.
const flat = collected.flatMap((c) => c.items);
log(`Collected ${flat.length} items`);
phase('Curate');
// The barrier node: needs the WHOLE set to dedupe + rank.
const curated = await agent(
`Dedupe and rank these by impact:\n${JSON.stringify(flat)}`,
{ phase: 'Curate', schema: CURATED_SCHEMA },
);
14. 让 Claude 画图——自路由
最后一步是停止为你无法提前计划的任务手动画图。
借助动态工作流(dynamic workflows),你描述目标,Claude 自己编写编排脚本——分解任务,选择扇出,生成协调的子智能体集群,并合成结果。你得到的是为这次运行量身定制的图,而不是你希望能够适用的固定图。
const seen = new Set(); const confirmed = []; let dry = 0;
while (dry < 2) { // stop after 2 empty rounds
const found = (await parallel(
FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS }))
)).filter(Boolean).flatMap((r) => r.bugs);
const fresh = found.filter((b) => !seen.has(key(b)));
if (!fresh.length) { dry++; continue; } // nothing new → toward dry
dry = 0;
fresh.forEach((b) => seen.add(key(b))); // dedupe vs SEEN, not confirmed
// diverse-lens verify each fresh finding before it counts
const judged = await parallel(fresh.map((b) => () =>
parallel(['correctness', 'security', 'repro'].map((lens) => () =>
agent(`Judge "${b.desc}" via ${lens} — real?`, { schema: VERDICT })))
.then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 }))));
confirmed.push(...judged.filter((v) => v.real).map((v) => v.b));
}
有三种进入方式。在你的提示词中说出“工作流(workflow)”这个词,Claude 就会为任务编写一个。运行一个保存的或捆绑的——/deep-research 是一个在生产环境中发布的真实图:范围 → 并行搜索 → 获取 → 对抗性验证 → 合成,正是本课程中的骨架。
或者打开 ultracode,Claude 会为会话中的每个实质性任务规划一个工作流。当一次运行良好时,按 s 将其脚本保存到 .claude/workflows/ 中——受版本控制,可按名称重新运行,任何克隆该仓库的人都可以启动这个图。

本周用 Claude 构建的六个图

-
跨每个路由的安全扫描。Claude 为每个路由文件生成一个子智能体,每个子智能体都在寻找缺失的身份验证检查,然后验证器通道在每个发现到达报告之前对其进行确认。这是单一上下文无法容纳的广度。
-
使用 /deep-research 的引用报告。一个已经内置在 Claude Code 中的图。Claude 将你的问题分解为不同的角度,运行并行搜索,对来源进行去重,然后在写作之前用三票怀疑者对抗性地验证每一个主张。
-
逐文件移植模块。Bun 的天花板,扩展到你的仓库。Claude 在文件之间扇出翻译,运行测试套件作为每个文件的关卡,并将失败循环回去——对抗性审查捕捉到单次传递会发布的损坏内容。
-
对差异(diff)的对抗性审查。Claude 根据差异大小进行路由:小更改进行一次快速传递,大更改触发全面并行审计,审查员具有不同的视角——正确性、安全性、性能——然后由评委团进行合成。
-
按计划进行的生态系统扫描。保存一次,永远重新运行。Claude 并行检查许多来源——发布(releases)、博客、讨论——在屏障处按影响排序,并编写摘要。在 .claude/workflows/ 中受版本控制,可按名称启动。
-
未知大小的发现。你不知道那里有多少个错误。Claude 并行运行发现者,将每个新发现与看到的所有内容进行去重,验证幸存者,并不断循环,直到两轮没有出现新东西——然后停止。
结论:
提示词工程师(prompter)提出问题。架构师(architect)画一个图。
线性智能体从来都不是天花板——它只是第一个形状,每个人都会去尝试的形状,因为它符合我们打字的方式。一条线,一个头脑,一次做一件事。
一旦你能看到节点和边,你就不再要求智能体做更多,而是开始要求图做得更宽:在工作独立的地方扇出,在信心重要的地方把关边,在不需要判断力的地方对模型进行分层。
大多数人会继续排队执行步骤。那些学会画图的人将运行一个集群——并且永远不会注意到其他人被困在下面的天花板。