多 Agent¶
lovia 只提供三种多 Agent 协作机制,不引入编排 DSL。它们的核心区别在于谁掌握 对话、目标 Agent 能看到哪些上下文,以及父 Agent 是否需要等待:
- Handoff 移交对话,由专家 Agent 带着完整历史接手。
- Agent-as-tool 委派子任务,父 Agent 等到结果后继续。
- Subagents 把独立任务放到后台,父 Agent 默认无需等待,可以同时处理其他工作。
更复杂的流程可以直接用普通 Python 组合这三种机制。
Handoff¶
from lovia import Agent, Runner
billing = Agent(name="billing", instructions="处理账单问题。", model="<model>")
support = Agent(name="support", instructions="处理技术问题。", model="<model>")
triage = Agent(
name="triage",
instructions="把用户转给合适的专家。",
model="<model>",
handoffs=[billing, support],
)
result = await Runner.run(triage, "我被重复扣款了。")
print(result.final_agent.name) # "billing"
handoffs 中的每个条目都会注册为 transfer_to_<name> 工具。工具名会被规范成
Provider 接受的格式:仅使用 ASCII,最长 64 个字符,必要时附加稳定的摘要后缀。
模型调用这个工具后,运行循环会切换到目标 Agent 并继续。
目标 Agent 可以看到哪些内容。 Handoff 会把对话历史一并移交,包括此前的消息、
工具调用和工具结果。只有开头的系统提示会按目标 Agent 重新生成,其中包含它自己的
instructions、Workspace、插件和结构化输出约束。运行级 extra_instructions 也会
重新应用到 Handoff 到达的每个 Agent。
还会改变什么。 系统会重新解析目标 Agent 的 Provider、工具、插件和 Workspace,
并执行各插件的 setup();同时触发 HandoffOccurred 事件,交给移交双方的 hooks。
运行本身则保持不变:max_turns、预算、取消令牌、Mailbox、Session、Checkpoint,
以及初始 Agent 配置的重试策略和上下文策略都会沿用。
自定义 Handoff¶
需要自定义移交工具时,可以用 Handoff 包装目标 Agent:
from lovia import Agent, Handoff
triage = Agent(
name="triage",
model="<model>",
handoffs=[
Handoff(
target=billing,
description="账单:退款、重复扣款、发票、支付方式。",
on_handoff=lambda args, ctx: audit_log(ctx.session_id, args.get("reason")),
),
support, # 也可以直接传入 Agent
],
)
| 字段 | 默认值 | 用途 |
|---|---|---|
target |
必填 | 要转交给哪个 Agent |
name |
transfer_to_<slug> |
覆盖工具名 |
description |
通用移交说明 | 路由依据。父 Agent 需要在相似专家间选择时,应写清目标专长 |
on_handoff |
None |
Handoff 发生时调用的同步或异步回调 (args, ctx);args 包含模型可选填的 reason |
默认 description 只有 Agent 名称,信息很少。需要可靠路由时,应明确填写目标 Agent
擅长处理的问题。
Handoff 语义¶
- 只执行第一个 Handoff。 Handoff 工具始终作为屏障执行,
不会与其他工具调用并发;同一轮里的第二个 Handoff 会在触发
on_handoff之前 被拒绝。 - 切换发生在当前轮次结束时。 系统会先处理本轮剩余的工具结果,下一次模型调用 才交给目标 Agent。
- Handoff 后仍可恢复。 Checkpoint 会按名称记录 当前 Agent;恢复时会从入口 Agent 的 Handoff 关系中找到它并继续运行。
- Handoff 不会创建嵌套运行。 无论连续移交多少次,始终只有一个 Run、 一份 Transcript 和一套预算。
Agent-as-tool¶
这是委派,不是移交:子 Agent 在独立的运行循环中处理任务,只能看到交给它的提示词, 无法访问父 Agent 的对话历史。完成后,结果会作为工具结果返回。
summarizer = Agent(
name="summarizer",
instructions="用五个要点总结文本。",
model="<model>",
)
manager = Agent(
name="manager",
instructions="需要总结时,把任务委派出去。",
model="<model>",
tools=[summarizer.as_tool(description="总结一段文本。")],
)
agent.as_tool(*, name=None, description=None, max_turns=50, budget=None,
retry=None, context_policy=None):
- 工具默认名为
ask_<slug>,只接受一个由模型填写的参数input,也就是交给子 Agent 的提示词。max_turns等执行策略由应用代码固定,不会暴露给模型。子 Agent 有自己的 运行循环,因此尤其建议明确限制max_turns。 budget每次调用都会复制一份,所以限制作用于每个子运行,而不是跨调用累计。- 子运行会继承父运行的
context(deps)、cancel_token(一次取消停止整棵树)和 Tracer(子 Span 会加入同一条 Trace);Token 用量会计入父运行的usage。 - 子运行拥有独立的 Mailbox,不会与父运行共用。Mailbox 中的消息一经读取就会被消费, 而且每条注入消息只应属于一段对话。
- 子运行耗尽预算时,父 Agent 会收到一个工具错误结果并决定如何处理。这属于可恢复的 委派失败,不会直接结束父运行,详见错误语义。
后台子 Agent¶
Agent-as-tool 会让父 Agent 等待;Subagents 则把任务放到后台。
spawn_subagent 启动任务后立即返回任务 ID,子 Agent 在同一事件循环中并发执行,
父 Agent 可以继续工作。任务完成后,报告会在轮次边界作为用户侧消息进入对话,
不会变成属于较早一轮的工具结果。
from lovia import Agent, Runner, Subagents
researcher = Agent(name="researcher", instructions="调研并输出报告。",
model="<model>", tools=[...])
agent = Agent(
name="assistant",
model="<model>",
plugins=[Subagents([researcher])], # 或用 Subagents() 克隆当前 Agent
# (不包含 plugins 和 handoffs)
)
result = await Runner.run(agent, "调研 X 和 Y,然后对比总结。")
父 Agent 会获得三个工具;系统提示还会在每轮注明后台任务的当前状态:
spawn_subagent(prompt, agent=...):用一段无需依赖父对话的完整提示词启动子 Agent; 子 Agent 看不到父对话。正在运行的任务达到max_concurrent后,新任务会被拒绝。wait_subagents(ids=None, timeout_seconds=60):等待指定任务完成或超时,并收取 已完成任务的报告。如果报告已经自动进入消息队列、但还没有被模型读取,它会从 队列中撤回并直接返回,因此同一份报告不会出现两次。cancel_subagent(id):请求子 Agent 停止;被取消的任务不会产生报告。
Subagents(agents=(), deliver=None, max_concurrent=4, max_turns=50,
budget=None, max_result_chars=16_000, instructions=None):
- 子运行继承父运行的
context(deps)和 Tracer,Token 用量也计入父运行的usage,这一点与as_tool相同。每个子 Agent 使用独立的取消令牌;budget则在每次启动任务时复制一份。 deliver决定任务是否能脱离当前 Run。 默认不传deliver:报告进入 当前 Run 的消息队列;Run 结束时,尚未完成的任务会被取消。内置提示会要求模型 在结束前等待这些任务,或明确取消它们。传入deliver回调后,报告全部交给 回调处理,子 Agent 也可以在父 Run 结束后继续运行。这种方式主要供 Web 服务等 托管场景使用,详见 Web UI。- 默认模式下,子 Agent 无人值守运行。没有人处理的工具审批会被自动拒绝, 因此应只给它配置无需人工审批的工具。在 Web UI 中,后台任务是可查看的独立会话, 可以直接处理其中的审批。
Subagents()未指定agents时,会以当前 Agent 为模板,但移除plugins和handoffs。这样既不会递归派生,也不会共享插件状态;模型、指令、工具和 Workspace 保持不变。run_child是面向服务层的高级扩展点,用来接管子 Agent 的执行过程。它接收ChildSpec并返回子运行的RunResult;Web 层借此把任务交给自己的运行管理器。 应将它与deliver配套设置。若只替换run_child,当前 Run 结束时,Subagents无法取消由外部管理的任务。
如何选择协作方式¶
| 方式 | 谁继续回答用户 | 目标 Agent 看到什么 | 父 Agent 是否等待 | 结果如何返回 |
|---|---|---|---|---|
| Handoff | 接手的专家 Agent | 完整对话历史 | 不适用,控制权已移交 | 专家在同一次 Run 中继续回答 |
| Agent-as-tool | 父 Agent | 本次委派的提示词 | 等待 | 作为工具结果返回 |
| Subagents | 父 Agent | 本次委派的提示词 | 默认不等待,也可主动等待 | 稍后作为消息送达,或由等待工具直接返回 |
需要专家接管后续对话,选择 Handoff;父 Agent 必须先拿到子任务结果,选择 Agent-as-tool;任务可以独立在后台完成,选择 Subagents。
链式调用、路由、并行处理、编排者—执行者和评估循环等更复杂的流程,都不需要额外的
框架抽象:在 Runner.run 外层用普通 Python 组合即可。
examples/workflows/ 目录用一页代码实现了 Anthropic
Building effective agents 里的每个模式。
注意事项¶
- 为 Handoff 目标写清
description。 如果两个专家都使用默认说明,路由 Agent 几乎无法区分它们。遇到错误路由时,应先检查提示词和说明,再排查框架行为。 - 运行级
output_type覆盖跟随 Run,而不是 Agent。Runner.run(..., output_type=...)指定的输出类型会应用到 Handoff 到达的每个 Agent。 未指定时,各 Agent 使用自己的output_type;如果分流 Agent 和专家的类型不同, 输出约束会在移交后发生变化。 - 非 ASCII 的 Agent 名称会生成带摘要的工具名。
transfer_to_agent_a1b2c3d4可以正常路由,但日志不易阅读。需要可读名称时, 请设置Handoff(name=...)或as_tool(name=...)。 - Agent-as-tool 嵌套越深,成本越高。 用量会逐层向上汇总,因此
result.usage表示整棵调用树的总用量;根 Run 的预算应按这个总量设置。
延伸阅读¶
- 工具:三种机制如何通过普通工具工作
- Session 与 Checkpoint:Handoff 后恢复运行
- 示例:
07_handoff.py,08_agent_as_tool.py,30_subagents.py,workflows/