Agent Graph Engineering:从线性 Workflow 到可扩展 Agent 系统 工作流的真实结构要理解 Agent 图编排先得了解一件事即图可以表示任何一个 / 多个任务尤其适合表示复杂任务。1从 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 产出 ListB 消费 List”就明确描述了两者之间的数据流A 的输出是否会被 B 使用B 是否依赖 A 提供的数据如果答案是否定的那么这条边实际上就不存在。其次基于数据约定来定义边可以让节点替换变得容易。只要两个节点产生相同格式的数据它们就可以在 Workflow 中互相替换Source A↓List↓Process AgentSource B↓List↓Process Agent虽然两个节点的实现不同但只要它们满足相同的数据约定都能连接到后续处理节点。如果只按照执行顺序设计流程这些数据关系和节点替换空间都会被隐藏。除了提升可组合性重新理解“边”还有一个重要的工程价值减少不必要的 Agent 调用。很多 Agent 系统消耗的 token并不是用在复杂推理上而是花在了本可以由代码完成的数据处理上。以典型的 fan-out → reduce → synthesis 流程为例reduce 阶段经常只是完成合并结果去除重复项筛选字段调整数据结构。其实这些操作有明确规则一般不用额外调用 Agent。参考// 边处理纯代码完成不消耗 tokenconst flat collected.flatMap(© 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 执行失败稳妥的做法是把失败限制在当前节点而不是中断整个 WorkflowAgent A ✓Agent B ✓Agent C ✕Agent D ✓↓继续处理 A / B / D 的结果因此fan-in 阶段要考虑部分结果缺失的情况而不是默认所有节点都会成功返回。这也是图结构相比串行流程的一个优势在线性 Workflow 中一个节点失败可能导致整个链路停止在图结构中失败可以被隔离在单个节点范围内。除了并行之外另一个影响 Workflow 性能的设计选择是parallel() 还是 pipeline()并行减少等待这种方式比较适合彼此独立的多个任务参考Agent A ↗Input → Agent B↘Agent C所有节点可以同时开始整体等待时间接近最慢节点的耗时。以上图为例A 要 5sB 要 20sC 要 10s 的话整体将会接近 20s。流水线让任务持续流动这种方式适合经过相同处理流程的多个输入参考Input A → Stage 1 → Stage 2 → Stage 3Input 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 从不同角度尝试推翻同一个结果。例如

本月热点