返回列表
🧠 阿头学 · 🪞 Uota学

抛弃线性思维,用图拓扑重构多步智能体

这篇文章认为绝大多数多步智能体因线性执行而陷入低效天花板,只有将工作流重构为基于数据契约的图拓扑(节点为判断、边为代码、并行扇出、对抗性验证),才能真正运行集群级智能体,但这一主张夹杂着对Claude Code功能的产品推广和未经证实的统计断言。
打开原文 ↗

阅读简报
双语对照
完整翻译
原文
讨论归档

核心观点

  • 线性脚本本质是退化图 绝大多数开发者按顺序排列智能体步骤,但其中大量"然后"关系并不传递数据,这种伪依赖导致不必要的串行等待,而将图压扁变宽可释放并行性。
  • 节点必须有契约,边必须是数据流 真正的节点应具备有边界的输入和经过验证的结构化输出(JSON Schema),边仅当变量实际穿越时才存在;数据转换、展平、去重应交给纯代码执行,而非消耗token的"管道工"智能体。
  • 并行扇出与屏障扇入是核心拓扑 通过parallel()将独立任务分发到多个子智能体,再以屏障节点收集全部结果进行跨来源去重与排序,这是菱形拓扑(fan-out → reduce → synthesize)的工程基础。
  • 动态路由与对抗性验证决定可靠性 运行时用确定性代码(if/switch)基于节点输出进行条件路由,并在边上设置验证器节点(多视角怀疑者、评委团)扼杀错误发现,而非依赖单一智能体的自我纠错。
  • 模型分层与拓扑选择直接决定成本 将重复性节点路由到廉价模型,仅在需要判断的合并节点使用高级模型;同时默认使用pipeline()减少屏障等待,避免为伪同步支付真实的挂钟时间成本。

跟我们的关联

  • 对ATou意味着:在构建复杂AI工作流时,必须首先用"数据是否穿越"检验每一步依赖关系,切断伪依赖后才能识别真正的并行机会,否则将因线性排队导致上下文窗口浪费和整体脆弱。
  • 对Neta意味着:产品设计中应将"确定性代码处理边、大模型负责节点判断"作为架构铁律,同时把对抗性验证(如多视角评委团)内建为质量关卡,而非依赖单次抽卡式生成。
  • 对Uota意味着:动态工作流的自路由能力虽然诱人,但需警惕将编排控制权完全交给模型自动生成的风险——如果根节点对任务分解理解错误,系统性偏差会在整个图中被放大,人工审查拓扑仍是必要成本。

讨论引子

  • 如果对抗性验证和并行扇出会显著增加总token消耗,那么在什么成本阈值下,线性执行反而比图拓扑更经济?
  • 当子智能体被隔离在各自上下文中并行工作时,如何确保它们不会丢失跨节点隐式依赖(如代码库的全局架构约束),这种"上下文碎片化"是否构成图拓扑的硬边界?
  • 让Claude自动生成动态工作流脚本虽然降低了手动构图门槛,但如何在不增加人工审查负担的前提下,防止模型在编排层面产生系统性错误路由?

大多数尝试构建多步智能体(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 轮没有出现新东西,然后停止。决定成败的一个细节——也是几乎每个人第一次都会犯的错误——是你针对什么进行去重。

针对看到的所有内容进行去重,而不仅仅是针对确认的结果。否则,被拒绝的发现会在每一轮重新出现,循环永远不会枯竭,你就建造了一台花钱永远重新发现相同死胡同的机器。

https://movez.substack.com/

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)画一个

线性智能体从来都不是天花板——它只是第一个形状,每个人都会去尝试的形状,因为它符合我们打字的方式。一条线,一个头脑,一次做一件事。

一旦你能看到节点和边,你就不再要求智能体做更多,而是开始要求图做得更宽:在工作独立的地方扇出,在信心重要的地方把关边,在不需要判断力的地方对模型进行分层。

大多数人会继续排队执行步骤。那些学会画图的人将运行一个集群——并且永远不会注意到其他人被困在下面的天花板。

Most people who try to build a multi-step agent end up with a straight line. Step one, step two, step three - each waiting politely for the last to finish before it starts.

大多数尝试构建多步智能体(agent)的人最终都会得到一条直线。第一步、第二步、第三步——每一步都礼貌地等待上一步完成才开始。

9/10 notice that half those steps never needed to wait at all.

十分之九的人会发现,其中一半的步骤根本不需要等待。

They don’t route. They don’t branch. They don’t parallelize. They just queue - one head, one context, one thing at a time, until the window fills up and the agent forgets what it was doing.

它们不路由(route)。它们不分支(branch)。它们不并行(parallelize)。它们只是排队——一个头脑,一个上下文(context),一次做一件事,直到窗口填满,智能体忘记了它在做什么。

Follow my Substack to get fresh AI alpha: movez.substack.com

关注我的 Substack 获取最新的 AI 洞察(alpha):movez.substack.com

This is the 14-step roadmap that turns that single-file line into a graph: one that fans out across a fleet, verifies its own findings, and converges on a result a lone agent could never hold.

这是 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.

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.

Here’s the shift nobody spells out. A prompt is a sentence. A loop is a cycle. A harness is the floor the agent stands on.

这是一个没人明说的转变。提示词(prompt)是一个句子。循环(loop)是一个周期。测试工具(harness)是智能体站立的地板。

But the shape of the work itself - what runs before what, what can run at the same time, what has to wait for everything else - that shape is a graph. Nodes do the thinking. Edges carry the results.

