聊一下多 Agent 编排架构的应用实践

背景

在我所做的公司业务中,想完成一个完整的项目,需要跨多个系统做创建资源,交互流转,比如:

makefile 复制代码
步骤1: 登录系统A(项目管理平台)  → 创建业务项目 → 记下项目编号
步骤2: 登录系统B(资源管理平台)  → 根据活动类型复制模板 → 记下资源ID
步骤3: 回到系统A                 → 填写资源配置表单 → 关联项目编号和资源ID
步骤4: 登录系统C(审批平台)      → 提交审批申请 → 等待审批结果
步骤5: 审批通过后 → 回到系统B     → 发布配置 → 获取发布结果
步骤6: 整理结果                   → 汇总信息 → 通知相关人员

存在的问题

问题 具体表现
跨系统跳转 需要在 3 个系统间反复切换,容易遗漏步骤
数据依赖传递 步骤 3 依赖步骤 1 的项目编号和步骤 2 的资源 ID,手动拷贝容易出错
无法并行 步骤 1 和步骤 2 互不依赖,但人工只能串行操作
知识碎片化 每个系统的操作规则、字段含义分散在文档各处,新人上手成本高
无统一追踪 出错了难以快速定位是哪个环节的问题
审批阻断 步骤 4 需要等人点批准,整个过程被迫中断等待

引入Agent

有了AI之后,我们尝试把流程工作转交给Agent完成,让他按照既定流程,进行资源创建,流转。然而,简单的任务可以通过这样实现,一旦任务比较复杂,步骤较多,就会有下面这些问题:

  • prompt 极其庞大(意图识别 + N 个领域知识 + 工具使用规范)
  • 工具数量爆炸(10+ 领域 × 每领域 M 个操作)
  • 上下文窗口被工具定义挤占
  • 领域间相互干扰(A 领域的指令误触发 B 领域动作)
  • 无法独立迭代升级
  • 单点故障影响全部能力

引入 Agent 编排

然后我们引入了Agent编排,有了Agent编排,不再需要一个单Agent节点负担那么重的任务,例如:

ini 复制代码
用户: "帮我创建一个XXX项目"

编排器 Agent:
  ├─ 节点1 (并行) → 系统A: 创建项目 → projectId=A001
  ├─ 节点2 (并行) → 系统B: 复制资源模板 → resourceId=R002
  └─ 节点3 (依赖1+2) → 系统A: 填写配置表单(projectId=A001, resourceId=R002)
       └─ 节点4 (依赖3) → 系统C: 提交审批
            └─ 审批通过 → 节点5 → 系统B: 发布配置
                 └─ 返回汇总结果给用户

二、架构总览


三、进程隔离机制

3.1 核心设计:每 Agent 独立进程

scss 复制代码
主进程 (Fastify Server)
│
├── Worker (PID:1001) --- 编排器 Agent
│   └── AgentLoop
│       └── dispatch_to_agent("domain-a")
│           │
│           ╲ fork() ╱  ← 创建独立子进程
│             ▼
│   ┌─────────────────────────┐
│   │ Sub Worker (PID:1002)   │  ← 完全独立的操作系统进程
│   │ AGENT_DOMAIN_KEY=        │
│   │   domain-a              │
│   │                         │
│   │ · 独立内存空间           │
│   │ · 独立 AgentLoop 实例    │
│   │ · 独立 messages[] 历史  │
│   │ · 独立 runtime (Skill)  │
│   │ · 独立沙箱租约           │
│   └─────────────────────────┘
│
│       ─── 或 DAG 编排 ───
│
│   ┌─────────────────────────────────────────┐
│   │ DAG Execution Context                    │
│   │                                          │
│   │  node_query (skill_task)  PID:1003       │  ← 并行执行
│   │  node_process (agent_task) PID:1004      │  ← 依赖 node_query
│   │  node_notify (skill_task)  PID:1005      │  ← 依赖 node_process
│   │                                          │
│   │  sharedData: Map<key, value>             │  ← 节点间数据传递
│   └─────────────────────────────────────────┘

3.2 Fork 实现

