14 步路线图:从提示词到为你工作的 Agent 集群
大多数人在构建多步骤 Agent 时,最终得到的都是一条直线。
第一步,第二步,第三步。每一步都礼貌地等待前一步完成,然后才开始。
而且几乎没有人意识到,其中一半的步骤根本不需要等待任何东西。
它们不会路由。不会分支。不会并行。只是排队。一个脑袋,一个上下文,一次只做一件事,直到窗口填满,Agent 忘记自己在做什么。
这就是将那条单行线变成一张图的 14 步路线图。一张图,能扩展到整个集群,能自行验证发现,并收敛到一个单一 Agent 永远无法维持的结果。
到最后,你会知道三件事:
→ 当前系统在哪些地方毫无理由地等待 → 如何在不牺牲输出可信度的情况下分配工作 → 如何在不触碰质量的前提下降低账单
没人解释的框架转变
提示词是一个句子。循环是一个周期。框架是 Agent 站立的土地。
但工作本身的形态——什么先运行,什么可以同时运行,什么必须等待其他所有事情——这个形态是一张图。
节点负责思考。边负责传输结果。
而 Claude Code 已经提供了直接构建它们的工具:动态工作流。
Claude 用纯 JavaScript 编写一个编排脚本,然后启动一个协调的子 Agent 集群来执行它。协调本身消耗零模型 Token,因为它是代码,而不是对话。

路线图分为四个区块:
→ 01 到 04,看清你已有的图 → 05 到 08,运转集群 → 09 到 11,信任产出 → 12 到 14,确保它不会毁了你
区块 1 · 看清你已有的图
01. 节点是任务,边是流动的内容
一张图只有两样东西,搞清楚它们几乎能解决所有混乱。
→ 节点:一个工作单元。一个 Agent、一个有限任务、一个输入、一个输出。 → 边:一个依赖关系。它表示这个节点的输出作为那个节点的输入。仅此而已。
常见的错误是把"然后"当作一条边。
"总结这个文件,然后告诉我天气"——这两个部分之间没有边。天气预报并不消耗摘要。它们是两个不相连的节点,被一个线性的脚本不必要地串在一起。
只有当数据真正跨越时,边才存在。

把它画成方框和箭头。一个方框就是一次对 agent() 的调用。一个箭头就是一个变量,它从一个调用的返回值中出来,进入另一个调用的提示词中。
如果你画不出箭头,如果没有变量跨越,那么这两个方框就是独立的。
而正是这种独立性,你将在接下来的 13 步中加以利用。
→ 规则:对于 Agent 中的每一个"然后",问自己下一步是否读取了上一步的输出。如果不是,那么等待就是浪费时间。
02. 你的线性脚本已经是一张图,只不过是一张退化的图
当你把 Agent 写成"做 A,然后 B,然后 C,然后 D"时,你已经画出了一张图。一条没有分支的链。每个节点恰好有一条输入边和一条输出边。
它也能工作。但它运行缓慢且脆弱,因为链没有冗余:如果 C 卡住了,D 永远不会发生,而 A 的工作被困在上游,无处可去。
图工程的第一项真正技能是重新绘制这条链。
拿起你的线性 Agent,针对每条箭头,问自己步骤 01 中的问题。
大多数链有两到三条箭头并不传输数据。它们之所以存在,仅仅因为那是你打字时的顺序。
砍掉那些箭头,链就会崩塌成更宽的东西:几个独立的节点,可以同时运行,然后输入到一个真正需要它们全部的实际节点。
这不需要花钱,不需要新工具,而且通常比你能买到的任何东西消除更多的等待。
→ 规则:在添加任何东西之前,先删除那些不传输数据的边。
03. 为每个节点制定一个契约
一个你无法推理的节点,就是一个你无法并行化的节点。
解决办法是制定一个契约:有限的输入、有限的输出、单一的任务。
→ 输入是节点读取的内容,显式传递,绝不从共享窗口中假设 → 输出是一个定义好的形状,最好经过验证,这样下一个节点无需猜测就能消费它

