Handoff 机制详解
Handoff(移交)是指一个 Agent 将当前对话的控制权转交给另一个 Agent 的机制。被移交的 Agent 会接收到完整的对话历史,就像它从一开始就参与了这个对话。
用户:"我想退款,但密码也忘了"
│
▼
┌─────────────────────────────────────────────┐
│ 分诊 Agent(Orchestrator) │
│ 分析用户意图:涉及账号 + 退款两个问题 │
│ 决策:先转账号问题,再处理退款 │
└──────────────┬──────────────────────────────┘
│ Handoff
▼
┌─────────────────────────────────────────────┐
│ 账号支持 Agent(Worker) │
│ 专注:密码重置、账号验证 │
│ 完成密码重置后... │
└──────────────┬──────────────────────────────┘
│ Handoff
▼
┌─────────────────────────────────────────────┐
│ 退款专员 Agent(Worker) │
│ 专注:退款政策、退款操作 │
│ 处理退款请求 │
└─────────────────────────────────────────────┘
基础 Handoff 实现
from agents import Agent, Runner, handoff
import asyncio
# ── 专家 Agent(Workers)──────────────────────────────────
tech_agent = Agent(
name="技术支持专员",
instructions="""你是技术支持专员,专注于解决产品技术问题。
遇到非技术问题(账单、退款、账号)时,明确说明需要转交给其他专员。
""",
model="gpt-4o-mini"
)
billing_agent = Agent(
name="账单专员",
instructions="""你是账单处理专员,专注于发票、付款、退款问题。
退款超过 1000 元时,需要获取用户同意后才能操作。
""",
model="gpt-4o-mini"
)
account_agent = Agent(
name="账号安全专员",
instructions="""你是账号安全专员,专注于密码重置、账号验证、安全设置。
操作前必须通过安全验证(手机号或邮箱验证码)。
""",
model="gpt-4o-mini"
)
# ── Orchestrator Agent ────────────────────────────────────
# handoffs 参数声明该 Agent 可以移交给哪些 Agent
triage_agent = Agent(
name="客服分诊",
instructions="""你是客服分诊助手,负责接待用户并将问题转交给正确的专员。
转交规则(严格遵守):
- 产品使用、bug、错误信息 → 技术支持专员
- 发票、扣费、退款、付款 → 账单专员
- 密码、账号、登录、安全 → 账号安全专员
- 问题涉及多个领域时,优先处理安全问题,再处理账单,最后技术
你只负责分诊,不直接解决具体问题。
""",
model="gpt-4o-mini",
handoffs=[tech_agent, billing_agent, account_agent] # 声明可移交对象
)
async def main():
result = await Runner.run(
triage_agent,
"你好,我的账单里有一笔重复扣款,能帮我退款吗?"
)
print(f"最终回复(来自 Agent: {result.last_agent.name}):")
print(result.final_output)
asyncio.run(main())
自定义 Handoff 配置
使用 handoff() 函数可以精细控制移交行为:
from agents import Agent, handoff, RunContextWrapper
from dataclasses import dataclass, field
@dataclass
class SupportContext:
user_id: str
handoff_count: int = 0 # 记录转交次数
handoff_reasons: list[str] = field(default_factory=list)
# on_handoff 回调:每次发生移交时触发
def on_tech_handoff(ctx: RunContextWrapper[SupportContext]):
ctx.context.handoff_count += 1
ctx.context.handoff_reasons.append("技术问题")
print(f"[监控] 用户 {ctx.context.user_id} 被转接到技术支持")
def on_billing_handoff(ctx: RunContextWrapper[SupportContext]):
ctx.context.handoff_count += 1
ctx.context.handoff_reasons.append("账单问题")
print(f"[监控] 用户 {ctx.context.user_id} 被转接到账单专员")
# 使用 handoff() 函数创建带配置的移交
triage_agent_v2 = Agent(
name="高级客服分诊",
instructions="将用户问题转交给正确的专员处理。",
model="gpt-4o-mini",
handoffs=[
handoff(
tech_agent,
tool_name_override="transfer_to_tech_support", # 自定义工具名
tool_description_override="转接到技术支持,处理产品使用和技术故障问题",
on_handoff=on_tech_handoff, # 移交时的回调函数
),
handoff(
billing_agent,
tool_name_override="transfer_to_billing",
tool_description_override="转接到账单专员,处理付款、退款和发票问题",
on_handoff=on_billing_handoff,
),
]
)
完整客服系统实战
from agents import Agent, Runner, handoff, function_tool, RunContextWrapper
from dataclasses import dataclass, field
from datetime import datetime
import asyncio
@dataclass
class SupportContext:
user_id: str
session_id: str
start_time: datetime = field(default_factory=datetime.now)
resolved: bool = False
satisfaction_score: int | None = None
# ── 账单工具 ──────────────────────────────────────────────
@function_tool
async def lookup_invoice(
ctx: RunContextWrapper[SupportContext],
invoice_id: str
) -> str:
"""查询发票详情。"""
user_id = ctx.context.user_id
# 实际查询数据库...
return f"用户 {user_id} 的发票 {invoice_id}:金额 ¥299,状态:已付款,时间:2025-03-15"
@function_tool
async def process_refund(
ctx: RunContextWrapper[SupportContext],
invoice_id: str,
amount: float,
reason: str
) -> str:
"""处理退款申请。需要先查询发票确认金额。"""
if amount > 1000:
return "退款金额超过 1000 元,需要人工审核,预计 1-3 个工作日处理。"
# 实际退款操作...
ctx.context.resolved = True
return f"退款申请已提交:¥{amount},退款原因:{reason},预计 3-5 个工作日到账。"
# ── 技术支持工具 ──────────────────────────────────────────
@function_tool
async def search_knowledge_base(query: str) -> str:
"""在技术文档库中搜索解决方案。"""
# 实际搜索知识库...
return f"搜索结果({query}):找到 3 篇相关文章..."
# ── 专家 Agent 定义 ───────────────────────────────────────
billing = Agent(
name="账单专员",
instructions="""你是账单专员。
处理退款前,必须先用 lookup_invoice 工具查询发票确认金额。
退款超过 1000 元时,明确告知需要人工审核。
""",
model="gpt-4o-mini",
tools=[lookup_invoice, process_refund]
)
tech_support = Agent(
name="技术支持",
instructions="""你是技术支持专员。
遇到技术问题时,先搜索知识库,找到方案后给用户详细指引。
""",
model="gpt-4o-mini",
tools=[search_knowledge_base]
)
# ── 分诊 Agent(入口)────────────────────────────────────
def log_handoff(target_name: str):
def callback(ctx: RunContextWrapper[SupportContext]):
elapsed = (datetime.now() - ctx.context.start_time).seconds
print(f"[{elapsed}s] 转接到: {target_name}")
return callback
triage = Agent(
name="客服分诊",
instructions="""你是客服总入口,礼貌接待用户并快速判断问题类型。
账单/退款/发票 → 账单专员
技术/bug/使用 → 技术支持
不确定时先询问用户具体情况再转接。
""",
model="gpt-4o-mini",
handoffs=[
handoff(billing, on_handoff=log_handoff("账单专员")),
handoff(tech_support, on_handoff=log_handoff("技术支持")),
]
)
async def handle_customer(user_id: str, message: str):
ctx = SupportContext(user_id=user_id, session_id=f"sess_{user_id[:8]}")
result = await Runner.run(triage, message, context=ctx)
print(f"\n最终回复({result.last_agent.name}):\n{result.final_output}")
print(f"问题已解决: {ctx.resolved}")
asyncio.run(handle_customer("user_abc123", "我的订单被重复扣款了,发票号是 INV-2025-001"))
防止过度转交
在 instructions 中明确转交条件,避免 Agent 动不动就转交。一个好的原则是:如果能在当前 Agent 内解决,就不要转交。设置转交前需要尝试自己解决的规则,如"只有在问题明确超出你的处理范围时,才进行转交"。