OpenAI Agents SDK 工程笔记:RunResult.last_agent 多交接后责任归属、终稿误挂与生产禁区

多 Agent 交接跑完以后,日志里常出现一个尴尬问题。入口叫分诊,终稿却是计费专家写的,告警和计费却挂在分诊头上。RunResult 上有一个专门字段处理这件事。本机 openai-agents 0.23.1 里,属性名就是 last_agent,类型是 Agent。它指向本轮跑完时最后一个仍被框架握着的 Agent。标题里说的责任归属,指的就是用这个字段钉终稿作者,而不是用入口名或展示名猜。

先说清语义边界。last_agent 回答的是「最后是谁在应答」。它不自动等于「业务上谁该负责」。交接回环时,专家处理完又交回分诊,last_agent 可能变回分诊,而账单内容仍是专家产出的。这种时候要告警,不能悄悄记账。本文离线 smoke 用假 Agent 与假 Run 模拟路由,不连模型,路径在 _w/last-agent-smoke/。

图注 左侧为归属字段应守住的口径,右侧为种植的四类误挂。

目标说明

  1. 用 result.last_agent 读取最后应答者,并记录其稳定 id。
  2. 区分入口 Agent、中间专家、回环后的最后应答者。
  3. 在 handoff 成功但专家未产出时拒绝写入审计。
  4. 禁止跨租户、跨 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 走。若产品规定「分诊免费、专家按次计费」,你却按入口计费,账单会长期偏少。反过来,把回环后的分诊应答也算成专家计费,账单会偏多。这两种偏差都来自同一件事,作者字段取错了。

预发检查

  1. 单测里构造 triage 到 billing 的假 run,断言审计作者 id 等于 billing。
  2. 构造回环路径,断言产生告警事件。
  3. handoff 成功且终稿为空时,写入审计应失败。
  4. 两个租户并行 run,审计表按租户查询互不泄漏。
  5. 权限中间件只认稳定 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 写进审计和权限,展示名只作给人看的标签。

相关推荐
AIGC大时代16 小时前
OpenAI Agents SDK 工程笔记:AgentOutputSchema 结构化输出声明、strict 校验失败与生产禁区
结构化输出·agents sdk·生产禁区·strict
AIGC大时代3 天前
OpenAI Agents SDK 工程笔记:TraceProcessor 导出、敏感字段脱敏与生产禁区
脱敏·tracing·agents sdk·生产禁区·traceprocessor
AIGC大时代4 天前
OpenAI Agents SDK 工程笔记:自定义 Session 接口(get_items / add_items / clear)与多租户隔离的生产禁区
session·多租户·agents sdk·生产禁区·get_items
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:SQLiteSession 多轮会话持久化与生产禁区
agents sdk·生产禁区·sqlitesession·会话持久化
AIGC大时代14 天前
OpenAI Agents SDK 工程笔记:AgentsException 族、失败分类与生产恢复禁区
agents sdk·agentsexception·maxturns·behaviorerror
AIGC大时代14 天前
OpenAI Agents SDK 工程笔记:WebSearchTool 与 FileSearchTool 托管工具边界与生产禁区
agents sdk·websearchtool·filesearchtool·托管检索
AIGC大时代16 天前
OpenAI Agents SDK 工程笔记:tool_use_behavior 与 is_enabled 动态工具开关生产禁区
agents sdk·生产禁区·is_enabled
AIGC大时代16 天前
OpenAI Agents SDK 工程笔记:LocalShellTool 与 ApplyPatchTool 执行面边界与生产禁区
agents sdk·生产禁区·localshelltool·applypatchtool
AIGC大时代17 天前
OpenAI Agents SDK 工程笔记:ModelProvider 多模型接入、Responses/Chat Completions 切换与生产禁区
responses·生产禁区·modelprovider·multiprovider