Agent 编排中的依赖图解析:DAG 节点的前置条件与重试链
Agent 编排中的依赖图解析DAG 节点的前置条件与重试链一、Agent A 要等 Agent B 的结果但 B 在等 C 的结果C 超时了——整个编排死锁Agent 编排的核心难点不是单个 Agent 的执行是多 Agent 之间的依赖管理。一个典型的复杂任务用户请求分析竞品 A 的财报并生成对比报告。这个任务需要拆成多个子任务数据收集 Agent → 数据分析 Agent → 报告生成 Agent → 图表生成 Agent。后两个 Agent 都依赖前两个 Agent 的输出但彼此之间不依赖——可以并行执行。这就形成了有向无环图DAG。DAG 的每个节点是一个 Agent 的执行单元边表示依赖关系。编排引擎需要做三件事解析 DAG 确定执行顺序拓扑排序、检测循环依赖这图不能有环、处理节点失败的重试和降级。当前大部分 Agent 框架的编排是线性链——Agent A → Agent B → Agent C串联执行。这在简单场景够用但复杂的多 Agent 场景需要真正的 DAG 执行引擎。线性链的致命问题是B 和 C 都依赖 A 的输出——但 B 和 C 之间互相不依赖它们完全可以并行执行。线性链把并行变成了串行浪费了大量时间。二、底层机制与原理剖析DAG 解析的四个关键组件拓扑排序Topological Sort给定一个有向无环图确定所有节点的执行顺序。标准 Kahn 算法每次选择入度为 0 的节点执行执行完成后将其从图中移除它的后继节点入度 -1。入度为 0 的节点之间没有依赖关系——这批节点可以并行执行。并行执行批次Wave拓扑排序的结果可以被分组为波次——同一波次内的节点没有相互依赖可以并行执行。第一批入度为 0→ 第二批第一批完成后入度变为 0 的节点→ …… 每批之间是串行的等上一批全部完成批内是并行的。依赖数据传递节点 B 的执行需要节点 A 的输出数据。编排引擎在 A 完成时将 A 的结果传递给 B 的输入。数据结构通常是 JSON——A 的输出作为 B 的context的一部分。重试和降级DAG 中任何一个节点失败所有依赖于它的下游节点都会阻塞。必须处理失败重试指数退避、降级使用默认值或备用方案、跳过非关键节点可以跳过。三、生产级代码实现 Agent DAG 编排引擎 核心能力 1. DAG 解析邻接表 拓扑排序 2. 并行波次执行 3. 节点失败的重试和降级 4. 依赖数据传递和上下文合并 import asyncio import logging from typing import Dict, List, Optional, Set, Any, Callable from dataclasses import dataclass, field from enum import Enum from collections import deque import time logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class NodeStatus(Enum): PENDING pending # 等待前置依赖 RUNNING running # 正在执行 COMPLETED completed # 执行成功 FAILED failed # 执行失败 SKIPPED skipped # 被跳过前置节点失败导致 class DAGError(Exception): DAG 解析错误 pass dataclass class DAGNode: DAG 节点定义 node_id: str name: str # 执行函数接收合并后的上下文返回结果字典 executor: Callable # 前置节点列表 dependencies: List[str] field(default_factorylist) # 重试配置 max_retries: int 2 retry_delay_seconds: float 1.0 # 指数退避的基数 # 降级配置 fallback: Optional[Callable] None # 降级函数失败时调用 # 是否关键节点——关键节点失败会导致整个 DAG 终止 critical: bool True # 执行超时 timeout_seconds: float 120 dataclass class NodeResult: 节点执行结果 node_id: str status: NodeStatus output: Optional[Dict[str, Any]] None error: Optional[str] None retries: int 0 elapsed_ms: int 0 class DAGOrchestrator: DAG 编排引擎 核心算法 1. 从邻接表构建 DAG同时检测循环依赖 2. Kahn 拓扑排序生成执行波次 3. 逐波次并行执行节点 def __init__(self, nodes: List[DAGNode]): self.nodes {n.node_id: n for n in nodes} self._validate() # 邻接表node_id → [后继 node_id 列表] self.successors: Dict[str, List[str]] {} # 入度计数 self.in_degree: Dict[str, int] {} self._build_graph() def _validate(self): 验证 DAG 完整性 all_ids set(self.nodes.keys()) for node in self.nodes.values(): for dep in node.dependencies: if dep not in all_ids: raise DAGError( fNode {node.node_id} depends on unknown node {dep} ) # 检查每个依赖节点是否在 nodes 中已有定义 # 如果有外部依赖不在本 DAG 中的节点需要由外部先执行传入结果 def _build_graph(self): 构建邻接表 入度表 for node_id in self.nodes: self.successors[node_id] [] self.in_degree[node_id] 0 for node_id, node in self.nodes.items(): for dep in node.dependencies: if dep in self.successors: self.successors[dep].append(node_id) self.in_degree[node_id] 1 def detect_cycle(self) - bool: 检测 DAG 中是否存在环 使用 DFS 三色标记法 white (0): 未访问 gray (1): 正在访问在当前递归栈中 black (2): 已完成访问 如果遇到 gray 节点 → 存在环 WHITE, GRAY, BLACK 0, 1, 2 color {node_id: WHITE for node_id in self.nodes} def dfs(node_id: str) - bool: color[node_id] GRAY for successor in self.successors.get(node_id, []): if color[successor] GRAY: logger.error(Cycle detected: %s → %s, node_id, successor) return True if color[successor] WHITE: if dfs(successor): return True color[node_id] BLACK return False for node_id in self.nodes: if color[node_id] WHITE: if dfs(node_id): return True return False def get_execution_waves(self) - List[List[str]]: Kahn 算法生成执行波次 返回[[wave1_node_ids], [wave2_node_ids], ...] 同一 wave 内的节点没有依赖关系可以并行执行 if self.detect_cycle(): raise DAGError(DAG contains a cycle — cannot generate execution plan) in_degree dict(self.in_degree) waves [] queue deque() # 第一批入度为 0 的节点 for node_id, degree in in_degree.items(): if degree 0: queue.append(node_id) while queue: wave [] # 当前 wave 的所有节点入度为 0 for _ in range(len(queue)): node_id queue.popleft() wave.append(node_id) # 更新后继节点的入度 for successor in self.successors.get(node_id, []): in_degree[successor] - 1 if in_degree[successor] 0: queue.append(successor) waves.append(wave) # 检查是否有未执行的节点图中可能有孤立或不连通的部分 total_nodes sum(len(w) for w in waves) if total_nodes ! len(self.nodes): missing set(self.nodes.keys()) - set( nid for w in waves for nid in w ) raise DAGError(fUnreachable nodes (possible cycle): {missing}) logger.info(Execution plan: %d waves for %d nodes, len(waves), len(self.nodes)) for i, wave in enumerate(waves): logger.info( Wave %d: %s, i 1, wave) return waves async def execute(self, initial_context: Dict[str, Any] None ) - Dict[str, NodeResult]: 执行整个 DAG 参数: initial_context: 初始上下文外部传入的预填充数据 返回: {node_id: NodeResult} 每个节点的执行结果 initial_context initial_context or {} # 1. 生成执行计划 waves self.get_execution_waves() # 2. 存储每个节点的输出用于传递给下游节点 node_outputs: Dict[str, Any] {} results: Dict[str, NodeResult] {} # 3. 逐波次执行 for wave_idx, wave in enumerate(waves): logger.info( Wave %d/%d: %d nodes , wave_idx 1, len(waves), len(wave)) # 过滤掉因上游失败而被跳过的节点 active_nodes [ nid for nid in wave if results.get(nid, NodeResult(nid, NodeStatus.PENDING)).status ! NodeStatus.SKIPPED ] if not active_nodes: continue # 构建每个节点的上下文 tasks [] for node_id in active_nodes: node self.nodes[node_id] context self._merge_context(node, node_outputs, initial_context) tasks.append(self._execute_node_with_retry(node, context)) # 并行执行当前波次的所有节点 wave_results await asyncio.gather(*tasks, return_exceptionsTrue) # 4. 处理结果 has_critical_failure False for node_id, result in zip(active_nodes, wave_results): if isinstance(result, Exception): result NodeResult( node_idnode_id, statusNodeStatus.FAILED, errorstr(result), ) results[node_id] result node_outputs[node_id] result.output or {} if result.status NodeStatus.COMPLETED: logger.info( ✓ %s (%dms), node_id, result.elapsed_ms) else: node self.nodes[node_id] logger.error( ✗ %s: %s, node_id, result.error) if node.critical: has_critical_failure True # 关键节点失败 → 标记所有下游节点为 SKIPPED if has_critical_failure: self._skip_downstream(wave, results) return results def _merge_context(self, node: DAGNode, node_outputs: Dict[str, Any], initial: Dict[str, Any]) - Dict[str, Any]: 合并节点的执行上下文 上下文来源 1. initial_context外部传入权重最低 2. 所有前置依赖的输出按依赖顺序叠加后覆盖前 merged dict(initial) for dep_id in node.dependencies: if dep_id in node_outputs: merged.update(node_outputs[dep_id]) # 添加节点自身元信息 merged[_node_id] node.node_id merged[_dependencies] node.dependencies return merged async def _execute_node_with_retry(self, node: DAGNode, context: dict) - NodeResult: 执行单个节点带重试和降级 last_error None start_time time.time() for attempt in range(node.max_retries 1): try: # 执行带超时 output await asyncio.wait_for( node.executor(context), timeoutnode.timeout_seconds, ) elapsed int((time.time() - start_time) * 1000) return NodeResult( node_idnode.node_id, statusNodeStatus.COMPLETED, outputoutput if isinstance(output, dict) else {result: output}, retriesattempt, elapsed_mselapsed, ) except asyncio.TimeoutError: last_error fTimeout after {node.timeout_seconds}s logger.warning(Node %s: attempt %d/%d — %s, node.node_id, attempt 1, node.max_retries 1, last_error) except Exception as e: last_error str(e) logger.warning(Node %s: attempt %d/%d — %s, node.node_id, attempt 1, node.max_retries 1, last_error) # 指数退避 if attempt node.max_retries: delay node.retry_delay_seconds * (2 ** attempt) await asyncio.sleep(delay) # 所有重试失败 → 尝试降级 if node.fallback: try: logger.info(Node %s: attempting fallback, node.node_id) fallback_result node.fallback(context) elapsed int((time.time() - start_time) * 1000) return NodeResult( node_idnode.node_id, statusNodeStatus.COMPLETED, # 降级成功视作完成 outputfallback_result, retriesnode.max_retries 1, elapsed_mselapsed, ) except Exception as e: last_error fFallback also failed: {e} elapsed int((time.time() - start_time) * 1000) return NodeResult( node_idnode.node_id, statusNodeStatus.FAILED, errorlast_error, retriesnode.max_retries 1, elapsed_mselapsed, ) def _skip_downstream(self, current_wave: List[str], results: Dict[str, NodeResult]): 标记所有尚未执行的下游节点为 SKIPPED failed_ids [ nid for nid in current_wave if results.get(nid) and results[nid].status NodeStatus.FAILED ] # BFS 遍历下游 visited set(failed_ids) queue deque(failed_ids) while queue: node_id queue.popleft() for successor in self.successors.get(node_id, []): if successor not in visited: visited.add(successor) queue.append(successor) results[successor] NodeResult( node_idsuccessor, statusNodeStatus.SKIPPED, errorfUpstream node {node_id} failed, ) # --------------------------------------------------------------------------- # 使用示例 # --------------------------------------------------------------------------- async def main(): 示例财报分析的多 Agent DAG 编排 # 定义节点 async def collect_data(ctx): await asyncio.sleep(1) return {financial_data: Q2 营收 50 亿, competitors: [A, B]} async def analyze_data(ctx): data ctx.get(financial_data, ) await asyncio.sleep(1.5) return {analysis: f分析结果: {data}} async def search_web(ctx): competitors ctx.get(competitors, []) await asyncio.sleep(2) return {web_results: f搜索到的竞品信息: {competitors}} async def generate_report(ctx): analysis ctx.get(analysis, ) web ctx.get(web_results, ) await asyncio.sleep(1) return {report: f报告: {analysis} {web}} async def generate_chart(ctx): data ctx.get(financial_data, ) await asyncio.sleep(0.5) return {chart: f图表: {data}} nodes [ DAGNode(collect_data, 数据收集, collect_data), DAGNode(analyze_data, 数据分析, analyze_data, dependencies[collect_data]), DAGNode(search_web, 网络搜索, search_web, dependencies[collect_data]), DAGNode(generate_report, 报告生成, generate_report, dependencies[analyze_data, search_web]), DAGNode(generate_chart, 图表生成, generate_chart, dependencies[collect_data]), ] orchestrator DAGOrchestrator(nodes) results await orchestrator.execute({user_query: 分析竞品 A 的财报}) print(\n DAG 执行结果 ) for node_id, result in results.items(): status_icon ✓ if result.status NodeStatus.COMPLETED else ✗ print(f {status_icon} {node_id}: {result.status.value} ({result.elapsed_ms}ms)) if __name__ __main__: asyncio.run(main())四、边界分析与架构权衡DAG 中的循环依赖实际场景中可能出现伪循环依赖——A 需要 B 的部分结果B 需要 A 的部分结果。这本质上是A 和 B 不能完全独立解决方案合并为一个节点如果它们强耦合或者通过外部存储/消息队列解耦不纯的 DAG 模型节点超时对 DAG 的影响单个节点超时可能导致整个 wave 的其余节点等它超时后才继续——降低了并行效率解决方案为每个节点设定独立的超时超时后如果非关键节点则直接进入降级不等其他节点什么场景不该用 DAG 编排节点线性依赖A → B → C——DAG 的复杂度不值得实时性要求极高的场景——DAG 的 wave 调度引入了额外的批处理开销Agent 数量 3——并行执行几乎没有收益直接串行更简单五、总结Agent DAG 编排的核心是将多 Agent 执行拆成有向无环图通过拓扑排序生成并行执行波次最大化并行度。Kahn 算法将 DAG 分波次调度——同一波次内的节点并行执行、波次间串行等待。关键是每个节点的失败处理重试指数退避、降级备用方案、跳下游关键节点失败后标记所有下游为 SKIPPED。DAG 编排的价值在 Agent 数量 5 且存在复杂依赖关系时才体现——线性链的场景用 DAG 反而增加了复杂度。

