背景
在我所做的公司业务中,想完成一个完整的项目,需要跨多个系统做创建资源,交互流转,比如:
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_agent 和 dag_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 系统设计,希望对你有用,也欢迎一起交流~~