
多 Agent 交接跑完以后,日志里常出现一个尴尬问题。入口叫分诊,终稿却是计费专家写的,告警和计费却挂在分诊头上。RunResult 上有一个专门字段处理这件事。本机 openai-agents 0.23.1 里,属性名就是 last_agent,类型是 Agent。它指向本轮跑完时最后一个仍被框架握着的 Agent。标题里说的责任归属,指的就是用这个字段钉终稿作者,而不是用入口名或展示名猜。
先说清语义边界。last_agent 回答的是「最后是谁在应答」。它不自动等于「业务上谁该负责」。交接回环时,专家处理完又交回分诊,last_agent 可能变回分诊,而账单内容仍是专家产出的。这种时候要告警,不能悄悄记账。本文离线 smoke 用假 Agent 与假 Run 模拟路由,不连模型,路径在 _w/last-agent-smoke/。

图注 左侧为归属字段应守住的口径,右侧为种植的四类误挂。
目标说明
- 用
result.last_agent读取最后应答者,并记录其稳定 id。 - 区分入口 Agent、中间专家、回环后的最后应答者。
- 在 handoff 成功但专家未产出时拒绝写入审计。
- 禁止跨租户、跨 run 把多个
last_agent塞进同一行审计。
字段怎么读
python
# Runner.run 返回 RunResult
result = await Runner.run(triage_agent, input)
author = result.last_agent
# 生产审计请优先用你自己挂在 Agent 上的稳定 id
# 展示名可变,id 不要变
audit = {
"author_id": getattr(author, "agent_id", author.name),
"author_name": author.name,
"entry_id": triage_agent.name,
"final_preview": str(result.final_output)[:80],
}
本机源码里,last_agent 是属性,内部先看 _last_agent,再回退到弱引用 _last_agent_ref。调用 release_agents 一类释放逻辑后,强引用可能被清掉,只留弱引用。长时间握着 RunResult 做跨请求缓存时,要注意作者对象是否还活着。审计应在 run 刚结束时把 id 与名称拷贝出去,不要把 Agent 实例本身塞进队列。
final_output 只说明终稿内容,不说明作者。入口 Agent 的 name 只说明这场对话从谁起头。三件事分开记,事后才查得清。
离线 smoke 结果
text
P0 PASS last_agent 钉终稿作者 -> author=agt_billing entry=agt_triage
P1 PASS 回环后 last 与业务期望不一致应告警 -> last=agt_triage alert=True
P2 PASS handoff 成功但专家未产出应拒
P3 PASS 权限判断用稳定 agent_id
F1 FAIL 种植:把入口 Agent 当终稿作者 -> author=agt_triage
F2 FAIL 种植:回环后归属漂移却不告警 -> silent_author=agt_triage
F3 FAIL 种植:handoff 工具成功当成专家已交付
F4 FAIL 种植:跨 run 把多 last_agent 串进同行
F5 FAIL 种植:只用展示名做权限判断
路由样例在 route_log.json,失败表在 last_agent_fail.csv。
四类失败样本拆解
F1 入口当作者。 分诊把会话交给计费专家,专家写出账单摘要。反例审计仍写 author=agt_triage。值班的人按分诊的错误预算告警,计费专家的提示词问题永远排不到优先级。好路径写 author=agt_billing,同时保留 entry_id 便于还原路径。
F2 回环后漂移不告警。 路径是分诊到计费再回到分诊,业务期望「账单仍由计费负责」,但 last_agent 已是分诊。好路径发现路由里出现过计费、最后却不是计费时打告警。反例直接把分诊记成作者,计费侧的 SLA 统计被稀释。
F3 handoff 成功当成已交付。 工具调用显示交接成功,专家侧却没有写出任何终稿。好路径在写入审计前检查 expert_produced,否则抛错。反例只看 handoff 布尔值,监控显示交接成功率百分之百,用户侧拿到空回复。
F4 跨 run 串租户。 两个租户各跑一次,审计模块图省事,把两次的 last_agent 和终稿预览塞进同一行,租户字段只留了第一个。结果是 acme 的审计行里出现了 zen 的账单预览。多租户系统里,这是合规事件,不是小瑕疵。
另外种了 F5。两个 Agent 展示名都叫计费专家,稳定 id 不同。反例只用展示名做权限判断,权限表一次放行,两个租户的专家能力被打通。好路径权限键用 agent_id。
生产禁区表
| 编号 | 禁区 | smoke 证据 | 替代做法 |
|---|---|---|---|
| D1 | 用入口 Agent 当终稿作者 | F1 | 写 last_agent 的稳定 id |
| D2 | 回环后归属变化不告警 | F2 | 对比业务期望作者与 last,不一致告警 |
| D3 | handoff 成功即视为专家交付 | F3 | 检查终稿非空或专家侧产出标记 |
| D4 | 多 run 归属写入同一审计行 | F4 | 一行一 run,租户键必填 |
| D5 | 用展示名做权限判断 | F5 | 权限与计费键使用稳定 id |
和 handoff 日志怎么配合
只记 last_agent 不够还原现场。smoke 里的好路径同时写下整条 route,用稳定 id 以箭头连接。线上可以把每次 handoff 事件写成独立日志,run 结束时再落一行汇总。汇总行里有入口、最后应答、路由、租户、终稿哈希。出了问题先看汇总,再按路由打开明细。
计费归属同样跟 last_agent 走。若产品规定「分诊免费、专家按次计费」,你却按入口计费,账单会长期偏少。反过来,把回环后的分诊应答也算成专家计费,账单会偏多。这两种偏差都来自同一件事,作者字段取错了。
预发检查
- 单测里构造 triage 到 billing 的假 run,断言审计作者 id 等于 billing。
- 构造回环路径,断言产生告警事件。
- handoff 成功且终稿为空时,写入审计应失败。
- 两个租户并行 run,审计表按租户查询互不泄漏。
- 权限中间件只认稳定 id,展示名变更不改权限。
和 new_items、final_output 的分工
new_items 适合对账「这轮新增了哪些消息与工具调用」。final_output 适合给用户看的内容。last_agent 适合问责与计费。三个字段一起看,才能回答「谁在什么租户下,用哪些工具,交出了什么终稿」。只盯终稿文本,会把作者问题推到人工翻日志。
本机 0.23.1 的属性名就是 last_agent。若你读到的旧文章写的是别的名字,以当前包为准。升级后复跑 _w/last-agent-smoke/smoke.py,确认属性仍在、弱引用语义未变。
告警怎么设才不吵
回环并不总是错误。分诊收回会话、向用户追问卡号,是合法路径。告警应绑在业务期望作者上,不要只因为路径里出现过回环就响。
做法是给每类任务挂一个期望作者 id。账单类期望 agt_billing,退款类期望 agt_refund。run 结束时比较 last_agent 与期望。一致则安静。不一致则告警,并附上完整路由。这样分诊追问不会天天响,计费专家被绕过才会响。
展示名可以给运营看,权限、计费、告警订阅都只认稳定 id。改名是产品的事,改 id 是事故。
多交接时的最小审计行
我建议最少包含这些列。租户、run id、入口 id、最后应答 id、路由、handoff 是否成功、专家是否产出、终稿哈希、时间戳。缺租户就会回到 F4。缺专家产出标记就会回到 F3。缺路由就无法解释 F2。
这些列可以落在一张窄表里,大字段(完整消息)另存。值班先扫窄表,再按需打开明细。比一上来把整个 RunResult 序列化进日志便宜,也更安全。
和 Session、trace 的衔接
若你同时自定义了 Session,审计里的租户键应与 Session 存储键里的租户段一致。两边各写各的,出现串租时会对不上。trace 导出若带 Agent 名,也请附带稳定 id,避免展示名撞车。
长时间运行里调用了 release_agents 之后,再读 last_agent 可能只靠弱引用。若弱引用已失效,属性会按源码路径处理缺失情况。稳妥做法是在 run 回调的第一时间把作者 id 拷贝到你自己的上下文对象里,后续只读拷贝。
若你们用展示名做过一版权限表,迁移时可以保留展示名列作备注,权限判断改读 id。迁移窗口里双写两列,旧逻辑读名,新逻辑读 id,对比拒绝率。拒绝率明显上升时,先查是不是有同名 Agent 被误伤,再决定是否回滚。
把这些规则写进你们的 run 结束钩子,比事后翻聊天记录便宜。钩子里只做三件事。拷贝 last_agent 的稳定 id,检查专家产出标记,按租户写入一行审计。其余分析放离线任务。钩子越胖,越容易在失败路径上再失败。
计费与权限的两张表
计费表按 last_agent 的稳定 id 汇总次数与token。权限表按同一 id 决定谁能读哪类工具结果。两张表的主键必须一致。曾经有团队计费用展示名、权限用 id,改名周出现一批「有权限却计不到费」的工单,查了两天才发现是主键不一致。
预发环境用两个展示名相同、id 不同的 Agent 各跑一次,断言计费行与权限行都拆成两条。这一条单测能挡住 F5 类问题反复出现。
回环告警的阈值可以按任务类型配置。纯问答允许分诊收回。涉及金钱、权限、删除的任务,期望作者一旦离开专家就告警,即使最终话术由分诊发出。配置写在代码里,和 Agent 定义放一起,避免散落在wiki。
线上若出现「作者 id 为空」,优先怀疑 release_agents 之后才读属性。把拷贝动作前移到 Runner 回调或你自己的 run 包装器返回前。空作者直接判审计失败,比写一个叫 unknown 的兜底更安全,unknown 会污染计费聚合。
预发检查五条都通过后,再打开真实 handoff。假路由表挡不住提示词级的误交,但能挡住审计与计费的系统性误挂。先把记账做对,再优化交接策略。
做完这些,再谈交接策略优化不迟。归属字段错了,策略再漂亮也记到错误的人头上。
小结
多交接场景里,入口名不能当终稿作者。last_agent 给出最后应答者,业务期望与字段不一致时要告警。handoff 成功不等于专家已交付。跨 run、跨租户的归属必须分行。把稳定 id 写进审计和权限,展示名只作给人看的标签。