相关新闻

.NET桌面应用自动更新方案与最佳实践

.NET桌面应用自动更新方案与最佳实践

1. .NET桌面应用自动更新的核心挑战在桌面应用开发领域,自动更新功能一直是开发者必须面对的硬需求。不同于Web应用可以实时部署更新,桌面应用需要一套完整的更新机制来确保用户始终使用最新版本。我在多个企业级.NET WinForms和WPF项目中,经…

2026/10/13 4:07:40 阅读更多 →
Agent 对话中断恢复:用户断线后如何无缝继续会话

Agent 对话中断恢复:用户断线后如何无缝继续会话

Agent 对话中断恢复:用户断线后如何无缝继续会话 一、用户切了个网络,Agent 忘了刚才聊到哪了 Agent 对话最大的不确定性不是模型幻觉,不是工具调用失败,是网络。用户在地铁上跟 Agent 聊产品需求分析,信号断了 30 秒&…

2026/10/2 10:49:42 阅读更多 →
OpenClaw与CoPaw:轻量级AI代理的Token优化指南

OpenClaw与CoPaw:轻量级AI代理的Token优化指南

1. OpenClaw与CoPaw项目概述OpenClaw和CoPaw是阿里巴巴开源的轻量级AI代理组合,专为解决AI代理运行时的内存占用和本地自动化问题而设计。这对组合采用Manager-Worker多代理协作架构,其中Manager作为"AI管家"负责协调多个专业Worker&#xff0…

