使用 Claude 进行 Graph Engineering:从 0 到成为图架构师所需的 14 步路线图

大多数人构建多步骤 Agent,最后都得到一个直线流程:第一步、第二步、第三步——每一步都礼貌地等前一步跑完才开始。然后十分之九的人会意识到,其中一半的步骤根本不需要等待。

它们不会路由、不会分支、不会并行,只是排队:一个头部、一个上下文、一次只做一件事,直到窗口被填满、Agent 忘记自己在干什么。

真正的转变很少有人说破:提示词是一句话,循环是一个周期,调用框架是 Agent 站的地板。但工作本身的形状——什么可以先跑、什么能并行、什么必须等所有东西就绪——这个形状是一个图。

节点负责思考,边承载结果。而 Claude Code 的动态工作流(dynamic workflow)就是直接画这张图的工具:Claude 写一段纯 JavaScript 编排脚本,生成一个协调的子 Agent 舰队去执行它——协调本身花 0 个模型 Token,因为它是代码,不是对话


一、先建立世界观:节点和边

一个图只有两种东西,想清楚它们能解决大部分困惑:

  • 节点(Node) 是一个工作单元——一个 Agent、一个有边界的任务、一个输入、一个输出。
  • 边(Edge) 是一个依赖关系:它声明「这个节点的输出,提供给那个节点的输入」。仅此而已。

最常见的错误,是把「然后」当成一条边。「总结文件,然后告诉我天气」——这两者之间没有边,因为天气不需要消费总结。这是两个断开的节点,被线性脚本强行串在了一起。只有数据真正通过边流动时,边才存在。

给自己立一条规矩:每遇到一个「然后」,问一句——下一步真的读取了上一步的输出吗?如果不是,就没有边,等待就是浪费。

画图就画方框和箭头:一个方框是一次 agent() 调用,一个箭头是一个变量——从一次调用的返回值,传到下一次调用的提示词里。画不出箭头(没有变量传递),两个方框就是独立的;而独立性,正是后面要利用的东西。

这是一条包含 14 个步骤的路线图,它可以将那单行线转化为一个图形结构。这个图形可以扩展到整个舰队范围内,验证自身的发现结果,并最终得出一个单个体永远无法达到的完美结果。
在这里插入图片描述


二、14 步路线图

01 · 节点是任务,边是流动的东西

如前所述。节点有边界、有输入有输出;边是数据依赖。把「然后」和「边」区分开,是图工程的第一课。
在这里插入图片描述

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

当你把 Agent 写成「做 A,然后 B,然后 C,然后 D」,你确实画了一个图——一条无分支的单链,每个节点恰好一条入边一条出边。它能跑,但慢且脆弱:链条没有冗余,C 卡住 D 永远不会发生,A 的工作困在上游无处可去。
在这里插入图片描述

图工程的第一项真功夫是重画链条:拿你的线性 Agent,对每条箭头问步骤 01 的问题。大多数链条有两三条箭头不携带数据——它们只是你碰巧键入的顺序。砍掉这些箭头,链条就塌成更宽的东西:几个独立节点可同时跑,再汇聚到一个需要它们全部结果的节点上。

03 · 给每个节点一个契约

一个你无法推理的节点,就是一个你无法并行化的节点。解法是契约:有边界的输入、有边界的输出、恰好一个任务。输入是节点读取的一切——显式传入,绝不从共享窗口里假设。输出是一个定义的形状,最好经过验证,让下一个节点无需猜测就能消费。

输入是节点所读取到的内容——需要明确传递进来,而不是从共享窗口中自动获取。输出则是一个定义好的格式,最好经过验证,这样下一个节点就可以直接接收它,而无需进行猜测操作。
在这里插入图片描述

在工作流里,契约靠 schema 强制。给 Claude 一个带 JSON schema 的 agent() 调用,生成的子 Agent 被强制返回经过验证的结构化数据——验证发生在工具调用层,所以不匹配时 Claude 会自动重试,而不是丢给你一段需要解析和祈祷的自由文本。

// 一个有真实契约的节点:有界输入,验证输出,一个任务。
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,           // 强制验证结构化输出
  agentType: 'general-purpose',
});
// result 现在是一个下一个节点可以信任的形状——而不是自由文本。

这就是「能被 Claude 接入图的节点」与「只有人类读得懂其输出的节点」的本质区别。

04 · 把边当成数据契约

一条边不只是「B 在 A 之后」,它是关于「什么会跨过边」的承诺:A 产出这个形状,B 被构建为消费这个形状。用其数据而不是其顺序来命名边,两件事变容易:你能立刻看出边是否真实(数据真的移动了吗?),并且只要形状不变,你可以替换两端节点而不破坏整张图。
在这里插入图片描述

实践中,边就是纯 JavaScript。扇出与合成之间的 reduce(扁平化、去重、过滤)只是对节点返回形状做操作的代码——不需要 Agent

诱惑是生成一个 Agent 来「合并结果」。抵制它。
如果合并意味着扁平化和去重,那就是 results.flatMap(...)
加一个 Set —— 确定性、即时、零 Token。把 Agent 留给
判断,而不是管道。每条边都是一个 Agent 的图,是在为
自己的布线支付租金。

05 · 用 parallel() 做扇出

这是让一切值回票价的关键操作。当你有 N 个独立节点——N 个要查的来源、N 个要审的文件、N 个要审计的路由——别串联,让 Claude 把它们扇出同时跑。在工作流里就是 parallel():Claude 接受一个 thunk 数组,为每个生成一个子 Agent,全部并发执行,再把结果数组返回给你。
在这里插入图片描述

两个细节让它健壮:① parallel() 是屏障,等每个 thunk 完成才返回,下一阶段看到完整集合;② 抛错的 thunk 解析为 null 而不是拖垮整批,所以一个不稳定的 Agent 搞不垮整次运行。永远对结果做 .filter(Boolean) 并发数受核心数限制,多余任务排队,传一百个 thunk 它们都会完成,只是一次处理少数几个。

phase('Research');

// 九个来源,九个 Agent,全部同时进行。
const raw = await parallel(
  SOURCES.map((s) => () =>
    agent(s.prompt, {
      label: `research:${s.key}`,
      phase: 'Research',
      schema: ITEM_SCHEMA,     // 每个节点返回经过验证的 JSON
      agentType: 'general-purpose',
    }),
  ),
);

const collected = raw.filter(Boolean);  // 丢弃失败 Agent 的 null

扇出存在于 Claude 写的代码里,不在模型对话里。Claude 自己的上下文从不会同时持有九个来源——每个子 Agent 带自己的来源,只返回最终答案。这就是为什么 Claude 能把工作流扩展到数十、数百个子 Agent 而不淹没会话:编排层花 0 Token,因为它不是 Claude 思考的又一轮。

06 · 在屏障处扇入

扇出只有有人收集才有用。扇入是边汇聚的节点——一个 Agent(或一段代码)一次性看到所有上游结果,做需要整个集合的事:跨来源去重、按影响排名、总结果为空则提前退出。这是屏障值得消耗墙上时钟时间的唯一地方。
在这里插入图片描述

// 边:纯 JS,没有 Agent,零 Token。
const flat = collected.flatMap((c) => c.items);
log(`收集了 ${flat.length} 个项目`);

phase('Curate');
// 屏障节点:需要整个集合来去重 + 排名。
const curated = await agent(
  `按影响程度去重并排名:\n${JSON.stringify(flat)}`,
  { phase: 'Curate', schema: CURATED_SCHEMA },
);

保持图快的原则:仅当一个阶段真正需要所有先前结果同时在一起时才用屏障。 跨所有来源去重?用屏障,正确。但仅仅扁平化一个列表?那是边,内联做就行。残酷的气味测试:如果你写了 parallel → transform → parallel,而中间那个 transform 没有跨项目依赖,你应该用管道(pipeline)并完全跳过屏障。

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

把扇出和扇入放一起,就得到每个严肃 Agent 图的主力拓扑:菱形。一个节点拆任务,许多节点并行干活,一个节点合并。它是市场扫描、依赖审计、代码审查、研究报告背后的形状——换来源和提示词,同一副骨架就能适配。
在这里插入图片描述

规范形式有个值得记的名字:扇出 → 规约 → 合成(fan-out → reduce → synthesize)。扇出去拿广度,用纯代码规约来压缩,用最终的 Agent 合成来写答案。

一旦你看见菱形,你就不再问「怎么让我的 Agent 做更多步骤」,而开始问「拆分点在哪、合并点在哪」——这才是真能扩展的问题。

08 · 用条件语句在运行时路由边

不是每张图都固定。有时走哪条边取决于某个节点发现了什么。路由器节点检查一个结果,决定哪个下游路径触发:分类工单后分支到正确处理器;检查 diff 大小后,要么快速审查,要么启动全面审计。

在工作流里,这只是对节点验证输出的 JavaScript if / switch,因为控制流在代码里。

// 路由器节点:一个 Agent 做分类,代码选边。
const { severity } = await agent(
  `分类这个 diff 的风险:\n${diff}`,
  { schema: { type: 'object',
      properties: { severity: { enum: ['low', 'high'] } },
      required: ['severity'] } },
);

let review;
if (severity === 'high') {
  // 重路径:完整的并行审计
  review = await parallel(FILES.map((f) => () => agent(`审计 ${f}`)));
} else {
  // 轻路径:一次快速通过
  review = await agent(`快速审查 ${diff}`);
}

这正是确定性成为特性而非限制的地方。路由器的决策可由 Claude 驱动(一个子 Agent 做分类),但路由本身是 Claude 写的代码——所以相同分类每次运行方式一致。你在节点拿到 Claude 的判断,在边拿到脚本的可靠。没有突发的「Claude 决定跳过审计」——因为跳过必须写进图里,而它没有。
在这里插入图片描述

09 · 在边上放验证器

图的真正杠杆不是更多 Agent,而是你能围绕它们构建、用来产生信心的结构。验证器节点位于结果被允许向下游传递之前的边上,唯一任务是尝试否定这个发现。幸存则通过,否则它永远到不了答案。
在这里插入图片描述

三个值得掌握的模式:

  • 对抗性验证:对每个发现生成 N 个独立怀疑者,提示它们反驳;只有多数幸存才保留。
  • 视角多样化验证:给每个验证器一个独特视角——正确性、安全性、能否复现——多样性能捕获 N 个相同检查永远发现不了的失败模式。
  • 评审团:从不同角度生成 N 个尝试,用并行评审员打分,从胜出者合成,同时嫁接最佳亚军的成果。

这正是某真实团队把 Bun 运行时移植到新平台、并把对抗性代码审查内嵌进循环的做法。

10 · 隔离节点,让一个失败毒不了一整张图

链条里失败会级联——C 失败,D 永不运行,整件事停摆。图里失败应被限制在节点内。这已经部分成立:parallel() 内抛错的 thunk 解析为 null,八个好 Agent 仍返回,坏的退出。你的 .filter(Boolean) 就是隔离。设计每个扇入节点去容忍缺失输入,而不是假设拿到完整集合。
在这里插入图片描述

更微妙的失败是节点相互冲突——Agent 并行写文件时会撞车。解法是隔离「工作树(worktree)」:每个 Agent 在自己的 git 工作树里跑,沙箱中干活,再干净合并。只在节点真在并行写文件时才用它,它是需要它的拓扑的安全带,不是每次运行的默认开销。

11 · 加一个循环——但必须让它收敛

有时你进入任务才知道任务多大:未知规模的发现、一次 bug 扫描、找到一个 bug 会牵出另外三个。这需要循环——一条回到早期节点的受控边。危险显而易见:不收敛的循环就是无限循环,会生成 Agent 直到预算耗尽。
在这里插入图片描述

收敛的模式是直到干涸的循环(loop-until-dry):持续生成查找器,直到连续 K 轮没发现新东西就停。几乎所有人第一次都会犯的错——你针对什么去重。针对所有已见内容去重,而不只是确认的结果。 否则被拒绝的发现每轮都重新出现,循环永不干涸,你造了台永远重发现同一死胡同的机器。

const seen = new Set(); const confirmed = []; let dry = 0;

while (dry < 2) {                       // 2 轮空结果后停止
  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; } // 没新东西 → 趋向干涸
  dry = 0;
  fresh.forEach((b) => seen.add(key(b))); // 针对 SEEN(已见)去重,而非 confirmed(已确认)

  // 计数前对每个新发现做多样化视角验证
  const judged = await parallel(fresh.map((b) => () =>
    parallel(['correctness', 'security', 'repro'].map((lens) => () =>
      agent(`通过 ${lens} 判断 "${b.desc}" — 真实吗?`, { 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));
}

12 · 跨节点分层用模型

不是每个节点都需要你最好的模型。图把这变得显而易见,而单个 Agent 永远做不到:有些节点有边界且重复(提取字段、分类工单),有些承载真正判断(合成报告、裁决发现)。在便宜模型上跑无聊的节点,把昂贵的 Token 花在判断真正发生的地方。
在这里插入图片描述

在工作流里,Claude 生成的每个子 Agent 默认继承你的会话模型,除非脚本覆盖它——所以大型运行默认完全按你的会话层级计费。单个 agent() 调用上的模型选项告诉 Claude 把那个节点路由到别处。大型运行前查一下 /model,让 Claude 把扇出的重复节点路由到更便宜模型、把合并节点留在高层级。这是把 Token 饥饿的图从昂贵变经济的杠杆,且不碰它的形状。

13 · 拓扑结构决定成本和延迟

图的形状不是装饰——它是墙上时钟时间最大的单一杠杆。一个坑翻所有人的选择:parallel() vs pipeline()parallel() 的屏障让所有内容在下一阶段开始前等最慢的节点。pipeline() 独立地把每个项目流经所有阶段、没有屏障——项目 A 在阶段 3 时,项目 B 还在阶段 1。快的项目提前完成,而不是在慢项目后面闲置。
在这里插入图片描述

默认用 pipeline() 仅当一个阶段真正需要所有先前结果同时在一起时才用屏障——跨集合去重、总结果提前退出、比较「其他发现」的提示词。「代码更干净」「阶段感觉独立」不是理由;屏障延迟是真实、可测量、被浪费的时间。独立不等于同步。

14 · 让 Claude 画图——自动路由

最后一步:对无法提前规划的任务,停止手动画图。

用动态工作流,你描述目标,Claude 自己写编排脚本——拆任务、选扇出、生成协调的子 Agent 舰队、合成结果。你拿到一张为本次运行量身定制的图,而不是一张你希望合适的固定图。
在这里插入图片描述

三种进入方式:

  1. 提示词里说「workflow」这个词,Claude 就为任务写一张。
  2. 运行一个保存的或捆绑的——/deep-research 就是生产环境里真实存在的图:范围界定 → 并行搜索 → 获取 → 对抗性验证 → 合成,正是本课程的骨架。
  3. 打开 ultracode,Claude 会为会话里每个重要任务规划一张工作流。跑得好时按 s 把脚本存到 .claude/workflows/——版本控制、可按名重跑、任何克隆仓库的人都能启动的图。
› 运行一个工作流来审计 src/routes/ 下每个路由是否缺少认证。
  为每个路由文件生成一个 Agent,然后在报告前验证每个发现。
● Claude 编写了一个编排脚本 · 在后台启动…
/workflows — auth-audit · running ✓
  范围界定 1/1  2.1k token · 4s
  ✓ 扇出 18/18  每个路由文件一个 Agent
  ◯ 验证 11/18  每个发现 3 票怀疑者…
  ○ 合成 0/1    等待验证
  会话保持响应 — 在舰队运行时继续工作

三、本周用 Claude 构建的六个真实图

原文作者给了六个可直接借鉴的实例,证明这张思维模型不是玩具:

  1. 跨每个路由的安全扫描:Claude 为每个路由文件生成子 Agent,各自找缺失的认证检查,再由验证器逐个确认发现,才进入报告。单个上下文装不下的广度。
  2. /deep-research 出带引用的报告:Claude 把问题拆成不同角度,跑并行搜索,去重来源,写之前用三票怀疑者对抗性验证每条声明。
  3. 逐文件移植一个模块:Claude 跨文件扇出翻译,把测试套件当作每个文件的关卡(gate),失败循环回去——对抗性审查捕获单次通过会放过的坑。
  4. 对 diff 做对抗性审查:按 diff 大小路由——小的走快速通过,大的触发完整并行审计,评审员用不同视角(正确性 / 安全性 / 性能)再合成。
  5. 按计划做生态扫描:保存一次,永久重复跑。并行查许多来源(发布、博客、讨论),在屏障处按影响排名,写摘要。版本控制在 .claude/workflows/,可按名启动。
  6. 未知规模的发现(bug hunt):你不知道有多少 bug。Claude 并行跑查找器,针对所有已见内容去重每个新发现,验证幸存者,持续循环直到两轮无新东西才停。
    在这里插入图片描述

四、结语:提示者提问,架构师画图

线性 Agent 从来不是天花板——它只是第一个形状,因为符合我们打字的方式,人人都用它。一行、一个头部、一次一件事。

一旦你能看见节点和边,你就不再要求 Agent「做更多」,而开始要求图「做更宽」:在工作独立处扇出,在信心重要处把关边,在判断不需要处分层模型。

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