大多数尝试构建多步骤 Agent 的人,最终得到的都是一条直线。第一步,第二步,第三步——每一步都礼貌地等待上一步完成,然后才开始。
十有八九的人会发现,其中一半的步骤根本不需要等待。
它们不会 路由,不会 分支,也不会 并行。它们只是排队——一个大脑,一个上下文,一次只做一件事,直到窗口被填满,Agent 忘记了自己在做什么。
关注我的 Substack,获取最新 AI 前沿资讯:
这是一份 14 步路线图,它将那条单文件线变成了一个 图:一个能够扩展到整个集群、验证自身发现、并汇聚成一个独立 Agent 永远无法企及的结果的图。

这里有一个没人明说的转变。提示词是一个句子。循环是一个周期。而框架是 Agent 所站立的平台。
但 工作本身的形态——什么先运行,什么可以同时运行,什么必须等待其他所有任务——这个形态是一个图。节点负责思考。边负责传递结果。
Claude Code 直接提供了构建这些图的工具:动态工作流。
Claude 编写一个纯 JavaScript 编排脚本,然后生成一个协调的子 Agent 集群来执行它——而协调本身消耗零模型 Token,因为它是代码,而不是对话。
01. 节点是任务。边是 流动 的东西。
一个图只有两样东西,搞清楚它们就能解决大部分困惑。一个 节点 是一个工作单元——一个 Agent,一个有边界的任务,一个输入和一个输出。
一条 边 是一个依赖关系:它表示 这个节点的输出供给那个节点的输入。 仅此而已。

常见的错误是把“然后”当作一条边。“总结文件 然后 告诉我天气”这两者之间没有边——天气并不消费总结的内容。
这是两个断开的节点,线性脚本不必要地将它们串联了起来。只有当数据实际在节点间移动时,边才存在。
学会为你 Agent 中的每一个“然后”提问:下一步是否读取了上一步的输出? 如果不是,就没有边,等待就是浪费。
1把它画成方框和箭头。方框是一个 agent() 调用。2箭头是一个变量,从一个调用的返回值传递到另一个调用的3提示词中。如果你画不出箭头——如果没有变量传递——那么这两个4方框就是独立的,而独立性正是你将在本课程后续内容中5利用的东西。
02. 你的线性脚本是一个 退化的 图
当你把 Agent 写成“做 A,然后 B,然后 C,然后 D”时,你实际上画了一个图——一个单一、无分支的链条。每个节点恰好有一条输入边和一条输出边。
它能正确运行。但它也运行得缓慢且脆弱,因为链条没有冗余:如果 C 卡住了,D 就永远不会发生,而 A 的工作成果就被困在上游,无处可去。

图工程的第一项真正技能是 重新绘制链条。拿起你的线性 Agent,对每条箭头,问一下步骤 1 中的问题。
大多数链条都有两三条不承载数据的箭头——它们只是你碰巧按顺序输入的内容。
砍掉这些箭头,链条就会坍塌成更宽的形状:几个可以同时运行的独立节点,共同供给一个需要它们所有结果的节点。
03. 给每个节点一个 契约
一个你无法推理的节点,就是你无法并行化的节点。解决方法是契约:有边界的输入,有边界的输出,恰好一个任务。
输入是节点读取的任何内容——显式传入,绝不从共享窗口中假定。输出是一个定义的形状,最好是经过验证的,这样下一个节点就可以在不猜测的情况下消费它。

在工作流中,这个契约通过一个 模式 来强制执行。当你向 Claude 提供一个带有 JSON 模式的 agent() 调用时,生成的子 Agent Claude 会被强制返回经过验证的结构化数据——验证发生在工具调用层,因此 Claude 会在不匹配时重试,而不是交给你需要解析和祈祷的自由文本。
这就是一个可以被 Claude 接入图的节点,与一个只有在人类读取其输出时才能工作的节点之间的区别。
1// 一个带有真实契约的节点:有界输入,验证输出,单一任务。2const ITEM = {3 type: 'object', additionalProperties: false,4 properties: {5 title: { type: 'string' },6 url: { type: 'string' },7 impact: { type: 'string', enum: ['high', 'medium', 'low'] },8 },9 required: ['title', 'url', 'impact'],10};1112const result = await agent(source.prompt, {13 label: `research:${source.key}`,14 schema: ITEM, // 强制验证结构化输出15 agentType: 'general-purpose',16});17// result 现在是下一个节点可以信任的形状——而不是自由文本。
04. 将 边视为数据契约
一条边不仅仅是“B 在 A 之后”。它是一个关于传递内容的承诺:A 产生 这个形状,而 B 被构建用来消费 这个形状。当你用数据——而不是顺序——来命名边时,两件事会变得更容易。