在工作流中,这个契约通过模式来强制执行。当你向 Claude 传递一个带有 JSON 模式的 agent() 调用时,子 Agent 被迫返回有效的结构化数据。验证发生在工具调用层,因此当数据不符合时,会进行重试,而不是返回自由文本让你自己去解析并祈祷结果良好。
1// 一个带有真实契约的节点:有限输入、验证输出、单一任务2const ITEM = {3 type: 'object',4 additionalProperties: false,5 properties: {6 title: { type: 'string' },7 url: { type: 'string' },8 impact: { type: 'string', enum: ['high', 'medium', 'low'] },9 },10 required: ['title', 'url', 'impact'],11};1213const output = await agent(source.prompt, {14 label: `research:${source.key}`,15 schema: ITEM,16 agentType: 'general-purpose',17});
这就是一个能被 Claude 接入图的节点,与一个只有人类阅读才能工作的节点之间的区别。
→ 规则:如果一个节点的输出没有形状,那它就不是一个节点。它只是一次对话。
04. 边也是一种契约,而且是免费的
一条边不是"B 在 A 之后"。它是一个关于什么在跨越的承诺:A 产生这个形状,B 被构建来消费这个形状。
当你用数据来命名边,而不是用顺序来命名时,两件事就变得容易了:
→ 你立刻就能看出边是否真实,因为你可以问是否有东西通过它移动 → 只要形状保持不变,你可以改变任意一端的节点而不破坏图
在实践中,边存在于纯 JavaScript 中。从扇出到综合之间的归约步骤——展平、去重、过滤——只是对节点返回的形状进行操作的代码。
那里不需要 Agent。
而这是以图的方式思考的一个隐形胜利:人们用 Token 支付的很大一部分东西,实际上仅仅是边,而边是免费的。
诱惑在于启动一个 Agent 来"合并结果"。抵制它。
如果合并意味着展平和去重,那就是一个 flatMap 和一个 Set。确定性的、瞬间的、零 Token。
→ 规则:Agent 用于判断,而不是用于管道。一张每条边都是 Agent 的图,是在为它自己的布线支付租金。
区块 2 · 运转集群
05. 使用 parallel() 进行扇出
这是为其他所有步骤买单的操作。
当你拥有 N 个独立节点、N 个需要咨询的来源、N 个需要审查的文件、N 条需要审计的路由时,不要把它们串起来。你告诉 Claude 将它们扇出并同时运行。
在工作流中,这就是 parallel():它接受一个 thunk 数组,每个 thunk 启动一个子 Agent,所有子 Agent 并发执行,并返回结果数组。

两个细节让它变得健壮:
→ parallel() 是一个屏障。它在返回前等待所有 thunk 完成,因此下一阶段会看到完整的集合。 → 抛出异常的 thunk 会解析为 null,而不是拖垮整个批次,因此一个不稳定的 Agent 不会拖垮执行。
这就是为什么你总是要对输出进行 .filter(Boolean)。
并发数大致受限于你的核心数,多余的会排队,因此你可以传递一百个 thunk,它们都会完成,只是分批进行。
1phase('Research');23const raw = await parallel(4 SOURCES.map((f) => () =>5 agent(f.prompt, {6 label: `research:${f.key}`,7 phase: 'Research',8 schema: ITEM,9 agentType: 'general-purpose',10 }),11 ),12);1314const collected = raw.filter(Boolean); // 移除失败 Agent 的 null
这里重要的一点是:扇出存在于 Claude 编写的代码中,而不是与模型的对话中。
Claude 自己的上下文永远不会同时持有九个来源。每个子 Agent 加载自己的来源,只有最终响应返回。
这就允许扩展到数十或数百个子 Agent,而不会淹没会话。编排层消耗零 Token,因为它不是 Claude 思考的另一个回合。
→ 规则:如果 N 件事不互相读取,就不要排队。扇出它们。
06. 在屏障处闭合扇出
扇出只有在有东西收集它时才有效。
扇入是边汇聚的节点。在那里,一个 Agent 或一段代码同时看到所有上游结果,并做一些需要整个集合的事情:跨来源去重、按影响排序、如果完全没有任何结果则提前退出。
这是唯一一个屏障在时钟时间上值得付出成本的地方。
跨所有来源去重?屏障,正确。
只是展平一个列表?那是边,内联处理。
1// 边:纯 JS,没有 Agent,零 Token2const flat = collected.flatMap((c) => c.items);3log(`Collected ${flat.length} elements`);45phase('Curate');67// 屏障节点:需要整个集合来去重和排序8const curated = await agent(9 `按影响去重并排序这些项:10${JSON.stringify(flat)}`,11 { phase: 'Curate', schema: CURATED },12);
测试很简单:如果你写了 parallel → transform → parallel,而中间那个转换在元素之间没有依赖关系,那么你应该使用管道,并完全跳过屏障。
→ 规则:只有当某个阶段真正需要所有先前结果一起时,才使用屏障。
07. 菱形:拆分、工作、合并
将扇出和扇入结合起来,你就得到了任何严肃 Agent 图的工作拓扑结构:菱形。
一个节点拆分任务。许多节点并行工作。一个节点合并结果。