2026/10/2 6:21:38 阅读更多 →

最新新闻

mruby-struct 深入解析:mruby 中的 Struct 类定义、keyword_init 模式与模式匹配支持

mruby-struct 深入解析:mruby 中的 Struct 类定义、keyword_init 模式与模式匹配支持

编程语言语言运行时编译器解释器嵌入式 【免费下载链接】mruby Lightweight Ruby 项目地址: https://gitcode.com/gh_mirrors/mr/mruby 点击查看 免费下载 导读 mruby-struct 是 mruby 标准库中提供 Struct 类的 mrbgem。它允许你无需手写类定义,即可将…

2026/10/13 4:07:00 阅读更多 →
SHIB 250%暴涨背后:真假突破与链上数据的判断逻辑

SHIB 250%暴涨背后:真假突破与链上数据的判断逻辑

这几天,SHIB三个字母几乎是币圈社群里被刷得最多的词。一只柴犬主题的Meme币,价格在短时间窗口里拉出约百分之250的涨幅,于是“重大价格突破或即将到来”的说法,迅速从各个分析帖里蔓延开来。很多人心里其实都在打鼓:这…

2026/10/13 4:07:00 阅读更多 →
Linux页框回收机制深度解析:从水位线到回收路径

Linux页框回收机制深度解析:从水位线到回收路径

