handoff vs orchestrator-worker — 控制权转移的两种语义
handoff vs orchestrator-worker — 控制权转移的两种语义
日期: 2026-07-14 阶段: Phase 2 - AI-native 参考架构 标签: #handoff #orchestrator #multi-agent-pattern
核心问题
Day 29 把 Anthropic 的 orchestrator-worker 拆透了:lead 自己不干活,派子 agent 并行检索、回收压缩摘要、自己综合。但这只是多 agent 协作的一种范式。OpenAI Agents SDK 主推的是另一种——handoff(控制权移交)。两者长得像(都是「一个 agent 调动别的 agent」),但底层语义完全不同,选错会在 AML 这种强可观测、强可溯源的场景上踩坑。
今天回答三个问题:
- handoff 到底「转移」了什么?它和 orchestrator「调用」子 agent 的本质区别在哪?
- 两种范式在状态传递 / 失败恢复 / 可观测性这三个工程维度上的语义差异是什么?
- AML Copilot 该选哪个,为什么?
本仓库的 orchestratorAgent.ts 走的是 orchestrator-worker(lead 始终持有控制权)。要论证这个选择是对的,必须先讲清 handoff 这条没走的路差在哪。一手依据是 OpenAI Agents SDK 官方 handoff 文档(2026 当周版)。
关键内容
A. handoff 的机制:控制权完全转移,下一个 agent 接管整段对话
OpenAI Agents SDK 文档对 handoff 的定义:「Handoffs allow an agent to delegate tasks to another agent.」关键在「delegate」的语义——不是调用,是移交。原文:「it's as though the new agent takes over the conversation, and gets to see the entire previous conversation history.」
实现上,handoff 被伪装成一个工具暴露给 LLM:「Handoffs are represented as tools to the LLM. So if there's a handoff to an agent named Refund Agent, the tool would be called transfer_to_refund_agent.」命名走 Handoff.default_tool_name(),即 transfer_to_<agent_name>。
模型「调用」这个工具的瞬间,发生的不是「拿到返回值继续推理」,而是控制权易主——triage agent 的本轮就此结束,refund agent 接过整段历史继续。这跟 Day 29 orchestrator 的 Agent.as_tool() 形成对照:后者「allows structured input within a single agent's execution without conversation transfer」——子 agent 算完把结果还给主 agent,主 agent 始终在驾驶座。
两种控制流的状态机对照:
【handoff(控制权转移)】 【orchestrator-worker(控制权保留)】
[Triage] [Orchestrator] ◄──────────┐
│ transfer_to_refund_agent │ as_tool(subQ) │ 返回 {text}
│ (本轮结束,交出方向盘) │ (持有方向盘) │
▼ ▼ │
[Refund] ── 接管整段历史 [Sub-agent A] ──────────┘
│ 继续与用户对话 (算完即还,不接管对话)
▼
最终由 Refund 收尾 Orchestrator 综合 N 个返回 → 收尾
(Triage 已退出) (子 agent 全程不直面用户)
handoff 的历史传递可调:默认接管「entire previous conversation history」,但可通过 input_filter(接收 HandoffInputData、返回新的 HandoffInputData)裁剪,比如内置的 agents.extensions.handoff_filters.remove_all_tools 去掉工具调用历史。HandoffInputData 含 input_history / pre_handoff_items / new_items / input_items / run_context 五段。还可用 on_handoff 回调(「kicking off some data fetching as soon as you know a handoff is being invoked」)和 input_type(Pydantic 模型,给移交带 reason/priority/summary 等结构化元数据)。
反直觉洞察①(handoff 是「单工」不是「分身」):直觉以为 handoff 也是一种「并行调多个专家」。错。handoff 是串行的方向盘交接——同一时刻只有一个 agent 在驾驶,triage 交给 refund 后自己就下车了。它根本不能并行(没有「同时移交给三个 agent」这回事,文档明说
input_type「does not dispatch between destinations」)。而 Day 29 orchestrator 的全部威力恰恰来自并行。所以 handoff 与 orchestrator 不是「同一件事的两种写法」,而是解决不同问题:handoff 解决「该谁来接这个对话」,orchestrator 解决「怎么并行榨干 token 预算」。
B. 三个工程维度的语义差异
| 维度 | handoff(控制权转移) | orchestrator-worker(控制权保留) |
|---|---|---|
| 状态传递 | 隐式:接管方继承整段历史(或经 input_filter 裁剪);状态「随对话流动」 | 显式:主 agent 把 subQuery 作为参数下发,子 agent 只回 {text};状态「汇聚回中心」 |
| 失败恢复 | 难:refund agent 中途失败,triage 已退出,无人接管,需外层兜底重路由 | 易:子 agent 抛错,主 agent 仍在驾驶座,可重试/换路由/降级,控制点单一 |
| 可观测性 | 控制流是 DAG,无中心节点;跨 handoff 追错需把 trace context 透传到每个消息边界(实践中要给每个 inter-agent payload 加 span_context/state transition span 才能拼回全图,2026 口径) | 控制流是星形,主 agent 是天然的中心 span,每次 dispatch 是一对清晰的 call/return,trace 树自然成形 |
| 并行能力 | 无(串行交接) | 有(这是它的核心价值) |
| 可溯源 | 弱:最终答案由接管方产出,来源散在被移交的各段历史里 | 强:所有结论汇到主 agent,可统一挂引用(Day 29 的 CitationAgent 一趟) |
状态传递的差异最致命。handoff 的状态是「随对话流动」的——一旦 refund agent 改了共享状态又失败,triage 已经下车,没有干净的回滚点。OpenAI 自己的可观测指南(2026)承认这点:并发子 agent 场景里「one poisons shared state」时,必须「trace context propagated through every message boundary」,团队不得不「added a span_context field to every inter-agent payload」才能把 DAG 拼起来。这恰恰说明:handoff 的可观测性需要额外工程才能补齐,而 orchestrator 的星形拓扑天生就有中心。
把失败恢复这一行再拆细,因为它对 AML 是生死线。考虑一个三步失败场景:triage→refund 移交成功,refund 调外部退款 API 时超时失败。在 handoff 模型下,此刻「谁负责重试」是个没有明确答案的问题——triage 已经退出了控制流,refund 自己正卡在失败里,没有一个更高层的协调者持有「重路由 / 降级 / 通知人工」的决策权。要补救,只能在 handoff 之外再套一层外部编排(如 Temporal 工作流)来捕获并重启,这等于承认「单纯的 handoff 不足以支撑可靠的失败恢复」。而在 orchestrator 模型下,sub-agent 的失败只是一次工具调用抛了异常,主 agent 仍稳稳坐在驾驶座,可以当场决定「换一个 sub-agent 重试 / 降级到规则引擎 / 把这笔标记为待人工」——决策权从未离开过中心。对一个错一次就可能触发监管事件的 AML 系统,「失败时永远有一个明确的人/agent 负责」这件事本身,就值得为它放弃 handoff 那几行代码的便利。
反直觉洞察②(handoff 的「省事」是把复杂度从设计期挪到调试期):handoff 写起来很爽——一行
handoff(refund_agent)就把活甩出去,不用写综合逻辑。但这份省事是借的:出问题时,控制权已经在多个 agent 间击鼓传花过,你要从一条没有中心节点的 DAG 里反查「是哪次移交丢了上下文」。orchestrator 写起来啰嗦(要写分解+综合),但调试时永远有一个中心 agent 可以下断点。对要过合规审计的 AML,调试期的确定性远比设计期的几行代码值钱。
C. 何时选哪个:决策表
| 场景特征 | 选 handoff | 选 orchestrator-worker |
|---|---|---|
| 任务是「分诊→转专科」的对话路由 | ✅ 天然契合(客服转账/退款) | ❌ 过度设计 |
| 需要并行探索多个独立方向 | ❌ 不能并行 | ✅ 唯一选择(Day 29) |
| 强可溯源 / 要过审计 | ❌ 来源散在各段历史 | ✅ 结论汇中心,统一挂引用 |
| 强失败恢复 / 要可回滚 | ❌ 控制点流动,回滚难 | ✅ 控制点单一 |
| 子任务结果需主逻辑综合 | ❌ 接管方各自收尾 | ✅ 主 agent 综合 |
| 单一专家能独立把对话走完 | ✅ 交出去最省事 | ❌ 没必要绕一圈 |
一句话判据:「该谁来接这个对话」用 handoff;「怎么把活拆开并行再合起来」用 orchestrator。 两者甚至可以嵌套——分层共识里(2026-04),宏观工作流编排(Temporal)+ 微观推理循环(LangGraph)就是把「路由」和「编排」分到不同层。
需要澄清一个常见误区:很多人把 handoff 当成「省 token 的多 agent」——因为 handoff 不像 orchestrator 那样要跑一个 lead 全程占着上下文。这个想法在「单一专家能独立把对话走完」时确实成立(比如纯客服分诊后专科自己收尾,省掉了 lead 综合那趟)。但它有个隐藏前提:任务必须是「一个接一个的串行专科」,而不是「需要把多路结果合起来」。一旦任务需要综合(AML 的「把三个对手方的尽调结果合成一个风险结论」就是),handoff 根本做不到——因为最后接管的那个 agent 只看得到它自己那一段历史 + 之前的对话,它没有「把另外两路独立结果汇总」的位置。强行用 handoff 串起来,会退化成「一个 agent 修改前一个 agent 的输出」的脆弱链条,每一环都可能丢掉前面的信息。所以 handoff 省的那点 token,是用「无法做综合型任务」换来的——对 AML 这种本质是「多源证据汇聚成一个判断」的场景,这笔交易不成立。
设计要点/决策表
| 决策点 | 取舍 | 理由 |
|---|---|---|
| AML 主编排范式 | orchestrator-worker | 强可溯源+强失败恢复+需并行,B/C 节三项全中 |
| 是否用 handoff | 仅在「AML 对话分诊→转某专门 sub-agent 后该 sub-agent 独立收尾」时考虑,当前不用 | AML 结论必须汇中心挂引用,handoff 散来源不可接受 |
| 状态传递 | 显式 subQuery 下发 + {text} 回收 | 对应 B 节「状态汇聚回中心」,trace 树自然成形 |
| 可观测 | 主 agent 作中心 span,无需给每个 payload 补 span_context | 星形拓扑天生有中心,省掉 handoff 那套补丁工程 |
对本项目的落地
orchestratorAgent.ts当前用的是 orchestrator-worker,且应坚持不切 handoff:代码里dispatchKnowledge/Research/Portfolio三个工具都是「子 agent 算完return { text: r.text }、控制权回到runOrchestrator」——这正是 B 节「状态汇聚回中心」。onSubAgentStart/onSubAgentEnd/onToolCall/onToolResult这套回调让主 agent 成为天然中心 span,对应src/agent/trace/useTraceStore.ts的 trace 收集。若改成 handoff,这套清晰的 call/return trace 会塌成无中心 DAG,AML 审计追溯成本陡增。- failure 恢复点设在主 agent:依据 B 节,子 agent(
runResearchAgent等)抛错时,runOrchestrator仍持有控制权——这给了budget.ts的assert*异常一个干净的捕获点。若走 handoff,子 agent 失败时主 agent 已退出,Budget 兜底会失效。这是选 orchestrator 的一条硬工程理由。 - handoff 的
input_type思路可借用到 dispatch 的参数约束:虽然不用 handoff 的控制转移,但它的input_type(给移交带reason/priority结构化元数据)启发我们:dispatchKnowledge的subQuery当前只是z.string().min(3),可计划升级为带{ subQuery, priority, expectedFormat }的结构化输入(对应 Day 29 A 节「给子 agent 死边界 objective+format」),降低子 agent 目标漂移——这是计划中的改造。 - 诚实标注:AML 暂不引入 handoff;若 P3 上线后出现「合规专家 agent 需独立接管整段对话直到结案」的真实场景,再评估在 orchestrator 内层嵌一个 handoff,届时须补
span_context透传以维持可观测。当前 W 仅落 orchestrator 路径。
参考资料
- OpenAI Agents SDK — Handoffs(官方文档):handoff 定义「new agent takes over the conversation」;
transfer_to_<agent>工具命名;HandoffInputData五段与input_filter;on_handoff回调;input_type结构化元数据;与Agent.as_tool()的对比 (2026 当周版) - OpenAI Agents SDK — Orchestrating multiple agents:handoffs(让 LLM 决策路由)vs LLM-as-orchestrator(agents-as-tools,主 agent 保持控制)两范式 (2026)
- OpenAI API — Integrations and observability:handoff trace 跨边界追踪难,需透传 trace context、给 inter-agent payload 加
span_context/state transition span 才能拼回 DAG (2026) - 本仓库
src/agent/orchestrator/orchestratorAgent.ts(orchestrator-worker,控制权保留)、src/agent/trace/useTraceStore.ts(中心 span trace)(2026-06)
SOTA 检查 (2026-06-11)
- handoff vs orchestrator 两范式在 2026-06 已是主流框架的标准对偶:OpenAI Agents SDK 把二者并列为「handoffs」与「agents-as-tools」;Microsoft Agent Framework 1.0(2026-04,AutoGen+SK 统一后继)、LangGraph 1.0(2025-10)也都同时支持「控制权转移的状态图」与「中心编排」两种结构,本笔记的语义对照仍是 live 的。
- 2026 趋势是「分层而非二选一」:分层共识(2026-04)——Temporal(×OpenAI Agents SDK GA,2026-03)管宏观工作流路由(接近 handoff 的「谁接活」),LangGraph 管微观推理循环(接近 orchestrator 的「拆-并-合」)。即把 handoff 和 orchestrator 分到不同抽象层,而非在同一层互斥。本项目当前规模只需单层 orchestrator,分层是 P3 规模化后的选项。
- handoff 可观测性的「补丁工程」在 2026 仍是痛点:把 trace context 透传到每个 inter-agent 消息边界、给 payload 加
span_context仍需手工(ThoughtWorks Radar Vol 34,2026-04,把 agent topologies 列为需谨慎评估项)。这反向印证本项目选 orchestrator(星形天生有中心 span)在可观测上的省力优势。 - 待跟踪:OpenAI Agents SDK 的 handoff API 在快速迭代,
HandoffInputData字段与input_filter接口可能调整;本项目若 P3 引入内层 handoff,执行当周须重新核对官方文档字段名,不沿用本笔记快照。