但是工作本身的形状——什么在什么之前运行,什么可以同时运行,什么必须等待其他一切——这个形状是一个图。节点(Nodes)负责思考。边(Edges)承载结果。

Claude Code shipped the tooling to build these graphs directly: dynamic workflows.

Claude Code 提供了直接构建这些图的工具:动态工作流(dynamic workflows)

Claude writes a plain JavaScript orchestration script, then spawns a coordinated fleet of subagents to execute it - and the coordination itself costs zero model tokens, because it’s code, not a conversation.

Claude 编写一个纯 JavaScript 编排脚本,然后生成一个协调的子智能体(subagents)集群来执行它——而协调本身消耗零个模型 token,因为它是代码,而不是对话。

01. Nodes are jobs. Edges are what flows.

01. 节点是任务。边是流动的东西。

A graph has exactly two things, and getting them straight fixes most of the confusion. A node is a unit of work - one agent, one bounded job, one input in and one output out.

一个图只有两样东西,理清它们就能解决大部分困惑。节点(node)是一个工作单元——一个智能体,一个有边界的任务,一个输入和一个输出。

An edge is a dependency: it says this node’s output feeds that node’s input. Nothing more.

边(edge)是一种依赖关系:它表示这个节点的输出作为那个节点的输入。仅此而已。

The mistake is treating “and then” as an edge. “Summarize the file and then tell me the weather” has no edge between the two - the weather doesn’t consume the summary.

错误在于将“然后(and then)”视为一条边。“总结文件然后告诉我天气”这两者之间没有边——天气并不消耗总结。

That’s two disconnected nodes that a linear script needlessly chains. The edge only exists when data actually moves across it.

这是两个不相关的节点,被线性脚本毫无必要地链接在一起。只有当数据实际穿过它时,边才存在。

Learn to ask, for every “and then” in your agent: does the next step read the last step’s output? If not, there is no edge, and the wait is wasted.

学会针对智能体中的每一个“然后”提问:下一步是否读取上一步的输出?如果没有,就没有边,等待就是浪费。

// 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}`);
}
// 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.

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. Your linear script is a degenerate graph

02. 你的线性脚本是一个退化的图

When you write an agent as “do A, then B, then C, then D,” you’ve drawn a graph - a single unbranching chain. Every node has exactly one edge in and one edge out.

当你把一个智能体写成“做 A,然后 B,然后 C,然后 D”时,你已经画了一个图——一条没有分支的单链。每个节点都恰好有一条输入边和一条输出边。

It runs correctly. It also runs slowly and fragile, because a chain has no redundancy: if C stalls, D never happens, and A’s work is trapped upstream with nowhere to go.

它运行正确。但它也运行缓慢且脆弱,因为链条没有冗余:如果 C 停滞,D 就永远不会发生,而 A 的工作被困在上游无处可去。

The first real skill of graph engineering is redrawing the chain. Take your linear agent and, for each arrow, ask the Step 1 question.

图工程(graph engineering)的第一个真正技能是重绘链条。拿出你的线性智能体,对于每一个箭头,问一下第 1 步的问题。

Most chains have two or three arrows that don’t carry data - they’re just the order you happened to type things in.

大多数链条有两三个不承载数据的箭头——它们只是你碰巧输入事物的顺序。

Cut those arrows and the chain collapses into something wider: a few independent nodes that could all run at once, feeding a single node that needs them all.

切断这些箭头,链条就会坍缩成更宽的东西:几个可以同时运行的独立节点,为一个需要它们全部的单一节点提供数据。

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
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. Give every node a contract

03. 给每个节点一个契约

A node you can’t reason about is a node you can’t parallelize. The fix is a contract: bounded input, bounded output, exactly one job.

一个你无法推理的节点,就是一个你无法并行的节点。解决方法是契约(contract):有边界的输入,有边界的输出,恰好一项任务。

The input is whatever the node reads - passed in explicitly, never assumed from a shared window. The output is a defined shape, ideally validated, so the next node can consume it without guessing.

输入是节点读取的任何内容——显式传入,绝不从共享窗口中假设。输出是一个定义的形状,最好经过验证,这样下一个节点就可以在不猜测的情况下使用它。

In a workflow this contract is enforced with a schema. When you hand Claude an agent() call with a JSON schema, the subagent Claude spawns is forced to return validated structured data - validation happens at the tool-call layer, so Claude retries on mismatch instead of handing you free text you have to parse and pray over.

在工作流中,这个契约由模式(schema)强制执行。当你交给 Claude 一个带有 JSON 模式的 agent() 调用时,Claude 生成的子智能体被迫返回经过验证的结构化数据——验证发生在工具调用(tool-call)层,因此 Claude 会在不匹配时重试,而不是交给你必须解析并祈祷其正确的自由文本。

This is the difference between a node Claude can wire into a graph and a node that only works when a human reads its output.

这就是 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.
// 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. Treat the edge as a data contract

04. 边视为数据契约

An edge isn’t just “B comes after A.” It’s a promise about what crosses: A produces this shape, and B is built to consume this shape. When you name the edge by its data - not its order - two things get easier.

边不仅仅是“B 在 A 之后”。它是关于什么穿过它的承诺:A 产生这种形状,而 B 被构建为消耗这种形状。当你用数据——而不是顺序——来命名边时,有两件事变得更容易了。

You can see instantly whether the edge is real (does data actually move?), and you can swap the node on either end without breaking the graph, as long as the shape holds.

