Agent Graph Engineering:从线性 Workflow 到可扩展 Agent 系统
很多人第一次设计多步 Agent 时,都会自然地写成一条串行流水线:A → B → C → D。这样设计很符合直觉,平时代码就是这样按序、一步接着一步地执行下去。
串行结构很好,但真实任务很少会完全符合这种结构。一次代码审查会同时检查安全问题、性能问题和代码规范;一次资料调研会并行处理多个来源。这些流程放在一起,比起串行的一条线,它更像一张关联的图。
Agent Graph Engineering(Agent 图编排)要解决的问题,就是找到真实的任务数据流,并按照这个图结构去组织 Agent。
本文将会介绍图编排背后的设计原则,再结合代码示例说明具体实现方式。其中本文提及的 agent()、parallel() 等接口只是 Claude Code 示例,不同版本的 Claude Code 或其他 Agent 框架可能存在差异。但 Graph Engineering 的核心思想不变,它同具体函数名称无关。
工作流的真实结构
要理解 Agent 图编排先得了解一件事,即图可以表示任何一个 / 多个任务,尤其适合表示复杂任务。

从 Agent Workflow 的角度看,一张图主要包含两个核心元素:
-
节点(Node):一个独立的工作单元。它可以是一个 Agent,也可以是一段确定性的代码逻辑。每个节点负责一类明确的任务,并有着清晰的输入和输出。
-
边(Edge):节点之间的数据依赖关系,用来表示某个节点产生的结果会被另一个节点使用。
而要构成一条边的必要条件是,数据真的从一个节点流向另一个节点时,它们之间才存在边。
例如,你让 Agent 完成这样一个任务:“总结这个文件,然后告诉我天气。”
在自然语言层面,这两个步骤是连续的,但它们之间没有数据依赖。天气查询并不需要文件总结的结果,因此两者(两个任务节点)实际不会存在关联边。
如果按照线性脚本设计流程,就会是这样的:
文件总结 Agent
↓
天气查询 Agent
按照这种串行的工作流,天气查询就会被迫等待一个与自己无关的任务完成。这就是 Agent Workflow 设计的常见问题,执行顺序被误认为数据依赖。代码里的先后顺序,只代表“什么时候执行”;而图里的边,代表“谁需要谁的结果”。
明白这点之后,很多 Agent 设计问题都会变得清晰。并行、隔离、分层这些优化,其实是在重新梳理任务之间的数据依赖:哪些步骤必须等待前置结果,哪些步骤彼此独立,可以被拆开执行。
所以,一个线性的 Agent 流程其实也能被图结构来解释,它其实是一张只有单一路径的有向图:A → B → C → D 罢了。这种工作流结构的问题在于,只要有一个节点成为瓶颈,将会影响后续所有步骤。如果这里 C 阶段失败了,D 任务将无法开始。同时,节点之间的连接关系也被固定下来,前面 A 节点产生的结果只能沿着预设路径传递,难以支持动态调整。
线性 Workflow 的限制,很多时候不在于代码实现,而是任务结构本身没有被正确表达出来。
节点设计
要想把一个节点放进 Agent 图中,它得有清晰的职责边界。如果一个节点依赖当前上下文中的隐含信息,就很难进行拆分、并行执行或者替换。这类问题通常来自未声明的输入依赖,会让节点和当前 Workflow 强绑定。
以“前面那个 Agent 已经分析过了,所以这里应该知道背景。”为例,这种隐含的依赖关系会让节点和当前 Workflow 强绑定。一旦调整执行顺序,或是尝试将它复用到另一个 Workflow 中,这个节点可能会无法正常工作。
因此,一个可自由组合的节点得定义清楚它的输入、输出约定:
-
输入是什么;
-
输出是什么格式;
-
下游如何使用这个结果。
有了约定之后,节点才更像一个标准的软件组件,可以被移动、替换和复用,而不是只能依赖某一次对话上下文才能运行的临时步骤。
在工程实现中,约定一般通过 schema 来定义:
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, { schema: ITEM });
上面这段代码的重点并不在 API 调用方式,而是说明节点约定应该在哪里建立。
通过 schema,子 Agent 的输出结构会被限制在预定义的数据结构中。如果返回结果不符合要求,运行时可以进行校验和处理,而不是把一段没有结构约束的自然语言输出交给下游节点重新解析。
有没有 schema 的区别在于,有 schema 的输出是下游可以直接消费的数据结构;没有约束的文本更适合作为人类阅读的结果,而不是节点之间稳定传递的数据格式。
schema 的作用,就是让 Agent 的输出具备明确的数据结构,从而可以连接到 Workflow 中的下一节点。
数据流设计
上面提到边代表的是节点之间的数据依赖关系,那么它应该按照传递的数据来定义,而不是按照执行顺序来描述。这个变化看似很小,但对 Workflow 的设计方式影响很大。
首先,它会让节点之间的依赖关系变得可验证。“A 之后执行 B”这句话说明的是执行顺序,并不能说明 A 和 B 之间是否真的存在数据关系。而:“A 产出 List<Item>,B 消费 List<Item>”就明确描述了两者之间的数据流:
-
A 的输出是否会被 B 使用?
-
B 是否依赖 A 提供的数据?
如果答案是否定的,那么这条边实际上就不存在。
其次,基于数据约定来定义边,可以让节点替换变得容易。只要两个节点产生相同格式的数据,它们就可以在 Workflow 中互相替换:
Source A
↓
List<Item>
↓
Process Agent
Source B
↓
List<Item>
↓
Process Agent
虽然两个节点的实现不同,但只要它们满足相同的数据约定,都能连接到后续处理节点。如果只按照执行顺序设计流程,这些数据关系和节点替换空间都会被隐藏。
除了提升可组合性,重新理解“边”还有一个重要的工程价值:减少不必要的 Agent 调用。很多 Agent 系统消耗的 token,并不是用在复杂推理上,而是花在了本可以由代码完成的数据处理上。
以典型的 fan-out → reduce → synthesis 流程为例,reduce 阶段经常只是完成:
-
合并结果;
-
去除重复项;
-
筛选字段;
-
调整数据结构。
其实,这些操作有明确规则,一般不用额外调用 Agent。参考:
// 边处理:纯代码完成,不消耗 token
const flat = collected.flatMap((c) => c.items);
const top = [...new Set(flat.map((x) => x.url))];
想这种“让模型帮忙整理一下前面几个 Agent 的结果。”需求,如果这里的“整理”只是格式转换、列表合并或去重,比较合适交给代码处理。Agent 更适合处理需要判断、分析和生成的任务,而不是承担数据搬运和格式转换。
如果 Workflow 中每条边都依赖 Agent 来连接,那么就是在为原本可以由代码完成的数据流转支付额外成本。
Workflow 决定 Agent 系统的成本与延迟
识别出任务之间的独立关系之后,下一步就是利用这种独立性。最直接的方式,就是把可以独立执行的节点并行展开。
这里的要点在于理解图编排和普通 Prompt 的区别。为什么多个子 Agent 可以扩展,而一个不断增长的巨大 Prompt 只会越来越难维护?
原因在于,Agent 编排发生在代码层。编排逻辑由程序负责执行,创建子 Agent 是运行时操作,并不会额外占用主 Agent 的上下文空间。
假设,系统要同时处理 10 个数据源。单个 Agent 需要将 10 个来源的信息全部放入自己的上下文; 而图编排会让每个子 Agent 只处理自己的输入,最后再汇总结果。
因此,即便后面任务规模扩大,单个 Agent 的上下文压力不会随着节点数量同步增长。这也是图结构的重要价值所在,通过拆分任务,系统可以处理单个 Agent 难以承载的复杂工作。
const raw = await parallel(
SOURCES.map((s) => () => agent(s.prompt, { schema: ITEM })),
);
const collected = raw.filter(Boolean);
上面代码中的 filter(Boolean) 对应了另一个重要设计:故障隔离。在并行执行中,不是所有节点都能够成功返回。如果某个子 Agent 执行失败,稳妥的做法是把失败限制在当前节点,而不是中断整个 Workflow:
Agent A ✓
Agent B ✓
Agent C ✕
Agent D ✓
↓
继续处理 A / B / D 的结果
因此,fan-in 阶段要考虑部分结果缺失的情况,而不是默认所有节点都会成功返回。这也是图结构相比串行流程的一个优势:
-
在线性 Workflow 中,一个节点失败可能导致整个链路停止;
-
在图结构中,失败可以被隔离在单个节点范围内。
除了并行之外,另一个影响 Workflow 性能的设计选择是:parallel() 还是 pipeline()?
并行:减少等待
这种方式比较适合彼此独立的多个任务,参考:
Agent A
↗
Input → Agent B
↘
Agent C
所有节点可以同时开始,整体等待时间接近最慢节点的耗时。以上图为例,A 要 5s,B 要 20s,C 要 10s 的话,整体将会接近 20s。
流水线:让任务持续流动
这种方式适合经过相同处理流程的多个输入,参考:
Input A → Stage 1 → Stage 2 → Stage 3
Input B → Stage 1 → Stage 2 → Stage 3
当 A 进入 Stage 3 时,B 已经可以开始 Stage 1。系统不需要等待所有输入都完成当前阶段,再进入下一轮处理。
设计 Workflow 时,需要避免一个常见误区:把逻辑上的阶段划分,误认为执行上的同步等待。只有当某一步依赖完整集合时,才要等待所有结果汇总:
-
跨来源去重;
-
根据整体数量提前终止;
-
比较所有候选结果。
除此之外,让所有任务等待同一时刻继续执行,只会增加额外延迟。
验证机制
图结构不只是让系统可以运行更多 Agent,还为 Agent 之间增加了一些单个模型难以可靠完成的环节,比如验证、复查和反向质疑。
一个 Agent 生成的结果,一般只代表一次推理过程。如果让同一个 Agent 检查自己的答案,它基于的信息还是之前那份,依赖的推理路径也是同一个,这会导致难以发现原始判断中的问题。
因此,提高系统可靠性的关键不是让生成者“再想一遍”,而是引入一个独立的验证节点。简单来说,Agent A 负责提出答案的话,就让另一个 Agent B 来负责尝试推翻答案。
参考:
// 对抗式验证:多个独立 verifier 尝试反驳发现
const passed = (
await parallel(
findings.map((f) => () =>
parallel(
[0, 1, 2].map(() => () =>
agent(
`尝试验证这个发现是否成立:${f.desc}`,
{ schema: VERDICT }
),
)
).then((v) => ({
f,
keep: v.filter(Boolean)
.filter((x) => x.real)
.length >= 2,
})),
)
)
)
.filter((x) => x.keep)
.map((x) => x.f);
这里的 verifier 并不是简单重复生成结果,而是在结果交付前承担一个明确职责:主动寻找当前结论可能存在的问题。只有经过验证仍然成立的结果,才会进入下一阶段。
这种设计利用的是 Agent 节点之间的独立性:不同节点拥有不同的判断路径,可以降低单次推理带来的错误风险。
常见的验证方式主要有三类:
对抗式验证
所谓对抗式验证,就是让多个 verifier 从不同角度尝试推翻同一个结果。例如:
-
这个结论是否有足够证据支持?
-
是否存在反例?
-
推理过程中是否存在漏洞?
多个独立 verifier 的判断结果汇总后,可以降低单次判断出错的概率。
多视角验证
多视角验证会让不同验证节点关注结果的不同维度。比如 Agent A 关注正确性、Agent B 关注安全性,可复现性由 Agent C 来负责,而 Agent D 则关注工程影响。
相比让多个 Agent 重复执行相同检查,多视角验证更容易发现不同类型的问题。
评审团模式
评审团模式让多个 Agent 分别生成或评价候选方案,再由综合节点汇总结果。它类似人工评审流程:
多个候选方案
↓
多个独立评审
↓
综合最终结果
这种方式适合需要比较、筛选和权衡的任务。
动态 Workflow 设计原则
有些 Agent 任务天然不是一次执行就能完成的。像持续发现新的资料、 不断定位新的 Bug、 反复尝试修复,直到满足目标条件…这类任务一般是一个循环结构:
搜索 → 发现 → 验证 → 继续搜索
↑ ↓
└──────────┘
但循环有个最大的问题,如果没有明确的停止条件,它就会变成一个持续消耗 token 的流程。所以,在设计 Agent Loop 时,我们得先定义什么叫“一轮有效进展”。
一种常见的定义方式是:loop-until-dry,连续 K 轮没有发现新结果后停止。
参考下面代码:
const seen = new Set();
let dry = 0;
while (dry < 2) {
const found = /* 并行运行 finders,收集结果 */;
const fresh = found.filter(
(b) => !seen.has(key(b))
);
if (!fresh.length) {
dry++;
continue;
}
dry = 0;
fresh.forEach((b) => {
seen.add(key(b));
});
// 对 fresh 进行验证,并加入 confirmed
}
这里有一个容易被忽略的细节:去重应该针对所有已经发现的结果,而不是只记录最终确认通过的结果。
例如:
发现 A
↓
验证失败
↓
下一轮重新发现 A
↓
再次验证失败
如果只记录“确认通过”的结果,失败项会不断重新进入循环。Workflow 表面上仍然在探索新内容,实际上只是在重复处理已经验证过的路径。而记录所有已经见过的结果:
seen = {
A,
B,
C
}
可以保证每轮探索都会排除已有结果,让循环逐渐收敛。这不是一个简单的代码细节,而是 Agent Loop 对“什么算是进展”的定义。
图结构带来的另一个价值是让系统能够更精细地控制模型成本。当节点拥有清晰的输入输出边界后,每个节点承担的判断任务也会更加明确。有些节点主要负责确定性工作:
-
分类;
-
信息抽取;
-
格式转换。
上面这些任务一般不需要复杂推理。而另一些节点需要:
-
权衡不同方案;
-
综合多个结果;
-
做最终决策。
这些节点才更需要强模型支持。所以,模型成本不应该平均分配给所有 Agent,而应该根据节点职责进行调整。例如:
agent(source.prompt, {
model: "smaller-model"
})
可以让大量重复性的节点使用成本更低的模型,而:
-
汇总节点;
-
验证节点;
-
决策节点;
这些节点继续使用能力更强的模型。
图结构的价值就在这里:它让不同职责的节点被拆分出来,因此模型选择和成本控制也变得更加灵活。相比单体 Agent,拆分后的 Workflow 更容易判断哪些环节值得投入更多计算资源。
小结
Agent 系统设计的关键,不是让模型执行更多步骤,而是找到任务真正的结构。线性 Workflow 是最容易开始的方式,但复杂任务要进一步考虑任务之间的数据关系:
-
哪些任务可以拆开并行;
-
哪些结果需要验证;
-
哪些工作可以交给代码完成。
通过节点和边的图结构来组织 Workflow,Agent 系统可以更好地利用任务之间的独立性,降低成本,提高稳定性。
本文介绍Agent图编排(Agent Graph Engineering)的核心思想:摒弃简单串行流程,以数据依赖关系构建节点(Agent/代码)与边(数据流)组成的有向图。强调清晰输入输出约定、并行执行、故障隔离、验证机制与动态循环设计,提升系统可组合性、稳定性与成本效率。
浙公网安备 33010602011771号