typescript 复制代码
/**
 * dispatchToAgent --- 子 Agent 调度核心
 *
 * 关键步骤:
 * 1. 校验目标 Agent 存在性
 * 2. fork 子进程(chat-worker.ts)
 * 3. 通过环境变量 AGENT_DOMAIN_KEY 区分子 Agent 身份
 * 4. stdin NDJSON 发送消息
 * 5. stdout NDJSON 读取结果
 */
export async function dispatchToAgent(
  toolUseId: string,
  input: { domainKey: string; message: string; taskId: string },
  userCtx: UserContext,
): Promise<ToolResult> {

  // 1. 验证目标 Agent 存在
  const agentLoader = getAgentLoader();
  agentLoader.load(input.domainKey); // 不存在则抛 AGENT_NOT_FOUND

  // 2. ★ Fork 子进程 ★
  const child = fork('server/worker/chat-worker.ts', args, {
    stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
    env: {
      ...process.env,
      AGENT_DOMAIN_KEY: input.domainKey,  // 关键!环境变量区分身份
    },
  });

  // 3. 通过 stdin 发送 chat 消息
  child.stdin.write(ndjsonSafeStringify({
    type: 'chat',
    sessionId,
    taskId: input.taskId,
    message: input.message,
    userContext: userCtx,
  }));

  // 4. 等待子 Worker 完成(带超时)
  const result = await collectResult(child, signal, WORKER_EXEC_TIMEOUT_MS);

  // 5. 返回结构化结果
  return buildToolResult(toolUseId, result);
}

3.3 子进程身份识别

typescript 复制代码
// chat-worker.ts 启动时读取环境变量确定自身身份
const domainKey = process.env.AGENT_DOMAIN_KEY ?? 'default';

// 加载对应 Agent 的配置(tools / skills / prompt / model)
const config = agentLoader.load(domainKey);
// domainKey='orchestrator' → 加载编排工具 (dispatch/dag_orch/recall)
// domainKey='domain-a' → 加载系统A的操作工具 + 页面 Skill
// domainKey='advisor' → 加载纯对话配置(无工具)

// 创建对应 Agent 的 AgentLoop 实例
const agentLoop = new AgentLoop({ config, ... });

3.4 隔离维度全景

隔离维度 父 Worker (编排器 Agent) 子 Worker (领域 Agent)
操作系统进程 PID:1001 PID:1002 (fork 出新进程)
AgentLoop 实例 父 Loop(保留完整对话历史) 全新实例(仅接收 dispatch 消息)
messages\[\] 历史记录 含全量历史(含 dispatch tool_result) 仅含 dispatch 消息 + 注入的上下文
runtime (Skill 状态) 编排器的激活状态 领域 Agent 的私有 Skill 集合
沙箱租约 (sandboxLease) 父沙箱(如有) 独立子沙箱
命令历史 父历史 独立(恢复时可合并)
AbortController 父 abort 控制器 独立(但 dispatch 会合并信号)
LLM Client 连接 共享连接池 共享连接池(同一 SDK 实例)
崩溃影响 不受子进程崩溃影响 不受父/兄弟进程崩溃影响

四、两种编排模式

4.1 模式选择指南

typescript 复制代码
// 编排器 Agent 的路由决策逻辑(写在 prompt 中)

const decisionMatrix = {
  // 单步简单任务 → dispatch_to_agent
  singleStep: {
    trigger: '只需一个平台的一次操作',
    example: '查项目列表 / 填单个表单 / 问策略问题',
    tool: 'dispatch_to_agent',
  },

  // 多步复杂任务 → dag_orch
  multiStep: {
    trigger: [
      '消息含顺序词(先...再.../然后/最后)',
      '提及多平台数据依赖',
      '需要 2 步以上且有数据依赖或条件分支',
      '需要同时查询多个数据源',
    ],
    tool: 'dag_orch',
  },
};
特征 dispatch_to_agent dag_orch
步骤数 1 步 2+ 步
数据依赖 有(后续步骤依赖前序输出)
条件分支/并行 支持
典型场景 查列表、填单、问答 多系统联动填报、多阶段审批
底层实现 包装为单节点 DAG 多节点 DAG