你可以立即看出边是否真实(数据真的在移动吗?),并且只要形状保持不变,你就可以交换两端的节点而不会破坏图。

In practice, the edge lives in plain JavaScript. The reduce step between fan-out and synthesis - flatten, dedupe, filter - is just code operating on the shapes your nodes returned.

在实践中,边存在于纯 JavaScript 中。扇出(fan-out)和合成(synthesis)之间的归约(reduce)步骤——展平(flatten)、去重(dedupe)、过滤(filter)——只是对节点返回的形状进行操作的代码。

No agent needed. One of the quiet wins of graph thinking: a huge amount of what people burn model tokens on is really an edge, and edges are free.

不需要智能体。图思维(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
› 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. Fan out with parallel()

05. 使用 parallel() 扇出

This is the move that pays for everything. When you have N independent nodes - N sources to check, N files to review, N routes to audit - you don’t chain them.

这是物超所值的一步。当你有 N 个独立节点——N 个要检查的来源,N 个要审查的文件,N 个要审计的路由——你不要将它们链接起来。

You tell Claude to fan them out and run them at once. In a workflow that’s parallel(): Claude takes an array of thunks and spawns one subagent per thunk, all executing concurrently, then hands you back the array of results.

你告诉 Claude 将它们扇出并同时运行。在工作流中,这就是 parallel():Claude 接收一个 thunk 数组,并为每个 thunk 生成一个子智能体,所有子智能体并发执行,然后将结果数组交还给你。

Two details make it robust. First, parallel() is a barrier - it waits for every thunk before it returns, so the next stage sees the complete set. Second, a thunk that throws resolves to null instead of rejecting the whole batch, so one flaky agent can’t sink the run.

有两个细节使其变得健壮。首先,parallel() 是一个屏障(barrier)——它在返回之前等待每个 thunk,因此下一阶段会看到完整的集合。其次,抛出异常的 thunk 会解析为 null,而不是拒绝整个批次,因此一个不稳定的智能体不会搞砸整个运行。

Always .filter(Boolean) the results. Concurrency is capped around your core count and the excess queues, so you can pass a hundred thunks and they’ll all finish - just a handful at a time.

始终对结果使用 .filter(Boolean)。并发性限制在你的核心数量和多余的队列周围,因此你可以传递一百个 thunk,它们都会完成——只是一次处理几个。

The fan-out lives in code Claude wrote, not in a model conversation. Claude’s own context never holds nine sources at once - each subagent carries its own, and only the final answer comes back.

扇出存在于 Claude 编写的代码中,而不是在模型对话中。Claude 自己的上下文永远不会同时容纳九个来源——每个子智能体都携带自己的来源,只有最终答案会返回。

That’s what lets Claude scale a workflow to dozens or hundreds of subagents without drowning the session. The orchestration layer costs zero tokens because it isn’t another turn of Claude thinking.

这就是让 Claude 能够将工作流扩展到几十或几百个子智能体而不会淹没会话的原因。编排层消耗零个 token,因为它不是 Claude 思考的又一个轮次。

06. Fan in at a barrier

06. 在屏障处扇入

A fan-out is only useful if something gathers it. The fan-in is the node where edges converge - where one agent (or one piece of code) sees all the upstream results at once and does something that requires the whole set: dedupe across sources, rank by impact, early-exit if the total came back empty. This is the one place a barrier earns its wall-clock cost.

只有当有东西收集它时,扇出才有用。扇入(fan-in)是边汇聚的节点——在这里,一个智能体(或一段代码)一次性看到所有上游结果,并执行需要整个集合的操作:跨来源去重,按影响排序,如果总数为空则提前退出。这是屏障赚回其挂钟时间(wall-clock cost)的唯一地方。

The rule that keeps graphs fast: use a barrier only when a stage genuinely needs every prior result together. Deduping across all sources? Barrier - correct.

保持图快速运行的规则:仅当一个阶段真正需要所有先前的结果在一起时才使用屏障。跨所有来源去重?屏障——正确。

Just flattening a list? That’s an edge, do it inline. The smell test is brutal and simple: if you wrote parallel → transform → parallel, and that middle transform has no cross-item dependency, you should have used a pipeline and skipped the barrier entirely.

只是展平一个列表?那是一条边,内联执行即可。气味测试(smell test)残酷而简单:如果你写了 parallel → transform → parallel,并且中间的 transform 没有跨项依赖,你应该使用管道(pipeline)并完全跳过屏障。

07. The diamond: split → work → merge

07. 菱形:拆分 → 工作 → 合并

Put fan-out and fan-in together and you get the workhorse topology of every serious agent graph: the diamond.

将扇出和扇入结合起来,你就得到了每个严肃智能体图的主力拓扑结构:菱形(diamond)

One node splits the job, many nodes do the work in parallel, one node merges. It’s the shape behind a market scan, a dependency audit, a code review, a research report - swap the sources and prompts and the same skeleton adapts.

一个节点拆分任务,许多节点并行工作,一个节点合并。这是市场扫描、依赖审计、代码审查、研究报告背后的形状——交换来源和提示词,相同的骨架依然适用。

The canonical form has a name worth memorizing: fan out → reduce → synthesize. Fan out to gather breadth, reduce with plain code to compress it, synthesize with a final agent to write the answer.

规范形式有一个值得记住的名字:扇出 → 归约 → 合成(fan out → reduce → synthesize)。扇出以收集广度,用纯代码归约以压缩它,用最终的智能体合成以写出答案。

Once you see the diamond, you stop asking “how do I make my agent do more steps” and start asking “where’s the split, where’s the merge” - which is the question that actually scales.

一旦你看到了菱形,你就不再问“我如何让我的智能体做更多步骤”,而是开始问“拆分在哪里,合并在哪里”——这才是真正能够扩展的问题。

08. Route the edge at runtime with a conditional

08. 使用条件在运行时路由边

Not every graph is fixed. Sometimes the edge to take depends on what a node found. A router node inspects a result and decides which downstream path fires - classify the ticket, then branch to the right handler; check the diff size, then either do a quick review or spin up a full audit.

并非每个图都是固定的。有时要走的边取决于节点发现了什么。路由节点(router node)检查结果并决定触发哪条下游路径——对工单进行分类,然后分支到正确的处理程序;检查差异(diff)大小,然后要么进行快速审查,要么启动全面审计。

In a workflow this is just a JavaScript if or switch on a node’s validated output, because control flow lives in code.

在工作流中,这只是对节点验证输出的 JavaScript if 或 switch,因为控制流(control flow)存在于代码中。

This is where determinism becomes a feature, not a limitation. The router’s decision can be Claude-powered (a subagent classifies), but the routing is code Claude wrote - so it runs the same way every time for the same classification.

这就是确定性成为一个特性而不是限制的地方。路由器的决定可以由 Claude 驱动(子智能体进行分类),但路由是 Claude 编写的代码——因此对于相同的分类,它每次都以相同的方式运行。

You get Claude’s judgment at the node and the script’s reliability at the edge. No emergent “Claude decided to skip the audit” surprises - because the skip would have to be written into the graph, and it isn’t.

你在节点处获得了 Claude 的判断力,在边处获得了脚本的可靠性。不会出现突发的“Claude 决定跳过审计”的惊喜——因为跳过必须被写入图中,而它并没有。

09. Put a verifier on the edge

09. 在边上放置一个验证器

The real leverage of a graph isn’t more agents - it’s the structure you can wrap around them to produce confidence.

图的真正杠杆作用不是更多的智能体——而是你可以围绕它们构建以产生信心的结构。

A verifier node sits on the edge before a result is allowed downstream, and its only job is to try to kill the finding. If it survives, it passes. If not, it never reaches the answer.

验证器节点(verifier node)位于结果被允许进入下游之前的边上,它唯一的工作就是试图扼杀该发现。如果它幸存下来,它就通过了。如果没有,它永远不会到达答案。

Three patterns are worth having in your hands.

有三种模式值得掌握。

  • Adversarial verify: for each finding, spawn N independent skeptics prompted to refute it; keep it only if a majority survive.
  • 对抗性验证(Adversarial verify):对于每一个发现,生成 N 个独立的怀疑者,提示他们反驳它;只有当多数幸存时才保留它。
  • Perspective-diverse verify: give each verifier a distinct lens - correctness, security, does-it-reproduce - because diversity catches failure modes that N identical checks never will.
  • 视角多样化验证(Perspective-diverse verify):给每个验证器一个不同的视角——正确性、安全性、是否可复现——因为多样性能够捕捉到 N 个相同检查永远无法捕捉到的故障模式。
  • Judge panel: generate N attempts from different angles, score them with parallel judges, synthesize from the winner while grafting the best of the runners-up.
  • 评委团(Judge panel):从不同角度生成 N 次尝试,用并行评委对它们进行评分,从获胜者中合成,同时嫁接亚军中最好的部分。

This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked into the loop.

这正是让一个真实团队在循环中内置对抗性代码审查来移植 Bun 运行时的模式。

10. Isolate nodes so one failure can’t poison the graph

10. 隔离节点,使一个故障不能毒害

In a chain, a failure cascades - C dies, D never runs, the whole thing halts. In a graph, failure should be contained to its node.

在链条中,故障会级联——C 死了,D 永远不会运行,整个过程停止。在图中,故障应该被限制在其节点内。

That’s already partly true: a thunk that throws inside parallel() resolves to null, so eight good agents still return while one bad one drops out. Your .filter(Boolean) is the containment.

这在某种程度上已经是真实的:在 parallel() 内部抛出异常的 thunk 会解析为 null,因此八个好的智能体仍然返回,而一个坏的智能体退出。你的 .filter(Boolean) 就是这种限制。

Design every fan-in to tolerate missing inputs rather than assume a full set.

设计每个扇入以容忍缺失的输入,而不是假设一个完整的集合。

The subtler failure is nodes stepping on each other. When agents write files in parallel, they can collide.

更微妙的故障是节点互相踩踏。当智能体并行写入文件时,它们可能会发生冲突。

The fix is isolation: "worktree" - each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly.

解决方法是隔离:“工作树(worktree)”——每个智能体在自己的 git 工作树中运行,在沙盒中完成工作,并干净地合并。

Reach for it only when nodes actually write in parallel. it’s the seatbelt for the one topology that needs it, not a default tax on every run.

只有当节点实际并行写入时才使用它。它是需要它的那种拓扑结构的安全带,而不是每次运行的默认税。

11. Add a cycle - but make it converge

11. 添加一个循环——但让它收敛

Sometimes you don’t know how big the job is until you’re in it: unknown-size discovery, a bug sweep where finding one bug reveals three more. That needs a cycle - a controlled edge back to an earlier node.

有时直到你身处其中,你才知道工作有多大:未知大小的发现,一次错误扫描中发现一个错误会揭示另外三个错误。这需要一个循环(cycle)——一条受控的边回到早期的节点。

The danger is obvious: a cycle that doesn’t converge is an infinite loop that spawns agents until your budget is gone.

危险是显而易见的:一个不收敛的循环是一个无限循环,它会不断生成智能体,直到你的预算耗尽。

The pattern that converges is loop-until-dry: keep spawning finders until K consecutive rounds surface nothing new, then stop. The one detail that makes or breaks it - and the mistake almost everyone makes the first time - is what you dedupe against.

收敛的模式是循环直到枯竭(loop-until-dry):不断生成发现者,直到连续 K 轮没有出现新东西,然后停止。决定成败的一个细节——也是几乎每个人第一次都会犯的错误——是你针对什么进行去重。

Dedupe against everything seen, not just against confirmed results. Otherwise rejected findings reappear every round, the loop never runs dry, and you’ve built a machine that pays to rediscover the same dead ends forever.

针对看到的所有内容进行去重,而不仅仅是针对确认的结果。否则,被拒绝的发现会在每一轮重新出现,循环永远不会枯竭,你就建造了一台花钱永远重新发现相同死胡同的机器。

12. Tier the models across the nodes

12. 在节点之间对模型进行分层

Not every node needs your best model. A graph makes this obvious in a way a single agent never does: some nodes are bounded and repetitive (extract this field, classify this ticket), and some carry the real judgment (synthesize the report, adjudicate the finding).

并非每个节点都需要你最好的模型。图以一种单一智能体永远无法做到的方式使这一点变得明显:一些节点是有边界和重复的(提取这个字段,分类这个工单),而一些节点承载着真正的判断(合成报告,裁定发现)。

Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives.

在更便宜的模型上运行无聊的节点,并将昂贵的 token 花在真正需要判断力的地方。

In a workflow every subagent Claude spawns inherits your session model unless the script overrides it - so by default a big run bills entirely at your session tier. The model option on a single agent() call tells Claude to route just that node elsewhere.

在工作流中,Claude 生成的每个子智能体都会继承你的会话模型,除非脚本覆盖它——因此默认情况下,一次大型运行完全按照你的会话层级计费。单个 agent() 调用上的模型选项告诉 Claude 仅将该节点路由到其他地方。

Check /model before a large run, then have Claude route the fan-out’s repetitive nodes down to a cheaper model and keep the merge node up. This is the lever that turns a token-hungry graph from expensive into economical without touching its shape.

在大型运行之前检查 /model,然后让 Claude 将扇出的重复节点路由到更便宜的模型,并保持合并节点的高级模型。这是一个杠杆,可以在不触及图形状的情况下,将消耗大量 token 的图从昂贵变为经济。

13. Topology is your cost and latency

13. 拓扑结构就是你的成本和延迟

The shape of the graph isn’t cosmetic - it’s the single biggest lever on wall-clock time. The choice that trips everyone up: parallel() versus pipeline(). A parallel() barrier makes everything wait for the slowest node before the next stage starts.

图的形状不是表面的——它是挂钟时间上最大的单一杠杆。让每个人都绊倒的选择:parallel() 与 pipeline()。一个 parallel() 屏障使一切都等待最慢的节点,然后下一阶段才开始。

A pipeline() streams each item through all stages independently, with no barrier - item A can be in stage 3 while item B is still in stage 1. Fast items finish early instead of idling behind slow ones.

一个 pipeline() 使每个项目独立地流经所有阶段,没有屏障——项目 A 可以在第 3 阶段,而项目 B 仍在第 1 阶段。快速的项目会尽早完成,而不是在慢速项目后面空转。

Default to pipeline(). Reach for a barrier only when a stage truly needs every prior result at once - a cross-set dedupe, an early-exit on the total, a prompt that compares against “the other findings.” “It’s cleaner code” and “the stages feel separate” are not reasons; barrier latency is real, measurable, wasted time. Separate is not the same as synchronized.

默认使用 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 },
);
// 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. Let Claude draw the graph - self-routing