你可以立即看出这条边是否真实(数据是否真的在移动?),并且只要形状保持不变,你就可以交换任意一端的节点而不会破坏图。
在实践中,边存在于纯 JavaScript 中。在扇出和综合之间的规约步骤——展平、去重、过滤——只是对节点返回的形状进行操作的代码。
不需要 Agent。 图思维的一个悄然胜利:大量人们用来消耗模型 Token 的事情实际上是一条边,而边是免费的。
1诱惑在于生成一个 Agent 来“合并结果”。抵制它。2如果合并意味着展平并去重,那就是 results.flatMap(...)3和一个 Set——确定性的、即时的、零 Token。把 Agent 留给4判断,而不是管道。一个每条边都是一个 Agent 的图,是一个5为其自身布线付费的图。
05. 使用 parallel() 进行扇出
这是为一切买单的操作。当你拥有 N 个独立节点——N 个要检查的来源,N 个要审查的文件,N 条要审计的路线——你不会将它们串联起来。
你告诉 Claude 将它们扇出并同时运行。在工作流中,这就是 parallel():Claude 接收一个 thunk 数组,为每个 thunk 生成一个子 Agent,所有子 Agent 并发执行,然后将结果数组交还给你。

两个细节使其健壮。首先,parallel() 是一个 屏障——它在返回前等待所有 thunk,因此下一阶段看到的是完整集合。其次,抛出异常的 thunk 会解析为 null,而不是拒绝整个批次,因此一个不稳定的 Agent 不会拖垮整个运行。
始终对结果使用 .filter(Boolean)。并发数受限于你的核心数和超额队列,因此你可以传递一百个 thunk,它们都会完成——只是一次处理几个。
1phase('Research');23// 九个来源,九个 Agent,同时运行。4const raw = await parallel(5 SOURCES.map((s) => () =>6 agent(s.prompt, {7 label: `research:${s.key}`,8 phase: 'Research',9 schema: ITEM_SCHEMA, // 每个节点返回经过验证的 JSON10 agentType: 'general-purpose',11 }),12 ),13);1415const collected = raw.filter(Boolean); // 丢弃失败 Agent 产生的 null
扇出存在于 Claude 编写的代码中,而不是模型对话中。Claude 自身的上下文永远不会同时容纳九个来源——每个子 Agent 携带自己的来源,只有最终答案会返回。
这就是让 Claude 能够将工作流扩展到 数十或数百个子 Agent 而不会淹没会话的原因。编排层消耗零 Token,因为它不是 Claude 思考的另一个回合。
06. 在屏障处扇入
扇出只有在有东西收集它时才有用。扇入是边汇聚的节点——一个 Agent(或一段代码)同时看到 所有 上游结果,并执行需要整个集合的操作:跨来源去重、按影响排序、如果总结果为空则提前退出。这是屏障值得花费其挂钟时间成本的地方。

保持图快速的规则是:仅当一个阶段真正需要所有先前结果在一起时,才使用屏障。 跨所有来源去重?屏障——正确。
1// 边:纯 JS,无 Agent,零 Token。2const flat = collected.flatMap((c) => c.items);3log(`收集到 ${flat.length} 个项目`);45phase('Curate');6// 屏障节点:需要整个集合来进行去重 + 排序。7const curated = await agent(8 `根据影响去重并排序这些项目:\n${JSON.stringify(flat)}`,9 { phase: 'Curate', schema: CURATED_SCHEMA },10);
仅仅是展平一个列表?那是一条边,内联处理即可。检验标准残酷而简单:如果你写了 parallel → transform → parallel,而中间的 transform 没有跨项目依赖,那么你应该使用管道并完全跳过屏障。
07. 菱形:拆分 → 工作 → 合并
将扇出和扇入放在一起,你就得到了每个严肃 Agent 图的主力拓扑结构:菱形。
一个节点拆分任务,多个节点并行工作,一个节点合并。它是市场扫描、依赖审计、代码审查、研究报告背后的形状——交换来源和提示词,相同的骨架就能适应。