4.2 统一执行路径

关键设计洞察:dispatch_to_agentdag_orch 最终都走同一个 DAG 引擎执行

typescript 复制代码
// toolInvoker 中统一入口
if (toolName === 'dispatch_to_agent' || toolName === 'dag_orch') {

  if (toolName === 'dispatch_to_agent') {
    // ★ 单次 dispatch → 自动包装为单节点 DAG ★
    dagDefinition = TaskOrchestratorDAG.createLinearPipeline(
      `single-${domainKey}-${Date.now()}`,
      `Dispatch: ${domainKey}`,
      [{
        id: 'dispatch_node',
        type: 'agent_task',
        executor: createAgentNodeExecutor(domainKey, message, userContext),
      }],
    );

  } else {
    // dag_orch → 使用 LLM 生成的多节点 DagDefinition
    dagDefinition = parseDagDefinition(input.dagDefinition);
  }

  // ★ 统一执行 ★
  const result = await executeDag(dagDefinition, { signal, onEvent, userContext });
  return result;
}

好处

  • 单节点和多节点共享:环检测、超时控制、事件追踪、错误处理
  • 减少代码路径,降低维护成本
  • 未来新增 DAG 特性(如重试、条件跳过)自动惠及单步 dispatch

五、DAG 编排引擎

5.1 数据结构

typescript 复制代码
/** DAG 节点类型 */
type DagNodeType =
  | 'agent_task'    // 调用子 Agent(最常用)
  | 'skill_task'    // 调用全局 Skill
  | 'tool_call'     // 直接调用工具
  | 'condition'     // 条件判断
  | 'parallel'      // 并行执行
  | 'sub_dag';      // 嵌套子 DAG

/** DAG 定义 */
interface DagDefinition {
  id: string;                // 唯一标识(建议含时间戳)
  label: string;             // 显示名称
  nodes: DagNode[];          // 节点定义数组
  terminalNodes: string[];   // 终止节点 ID 列表(必填!)
  timeoutMs?: number;        // 全局超时
  initialContext?: Record<string, unknown>; // 初始共享数据
}

/** DAG 节点 */
interface DagNode {
  id: string;                // 节点唯一标识
  label: string;             // 显示名称
  type: DagNodeType;         // 节点类型
  dependsOn: string[];       // 依赖的前置节点 ID 列表
  executor: (ctx: DagExecutionContext, signal?: AbortSignal) => Promise<DagNodeOutput>;
  optional?: boolean;       // 是否可选(失败时不阻断整个 DAG)
}

5.2 执行流程(Kahn 算法)

ini 复制代码
示例 DAG:
  node_A (skill_task) ──→ node_B (agent_task) ──→ terminal
        ↑                                           │
        └────── node_C (skill_task) ────────────────┘
        (与 A 并行,B 依赖 A+C)

执行过程:

Initial:  inDegree = { A:0, B:2, C:0 }
ReadyQueue: [A, C]  (入度为 0 的节点)

═══ Layer 1 (并行执行) ═══
  执行 A → 完成 → inDegree[B] = 2-1 = 1
  执行 C → 完成 → inDegree[B] = 1-1 = 0 → B 入队

ReadyQueue: [B]

═══ Layer 2 ═══
  执行 B → 完成 → B 是 terminalNode → DAG 结束 ✅

5.3 核心算法实现

