Agent 工程体系 · 第 64/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
多 Agent Supervisor 与 Handoff:控制权、上下文、返回和死循环
多 Agent 系统最容易被低估的部分,不是如何创建多个 Agent,而是谁在什么时候拥有控制权、下一个 Agent 能看到什么、一次运行最终返回什么,以及什么时候必须停止。
一个系统即使已经实现了分类、路由和多个专业 Agent,也可能出现以下问题:
- Supervisor 选择了专家,但专家无法理解前文;
- 专家完成了任务,却没有把结果正确返回给用户;
- 一个 Agent 以为自己只是调用了另一个 Agent,实际上已经把整轮对话交给了对方;
- 两个 Agent 互相 Handoff,Runner 持续消耗模型调用;
- 上一轮由退款 Agent 处理,下一轮却错误地回到了 Supervisor;
- 日志只能看到“最终答案”,无法知道控制权在哪一步发生了转移。
这些问题的共同根源是:把“多 Agent”误认为“多个模型调用”。实际上,多 Agent 更接近一个带有动态控制流的运行时系统。
一、先建立四个核心对象
为了讨论清楚 Supervisor 和 Handoff,需要先区分四个对象:
- Agent:具有指令、模型、工具、输出约束和可选 Handoff 的执行单元;
- Supervisor:一种架构角色,负责选择、协调或审查其他 Agent;
- Handoff:把当前对话的主动控制权转移给另一个 Agent 的机制;
- Run:从起始 Agent 和输入开始,到产生最终输出、异常或暂停为止的一次运行。
OpenAI Agents SDK 的基本抽象包括 Agent、工具、Handoff、Guardrail、Session 和 Tracing。官方文档把 Handoff 描述为一种 Agent 委派任务的机制,并将 Handoff 暴露为模型可调用的工具。(openai.github.io)
这里的 Supervisor 不一定是 SDK 中名为 Supervisor 的特殊类。在常见实现中,它通常就是一个普通 Agent,只是承担以下一种或多种职责:
- 对用户请求分类;
- 选择合适的专业 Agent;
- 调用多个专家并综合结果;
- 对专家结果做质量检查;
- 决定是否继续执行、重试、回退或结束。
因此,Supervisor 的关键不是名字,而是它是否拥有下一步控制流的决定权。
二、两种多 Agent 关系:调用专家,还是交出控制权
多 Agent 体系首先要区分两种关系:
flowchart LR
U[用户请求] --> S[Supervisor]
S -->|Agent as tool| R[研究 Agent]
R -->|返回子任务结果| S
S -->|综合后继续决策| F[最终回答]
S2[路由 Agent] -->|Handoff| C[客服 Agent]
C -->|直接继续当前对话| U2[用户]
1. Agents as tools:Supervisor 保留控制权
当 Supervisor 通过 Agent.as_tool() 调用专家时,专家相当于 Supervisor 的一个“高级工具”。
控制流是:
其中:
- 是 Supervisor;
- 是 Expert;
- 是最终输出。
专家完成子任务后,结果返回给 Supervisor。Supervisor 仍然可以:
- 调用另一个专家;
- 比较多个专家的结果;
- 修改问题;
- 继续调用工具;
- 决定最终回复。
这种模式适合“专家提供证据,Supervisor 负责最终回答”的场景。例如:
- 研究 Agent 搜集资料;
- 计算 Agent 计算指标;
- 风险 Agent 提供风险判断;
- Supervisor 汇总成一份报告。
2. Handoff:当前 Agent 交出控制权
Handoff 的控制流不同:
Handoff 发生后, 成为当前活跃 Agent。它通常直接面向用户完成当前轮对话。Supervisor 不会自动重新获得控制权,也不会自动替专家总结结果。
官方文档明确区分了这两种模式:Agents as tools 适合专家完成受限子任务、由管理 Agent 保持最终回答权;Handoffs 适合路由本身就是流程的一部分,并由被转交的专业 Agent 接管当前轮对话。(openai.github.io)
这也是最常见的误解:
Handoff 不是“调用专家并拿回返回值”,而是“把当前对话的执行主体换成专家”。
三、Supervisor 的控制权到底是什么
“控制权”不能只理解成一个变量 current_agent。它至少包括四部分:
其中:
- :当前活跃 Agent;
- :当前 Agent 将看到的输入;
- :当前运行的策略,例如最大轮数、工具权限和 Guardrail;
- :当前运行结果的归属,包括最后输出和下一轮建议使用的 Agent。
一次模型调用完成后,Runner 通常根据模型输出执行三种分支:
- 输出满足最终输出条件,运行结束;
- 请求 Handoff,更新当前 Agent 和输入后继续循环;
- 请求工具调用,执行工具、追加工具结果后继续循环。
OpenAI Agents SDK 的 Runner 正是按照这一生命周期运行,并通过 max_turns 限制 Agent-loop 的模型调用轮数。超过限制时会抛出 MaxTurnsExceeded。(openai.github.io)
可以形式化为:
若:
则:
若模型请求工具 ,则:
并继续由同一个 Agent 执行。
若模型请求 Handoff 到 ,则:
同时:
其中 是 Handoff 规则,Shape 可能保留全部历史,也可能通过过滤器或历史映射器改变下一 Agent 看到的输入。
所以,Handoff 的核心不是“传递一个函数返回值”,而是同时改变:
- 谁负责下一次模型调用;
- 下一次模型调用的输入;
- 最终输出的生产者;
- 下一轮继续对话时通常使用的 Agent。
四、为什么 Supervisor 不应默认通过 Handoff 调用所有专家
假设有三个 Agent:
triage_agent:负责分类;billing_agent:负责账单问题;refund_agent:负责退款问题。
最简单的 Handoff 写法如下:
from agents import Agent, Runner, handoff
billing_agent = Agent(
name="Billing agent",
instructions="处理账单、扣款和发票问题。",
)
refund_agent = Agent(
name="Refund agent",
instructions="处理退款资格、退款进度和退款说明。",
)
triage_agent = Agent(
name="Triage agent",
instructions=(
"先判断用户问题属于账单还是退款。"
"如果属于账单,转交 Billing agent;"
"如果属于退款,转交 Refund agent。"
),
handoffs=[
billing_agent,
refund_agent,
],
)
result = Runner.run_sync(
triage_agent,
"我被重复扣款了,想申请退款。",
max_turns=6,
)
print(result.final_output)
print(result.last_agent.name)
这段代码的生命周期是:
triage_agent接收用户消息;- 模型看到两个 Handoff 工具;
- 模型选择
transfer_to_refund_agent; - Runner 更新当前 Agent;
refund_agent继续处理同一轮输入;refund_agent产生最终文本;result.final_output是最后一个 Agent 的输出;result.last_agent通常是refund_agent。
Handoff 的工具名称和目标 Agent 之间存在直接关系。SDK 可以根据 Agent 名称生成类似 transfer_to_refund_agent 的工具;也可以通过 handoff() 覆盖工具名称、描述、回调和输入过滤逻辑。(openai.github.io)
但如果需求是:
先让账单 Agent 判断是否重复扣款,再让退款 Agent 生成退款策略,最后由 Supervisor 统一回复。
这就不适合简单地把两个 Agent 都注册成 Handoff。因为第一次 Handoff 后,账单 Agent 就拥有当前控制权,它不会自动把结果返回给 Supervisor。
这时更适合使用“Agent as tool”:
from agents import Agent, Runner
billing_agent = Agent(
name="Billing analyst",
instructions=(
"分析账单问题。只返回结构化、简洁的事实:"
"扣款类型、是否可能重复扣款、需要哪些证据。"
),
)
refund_agent = Agent(
name="Refund policy analyst",
instructions=(
"根据输入判断退款政策和下一步操作。"
"不要直接对用户说话,只返回给调用方。"
),
)
supervisor_agent = Agent(
name="Support supervisor",
instructions=(
"你负责最终回答用户。"
"先根据需要调用账单分析和退款政策分析,"
"再综合结果,用中文给出明确、可执行的回复。"
),
tools=[
billing_agent.as_tool(
tool_name="analyze_billing",
tool_description="分析账单、重复扣款和支付记录。",
),
refund_agent.as_tool(
tool_name="analyze_refund_policy",
tool_description="分析退款条件、证据和下一步流程。",
),
],
)
result = Runner.run_sync(
supervisor_agent,
"我被重复扣款了,想申请退款。",
max_turns=8,
)
print(result.final_output)
这里的控制流是:
专家返回的是子任务结果,而不是接管用户对话。官方编排文档也把“Supervisor 调用多个专家并综合输出”列为 Agents as tools 的典型用途。(openai.github.io)
五、Handoff 中的上下文:三个概念不能混用
“上下文”至少有三种含义:
- 本地运行上下文:Python 代码和工具可以读取的对象;
- 模型上下文:发送给 LLM 的消息、工具结果和历史;
- Handoff 输入元数据:模型在调用 Handoff 时额外生成的数据。
把这三者混在一起,是多 Agent 系统中最常见的数据边界错误。
1. 本地运行上下文:给代码,不给模型
OpenAI Agents SDK 使用 RunContextWrapper 承载本地上下文。调用 Runner.run(..., context=...) 时传入的对象,可以被工具、生命周期回调和 Handoff 回调读取;这个对象本身不会自动发送给 LLM。(openai.github.io)
from dataclasses import dataclass
from agents import Agent, Runner, RunContextWrapper
@dataclass
class AppContext:
user_id: str
request_id: str
locale: str
def get_user_id(ctx: RunContextWrapper[AppContext]) -> str:
return ctx.context.user_id
agent = Agent(
name="Support agent",
instructions="处理用户问题。",
)
result = Runner.run_sync(
agent,
"查询我的退款进度。",
context=AppContext(
user_id="u_123",
request_id="req_456",
locale="zh-CN",
),
)
AppContext 适合放:
- 用户 ID;
- 请求 ID;
- 数据库客户端;
- 日志器;
- 权限检查器;
- 内部服务客户端;
- 事务或租户信息。
不适合直接把它当成模型可见信息。模型若需要知道某个字段,必须通过:
- 用户消息;
- Agent 指令;
- 工具返回值;
- 显式模型输入。
这条边界很重要。例如,user_id 可以留在本地上下文中用于数据库查询,但不应默认把内部数据库连接、权限对象或密钥放入 Prompt。
2. 模型上下文:决定 Agent 能理解什么
模型上下文是 LLM 实际看到的输入。Handoff 默认情况下,接收方可以看到之前的对话历史。官方文档将 Handoff 描述为“新 Agent 接管对话”,并指出接收方通常会看到完整的前序历史,除非配置了 input_filter 或历史映射。(openai.github.io)
因此,下面两种设计含义完全不同:
输入 A:
用户:我的订单是 1001。
助手:我会查询订单。
工具:订单 1001 已发货。
Handoff:转交退款 Agent。
与:
输入 B:
用户:订单 1001 已发货,但我想退款。
Handoff:转交退款 Agent。
输入 A 中,退款 Agent 可能看到完整的工具调用和中间消息;输入 B 中,它只看到经过整理的用户请求。前者信息更完整,但容易带入无关内容、内部提示、旧决策和过期状态;后者更干净,但需要你显式保留足够事实。
3. Handoff 输入元数据:模型临时生成的数据
有时路由 Agent 不仅要选择目标,还需要附带一个原因、优先级或摘要。例如:
from pydantic import BaseModel
from agents import Agent, RunContextWrapper, handoff
class EscalationData(BaseModel):
reason: str
priority: str
async def on_escalation(
ctx: RunContextWrapper[None],
input_data: EscalationData,
):
print(
f"escalation reason={input_data.reason}, "
f"priority={input_data.priority}"
)
human_agent = Agent(
name="Human escalation agent",
instructions="说明需要人工介入,并整理交接信息。",
)
escalation_handoff = handoff(
agent=human_agent,
input_type=EscalationData,
on_handoff=on_escalation,
)
input_type 描述的是 Handoff 工具调用参数的结构。SDK 会把它暴露给模型,校验模型返回的 JSON,再将解析后的对象传给 on_handoff。它不是应用状态,也不会替换接收 Agent 的主输入,更不会根据参数自动选择另一个目标 Agent。(openai.github.io)
因此:
context:放已有的本地依赖和应用状态;input_type:放模型在路由时生成的小块元数据;input_filter:改变接收 Agent 看到的模型输入;- Handoff 目标:由注册的目标 Agent 决定。
六、Handoff 的输入历史如何变化
一次 Handoff 的输入可以抽象成:
其中:
- :运行开始前的输入历史;
- :Handoff 发生前已经产生的项目;
- :当前轮新产生的项目,包括 Handoff 调用和输出;
++:按顺序拼接。
SDK 的 HandoffInputData 将这些内容拆分为 input_history、pre_handoff_items 和 new_items,并允许通过 input_items 指定传递给下一 Agent 的实际输入。(openai.github.io)
这意味着“过滤历史”并不等于“删除运行记录”。一个过滤器可能只改变下一 Agent 的模型输入,但运行结果、Session 或审计记录仍然需要保留原始事件。
例如,下面这个过滤器只把用户消息和最近的工具结果传给退款 Agent:
from agents import Agent, HandoffInputData, handoff
def keep_relevant_input(data: HandoffInputData) -> HandoffInputData:
filtered = []
for item in data.input_items:
# 实际项目中应根据具体 InputItem 类型判断,
# 这里只展示过滤边界。
if item.get("role") in {"user", "tool"}:
filtered.append(item)
return HandoffInputData(
input_history=data.input_history,
pre_handoff_items=data.pre_handoff_items,
new_items=data.new_items,
input_items=filtered,
run_context=data.run_context,
)
refund_agent = Agent(
name="Refund agent",
instructions="只根据传入的相关事实处理退款。",
)
refund_handoff = handoff(
agent=refund_agent,
input_filter=keep_relevant_input,
)
生产代码不能简单假设所有输入项目都是字典,也不能仅按文本内容去重。消息内容相同,不代表它们是同一次事件;例如用户两次发送了完全相同的问题,应该保留为两个不同的输入事件。
当前 SDK 文档还描述了可选的嵌套 Handoff 历史能力,用于将可摘要的历史压缩成有序的助手摘要片段;该能力被标记为可选 Beta,默认关闭,且已有自定义 input_filter 的代码不会自动改变行为。(openai.github.io)
这类能力解决的是“历史太长或层层 Handoff 后难以阅读”的问题,不解决“状态属于谁”的问题。订单状态、退款状态和审批状态仍然应该由共享状态系统负责,而不是依赖 Prompt 中的一段摘要。
七、返回值:谁的返回值,返回到哪里
在多 Agent 系统中,“返回”至少有四种不同含义:
- 工具函数的返回值;
- Agent-as-tool 的子任务结果;
- 最后一个 Agent 的最终输出;
- 一次 Run 的完整运行记录。
1. 工具返回值
工具返回值回到调用它的 Agent,并作为下一次模型调用的输入项目:
工具没有改变当前 Agent 的控制权。
2. Agent-as-tool 返回值
当一个 Agent 被包装成工具时,它的结果返回给调用方 Supervisor。Supervisor 可以继续推理和调用其他专家。
3. Handoff 的最终输出
Handoff 不会把专家输出作为普通字符串返回给原 Supervisor。专家完成后,它的输出直接成为当前 Run 的最终输出。
SDK 的 final_output 表示最后一个运行 Agent 的最终输出;如果最后 Agent 没有配置 output_type,通常是字符串;如果配置了结构化输出,则是该 Agent 的输出类型;如果运行在审批中断等情况下停止,可能为 None。(openai.github.io)
这会带来一个类型问题:
Supervisor.output_type = SupervisorAnswer
RefundAgent.output_type = RefundAnswer
如果 Supervisor 可以 Handoff 给 RefundAgent,那么整个 Run 的 final_output 可能是两种不同类型之一。SDK 因为无法静态知道所有可能的最后 Agent 类型,所以 final_output 的类型边界不能简单理解为 Supervisor 的输出类型。(openai.github.io)
工程上有三种处理方法:
- 所有可能成为最后 Agent 的 Agent 使用统一输出协议;
- Handoff Agent 输出面向用户的文本,不在 Run 层强行统一结构;
- 由 Supervisor 使用 Agents as tools 调用专家,从而让 Supervisor 独占最终输出类型。
4. 完整运行记录
如果需要审计或诊断,不能只保存 final_output。RunResult 还提供:
new_items:包含消息、工具、Handoff 和审批等丰富运行项目;raw_responses:底层模型响应;last_agent:最后运行的 Agent;to_input_list():用于下一轮的输入列表;to_state():用于暂停和恢复的运行状态。
官方结果文档明确区分了这些结果面:final_output 适合展示给用户,new_items 适合日志和审计,last_agent 适合决定下一轮通常由谁继续处理,to_state() 适合可恢复运行。(openai.github.io)
八、下一轮应该回到 Supervisor 还是继续由专家处理
假设第一轮发生了:
用户随后说:
那退款大概什么时候到账?
如果系统每一轮都从 Triage 开始,路由 Agent 可能再次分类;如果系统直接复用上一轮的 Refund,则可以保留更准确的局部上下文。
SDK 的 last_agent 表示一次 Run 中最后运行的 Agent,通常可以将它作为下一轮的起始 Agent。(openai.github.io)
一个手动续接的示例:
from agents import Agent, Runner
refund_agent = Agent(
name="Refund agent",
instructions="持续处理退款相关问题,直接面向用户回答。",
)
result = Runner.run_sync(
refund_agent,
"我申请的退款还没有到账。",
max_turns=4,
)
print(result.final_output)
next_result = Runner.run_sync(
result.last_agent,
"那退款大概什么时候到账?",
max_turns=4,
)
print(next_result.final_output)
不过,last_agent 只是“通常适合继续处理”的运行结果,不是业务授权。以下情况仍应重新经过 Supervisor 或策略层:
- 用户切换了完全不同的主题;
- 当前专家只被授权处理一次性任务;
- 权限、租户或用户身份发生变化;
- 任务状态已经过期;
- 上一 Agent 的建议需要重新审核;
- 当前 Agent 不能处理新的操作。
因此可以把下一轮选择写成:
Session 只负责帮助保存和加载对话历史,不等同于路由策略。SDK 的 Session 可以自动读取运行前的历史,并在运行后存储新消息;不同 Session ID 对应不同对话。(openai.github.io)
九、死循环是如何形成的
多 Agent Handoff 的死循环通常不是 Python 层面的 while True,而是模型在状态空间中持续选择某条循环边。
设 Agent 图为:
其中:
- :Agent 集合;
- :允许的 Handoff 边。
例如:
如果每次模型调用后,输入状态都没有产生足够变化,那么就可能出现:
这里的 表示用户问题和业务状态几乎没有变化。图中有环并不必然错误,但环必须具有单调进展条件。
定义一个进度函数:
其中 是运行状态, 表示还剩多少未完成工作。要保证终止,至少需要:
例如:
- 已完成的路由不再重复执行;
- 已询问过的澄清问题不再重复询问;
- 已经失败的工具切换到回退路径;
- 已经尝试过某个 Agent 后不再无条件重试;
- 每次循环都增加已处理步骤或消耗有限预算。
如果只是把 max_turns 设置得很大,而没有定义进度函数,那么只是把死循环变成高成本循环。
一个典型反例
Supervisor:这个问题应该交给 Billing。
Billing:我无法处理,需要 Supervisor 判断。
Supervisor:这个问题应该交给 Billing。
Billing:我无法处理,需要 Supervisor 判断。
其状态变化是:
| 步骤 | 当前 Agent | 决策 | 业务状态 |
|---|---|---|---|
| 0 | Supervisor | Handoff → Billing | 未变化 |
| 1 | Billing | Handoff → Supervisor | 未变化 |
| 2 | Supervisor | Handoff → Billing | 未变化 |
| 3 | Billing | Handoff → Supervisor | 未变化 |
由于业务状态不变,且没有“已尝试 Billing”的记录,系统没有理由自行停止。
解决循环的三种方式
1. 破坏回边
让 Billing 不再 Handoff 回 Supervisor,而是直接:
- 请求用户补充信息;
- 返回无法处理的最终答案;
- 调用一个固定的回退 Agent;
- 产生结构化失败结果。
2. 记录路由历史
在本地上下文或共享状态中记录:
@dataclass
class RoutingState:
visited_agents: set[str]
handoff_count: int
路由前检查:
if "billing_agent" in state.visited_agents:
# 不再重复转交,进入回退路径
...
这不是防止所有错误的充分条件,因为同一个 Agent 可能在状态确实变化后再次处理;更精确的判定应使用:
只有当三者都没有变化时,才视为重复转交。
3. 设置有限预算
try:
result = Runner.run_sync(
triage_agent,
user_input,
max_turns=8,
)
except Exception as exc:
# 记录 request_id、最后 Agent、已发生的 Handoff 和异常类型
print(f"run failed: {type(exc).__name__}: {exc}")
当达到 max_turns 时,SDK 会抛出 MaxTurnsExceeded;也可以通过错误处理器将其转成受控的最终输出,而不是让请求直接失败。官方文档给出了针对 "max_turns" 的错误处理器机制。(openai.github.io)
需要注意:最大轮数是最后一道保险,不是路由正确性的证明。它可以保护成本和延迟,但不能说明系统已经完成了任务。
十、怎样区分“合理重试”和“死循环”
重试与死循环的区别,不在于是否重复调用,而在于状态是否发生了可验证变化。
设一次尝试的状态指纹为:
如果连续两次满足:
并且动作仍然相同,例如再次 Handoff 到同一个 Agent,那么这更接近死循环,而不是有效重试。
合理重试通常至少改变一个因素:
- 重试次数增加;
- 使用备用模型;
- 使用备用工具;
- 输入中加入错误信息;
- 缩小任务范围;
- 更新业务状态;
- 转人工;
- 改变路由目标。
例如:
第 1 次:Billing Agent 调用订单服务,超时
第 2 次:Billing Agent 使用只读缓存查询
第 3 次:Fallback Agent 告知用户系统暂时无法确认
这是有限的故障恢复路径。
相反:
第 1 次:Billing Agent 调用订单服务,超时
第 2 次:Billing Agent 再次调用同一个服务
第 3 次:Billing Agent 再次调用同一个服务
如果没有退避、次数上限、工具切换或状态变化,这只是重复消耗。
十一、Handoff 的授权不能只依赖模型选择
Handoff 工具可以被模型调用,并不意味着模型选择就等于业务授权。
例如,模型通过 Handoff 参数传入:
{
"reason": "high_value_refund",
"priority": "urgent"
}
此时系统仍然必须检查:
- 用户是否有权申请该退款;
- 当前租户是否允许该操作;
- 金额是否超过自动处理阈值;
- 是否需要人工审批;
- 是否已经存在相同退款请求。
SDK 文档特别指出,is_enabled 会在准备可用 Handoff 时执行,因此不能依赖它检查模型稍后才生成的参数值;如果授权依赖 Handoff 参数,应在 on_handoff 开始处检查,并在产生副作用前失败。Handoff 也不适用函数工具的输入 Guardrail。(openai.github.io)
因此,正确顺序是:
模型选择 Handoff
↓
解析并校验 input_type
↓
on_handoff 中执行授权检查
↓
授权通过后再写入状态或触发副作用
↓
接收 Agent 开始处理
不要在 Handoff 之后才检查授权,因为接收 Agent 可能已经看到“已升级”“已批准”之类的提示,也可能继续调用有副作用的工具。
十二、共享状态与 Handoff 上下文的边界
Handoff 传递的是对话控制流,不应承担所有共享状态管理职责。
推荐将系统状态拆成三类:
1. 对话历史
回答“模型之前说了什么、用户提供了什么、工具返回了什么”。
可由:
Session;to_input_list();- 服务端
conversation_id; previous_response_id
管理。
2. 业务状态
回答“订单当前是什么状态、退款是否已创建、审批是否完成”。
应由数据库或领域服务管理,并具有:
- 所有权;
- 版本号;
- 幂等键;
- 冲突检测;
- 事务边界。
3. 运行状态
回答“当前由谁处理、已经 Handoff 几次、是否暂停、是否等待审批”。
可记录:
{
"run_id": "run_123",
"current_agent": "refund_agent",
"handoff_path": [
"triage_agent",
"refund_agent"
],
"turn_count": 3,
"state_version": 7,
"pending_approval": false
}
Handoff 只负责把控制权从一个 Agent 转给另一个 Agent;它不会自动解决两个 Agent 同时修改退款状态时的并发冲突。
例如:
Refund Agent A 读取 refund_status = pending
Refund Agent B 读取 refund_status = pending
A 写入 approved
B 写入 rejected
如果没有版本检查,最终状态取决于写入顺序,而不是业务规则。
因此,Agent 之间共享的数据更新应使用类似:
更新行数为 0 时,说明发生了版本冲突,需要重新读取状态并重新决策,而不是让模型凭旧上下文继续执行。
十三、并发执行时,Supervisor 仍然要负责合并语义
多个 Agent 并行运行适合互不依赖的子任务,例如:
- 三个检索 Agent 分别查询不同数据源;
- 三个评审 Agent 独立检查同一份文本;
- 多个候选方案分别估算成本。
但并行结果不能简单地“拼接字符串”。Supervisor 或代码编排层必须定义:
并明确:
- 哪些字段可以合并;
- 哪些字段必须唯一;
- 冲突时谁优先;
- 缺失结果是否允许继续;
- 一个专家失败时是否整体失败;
- 是否需要再次调用裁决 Agent。
如果并行专家直接共享可变状态,而没有锁、版本或事件顺序,Handoff 只会让问题更难追踪:控制流是串行切换的,状态写入却可能是并发的。
十四、诊断:不要只看最终答案
多 Agent 故障通常隐藏在最终输出之前。至少需要记录以下事件:
run_started
agent_started
model_called
tool_called
tool_finished
handoff_requested
handoff_accepted
agent_finished
run_completed
run_failed
OpenAI Agents SDK 内置 Tracing,可以记录模型生成、工具调用、Handoff、Guardrail 和自定义事件,用于查看和监控 Agent 工作流。Tracing 默认启用,也可以全局或单次运行关闭。(openai.github.io)
一个有效的 Handoff 日志至少应包含:
{
"run_id": "run_123",
"turn": 4,
"from_agent": "triage_agent",
"to_agent": "refund_agent",
"reason": "duplicate_charge",
"input_fingerprint": "fp_abc",
"state_version": 12,
"history_items_forwarded": 9
}
诊断死循环时,按以下顺序检查:
current_agent是否在两个或多个 Agent 间反复变化;- Handoff 目标是否总是相同;
- 输入指纹是否没有变化;
- 业务状态版本是否没有变化;
- 是否重复执行同一工具;
max_turns是否被关闭或设置过大;- 是否将错误文本追加到上下文,却没有改变下一步策略;
- 最后一个 Agent 是否真的产生了最终输出。
new_items 比 final_output 更适合做这类分析,因为它保留了消息、工具调用、Handoff 和审批等运行项目。(openai.github.io)
十五、一个可验证的路由设计
可以把 Supervisor 的路由约束写成以下状态机:
stateDiagram-v2
[*] --> Triage
Triage --> Billing: billing_intent
Triage --> Refund: refund_intent
Triage --> Clarify: insufficient_information
Triage --> Fallback: unsupported_intent
Billing --> Done: final_answer
Refund --> Done: final_answer
Clarify --> Done: ask_user
Fallback --> Done: controlled_failure
Billing --> Fallback: tool_failure
Refund --> HumanApproval: high_risk_action
HumanApproval --> Refund: approved
HumanApproval --> Done: rejected
Triage --> Fallback: repeated_route
Refund --> Fallback: max_turns
这个状态机有三个重要性质:
1. 终止状态明确
Done、Fallback 和 HumanApproval 都不再无限 Handoff。即使任务失败,也会以受控状态结束。
2. 高风险操作不会直接等价于 Handoff 成功
Refund → HumanApproval 只是进入审批状态,不代表退款已经执行。审批结果必须经过业务状态验证。
3. 重复路由有专门出口
Triage → Fallback: repeated_route 防止 Supervisor 在相同任务和状态下重复选择相同路径。
模型仍然可以参与 Triage 的判断,但状态机负责限制允许的边。这样做的意义是:
这比完全让模型自由决定所有控制流更容易测试、评估和恢复。官方编排文档也区分了由 LLM 决策的编排方式与由代码控制的确定性编排方式,并建议在需要可预测速度、成本和性能时使用代码编排或结构化输出。(openai.github.io)
十六、生产中的取舍
适合使用 Handoff 的情况
- 用户请求需要由某个专业 Agent 直接接管;
- 专家应直接面向用户回答;
- 不需要 Supervisor 汇总多个专家结果;
- 路由之后的对话具有明显的领域连续性;
- Agent 之间的最终输出协议可以接受变化。
适合使用 Agents as tools 的情况
- Supervisor 必须保留最终回答权;
- 需要调用多个专家并比较结果;
- 专家只负责检索、计算、审查或分类;
- 需要统一最终输出结构;
- 需要由一个地方执行最终 Guardrail 或业务决策。
适合使用纯代码编排的情况
- 合规、审批、支付、退款等高风险流程;
- 状态机边界明确;
- 失败和回退路径必须可预测;
- 需要严格控制成本、延迟和副作用;
- 任务步骤适合写成显式工作流。
实际系统可以混用:
代码决定:
是否允许进入退款流程
是否需要人工审批
最大尝试次数
是否允许回退
LLM 决定:
用户意图
文本摘要
工具参数草稿
对多个候选结果的解释
不要让模型独自决定“是否已经完成支付”“是否已经得到授权”“是否可以无限重试”。这些是业务状态和运行时策略,不是自然语言判断。
结语:把 Handoff 看成控制流跳转,而不是消息传递
理解 Supervisor 与 Handoff 的关键,可以压缩成四个判断:
- 谁拥有控制权:Agents as tools 保留控制权,Handoff 转移控制权;
- 谁能看到什么:本地
context不自动进入模型,Handoff 历史决定接收 Agent 的可见信息; - 谁产生最终返回:Handoff 后通常由最后一个 Agent 产生
final_output; - 什么时候停止:最终输出、审批暂停、失败回退和
max_turns都必须是明确的终止或恢复路径。
一个可靠的多 Agent 系统不是“Agent 越多越智能”,而是能够证明:
当这些条件成立时,Supervisor 才真正是控制器,Handoff 才是可审计的控制流转移,而不是把多个模型调用连接起来后祈祷它们自行收敛。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:多 Agent 路由:分类、能力匹配、动态选择、回退和评测
- 下一篇:多 Agent 辩论与 Critic:独立证据、聚合、成本和伪共识
- 延伸:多 Agent 共享状态:所有权、版本、冲突、锁和事件溯源
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论