其规范形式有一个值得记住的名字:扇出 → 规约 → 综合。 扇出以获取广度,用纯代码规约以压缩,用最终 Agent 综合以撰写答案。
一旦你看到了菱形,你就会停止问“如何让我的 Agent 做更多步骤”,而开始问“拆分点在哪里,合并点在哪里”——这才是真正能扩展的问题。
08. 使用条件在运行时路由边
并非每个图都是固定的。有时要走的边取决于节点发现了什么。一个 路由器节点 检查结果并决定哪个下游路径被触发——对工单进行分类,然后分支到正确的处理程序;检查差异大小,然后要么进行快速审查,要么启动全面审计。
在工作流中,这只是一个基于节点验证输出的 JavaScript if 或 switch,因为控制流存在于代码中。

这就是确定性成为特性而非限制的地方。路由器的 决策 可以由 Claude 驱动(一个子 Agent 进行分类),但 路由 是 Claude 编写的代码——因此对于相同的分类,它每次都以相同的方式运行。
你在节点处获得 Claude 的判断,在边处获得脚本的可靠性。没有突发的“Claude 决定跳过审计”的意外——因为跳过必须被写入图中,而它没有被写入。
1// 路由器节点:一个 Agent 进行分类,代码选择边。2const { severity } = await agent(3 `对此差异的风险进行分类:\n${diff}`,4 { schema: { type: 'object',5 properties: { severity: { enum: ['low', 'high'] } },6 required: ['severity'] } },7);89let review;10if (severity === 'high') {11 // 重型路径:全面并行审计12 review = await parallel(FILES.map((f) => () => agent(`审计 ${f}`)));13} else {14 // 轻型路径:一次快速审查15 review = await agent(`快速审查 ${diff}`);16}
09. 在边上放置一个 验证器
图的真正杠杆不是更多的 Agent——而是你可以围绕它们构建以产生信心的结构。
一个验证器节点位于结果被允许向下游传递之前的边上,它的唯一任务是尝试 推翻 这个发现。如果它存活下来,就通过。如果没有,它就永远不会到达答案。

有三种模式值得掌握。
- 对抗性验证: 对于每个发现,生成 N 个独立的、被提示去反驳它的怀疑者;只有当大多数存活时才保留它。
- 多视角验证: 给每个验证器一个独特的视角——正确性、安全性、是否可复现——因为多样性可以捕捉到 N 次相同检查永远无法发现的失败模式。
- 评审团: 从不同角度生成 N 次尝试,用并行评审员评分,从获胜者那里综合,同时嫁接亚军的最佳部分。
这正是让一个真实团队在循环中嵌入对抗性代码审查来移植 Bun 运行时的模式。
10. 隔离节点,使一次失败不会 污染 整个图
在链条中,失败会级联——C 死了,D 永远不会运行,整个事情停止。在图中,失败应该被 限制在其节点内。
这已经部分成立:在 parallel() 内部抛出的 thunk 会解析为 null,因此八个好的 Agent 仍然返回,而一个坏的 Agent 被丢弃。你的 .filter(Boolean) 就是隔离机制。
设计每个扇入节点以容忍缺失的输入,而不是假设一个完整的集合。

更微妙的失败是节点相互干扰。当 Agent 并行写入文件时,它们可能会冲突。
解决方法是隔离:“工作树”——每个 Agent 在自己的 Git 工作树中运行,在沙箱中完成工作,并干净地合并。
仅在节点实际并行写入时才使用它。它是针对需要它的拓扑结构的安全带,而不是每次运行的默认开销。
11. 添加一个循环——但要让它 收敛
有时,直到你进入任务,你才知道它有多大:未知大小的发现,一个发现一个 bug 会引出三个更多的 bug 扫描。这需要一个 循环——一条回到早期节点的受控边。
危险显而易见:一个不收敛的循环是一个无限循环,它会不断生成 Agent,直到你的预算耗尽。

能够收敛的模式是 循环直到干涸: 持续生成查找器,直到连续 K 轮没有发现新东西,然后停止。一个决定成败的细节——也是几乎每个人第一次都会犯的错误——是你对什么进行去重。
对 所有已见 内容进行去重,而不仅仅是对已确认的结果。否则,被拒绝的发现每轮都会重新出现,循环永远不会干涸,你就建造了一台为永远重新发现相同死胡同而付费的机器。
1const seen = new Set(); const confirmed = []; let dry = 0;23while (dry < 2) { // 连续 2 轮空结果后停止4 const found = (await parallel(5 FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS }))6 )).filter(Boolean).flatMap((r) => r.bugs);78 const fresh = found.filter((b) => !seen.has(key(b)));9 if (!fresh.length) { dry++; continue; } // 无新发现 → 趋向干涸10 dry = 0;11 fresh.forEach((b) => seen.add(key(b))); // 对 SEEN 去重,而非 confirmed1213 // 在计入之前,对每个新发现进行多视角验证14 const judged = await parallel(fresh.map((b) => () =>15 parallel(['correctness', 'security', 'repro'].map((lens) => () =>16 agent(`通过 ${lens} 判断 "${b.desc}" — 真实吗?`, { schema: VERDICT })))17 .then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 }))));1819 confirmed.push(...judged.filter((v) => v.real).map((v) => v.b));20}
12. 跨节点分层使用模型
并非每个节点都需要你最好的模型。图以一种单一 Agent 永远无法做到的方式使这一点显而易见:有些节点是有边界且重复的(提取此字段,对此工单进行分类),而有些节点承载着真正的判断(综合报告,裁决发现)。
在便宜的模型上运行无聊的节点,并将你昂贵的 Token 花在判断实际存在的地方。

在工作流中,Claude 生成的每个子 Agent 都会继承你的会话模型,除非脚本覆盖它——因此默认情况下,一次大型运行完全按你的会话层级计费。单个 agent() 调用上的 model 选项告诉 Claude 仅将该节点路由到别处。
在大型运行前检查 /model,然后让 Claude 将扇出的重复节点路由到更便宜的模型,并保持合并节点在高层级。这是一个杠杆,可以在不改变其形状的情况下,将一个消耗大量 Token 的图从昂贵变为经济。
13. 拓扑 就是 你的成本和延迟
图的形状不是装饰性的——它是影响挂钟时间的最大单一杠杆。一个让每个人都绊倒的选择:parallel() 与 pipeline()。一个 parallel() 屏障使 所有东西 等待最慢的节点,然后下一阶段才开始。
一个 pipeline() 独立地将每个项目流经所有阶段,没有屏障——项目 A 可以在阶段 3,而项目 B 仍在阶段 1。快速的项目提前完成,而不是在慢速项目后面闲置。

默认使用 pipeline()。 仅当一个阶段真正需要所有先前结果同时出现时才使用屏障——跨集合去重、基于总数的提前退出、需要与“其他发现”进行比较的提示词。“代码更干净”和“阶段感觉是分开的”不是理由;屏障延迟是真实的、可测量的、浪费时间。分开不等于同步。
14. 让 Claude 绘制图—— 自路由
最后一步是停止为你无法提前计划的任务手动绘制图。
使用 动态工作流,你描述目标,Claude 自己编写编排脚本——分解任务,选择扇出,生成一个协调的子 Agent 集群,并综合结果。你得到一个为 本次 运行量身定制的图,而不是一个你希望它能适用的固定图。

有三种进入方式。在你的提示词中说 “workflow” 这个词,Claude 就会为任务编写一个。运行一个保存的或捆绑的工作流——/deep-research 是一个在生产中运行的真实图:范围界定 → 并行搜索 → 获取 → 对抗性验证 → 综合,正是本课程中的骨架。
或者打开 ultracode,Claude 会为会话中的每个重要任务规划一个工作流。当一个运行良好时,按 s 将其脚本保存到 .claude/workflows/ 中——版本控制,可按名称重新运行,任何克隆仓库的人都可以启动的图。
1› 运行一个工作流,审计 src/routes/ 下每条路由是否缺少2身份验证。为每个路由文件生成一个 Agent,然后在报告前验证每个发现。 ● Claude 编写了一个编排脚本 · 在后台启动…3/workflows — auth-audit · 运行中 ✓ 范围 1/1 2.1k tok ·44s ✓ 扇出 18/18 每个路由文件一个 Agent ◯ 验证 11/18 每个发现 3 票5怀疑者… ○ 综合 0/1 等待验证 会话保持响应 — 在集群运行时继续工作
本周用 Claude 构建的六个图

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