typescript 复制代码
async execute(definition: DagDefinition, opts?): Promise<DagExecutionResult> {

  // 1. 构建执行上下文
  const ctx: DagExecutionContext = {
    dagId: definition.id,
    sharedData: new Map(Object.entries(definition.initialContext ?? {})),
    nodeOutputs: new Map(),
    userContext: opts.userContext,
  };

  // 2. ★ Kahn 算法环检测 + 拓扑排序 ★
  this._detectCycle(definition.nodes);  // 有环则抛 CycleDetectedError

  // 3. 计算入度,初始化就绪队列
  const inDegree = new Map<string, number>();
  for (const node of definition.nodes) {
    inDegree.set(node.id, node.dependsOn.length);
  }
  let readyQueue = definition.nodes
    .filter(n => n.dependsOn.length === 0)
    .map(n => n.id);

  // 4. ★ 主循环:逐层执行 ★
  while (readyQueue.length > 0) {
    const batch = readyQueue.splice(0);

    // 同层节点可并行执行
    const results = await Promise.allSettled(
      batch.map(nodeId => this._executeNode(nodeId, ctx, signal))
    );

    for (const [i, result] of results.entries()) {
      const nodeId = batch[i];

      if (result.status === 'fulfilled') {
        // 5. 节点完成 → 写入 sharedData(供下游消费)
        this._promoteDefaultNodeOutput(node, ctx);

        // 6. 更新下游节点入度,新就绪节点入队
        for (const dependent of dependents.get(nodeId) ?? []) {
          inDegree.set(dependent, inDegree.get(dependent)! - 1);
          if (inDegree.get(dependent) === 0) readyQueue.push(dependent);
        }
      }
    }

    // 7. 检查终止条件
    if (terminalNodes.every(n => completedNodes.has(n))) break;
  }

  return { success: true, finalSharedData: Object.fromEntries(ctx.sharedData), ... };
}

5.4 节点间数据传递

这是 DAG 编排中最精妙的部分------上游输出如何自动注入到下游输入

typescript 复制代码
/**
 * 组装子 Agent 的完整消息:原始指令 + 上游节点输出 Markdown
 *
 * 处理两件事:
 * 1. ${nodeId} 占位符解析(如 ${node_query} 替换为实际值)
 * 2. 上游输出格式化为 Markdown 注入到消息末尾
 */
function buildDagAgentUserMessage(taskMessage: string, sharedData: Map<string, unknown>): string {
  // Step 1: 占位符替换
  let interpolated = interpolateDagMessage(taskMessage, sharedData);
  // "使用 ${node_query} 的ID填报" → "使用 ACT202606140001 的ID填报"

  // Step 2: 上游输出 Markdown 化
  const upstreamMarkdown = buildDagUpstreamMarkdown(sharedData);
  /*
    输出:
    ---
    ## DAG 上游节点执行结果(系统自动注入)

    ### node_query
    活动ID=ACT202606140001, 名称="某促销活动", 状态=进行中

    ### node_check_resource
    资源充足,预算剩余 ¥50,000
  */

  return `${interpolated}\n\n${upstreamMarkdown}`;
}

sharedData 写入规则(节点完成后自动执行):

typescript 复制代码
private _promoteDefaultNodeOutput(node, ctx): void {
  const output = ctx.nodeOutputs.get(node.id)?.data;

  // 写入三个层级(满足不同消费场景):
  ctx.sharedData.set(`nodes.${node.id}`, output);       // ① 按节点索引
  ctx.sharedData.outputs[node.id] = output;               // ② outputs 汇总表

  if (isTerminal(node)) {
    ctx.sharedData.set('output', extractConclusion(output)); // ③ 终止节点结论
    ctx.sharedData.set('success', !output.isError);
  }
}

六、上下文共享机制

6.1 核心挑战

子 Agent 运行在完全独立的进程中,没有父 Agent 的内存状态。如何让它获得足够的上下文来正确执行任务?

css 复制代码
问题场景:

编排器 Agent 的 messages[]:
  [
    { role:'user', content:'帮我创建一个XXX项目' },
    { role:'assistant', content:'好的,我来帮你创建', tool_use:[{name:'dispatch_to_agent', ...}] },
    { role:'tool', content:'项目创建成功,projectId=P88889' },     ← 第1轮结果
    { role:'assistant', content:'现在帮你填写配置表单', tool_use:[...] },
    { role:'tool', content:'表单填写成功,ticketId=T999' },        ← 第2轮结果
    { role:'user', content:'把截止日期改成6月20日' },               ← 当前请求
  ]

领域 Agent 启动时:
  messages[]: []  ← 空的!完全不知道之前发生了什么!

  ❌ 不知道 projectId 是多少
  ❌ 不知道已经填过哪些表单
  ❌ 不知道用户的原始意图是什么

6.2 四层上下文注入方案

