第17章:Graph Engineering —— 从循环到图,为并行编排设计关系
"There is no new product called Graph. And loops are not going anywhere. loops versus graphs is the wrong argument. Graphs have loops."
没有一个叫"图"的新产品,循环也不会消失。循环还是图,本身就是个伪命题——图里本来就跑着循环。
——towards_AI《这该死的图工程到底是什么》,2026-07-20
第 12 章讲 Loop Engineering,把 Agent 从"一问一答"升级成"行动→观察→评估→调整"的自主循环。第 8 章讲 goal-workflow 怎么把需求、规划、执行、交付串成一条闭环。这两章都在打磨"一个循环怎么跑得更好"。
本章要往上再走一层:当你手里有好几个循环要一起跑,它们之间的先后、并行、依赖该怎么安排?
回答这个问题的新词,2026 年 7 月冒了出来:graph engineering(图工程)。
X 上照例吵成一团。有人说这是又一次新瓶装旧酒的炒作,"图"根本不是什么新东西;有人说循环已经过时,图才是 Agent 编排的终极形态。towards_AI 那篇《这该死的图工程到底是什么》给了一个很克制的判断:图里本来就有循环,"循环还是图"是个假问题。
本章分三段走:先讲清楚图工程到底是什么,它的历史、概念和理论根;再看一个把图工程做成可用工具的 skill——goal-workflow 里的 /graph;最后拿一个正在跑的真实项目 pigo,看 /graph 跑起来到底长什么样、产出什么东西。
17.1 一条命名的演化线
要理解图工程,先把这几年的命名演化摆出来看:
- prompt engineering:怎么把一句话问好,让模型给出想要的答案。
- context engineering:单次调用喂什么、不喂什么,窗口里装什么。
- harness engineering:围绕模型搭一整套系统(工具、记忆、验证、恢复),让 Agent 在生产环境里可靠。
- loop engineering:不再把 Agent 当"一问一答",而是设计"行动→观察→评估→调整"的循环,让它能自主迭代。
- graph engineering:设计多个过程(往往就是多个循环)之间的关系——谁在谁之前、谁能并行、谁的输出喂给谁。
towards_AI 一句话点破本质:图工程是在设计这些过程之间的关系,名字是新的,但大部分难题都不是新的。
也就是说,图工程不是发明了新东西,而是给"Agent 编排里那些老大难问题"起了个新名字。这些老问题包括:怎么拆任务、怎么表达依赖、哪些能并行、失败了怎么恢复、状态放哪儿。
17.2 循环 vs 图:一个假问题
社区最大的争论是"循环还是图"。但这个二分本身是错的。
towards_AI 给出的操作路径最清楚:先从循环开始,再画出循环周围必须发生的事,画出来的那张东西就是图。
先有循环,就是那个最小的干活单元:读代码、改、测、再改。当你手里有好几个这样的循环要协调时,你自然会去画它们的先后和并行关系,画出来就是一张图。所以图不是来取代循环的,它更像是装循环的容器,图里头本来就跑着循环。
17.3 更深的理论根:都是状态机
Peter Steinberger 在 7 月中旬挑起过一场"循环还是图"的辩论,最后收束到一个更根本的答案:loops 和 graphs 本质上都是有限状态机(FSM)。
有限状态机的核心就一个式子:
(state, event) → nextState
形式化地写成五元组 M = (S, Σ, δ, s₀, F):状态集、事件集、转移函数、初始状态、终止状态集。这套东西的威力在于两点:
- 消灭不可能状态(illegal states unrepresentable):一个"加载中且已失败且已成功"的三重矛盾状态,在状态机里根本无法表达,因为转移函数 δ 里没有通向它的边。
- 转移函数即数据:δ 可以是一张表、一份 JSON,而不是一堆散落的
if/else。这让编排逻辑可读、可存档、可回放。
从 FSM 往上看:
- 循环是 FSM 的一种退化形态——通常是一两个状态之间反复转移。
- 图是 FSM 的完整展开——多个状态、多条边,天然支持分叉与汇合。
- Harel 的 Statecharts(1987) 在 FSM 上加了层级、并行和历史,正是现代 Agent 编排要的东西。
映射到 Agent,有几个很实际的推论:
- guard 边:某些转移要有守卫条件(比如"测试通过才能进入 ship 状态")。
- 持久化:状态要能落盘,Agent 崩了能从上一个状态恢复。
- 幻觉即"无边可走":如果模型吐出了一个当前状态下根本不存在的事件,转移函数找不到对应的边。这恰好是个天然的护栏点。
17.4 图工程绕不开的一个坑:Reality Anchor
图工程最锋利的一句警告来自 towards_AI:没有现实锚点,图不过是一场被项目管理包装得更漂亮的大型幻觉。
意思是,你可以画出一张无比精美的 DAG,节点整齐、依赖清晰、并行也漂亮,可要是每个节点的产出没跟真实世界挂上钩(测试真跑了吗?PR 真开了吗?CI 真过了吗?),那这张图不过是把幻觉组织得更有条理罢了。所以每个节点都得有个"现实锚点",一个能验证、真发生过的产物。
这其实就是上一节那台状态机的另一面。一个节点报告自己"完成",却拿不出真实产物,就等于状态机吐出了一个当前状态下根本走不通的转移——一次没有边可走的幻觉。锚点的作用,就是给"完成"这个事件配一条真实的边:PR 开了、测试绿了、CI 过了,转移才算数。少了这条边,图工程就退化成 Goodhart 定律在 Agent 系统里的老毛病:一个中间指标一旦变成目标,它就废了。
那怎么给一张图扎上锚点?落到工程上就三件事:
- 节点的"完成"由外部产物定义,不由 agent 自报。 每个节点退出时得留下一个能被独立验证的东西——一个 PR 号、一次绿色的测试、一条 CI 记录。agent 说"我做完了"不作数,
git log里查得到那条 commit 才作数。 - 依赖边升级成 guard 边。 下一个节点能不能开工,不看上一个节点声称完成,而看它的锚点是否真的落地了;锚点没到位,边就不放行,波次就卡在原地。
- 状态落盘、可复核。 整张图的进度写进一份可读文件,谁完成了、锚在哪个产物上一目了然,agent 崩了也能从上一个真实状态接着跑。
这三点到 17.6 会变得很具体——我最在意 pigo 那份状态文件里的,恰恰不是漂亮的波次分层,而是 "status": "shipped", "pr": 191 这一行真实产物。
顺带厘清一个容易混的点,它决定了本章这张图到底是什么图。Dale Everett(Polygres)说的"图"是数据图,节点是数据、边是数据关系;而经典 FSM/Statecharts 传统里的"图"是控制图,节点是状态、边是转移。同一个字,两拨人指的是两种东西。本章谈的图工程、后面 /graph 排的那张 DAG,都是控制图——排的是"谁先谁后、谁能并行"的执行顺序,不是数据血缘。动手前先分清这一点,多数 Agent 编排要的是后者。
17.5 把图工程做成工具:goal-workflow 的 /graph
理论讲完,来看落地。smallnest 的 goal-workflow skill 套件把图工程做成了一个能直接用的 skill:/graph。官网 https://goal.rpcx.io 的介绍已经很到位,这里提炼要点。
17.5.1 它在整个工作流里的位置
goal-workflow 是一套覆盖"需求→规划→执行→交付"的 skill 链:
/prd → /prd-to-spec → /to-issues ─┬─► /loop-it(串行执行)
└─► /graph(并行执行)
/prd 写需求文档,/prd-to-spec 转成技术规格,/to-issues 拆成一个个 issue。到执行环节有两个分叉:
/loop-it:把 issue 一个接一个串行做完。简单、稳、适合强依赖的线性任务。/graph:把 issue 之间的依赖建成 DAG,能并行的就并行做。适合任务多、依赖交错的场景。
17.5.2 /graph 的心智模型
/graph 借鉴了 LangGraph 的 StateGraph 和 Pregel/BSP 的 superstep 语义,再加上 Claude Code 的 dynamic workflow 思想。核心概念表:
| 概念 | 含义 |
|---|---|
| Node(节点) | 一个独立可完成的子任务(对应一个 issue) |
| Edge(边) | 节点间的依赖关系(A 完成才能开始 B) |
| Superstep / Wave(超步/波次) | 拓扑分层后,同一层的节点可并行执行 |
| Fan-out(扇出) | 一个波次里同时派多个子 agent 干活 |
| Fan-in(扇入) | 波次屏障:本波全部完成才进入下一波 |
| State channel | .graph_state 文件,持久化整张图的执行状态 |
| Live tracker | graph.html,可视化实时进度 |
| Dynamic re-plan | 执行中可以根据结果动态重规划 |
17.5.3 执行流程
/graph 跑起来大致是这么几步:
- 分解:把 PRD/issues 拆成节点。
- 建 DAG:给节点之间连依赖边。
- 拓扑分层:用类似 Kahn 算法的拓扑排序把节点分成一波一波(waves),同一波无依赖、可并行。
- 确认:把分好的波次拿给人确认。
- 逐波执行:每一波 fan-out 出多个子 agent(各自在独立的 git worktree 里干活,互不干扰),全部完成后 fan-in,再进下一波。
每个子 agent 用统一的 prompt 模板派发,跑在自己的 worktree 分支上,完成后开 PR。状态实时写进 .graph_state,graph.html 负责可视化。回头看前面的理论,这套东西其实就是一台状态机:它落了盘,每个节点靠 PR 和测试锚在现实里,波次之间还有 guard 边卡着(依赖没满足就进不了下一波)。
安装:
npx skills add smallnest/goal-workflow --skill graph
17.6 一次真实实践:pigo 的 provider 对齐
光看文档不够,来看一个正在跑的真实项目:pigo(smallnest/pigo,pi 的 Go 复刻版)。我正用 /graph 给它做一个功能:Provider 环境变量与默认端点对齐 pi。
17.6.1 任务背景
pigo 当前只内置了 5 个可用的 LLM provider,API key 解析只覆盖 9 个环境变量;而 pi 支持约 30 个 provider,每个都有约定的环境变量名、默认 base_url 和所属协议。这个功能要让 pigo 全量对齐——用户设好 DEEPSEEK_API_KEY,加个 --provider deepseek,pigo 就能用内置默认端点直接对话。
这个任务被 /prd → /to-issues 拆成了 9 个节点(#182–#190),节点之间有明确依赖,非常适合 /graph 来并行调度。
17.6.2 看 skill 的产物:.graph_state
/graph 跑起来后,第一个产物就是 .graph_state——整张 DAG 的持久化状态。pigo 里这份文件长这样(节选):
{
"task": "Provider 环境变量与默认端点对齐 pi (#182-#190)",
"repo": "smallnest/pigo",
"waves": [[182], [183, 184], [185], [186, 187, 188], [189, 190]],
"current_wave": 1,
"nodes": {
"182": { "title": "Provider 注册表(元数据单一来源)", "deps": [],
"status": "shipped", "pr": 191,
"branch": "feat/node-182-provider-registry" },
"183": { "title": "统一 API key 解析到注册表", "deps": [182],
"status": "in_progress" },
"184": { "title": "--provider 显式标志", "deps": [182],
"status": "in_progress" },
"185": { "title": "通用 <PROVIDER>_BASE_URL 覆盖约定", "deps": [182, 184],
"status": "pending" }
// ... 186-190
}
}
把它翻译成人话,这张 DAG 分成 5 个波次:
Wave 0: [182] ← 建 provider 注册表(单一数据来源)
│
Wave 1: [183, 184] ← key 解析 + --provider 标志(并行)
│
Wave 2: [185] ← <PROVIDER>_BASE_URL 覆盖约定
│
Wave 3: [186, 187, 188] ← OpenAI兼容 / Anthropic / 特殊鉴权(三路并行)
│
Wave 4: [189, 190] ← 模型目录扩展 + 文档(并行)
几个理论概念在这里全都对上号了:
- 拓扑分层:182 是所有人的地基(
deps: []),必须先做,独占 Wave 0。 - Fan-out:Wave 1 的 183 和 184 都只依赖 182,互不依赖,于是同时派了两个子 agent 去做。Wave 3 更狠,186/187/188 三路并行。
- Fan-in 屏障:185 依赖 184,所以必须等 Wave 1 整波做完(
current_wave从 1 推进到 2)才能开始。 - 现实锚点:182 的 status 是
shipped,pr: 191。这不是"agent 说自己做完了",而是真的开了 PR #191、真的合进了 master(git log里那条808267d Add central provider registry as single source of truth (#182) (#191))。这正是 towards_AI 说的 anchor:判断一个节点完没完,看的是真实产物,不是模型的自我报告。
17.6.3 另一个产物:git worktree 隔离
/graph 的并行不是在同一个工作目录里瞎改,而是给每个并行节点开一个独立的 git worktree:
.graph-worktrees/node-183 (feat/node-183-auth-registry)
.graph-worktrees/node-184 (feat/node-184-provider-flag)
183 和 184 各自在自己的分支、自己的目录里跑,互不干扰,做完各自开 PR。所谓"扇出多个子 agent 并行",落到地上就是这么回事:状态机的并行分支,对应文件系统里几个平行的 worktree。
17.6.4 还有 graph.html
/graph 还会渲染一个 graph.html 实时可视化整张图的进度(哪个节点绿了、哪个在跑、哪个还灰着),由 skills/graph/scripts/render_graph_html.py 从 .graph_state 生成:
python3 skills/graph/scripts/render_graph_html.py .graph_state graph.html
17.7 与全书方法论的对接
图工程不是凭空长出来的,它坐在前面几章的方法论上再往上一层。
和第 12 章 Loop Engineering 是同一条演化线的下一站。 第 12 章把 Agent 升级成能自主迭代的循环,本章要解决的是多个循环怎么协调。towards_AI 那句"图里本来就有循环"说的正是这个关系:图不取代循环,它是装循环的容器。第 12 章讲的持久化原语"Agent 会忘记,仓库不会",在这里长成了 .graph_state——整张图的状态落盘。
和第 8 章 Goal Workflow 是同一套工具链。 /graph 就是 goal-workflow 里 /prd → /to-issues 之后的执行分叉之一,和 /loop-it 并列:一个串行,一个并行。第 8 章打磨的是单条闭环,本章把闭环之间的依赖排成了 DAG。
和第 10 章 Harness Engineering 共享同一批硬骨头。 图工程列的那些"老大难问题"——拆任务、表达依赖、失败恢复、状态放哪儿——正是 Harness 要解决的工程问题。图只是把它们组织成了一张显式的关系图。
和第 16 章 agent-skills 的反自欺机制殊途同归。 第 16 章用反合理化表阻止 Agent 跳过步骤,本章用现实锚点阻止节点谎报完成。两者的核心信念一样:Agent 会为自己找借口,得用制度性的手段逼它拿出真实证据。反合理化表管的是"别跳过工序",现实锚点管的是"别假装做完"。
17.8 本章小结
图工程说到底是在设计循环之间的关系——谁先谁后、谁能并行、谁的输出喂给谁。名字是新的,难题都是老的。往下刨,循环和图都是有限状态机的不同展开,这给了图工程一套现成的理论工具:guard 边、持久化、"幻觉即无边可走"。
但一张图再漂亮,也可能是"组织得更好看的幻觉"。分水岭是现实锚点:每个节点的完成必须由外部产物定义,由 guard 边把关,落盘可复核。goal-workflow 的 /graph 把这套东西做成了能跑的工具,而 pigo 那 9 个节点、5 个波次就是它跑起来的样子。
我最在意的其实就是 .graph_state 里那行 "status": "shipped", "pr": 191。没有它,这张图再漂亮也只是幻觉。有了它,图才算真的在干活。
留言板
欢迎在此分享你的想法!评论通过 GitHub Issues 存储,需要 GitHub 账号登录。