14. 让 Claude 画图——自路由

The final move is to stop drawing the graph by hand for jobs you can’t plan in advance.

最后一步是停止为你无法提前计划的任务手动画图。

With dynamic workflows, you describe the objective and Claude writes the orchestration script itself- decomposing the task, choosing the fan-out, spawning a coordinated fleet of subagents, and synthesizing the result. You get a graph tailored to this run instead of a fixed one you hoped would fit.

借助动态工作流(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));
}
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));
}

There are three ways in. Say the word “workflow” in your prompt and Claude writes one for the task. Run a saved or bundled one - /deep-research is a real graph shipping in production: scope → parallel search → fetch → adversarial verify → synthesize, the exact skeleton from this course.

有三种进入方式。在你的提示词中说出“工作流(workflow)”这个词,Claude 就会为任务编写一个。运行一个保存的或捆绑的——/deep-research 是一个在生产环境中发布的真实图:范围 → 并行搜索 → 获取 → 对抗性验证 → 合成,正是本课程中的骨架。

Or turn on ultracode and Claude plans a workflow for every substantial task in the session. When a run is good, press s to save its script into .claude/workflows/ - version-controlled, re-runnable by name, a graph anyone who clones the repo can launch.

或者打开 ultracode,Claude 会为会话中的每个实质性任务规划一个工作流。当一次运行良好时,按 s 将其脚本保存到 .claude/workflows/ 中——受版本控制,可按名称重新运行,任何克隆该仓库的人都可以启动这个图。

Six graphs to build with Claude this week

本周用 Claude 构建的六个图

  • Security sweep across every route. Claude spawns one subagent per route file, each hunting for missing auth checks, then a verifier pass confirms every finding before it reaches the report. Breadth no single context could hold.
  • 跨每个路由的安全扫描。Claude 为每个路由文件生成一个子智能体,每个子智能体都在寻找缺失的身份验证检查,然后验证器通道在每个发现到达报告之前对其进行确认。这是单一上下文无法容纳的广度。
  • Cited report with /deep-research. A graph that ships in Claude Code already. Claude decomposes your question into distinct angles, runs parallel searches, dedupes sources, then adversarially verifies every claim with three-vote skeptics before writing.
  • 使用 /deep-research 的引用报告。一个已经内置在 Claude Code 中的图。Claude 将你的问题分解为不同的角度,运行并行搜索,对来源进行去重,然后在写作之前用三票怀疑者对抗性地验证每一个主张
  • Port a module, file by file. The Bun ceiling, scaled to your repo. Claude fans out translation across files, runs the test suite as a gate on each, and loops the failures back - adversarial review catching what a single pass would ship broken.
  • 逐文件移植模块。Bun 的天花板,扩展到你的仓库。Claude 在文件之间扇出翻译,运行测试套件作为每个文件的关卡,并将失败循环回去——对抗性审查捕捉到单次传递会发布的损坏内容。
  • Adversarial review of a diff. Claude routes on diff size: a small change gets one quick pass, a large one triggers a full parallel audit with reviewers on distinct lenses - correctness, security, performance - then a judge panel synthesizes.
  • 对差异(diff)的对抗性审查。Claude 根据差异大小进行路由:小更改进行一次快速传递,大更改触发全面并行审计,审查员具有不同的视角——正确性、安全性、性能——然后由评委团进行合成。
  • Ecosystem scan on a schedule. Save it once, re-run it forever. Claude checks many sources in parallel - eleases, blogs, discussion - ranks by impact at a barrier, and writes the digest. Version-controlled in .claude/workflows/, launchable by name.
  • 按计划进行的生态系统扫描。保存一次,永远重新运行。Claude 并行检查许多来源——发布(releases)、博客、讨论——在屏障处按影响排序,并编写摘要。在 .claude/workflows/ 中受版本控制,可按名称启动。
  • Discovery of unknown size. You don’t know how many bugs are there. Claude runs finders in parallel, dedupes each new find against everything seen, verifies survivors, and keeps looping until two rounds turn up nothing new - then stops.
  • 未知大小的发现。你不知道那里有多少个错误。Claude 并行运行发现者,将每个新发现与看到的所有内容进行去重,验证幸存者,并不断循环,直到两轮没有出现新东西——然后停止。

Conclusion:

结论:

A prompter asks a question. An architect draws a graph.

提示词工程师(prompter)提出问题。架构师(architect)画一个

The linear agent was never the ceiling - it was just the first shape, the one everyone reaches for because it matches how we type. One line, one head, one thing at a time.

线性智能体从来都不是天花板——它只是第一个形状,每个人都会去尝试的形状,因为它符合我们打字的方式。一条线,一个头脑,一次做一件事。

Once you can see the nodes and the edges, you stop asking the agent to do more and start asking the graph to do it wider: fan out where the work is independent, gate the edges where confidence matters, tier the models where judgment doesn’t.

一旦你能看到节点和边,你就不再要求智能体做更多,而是开始要求图做得更宽:在工作独立的地方扇出,在信心重要的地方把关边,在不需要判断力的地方对模型进行分层。

Most people will keep queueing steps in a line. The ones who learn to draw the graph will run a fleet - and never notice the ceiling the rest are stuck under.

大多数人会继续排队执行步骤。那些学会画图的人将运行一个集群——并且永远不会注意到其他人被困在下面的天花板。

Most people who try to build a multi-step agent end up with a straight line. Step one, step two, step three - each waiting politely for the last to finish before it starts.

9/10 notice that half those steps never needed to wait at all.

They don’t route. They don’t branch. They don’t parallelize. They just queue - one head, one context, one thing at a time, until the window fills up and the agent forgets what it was doing.

Follow my Substack to get fresh AI alpha: movez.substack.com

This is the 14-step roadmap that turns that single-file line into a graph: one that fans out across a fleet, verifies its own findings, and converges on a result a lone agent could never hold.

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.

Here’s the shift nobody spells out. A prompt is a sentence. A loop is a cycle. A harness is the floor the agent stands on.

But the shape of the work itself - what runs before what, what can run at the same time, what has to wait for everything else - that shape is a graph. Nodes do the thinking. Edges carry the results.

Claude Code shipped the tooling to build these graphs directly: dynamic workflows.

Claude writes a plain JavaScript orchestration script, then spawns a coordinated fleet of subagents to execute it - and the coordination itself costs zero model tokens, because it’s code, not a conversation.

01. Nodes are jobs. Edges are what flows.

A graph has exactly two things, and getting them straight fixes most of the confusion. A node is a unit of work - one agent, one bounded job, one input in and one output out.

An edge is a dependency: it says this node’s output feeds that node’s input. Nothing more.

The mistake is treating “and then” as an edge. “Summarize the file and then tell me the weather” has no edge between the two - the weather doesn’t consume the summary.

That’s two disconnected nodes that a linear script needlessly chains. The edge only exists when data actually moves across it.

Learn to ask, for every “and then” in your agent: does the next step read the last step’s output? If not, there is no edge, and the wait is wasted.

// 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. Your linear script is a degenerate graph