css 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 1: parentContext(显式传递)                                  │
│  ─────────────────────────────────────────────────────────────────  │
│  来源: LLM 生成的 dispatch input.context                            │
│  内容: requiredPageKeys / __activatedSkill / 其他元数据             │
│  注入时机: dispatchToAgent() → 写入子 Worker 的 chat 消息          │
│  用途: 告诉子 Agent 应该预激活哪些 Skill                             │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 2: getRecentDispatchResults(跨轮记忆)                       │
│  ─────────────────────────────────────────────────────────────────  │
│  来源: 编排器 AgentLoop.messages 中前序 dispatch 的 tool_result      │
│  内容: 最近 3 条 dispatch 结果摘要 [{domainKey, summary}]           │
│  注入时机: dispatch 前自动检测并注入消息前缀                         │
│  用途: 解决"子 Agent 每次在新进程启动、无跨轮记忆"的问题             │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 3: DAG sharedData(节点间数据传递)                           │
│  ─────────────────────────────────────────────────────────────────  │
│  来源: 上游 DagNodeOutput.data → ctx.sharedData                     │
│  内容: nodes.{nodeId} / outputs / output / 任意自定义 key           │
│  注入时机: buildDagAgentUserMessage() → 格式化为 Markdown            │
│  用意: ${nodeId} 占位符解析 + 上游输出自动注入                       │
├─────────────────────────────────────────────────────────────────────┤
│  Layer 4: userContext + taskId + subjectToken(运行时透传)          │
│  ─────────────────────────────────────────────────────────────────  │
│  来源: SessionManager.sendChat() 创建                               │
│  内容: userId/teamId/userMisId/pageContext/formValues/subSystemRoles │
│  注入时机: chat-worker → AgentLoop → ToolExecutor → dispatchToAgent │
│  用途: 安全校验 / SSO 换票 / 审计追踪 / HITL Token 持久化          │
└─────────────────────────────────────────────────────────────────────┘

6.3 Layer 2 详解:跨轮上下文自动注入

这是解决 Worker 进程模型固有问题 的关键设计------编排器 Agent 自动将历史 dispatch 结果注入到新的 dispatch 消息中

typescript 复制代码
// AgentLoop 中的方法
getRecentDispatchResults(maxResults = 3, maxCharsPerResult = 2000):
    Array<{ domainKey: string; summary: string }> {

  const results = [];
  const dispatchToolNames = new Set(['dispatch_to_agent', 'dag_orch']);

  // 从 messages 末尾逆序扫描,提取 dispatch 类工具的 tool_result
  for (let i = this.messages.length - 1; i >= 0 && results.length < maxResults; i--) {
    const msg = this.messages[i];
    if (msg.role === 'tool_result' && dispatchToolNames.has(msg.tool_use_name)) {
      results.unshift({
        domainKey: extractDomain(msg.content),
        summary: truncate(msg.content, maxCharsPerResult),
      });
    }
  }
  return results;  // 按时间正序(旧→新)
}

注入效果示例

typescript 复制代码
// dispatch 时自动组装的消息
const recentResults = currentLoopRef?.getRecentDispatchResults() ?? [];

// 如果 LLM 自己已经在消息中写了上下文则跳过(防重复)
if (recentResults.length > 0 && !llmAlreadyInjectedContext) {
  contextPrefix = `[前序执行上下文]\n以下是本会话中之前轮次的执行结果摘要:\n`
    + `  1. [domain-a] 项目创建成功,projectId=P88889\n`
    + `  2. [resource-service] 资源模板复制成功,resourceId=R002\n\n`;
}

// 还会追加原始用户请求(防御性注入,确保子 Agent 知道用户真正想要什么)
message = `[原始用户请求] ${rawUserMessage}\n\n[路由说明] ${llmInstruction}`;

子 Agent 最终收到的消息:

ini 复制代码
[前序执行上下文]
以下是本会话中之前轮次的执行结果摘要,可作为当前任务的参考:
  1. [domain-a] 项目创建成功,projectId=P88889
  2. [resource-service] 资源模板复制成功,resourceId=R002

[原始用户请求] 把配置表单的截止日期改成6月20日

[编排器 Agent 路由说明] 请使用 form_fill skill 修改 projectId=P88889 的配置表单,将 deadline 字段更新为 2026-06-20

6.4 Layer 3 详解:DAG 数据流实例

css 复制代码
用户: "先帮我在系统中查一下活动ID,然后用这个ID填写配置表单"

编排器 Agent 输出 dag_orch:
{
  nodes: [
    {
      id: "query_activity",
      type: "skill_task",
      dependsOn: [],
      executor: { skillName: "data-query", message: "查询最近的活动" }
    },
    {
      id: "fill_form",
      type: "agent_task",
      dependsOn: ["query_activity"],  // ★ 依赖上游 ★
      executor: {
        domainKey: "domain-a",
        message: "使用 ${query_activity} 的活动ID填写配置表单"
        //                    ^^^^^^^^^^^^^^
        //         占位符!执行时会被替换为实际值
      }
    }
  ],
  terminalNodes: ["fill_form"]
}

执行时的 sharedData 变化:

ini 复制代码
初始: sharedData = {}

↓ 执行 query_activity (skill_task) 完成

sharedData = {
  "outputs": {
    "query_activity": { output: "活动ID=ACT001, 名称=某促销活动", success: true }
  },
  "nodes.query_activity": { output: "活动ID=ACT001, 名称=某促销活动", success: true }
}

↓ 执行 fill_form (agent_task) 前,消息组装

子 Agent 收到的 message:
"""
使用 活动ID=ACT001, 名称=某促销活动 的活动ID填写配置表单

---
## DAG 上游节点执行结果(系统自动注入,请直接用于本步任务)

### query_activity
活动ID=ACT001, 名称=某促销活动
"""

↓ fill_form 完成

sharedData = {
  "output": "配置表单填写成功,ticketId=TICKET123",  // 终止节点结论
  "success": true,
  "outputs": {
    "query_activity": { ... },
    "fill_form": { output: "配置表单填写成功", success: true }
  },
  "nodes.query_activity": { ... },
  "nodes.fill_form": { output: "配置表单填写成功", success: true }
}

七、跨进程 HITL(人工审批)路由

7.1 问题

子 Agent 执行敏感操作时触发人工审批(HITL),审批请求需要显示在前端,审批结果需要送回正确的子进程。

c 复制代码
子 Worker (PID:1002) 触发 HITL
    │
    ├─ yield hitl_request → stdout
    │
    ▼
父 Worker (PID:1001) 读取 stdout
    │
    ├─ 解析 hitl_request → 转发 WsGateway → 前端弹窗
    │
    ├─ hitlRouteTable.register(token, child.pid)  // ★ 注册路由映射 ★
    │
    ▼ (等待用户操作...)
    │
    ▼ 用户点击"批准"
    │
WsGateway → SessionManager.handleHitlConfirm(sessionId, token, true)
    │
    ├─ 原子消费 Token(防重放攻击)
    │
    ├─ hitlRouteTable[token] → 找到 PID:1002  // ★ 路由查找 ★
    │
    ▼
写 {type:"hitl_result", token, approved:true} 到 PID:1002 的 stdin
    │
    ▼
子 Worker 收到 → HitlChannelRegistry.resolve(token, true) → 解除阻塞 → 继续执行

7.2 路由表实现

typescript 复制代码
/** 带 TTL 的子进程路由表 */
class ChildProcessRouteTable<T> {
  private map = new Map<string, { value: T; expiresAt: number }>();

  /** 注册路由(带 TTL 自动过期) */
  register(key: string, value: T): void {
    this.map.set(key, { value, expiresAt: Date.now() + this.ttlMs });
  }

  /** 查找路由(过期自动清理) */
  get(key: string): T | undefined {
    const entry = this.map.get(key);
    if (!entry) return undefined;
    if (Date.now() > entry.expiresAt) {
      this.map.delete(key);  // 过期清理
      return undefined;
    }
    return entry.value;
  }
}

// 两个路由表实例:
const frontActionRouter = new ChildProcessRouteTable<string>({ ttlMs: 5 * 60 * 1000 });  // 5分钟
const hitlRouteTable     = new ChildProcessRouteTable<string>({ ttlMs: 10 * 60 * 1000 }); // 10分钟(对齐 HITL Token 过期时间)

八、超时与容错

8.1 分级超时策略

ini 复制代码
超时层级:

┌─ L1: ToolExecutor 单工具超时 ──────────┐  (默认 30s~120s,按工具类型配置)
│  单个工具执行的最长时间                      │
└──────────────────────────────────────────┘
         │ 超时 → 该工具返回错误,AgentLoop 决定是否重试
         ▼
┌─ L2: dispatch_to_agent 超时 ─────────────┐  (180s = 3 分钟)
│  单次子 Agent 调用的总超时                   │
└──────────────────────────────────────────┘
         │ 超时 → SIGKILL 子进程 → 返回 ToolError
         ▼
┌─ L3: DAG 子 Agent 超时 ──────────────────┐  (600s = 10 分钟)
│  DAG 中 agent_task 节点的超时(更长窗口)     │
│  因为可能包含多轮 Skill 操作                  │
└──────────────────────────────────────────┘
         │ 超时 → 同上
         ▼
┌─ L4: DAG 全局超时 ───────────────────────┐  (可选,默认无)
│  整个 DAG 执行的总超时                      │
└──────────────────────────────────────────┘

8.2 AbortSignal 合并

当父 Agent 取消时,需要同时取消正在执行的子 Agent:

typescript 复制代码
// 创建合并的 AbortSignal(任一取消即触发)
private _createMergedAbortSignal(signal1: AbortSignal, signal2: AbortSignal): AbortSignal {
  const controller = new AbortController();
  if (signal1.aborted || signal2.aborted) {
    controller.abort();
    return controller.signal;
  }
  signal1.addEventListener('abort', () => controller.abort(), { once: true });
  signal2.addEventListener('abort', () => controller.abort(), { once: true });
  return controller.signal;
}

// 使用:DAG 的 signal 与外层 dispatch 的 signal 合并
const mergedSignal = this._createMergedAbortSignal(opts.signal, dispatchSignal);
await node.executor(ctx, mergedSignal);  // 任一取消都能中断

8.3 错误传播策略

错误类型 行为 对 DAG 的影响
节点执行失败 记录 error 到 nodeOutputs 非 optional 节点 → DAG 终止;optional → 跳过继续
HITL 拒绝 携带 _hitlRejected 标记 可选择终止 DAG 或让下游条件节点处理
子进程崩溃 (exitCode≠0) 捕获 exit 事件,返回错误 同节点执行失败
DAG 超时 设置所有 running 节点为 failed DAG 终止,返回 timeout 错误
循环依赖 Kahn 算法检测到 → 抛 CycleDetectedError DAG 立即终止,不执行任何节点

最后

本文介绍了一种基于 child_process.fork + DAG 编排引擎的多 Agent 协作架构,适用于需要多个专业 Agent 分工协作的复杂 AI 系统设计,希望对你有用,也欢迎一起交流~~

相关推荐
Sunia2 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
米小虾3 小时前
Loop Engineering —— 循环的设计与自主执行
人工智能·agent
米小虾3 小时前
Harness Engineering —— 系统的安全护栏
人工智能·agent
武子康5 小时前
调查研究-200 llama.cpp b9754:一次很小但很关键的 Agent 工具调用修复
人工智能·agent·llama
武子康5 小时前
调查研究-199 MCP Zero-Touch OAuth:为什么它是 MCP 进入企业生产的关键门槛?
人工智能·agent·mcp
用户947850529276 小时前
Skill用得好,下班走得早:一文讲透Skill的结构与设计
agent
leeyi6 小时前
Batch 处理:并发控制与可中断批处理
aigc·agent·ai编程
冬奇Lab16 小时前
Workflow 系列(01):基础理论——三种执行模型与 Anthropic 5 种模式
人工智能·agent·工作流引擎
冬奇Lab16 小时前
每日一个开源项目(第143篇):page-agent - 纯 JS 的网页 GUI Agent,无需截图、无需插件、无需后端
前端·人工智能·agent