这个标准形式有一个值得记住的名称:扇出 → 归约 → 综合。
→ 扇出以获得广度 → 用纯代码归约以压缩它 → 用最终的 Agent 综合以写出响应
它是市场扫描、依赖审计、代码审查和研究报告背后的骨架。你改变来源和提示词,同样的骨架就能适应。
当你看到菱形时,你就不再问"如何让我的 Agent 走更多步骤",而开始问"拆分在哪里,合并在哪里"。这才是真正能扩展的问题。
→ 规则:不要设计步骤。设计它在哪拆分,在哪合并。
08. 在运行时路由边
并非所有图都是固定的。有时边走哪条路取决于节点发现了什么。
路由节点检查结果,并决定触发哪条路径。它对工单进行分类,然后分支到正确的处理程序。它查看 diff 的大小,选择快速审查还是启动全面审计。

在工作流中,这是一个简单的 JavaScript if 或 switch,基于节点验证后的输出,因为流程控制存在于代码中。
1// 路由节点:Agent 分类,代码选择边2const { severity } = await agent(3 `分类这个 diff 的风险:4${diff}`,5 { schema: {6 type: 'object',7 properties: { severity: { enum: ['low', 'high'] } },8 required: ['severity'],9 }},10);1112let review;13if (severity === 'high') {14 review = await parallel(FILES.map((a) => () => agent(`审计 ${a}`)));15} else {16 review = await agent(`快速审查 ${diff}`);17}
这里,确定性是一个特性,而不是限制。
路由器的决定可以来自 Claude,但路由是代码。对于相同的分类,它每次都按相同的方式运行。
节点中的模型判断,边中的脚本可靠性。不会出现突然的意外,比如"Agent 决定跳过审计",因为那个跳过必须写在图中。而它没有。
→ 规则:让模型决定这是什么,让代码决定如何处理它。
反规则(继续阅读前请先看) 一张图能买来广度。
但它买不来更好的判断。
如果你工作的每一步都需要前一步的完整画面,那么把它分配给多个 Agent 并不会给你更好的答案。它给你的是同样的答案,只是更贵、更晚。图开始产生回报的准确时刻,是工作拆分成那些从不互相读取结果的任务时。在添加任何一个 Agent 之前,唯一决定你账单的问题是:
我的工作在哪里拆分?
如果它不拆分,坚持使用一个 Agent,省下剩下的 6 步。
区块 3 · 信任产出
09. 在边上放置一个验证器
图的真正杠杆不在于拥有更多 Agent。而在于你能围绕它们构建的结构,以产生信任。
验证器节点位于边上,在结果被允许向下传递之前。它的唯一工作就是试图杀死这个发现。如果它存活下来,就通过。如果没有,它永远不会到达答案。
而这一切背后有一条硬规则:永远不要让同一个 Agent 给自己打分。一个模型审查自己的输出会错过大部分自己的错误,因为它是在犯错误的地方进行评估。