When you write an agent as “do A, then B, then C, then D,” you’ve drawn a graph - a single unbranching chain. Every node has exactly one edge in and one edge out.

It runs correctly. It also runs slowly and fragile, because a chain has no redundancy: if C stalls, D never happens, and A’s work is trapped upstream with nowhere to go.

The first real skill of graph engineering is redrawing the chain. Take your linear agent and, for each arrow, ask the Step 1 question.

Most chains have two or three arrows that don’t carry data - they’re just the order you happened to type things in.

Cut those arrows and the chain collapses into something wider: a few independent nodes that could all run at once, feeding a single node that needs them all.

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. Give every node a contract

A node you can’t reason about is a node you can’t parallelize. The fix is a contract: bounded input, bounded output, exactly one job.

The input is whatever the node reads - passed in explicitly, never assumed from a shared window. The output is a defined shape, ideally validated, so the next node can consume it without guessing.

In a workflow this contract is enforced with a schema. When you hand Claude an agent() call with a JSON schema, the subagent Claude spawns is forced to return validated structured data - validation happens at the tool-call layer, so Claude retries on mismatch instead of handing you free text you have to parse and pray over.

This is the difference between a node Claude can wire into a graph and a node that only works when a human reads its output.

// 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. Treat the edge as a data contract

An edge isn’t just “B comes after A.” It’s a promise about what crosses: A produces this shape, and B is built to consume this shape. When you name the edge by its data - not its order - two things get easier.

You can see instantly whether the edge is real (does data actually move?), and you can swap the node on either end without breaking the graph, as long as the shape holds.

In practice, the edge lives in plain JavaScript. The reduce step between fan-out and synthesis - flatten, dedupe, filter - is just code operating on the shapes your nodes returned.

No agent needed. One of the quiet wins of graph thinking: a huge amount of what people burn model tokens on is really an edge, and edges are free.

› 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. Fan out with parallel()

This is the move that pays for everything. When you have N independent nodes - N sources to check, N files to review, N routes to audit - you don’t chain them.

You tell Claude to fan them out and run them at once. In a workflow that’s parallel(): Claude takes an array of thunks and spawns one subagent per thunk, all executing concurrently, then hands you back the array of results.

Two details make it robust. First, parallel() is a barrier - it waits for every thunk before it returns, so the next stage sees the complete set. Second, a thunk that throws resolves to null instead of rejecting the whole batch, so one flaky agent can’t sink the run.

Always .filter(Boolean) the results. Concurrency is capped around your core count and the excess queues, so you can pass a hundred thunks and they’ll all finish - just a handful at a time.

The fan-out lives in code Claude wrote, not in a model conversation. Claude’s own context never holds nine sources at once - each subagent carries its own, and only the final answer comes back.

That’s what lets Claude scale a workflow to dozens or hundreds of subagents without drowning the session. The orchestration layer costs zero tokens because it isn’t another turn of Claude thinking.

06. Fan in at a barrier

A fan-out is only useful if something gathers it. The fan-in is the node where edges converge - where one agent (or one piece of code) sees all the upstream results at once and does something that requires the whole set: dedupe across sources, rank by impact, early-exit if the total came back empty. This is the one place a barrier earns its wall-clock cost.

The rule that keeps graphs fast: use a barrier only when a stage genuinely needs every prior result together. Deduping across all sources? Barrier - correct.

Just flattening a list? That’s an edge, do it inline. The smell test is brutal and simple: if you wrote parallel → transform → parallel, and that middle transform has no cross-item dependency, you should have used a pipeline and skipped the barrier entirely.

07. The diamond: split → work → merge

Put fan-out and fan-in together and you get the workhorse topology of every serious agent graph: the diamond.

One node splits the job, many nodes do the work in parallel, one node merges. It’s the shape behind a market scan, a dependency audit, a code review, a research report - swap the sources and prompts and the same skeleton adapts.

The canonical form has a name worth memorizing: fan out → reduce → synthesize. Fan out to gather breadth, reduce with plain code to compress it, synthesize with a final agent to write the answer.

Once you see the diamond, you stop asking “how do I make my agent do more steps” and start asking “where’s the split, where’s the merge” - which is the question that actually scales.

08. Route the edge at runtime with a conditional

Not every graph is fixed. Sometimes the edge to take depends on what a node found. A router node inspects a result and decides which downstream path fires - classify the ticket, then branch to the right handler; check the diff size, then either do a quick review or spin up a full audit.

In a workflow this is just a JavaScript if or switch on a node’s validated output, because control flow lives in code.

This is where determinism becomes a feature, not a limitation. The router’s decision can be Claude-powered (a subagent classifies), but the routing is code Claude wrote - so it runs the same way every time for the same classification.

You get Claude’s judgment at the node and the script’s reliability at the edge. No emergent “Claude decided to skip the audit” surprises - because the skip would have to be written into the graph, and it isn’t.

09. Put a verifier on the edge

The real leverage of a graph isn’t more agents - it’s the structure you can wrap around them to produce confidence.

A verifier node sits on the edge before a result is allowed downstream, and its only job is to try to kill the finding. If it survives, it passes. If not, it never reaches the answer.

Three patterns are worth having in your hands.

  • Adversarial verify: for each finding, spawn N independent skeptics prompted to refute it; keep it only if a majority survive.

  • Perspective-diverse verify: give each verifier a distinct lens - correctness, security, does-it-reproduce - because diversity catches failure modes that N identical checks never will.

  • Judge panel: generate N attempts from different angles, score them with parallel judges, synthesize from the winner while grafting the best of the runners-up.