1. 内存明明够用,系统却在悄悄回收页框:从一次延迟尖峰说起1.1 现象:物理内存白白闲置,负载却出现换页先讲一个真实场景。前两年我维护一台容器节点,物理内存 128G,日常用量也就 60G 左右,内存看…

2026/10/13 4:07:00 阅读更多 →
Java开发者如何抓住AI应用红利:从Spring AI到LangChain4j的落地实践

Java开发者如何抓住AI应用红利:从Spring AI到LangChain4j的落地实践

1. AI应用开发红利期,Java开发者的机会窗口先说结论:AI应用开发这波红利,绝不是Python开发者的专属饭。某知名市场研究机构2025年发布的行业报告显示,AI应用层开发的市场规模正在以远超预期的速度膨胀,而这里面最缺的恰…

2026/10/13 4:07:00 阅读更多 →
Learn-Web-Hacking 深入解析:PHP 流(Streams)体系、封装协议与 php://filter 安全利用

Learn-Web-Hacking 深入解析:PHP 流(Streams)体系、封装协议与 php://filter 安全利用

文档网络安全教程 【免费下载链接】Learn-Web-Hacking Study Notes For Web Hacking / Web安全学习笔记 项目地址: https://gitcode.com/gh_mirrors/le/Learn-Web-Hacking 点击查看 免费下载 导读:PHP 流(Streams)自 PHP 4.3 引入…

2026/10/13 4:07:00 阅读更多 →
Flask实战:从零搭建在线教育平台的核心实现与避坑指南

Flask实战:从零搭建在线教育平台的核心实现与避坑指南

去年做模拟项目X的时候,接了一个比较完整的活儿:从零搭一个python网络课程在线学习平台。这类项目听起来好像就是“课程列表视频播放”,但真正动手才发现,一个能被老师真正拿去用、学生愿意天天打开的平台,背后牵扯的模…

2026/10/13 4:06:00 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →