# Graph Engineering with Claude: 14-Step roadmap from 0 to graph architect (Full Course)
**作者**: Codez
**日期**: 2026-07-20T11:23:31.000Z
**来源**: [https://x.com/0xCodez/status/2079165300625330317](https://x.com/0xCodez/status/2079165300625330317)
---

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.

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

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.
```
› 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
```
## 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.
## 相关链接
- [Codez](https://x.com/0xCodez)
- [@0xCodez](https://x.com/0xCodez)
- [408K](https://x.com/0xCodez/status/2079165300625330317/analytics)
- [movez.substack.com](https://movez.substack.com/)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [7:23 PM · Jul 20, 2026](https://x.com/0xCodez/status/2079165300625330317)
- [408K Views](https://x.com/0xCodez/status/2079165300625330317/analytics)
- [View quotes](https://x.com/0xCodez/status/2079165300625330317/quotes)
---
*导出时间: 2026/7/21 13:05:13*
---
## 中文翻译
# 使用 Claude 进行图工程:从 0 到图架构师的 14 步路线图(完整课程)
**作者**: Codez
**日期**: 2026-07-20T11:23:31.000Z
**来源**: [https://x.com/0xCodez/status/2079165300625330317](https://x.com/0xCodez/status/2079165300625330317)
---

大多数尝试构建多步骤智能体的人最终都得到了一条直线。第一步、第二步、第三步——每一步都礼貌地等待上一步完成后才开始。
十个人里有九个会发现,其中一半的步骤根本不需要等待。
它们不进行路由,不进行分支,也不进行并行化。它们只是排队——一个上下文窗口,一次只做一件事,直到窗口填满,智能体忘了自己在做什么。
> 关注我的 Substack 获取最新的 AI 洞察:movez.substack.com
这就是那个将单一线条转化为图的 14 步路线图:这个图会分散到整个集群,验证自己的发现,并汇聚成一个单一智能体永远无法承载的结果。

这是没人明说的转变。提示词是一句话。循环是一个周期。工具架是智能体站立的基座。
但工作本身的形状——什么在什么之前运行,什么可以同时运行,什么必须等待其他一切——这个形状就是一张图。节点负责思考,边负责传递结果。
Claude Code 发布了直接构建这些图的工具:动态工作流。
Claude 编写一个普通的 JavaScript 编排脚本,然后生成一个协调的子智能体集群来执行它——而且协调本身不消耗任何模型 token,因为这是代码,不是对话。
## 01. 节点是任务。边是流动的数据。
图只有两样东西,把它们搞清楚了,就能解决大部分困惑。节点是一个工作单元——一个智能体,一个有界的任务,一个输入和一个输出。
边是一个依赖关系:它说这个节点的输出馈送给那个节点的输入。仅此而已。

错误在于把“然后”当成一条边。“总结文件然后告诉我天气”这两者之间没有边——天气并不消费那个总结。
这是两个断开的节点,而线性脚本却毫无必要地把它们链接在一起。只有当数据真正在上面流动时,边才存在。
学会对你的智能体中的每一个“然后”提出这个问题:下一步是否读取上一步的输出?如果不,就没有边,等待就是浪费。
```
把它画成框和箭头。一个框就是一个 agent() 调用。
一个箭头就是一个从一个调用的返回值传递到另一个调用的
提示词中的变量。如果你画不出箭头——如果没有变量穿过——那么
这两个框就是独立的,而独立性是你将在本课程其余部分利用的东西。
```
## 02. 你的线性脚本是一个退化的图
当你把一个智能体写成“做 A,然后 B,然后 C,然后 D”时,你已经画了一张图——一条单分支链。每个节点都恰好有一条入边和一条出边。
它能正确运行。但它也运行缓慢且脆弱,因为链条没有冗余:如果 C 停滞,D 永远不会发生,而 A 的工作则被困在上游无处可去。

图工程的第一个真正技能是重画这条链条。拿出你的线性智能体,对每个箭头问一遍第 1 步的问题。
大多数链条都有两三个不传递数据的箭头——它们只是你碰巧输入内容的顺序。
切断那些箭头,链条就会坍缩成更宽的东西:几个可以同时运行的独立节点,馈送给一个需要它们全部的单个节点。
## 03. 给每个节点一个契约
一个无法推理的节点,是一个无法并行的节点。解决办法是契约:有界输入,有界输出,恰好一项任务。
输入是节点读取的任何内容——显式传入,永远不要从共享窗口假设。输出是一个定义好的形状,理想情况下经过验证,以便下一个节点可以在不猜测的情况下消费它。

在工作流中,这个契约通过模式来强制执行。当你交给 Claude 一个带有 JSON 模式的 agent() 调用时,Claude 生成的子智能体会被迫返回经过验证的结构化数据——验证发生在工具调用层,所以 Claude 会在不匹配时重试,而不是给你一段必须解析并祈祷的自由文本。
这就是 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 现在是下一个节点可以信任的形状——而不是自由文本。
```
## 04. 把边视为数据契约
边不仅仅是“B 在 A 之后”。它是关于穿过内容的承诺:A 产生这个形状,而 B 被构建来消费这个形状。当你根据数据而不是顺序来命名边时,两件事会变得更容易。

你可以立即看出边是否真实(数据真的在流动吗?),并且只要形状保持不变,你就可以交换任一端的节点而不破坏图。
在实践中,边存在于普通的 JavaScript 中。扇出和综合之间的归约步骤——扁平化、去重、过滤——只是操作你的节点返回的形状的代码。
不需要智能体。这是图思维的一个安静胜利:人们大量消耗模型 token 的东西,实际上很多都是边,而边是免费的。
```
诱惑是生成一个智能体来“合并结果”。抵制它。
如果合并意味着扁平化-去重,那就是 results.flatMap(...)
和一个 Set —— 确定性的、即时的、零 token。把智能体留给判断,
而不是留给管道工程。一个每条边都是一个智能体的图,是一个
在自己的线路上付租金的图。
```
## 05. 使用 parallel() 进行扇出
这是能回报一切的操作。当你有 N 个独立节点时——N 个来源要检查,N 个文件要审查,N 条路由要审计——不要把它们链接起来。
你告诉 Claude 把它们扇出并同时运行。在工作流中,这就是 parallel():Claude 接受一个 thunk 数组,并为每个 thunk 生成一个子智能体,所有智能体并发执行,然后交还给你结果数组。

两个细节使其鲁棒。首先,parallel() 是一个屏障——它在返回前等待每个 thunk,所以下一阶段能看到完整的集合。其次,抛出错误的 thunk 会解析为 null,而不是拒绝整个批次,所以一个不稳定的智能体不会搞砸整个运行。
始终对结果进行 .filter(Boolean)。并发性被限制在你的核心数左右,多余的会排队,所以你可以传递一百个 thunk,它们都会完成——只是每次一小批。
```
phase('Research');
// 九个来源,九个智能体,一次性完成。
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); // 丢弃来自失败智能体的 null
```
扇出存在于 Claude 编写的代码中,而不是模型对话中。Claude 自己的上下文永远不会同时容纳九个来源——每个子智能体携带自己的来源,只有最终答案会回来。
这就是让 Claude 能够将工作流扩展到数十或数百个子智能体而不会淹没会话的原因。编排层消耗零 token,因为它不是 Claude 思考的另一个回合。
## 06. 在屏障处扇入
只有当有东西收集它时,扇出才有用。扇入是边汇聚的节点——在这里,一个智能体(或一段代码)一次性看到所有上游结果并做需要整个集合的事情:跨来源去重、按影响排名、如果总数为空则提前退出。这是屏障值得其时钟成本的唯一地方。

保持图快速的规则:只有当一个阶段真正需要将所有先前的结果放在一起时,才使用屏障。跨所有来源去重?屏障——正确。
```
// 边:普通 JS,无智能体,零 token。
const flat = collected.flatMap((c) => c.items);
log(`Collected ${flat.length} items`);
phase('Curate');
// 屏障节点:需要整个集合来去重 + 排名。
const curated = await agent(
`Dedupe and rank these by impact:\n${JSON.stringify(flat)}`,
{ phase: 'Curate', schema: CURATED_SCHEMA },
);
```
只是扁平化一个列表?那是边,内联完成。嗅觉测试残酷而简单:如果你写了 parallel → transform → parallel,而中间的 transform 没有跨项目依赖,你应该使用管道并完全跳过屏障。
## 07. 菱形:拆分 → 工作 → 合并
将扇出和扇入放在一起,你就会得到每个严肃智能体图的骨干拓扑:菱形。
一个节点拆分任务,许多节点并行工作,一个节点合并。这是市场扫描、依赖审计、代码审查、研究报告背后的形状——交换来源和提示词,同一个骨架就能适应。

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

这就是确定性成为特性而非局限的地方。路由器的决策可以由 Claude 驱动(子智能体分类),但路由是 Claude 编写的代码——所以对于相同的分类,它每次都以相同的方式运行。
你在节点获得 Claude 的判断,在边获得脚本的可靠性。没有突发出现的“Claude 决定跳过审计”的惊喜——因为跳过必须被写入图中,而事实并非如此。
```
// 路由器节点:智能体分类,代码选择边。
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') {
// 重型路径:全面并行审计
review = await parallel(FILES.map((f) => () => agent(`Audit ${f}`)));
} else {
// 轻型路径:一次快速通过
review = await agent(`Quick review of ${diff}`);
}
```
## 09. 在边上放置验证器
图的真正杠杆不是更多的智能体——而是你可以围绕它们构建以产生信心的结构。
验证器节点位于允许结果下游之前的边上,它唯一的工作是试图否决发现。如果它存活下来,它就通过。如果不通过,它永远不会到达答案。

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

更微妙的失败是节点互相踩踏。当智能体并行写入文件时,它们可能会冲突。
解决办法是隔离:“worktree”(工作树)——每个智能体在自己的 git worktree 中运行,在沙箱中做工作,并干净地合并。
只有在节点真正并行写入时才使用它。它是唯一需要它的拓扑结构的安全带,而不是每次运行的默认税。
## 11. 添加一个循环——但要让它收敛
有时你在进入任务之前不知道它有多大:未知大小的发现,一个发现一个 bug 会暴露更多 bug 的 bug 扫描。这需要一个循环——一条受控的边回到之前的节点。
危险是显而易见的:一个不收敛的循环就是一个无限循环,会生成智能体直到你的预算耗尽。

收敛的模式是循环直到干涸:持续生成查找器,直到连续 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(`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));
}
```
## 12. 在节点之间分层使用模型
并非每个节点都需要你最好的模型。图让这一点显而易见,而单个智能体永远做不到:有些节点是有界和重复的(提取这个字段,分类这个工单),而有些承载真正的判断(综合报告,裁决发现)。
在无聊的节点上运行更便宜的模型,把昂贵的 token 花在判断真正存在的地方。
![Image](https://pbs.twimg.com/media/HNqrj4UWYAAvHC0