This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked into the loop.

10. Isolate nodes so one failure can’t poison the graph

In a chain, a failure cascades - C dies, D never runs, the whole thing halts. In a graph, failure should be contained to its node.

That’s already partly true: a thunk that throws inside parallel() resolves to null, so eight good agents still return while one bad one drops out. Your .filter(Boolean) is the containment.

Design every fan-in to tolerate missing inputs rather than assume a full set.

The subtler failure is nodes stepping on each other. When agents write files in parallel, they can collide.

The fix is isolation: "worktree" - each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly.

Reach for it only when nodes actually write in parallel. it’s the seatbelt for the one topology that needs it, not a default tax on every run.

11. Add a cycle - but make it converge

Sometimes you don’t know how big the job is until you’re in it: unknown-size discovery, a bug sweep where finding one bug reveals three more. That needs a cycle - a controlled edge back to an earlier node.

The danger is obvious: a cycle that doesn’t converge is an infinite loop that spawns agents until your budget is gone.

The pattern that converges is loop-until-dry: keep spawning finders until K consecutive rounds surface nothing new, then stop. The one detail that makes or breaks it - and the mistake almost everyone makes the first time - is what you dedupe against.

Dedupe against everything seen, not just against confirmed results. Otherwise rejected findings reappear every round, the loop never runs dry, and you’ve built a machine that pays to rediscover the same dead ends forever.

https://movez.substack.com/

12. Tier the models across the nodes

Not every node needs your best model. A graph makes this obvious in a way a single agent never does: some nodes are bounded and repetitive (extract this field, classify this ticket), and some carry the real judgment (synthesize the report, adjudicate the finding).

Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives.

In a workflow every subagent Claude spawns inherits your session model unless the script overrides it - so by default a big run bills entirely at your session tier. The model option on a single agent() call tells Claude to route just that node elsewhere.

Check /model before a large run, then have Claude route the fan-out’s repetitive nodes down to a cheaper model and keep the merge node up. This is the lever that turns a token-hungry graph from expensive into economical without touching its shape.

13. Topology is your cost and latency

The shape of the graph isn’t cosmetic - it’s the single biggest lever on wall-clock time. The choice that trips everyone up: parallel() versus pipeline(). A parallel() barrier makes everything wait for the slowest node before the next stage starts.

A pipeline() streams each item through all stages independently, with no barrier - item A can be in stage 3 while item B is still in stage 1. Fast items finish early instead of idling behind slow ones.

Default to pipeline(). Reach for a barrier only when a stage truly needs every prior result at once - a cross-set dedupe, an early-exit on the total, a prompt that compares against “the other findings.” “It’s cleaner code” and “the stages feel separate” are not reasons; barrier latency is real, measurable, wasted time. Separate is not the same as synchronized.

// 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. Let Claude draw the graph - self-routing

The final move is to stop drawing the graph by hand for jobs you can’t plan in advance.

With dynamic workflows, you describe the objective and Claude writes the orchestration script itself- decomposing the task, choosing the fan-out, spawning a coordinated fleet of subagents, and synthesizing the result. You get a graph tailored to this run instead of a fixed one you hoped would fit.

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));
}

There are three ways in. Say the word “workflow” in your prompt and Claude writes one for the task. Run a saved or bundled one - /deep-research is a real graph shipping in production: scope → parallel search → fetch → adversarial verify → synthesize, the exact skeleton from this course.

Or turn on ultracode and Claude plans a workflow for every substantial task in the session. When a run is good, press s to save its script into .claude/workflows/ - version-controlled, re-runnable by name, a graph anyone who clones the repo can launch.

Six graphs to build with Claude this week

  • Security sweep across every route. Claude spawns one subagent per route file, each hunting for missing auth checks, then a verifier pass confirms every finding before it reaches the report. Breadth no single context could hold.

  • Cited report with /deep-research. A graph that ships in Claude Code already. Claude decomposes your question into distinct angles, runs parallel searches, dedupes sources, then adversarially verifies every claim with three-vote skeptics before writing.

  • Port a module, file by file. The Bun ceiling, scaled to your repo. Claude fans out translation across files, runs the test suite as a gate on each, and loops the failures back - adversarial review catching what a single pass would ship broken.

  • Adversarial review of a diff. Claude routes on diff size: a small change gets one quick pass, a large one triggers a full parallel audit with reviewers on distinct lenses - correctness, security, performance - then a judge panel synthesizes.

  • Ecosystem scan on a schedule. Save it once, re-run it forever. Claude checks many sources in parallel - eleases, blogs, discussion - ranks by impact at a barrier, and writes the digest. Version-controlled in .claude/workflows/, launchable by name.

  • Discovery of unknown size. You don’t know how many bugs are there. Claude runs finders in parallel, dedupes each new find against everything seen, verifies survivors, and keeps looping until two rounds turn up nothing new - then stops.

Conclusion:

A prompter asks a question. An architect draws a graph.

The linear agent was never the ceiling - it was just the first shape, the one everyone reaches for because it matches how we type. One line, one head, one thing at a time.

Once you can see the nodes and the edges, you stop asking the agent to do more and start asking the graph to do it wider: fan out where the work is independent, gate the edges where confidence matters, tier the models where judgment doesn’t.

Most people will keep queueing steps in a line. The ones who learn to draw the graph will run a fleet - and never notice the ceiling the rest are stuck under.

📋 讨论归档

讨论进行中…