三个值得手头备用的模式:
→ 对抗性验证:对于每个发现,启动 N 个独立的怀疑者,命令他们反驳它。只有它经受住多数人的检验,才保留。
→ 多视角验证:给每个验证器一个不同的角度。正确、安全、可复现。多样性能够捕获 N 个相同检查永远找不到的失败模式。
→ 评审团:从不同角度生成 N 个尝试,用并行的评审者打分,从获胜者中综合,并嫁接那些接近的结果中最好的部分。
这是用 Agent 完成的最雄心勃勃的项目背后的模式,例如将整个运行时移植到内部,并且对抗性审查嵌入在循环内部,而不是在最后。
→ 规则:没有发现能在没有验证的情况下传递,而且两个验证器从不问同一个问题。
10. 隔离节点,使失败不会毒害整张图
在链中,失败会级联。C 死了,D 永远不会运行,一切停止。
在图中,失败应该被限制在它的节点内。
这已经部分实现了:在 parallel() 内部爆炸的 thunk 会解析为 null,因此八个好的 Agent 返回,而坏的那个独自倒下。你的 .filter(Boolean) 就是限制。
设计每个扇入,使其能够容忍缺失的输入,而不是假设完整的集合。
最微妙的失败是节点互相踩踏。当多个 Agent 并行写入文件时,它们会发生冲突。
解决办法是隔离:每个 Agent 在自己的 git 工作树中运行,在沙箱中完成工作,然后干净地合并。
只有当节点真正并行写入时才使用这种方法。它是唯一需要它的拓扑结构的安全带,而不是每次执行的默认税。
→ 规则:每个文件只有一个写入者。并且每个扇入都容忍间隙。
11. 添加一个循环,但要让它收敛
有时直到你进入内部,你才知道工作有多大。未知规模的发现。一个 Bug 扫描,发现一个会引出三个。
这就需要循环:一条受控的边回到之前的节点。
危险显而易见。不收敛的循环就是无限循环,不断启动 Agent 直到烧光你的预算。
能够收敛的模式是循环直到干涸:持续启动搜索者,直到连续 K 轮没有产生新东西,然后停止。

而决定成败的细节,几乎每个人第一次都会犯的错误,是你针对什么进行去重。
针对所有见过的东西去重,而不仅仅是已确认的东西。
否则,被拒绝的发现每一轮都会重新出现,循环永远不会干涸,你就造了一台花钱无限重新发现相同死胡同的机器。
1const seen = new Set();2const confirmed = [];3let dryRounds = 0;45while (dryRounds < 2) { // 2 轮空后停止6 const found = (await parallel(7 SEARCHERS.map((b) => () => agent(b.prompt, { schema: BUGS }))8 )).filter(Boolean).flatMap((r) => r.bugs);910 const newItems = found.filter((b) => !seen.has(key(b)));11 if (!newItems.length) { dryRounds++; continue; } // 没有新东西 → 向干涸迈进1213 dryRounds = 0;14 newItems.forEach((b) => seen.add(key(b))); // 针对 SEEN 去重,而非 confirmed1516 const judged = await parallel(newItems.map((b) => () =>17 parallel(['correct', 'security', 'repro'].map((lens) => () =>18 agent(`从 ${lens} 角度判断 "${b.desc}"。它真实吗?`, { schema: VERDICT })))19 .then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 }))20 ));2122 confirmed.push(...judged.filter((v) => v.real).map((v) => v.b));23}
→ 规则:每个循环都有一个轮次限制,并针对所有见过的东西去重。
区块 4 · 确保它不会毁了你
12. 跨节点错开模型
并非每个节点都需要你最好的模型。
图以一种单一 Agent 永远无法做到的方式让这一点变得显而易见。有些节点是有限且重复的——提取这个字段、分类这个工单。而有些节点是真正判断所在的地方——综合报告、裁决发现。
在便宜的模型上运行无聊的节点,把昂贵的 Token 花在判断重要的事情上。
在工作流中,每个子 Agent 都继承自你会话的模型,除非脚本覆盖它。默认情况下,大型执行完全按你的会话级别计费。
在特定 agent() 调用中的模型选项,只会将该节点路由到别处。
在大型执行前审查 /model,然后降低重复性扇出节点的模型,同时保持融合节点的高模型。
这就是将贪婪 Token 的图变成经济型图而不改变其形状的杠杆。
→ 规则:只有涉及判断时才使用昂贵模型。
13. 拓扑结构就是你的成本和延迟
图的形状不是装饰性的。它是影响时钟时间的最大杠杆。
那个让每个人纠结的决定:parallel() 与 pipeline()。
→ parallel() 屏障使所有东西等待最慢的节点,然后下一阶段才能开始 → pipeline() 让每个元素独立地流过所有阶段,没有屏障。元素 A 可能在阶段 3 时,B 还在阶段 1

快的元素提前完成,而不是被慢的元素卡住。
默认使用 pipeline。只有当某个阶段确实需要所有先前结果同时出现时,才诉诸屏障:针对集合去重、基于总数提前退出、需要与"其他发现"进行比较的提示词。
"看起来更干净"和"阶段感觉上是分开的"不是理由。屏障延迟是真实、可测量且浪费的时间。
分开不等于同步。
→ 规则:默认使用 pipeline,以合理的例外来使用屏障。
14. 让 Claude 绘制图
最后一步是停止手动绘制那些你无法提前规划的图。
使用动态工作流,你描述目标,Claude 自己编写编排脚本:分解任务、选择扇出、启动协调的子 Agent 集群、综合结果。
你得到一张针对该特定执行量身定制的图,而不是一个你希望它能凑合的固定图。

有三个入口点:
→ 在提示词中说"workflow"这个词,Claude 就会为任务编写一个 → 执行一个已保存的或标准的工作流。/deep-research 是一个在生产中运行的真实图:限定 → 并行搜索 → 获取 → 对抗性验证 → 综合。正是这个路线图的骨架。 → 激活模式,为会话中的每个重要任务规划一个工作流
当执行顺利时,将其脚本保存在 .claude/workflows/ 中。版本化、可按名称重新执行、并且任何克隆仓库的人都可以启动。
› 启动一个工作流,审计 src/routes/ 中的所有路由,查找缺少的认证。每个路由文件一个 Agent,并在报告之前验证每个发现。
● Claude 编写了一个编排脚本 · 在后台启动…
/workflows — auth-audit · 运行中
✓ 限定 1/1 2.1k tok · 4s
✓ 扇出 18/18 每个路由文件一个 Agent
◯ 验证 11/18 每个发现 3 票怀疑者…
○ 综合 0/1 等待验证
会话保持响应,你可以在集群运行时继续工作
→ 规则:如果工作的形状在每次执行中都变化,不要绘制图。描述它。
本周要构建的六张图
所有路由的安全扫描。 每个路由文件一个子 Agent,每个都寻找缺失的认证检查,然后是验证步骤,在报告之前确认每个发现。单一上下文无法维持的广度。
通过 /deep-research 带有来源的报告。 一张已经内置于 Claude Code 中的图。它将你的问题分解成不同角度,并行运行搜索,去重来源,并在写入之前用三个怀疑者对抗性验证每个声明。
逐文件移植模块。 将翻译扇出到各个文件,测试套件作为每个文件的关卡,失败回到循环中。对抗性审查捕获了单个通过可能会交付的破损内容。
对 diff 的对抗性审查。 基于大小路由:小改动快速通过,大改动触发全面并行审计,评审者使用不同的视角——正确、安全、快速——然后由评审团综合。
定时生态系统扫描。 保存一次,永久重新执行。并行咨询多个来源——发布、博客、论坛——在屏障处按影响排序,然后写摘要。版本化在 .claude/workflows/ 中,可按名称启动。
未知规模的发现。 你不知道有多少 Bug。并行搜索者,针对所有见过的东西去重每个新发现,验证幸存者,循环持续直到两轮空。然后停止。
启动你的第一张图之前的检查清单
→ 我是否删除了不传输数据的边? → 工作是否真的拆分了,还是每一步都需要前一步? → 每个节点是否都有有限的输入、有形状的输出和单一任务? → 我是否在为一个能用 flatMap 完成的事情付钱给 Agent? → 是否存在不需要整个集合的屏障? → 是否有任何发现到达结果而没有人试图杀死它? → 两个验证者是否问同一个问题? → 所有循环是否有轮次限制? → 是否有多个节点写入同一个文件? → 重复性节点是否在昂贵模型上运行?
如果你超过三个失败,你拥有的不是一张图。而是一条有更多步骤的链。
结论
提示的人问一个问题。架构的人绘制一张图。
线性 Agent 从来不是天花板。它只是第一种形式,每个人都会抓住它,因为它符合我们书写的方式。一行、一个脑袋、一次一件事。
当你开始看到节点和边时,你不再要求 Agent 走更多步骤,而是开始要求图变得更宽。
在工作独立的地方扇出。在信任重要的地方加门。在判断不关键的地方用便宜的模型。
大多数人会继续在单行线中排队步骤。
那些学会绘制图的人,将运转整个集群。而且他们永远不会注意到其他人被困在低天花板下。
今晚就画出你当前的系统。只要任务和它们之间的箭头。数出虚假的边并删除它们。
这是整个工艺的第一步,不花一分钱,而且通常比任何你能买到的工具都能消除更多的等待时间。





