AIWritePaper官方账号头像
OpenAI Agents SDK 工程笔记:tools_to_final_output_function 自定义终稿截取、错误吞没与生产禁区封面图

OpenAI Agents SDK 工程笔记:tools_to_final_output_function 自定义终稿截取、错误吞没与生产禁区

写作辅助工具

tool_use_behavior 总览里,字符串档位大家都见过。线上最容易翻车的点,是把一段自定义函数挂上去,想在某次工具结果出来时直接钉成终稿。标题里的 tools_to_final_output_function,在 openai-agents 0.23.1 里并不是单独 export 的工厂函数名。本机核对后,它是类型别名 ToolsToFinalOutputFunction。你把可调用对象赋给 Agent.tool_use_behavior,SDK 就按这条路径走。回调必须返回 ToolsToFinalOutputResult,字段只有两个,is_final_output 与 final_output。

is_final_output=True 时,本轮不再让模型看工具输出,直接把 final_output 当终稿。False 时,模型会再跑一轮,收到工具结果。听起来干净,线上翻车却几乎都发生在四个缝里。工具已经失败,回调仍返回成功终稿。半截 JSON 被当成业务结果。同一副作用工具被二次截取。密钥形态原样写进终稿审计。再和 stop_on_first_tool、StopAtTools 混配,注释写着还会再跑,运行却可能已经停机。

离线 smoke 在 _w/final-output-fn-smoke/,不连模型接口。好路径 P0 到 P4,反例种植 F1 到 F4。

正文图

图注 左侧为回调必须守住的契约,右侧为 smoke 种植的四类假成功。

目标说明

读完你应能做到四件事。

  1. 正确把自定义回调挂到 tool_use_behavior,并返回 ToolsToFinalOutputResult。
  2. 在失败、半截、重复、敏感四类输入上拒绝截取。
  3. 分清自定义回调与 stop_on_first_tool、stop_at_tool_names 的停机语义。
  4. 把终稿脱敏做成写前硬步骤,而不是事后补日志。

本机 API 披露

本机装的是 openai-agents 0.23.1。Agent.tool_use_behavior 接受四种形态。

  • 字符串 "run_llm_again"
  • 字符串 "stop_on_first_tool"
  • 字典型 StopAtTools,键为 stop_at_tool_names
  • 可调用对象,类型别名 ToolsToFinalOutputFunction

自定义截取走第四种。回调签名在类型上写成接收运行上下文与工具结果列表,返回 ToolsToFinalOutputResult。结果数据类注释写得很清楚。is_final_output 为假时,final_output 可以为 None。为真时,final_output 必须匹配 Agent 的 output_type。生产里若 Agent 声明了结构化输出,回调却返回自由文本,下一层还会再炸一次。标题沿用社区常见叫法 tools_to_final_output_function,审计日志请写真实挂载点 tool_use_behavior=callable,以免后人按函数名去 from agents import 找不到。

from agents import Agent, ToolsToFinalOutputResult

def to_final(ctx, tool_results):
    last = tool_results[-1]
    if getattr(last, "ok", True) is False:
        return ToolsToFinalOutputResult(is_final_output=False, final_output=None)
    # 校验完整 JSON、脱敏、幂等键……
    return ToolsToFinalOutputResult(is_final_output=True, final_output=cleaned)

agent = Agent(
    name="billing",
    instructions="...",
    tools=[lookup_order],
    tool_use_behavior=to_final,
)

stop_on_first_tool 是字符串快捷方式。第一次工具输出直接当终稿,没有自定义校验面。StopAtTools 只在名单里的工具名出现时停。自定义回调则可以按内容决定停不停。三者语义不同,不要在同一 Agent 上先挂回调、再在别处用字符串覆盖,最后靠记忆判断「这轮还会不会让模型看一眼」。

离线 smoke 结果

P0  PASS 合法工具结果可截成终稿
P1  PASS 同结果二次截取应拒绝(幂等)
P2  PASS 工具失败不截成终稿
P3  PASS 半截 JSON 不截成终稿
P4  PASS 终稿脱敏(去掉 Bearer 形态)
F1  FAIL 种植:工具失败被截成成功终稿
F2  FAIL 种植:半截 JSON 入库
F3  FAIL 种植:无幂等二次截取
F4  FAIL 种植:终稿含 Authorization 形态

对照文件在 before_after.json 与 final_output_fn_fail.csv。反例函数故意写短,和好路径并排放,方便 code review。

四类失败样本拆解

F1 工具失败被截成成功终稿。 工具返回 ERROR timeout,ok=False。好路径看失败标志,返回 is_final_output=False,让模型或上层重试策略接手。反例把 output 原样钉成终稿。监控里工具成功率与业务成功率脱钩,值班的人先去翻订单表,半小时后才发现是超时被当成已下单。

F2 半截 JSON 入库。 串停在 "amount_cents": 后面。好路径 json.loads 失败就拒绝截取。反例截前二十个字符当业务结果。下游解析再失败一次,或者更糟,用正则抠出半个金额写进库。账单对账时会出现「有订单号、金额为空」的幽灵行。

F3 无幂等二次截取。 同一次查找结果被回调处理两次。好路径用 (tool_name, output前缀) 做 seen 集,第二次返回 is_final_output=False。反例每次都截。若工具带扣款、发券一类副作用,二次截取等于默认允许双花。即便工具本身幂等,审计日志里也会出现两条终稿,对账脚本按条数统计就会虚高。

F4 终稿含 Authorization 形态。 工具为了调试把 Bearer sk-... 写进 JSON。好路径在截取前做脱敏替换。反例原样入库。终稿一旦进会话存储或导出包,密钥形态就离开了密钥保管边界。这和「工具内部日志是否该打密钥」是两件事。终稿面向更广的读者,默认按不可信数据处理。

和 stop 档位怎么混配

常见误配有三种。

第一种,Agent 上写了自定义回调,Runner 侧又按 stop_on_first_tool 的文档写注释,新人按注释以为回调只是润色,停不停仍由字符串档位决定。实际上挂了 callable,就走 callable。注释与代码不一致时,事故复盘会先吵半小时。

第二种,StopAtTools 名单里有 lookup,回调里却对 lookup 返回 is_final_output=False,指望模型再看一眼。名单触发的停机发生在工具层语义上,和回调不是同一条故事。生产配置应只保留一种停机权威,并在 smoke 里用假工具结果打穿。

第三种,回调里抛未捕获异常。SDK 边界如何包装取决于版本与 Runner 路径。更稳妥的做法是回调内把可预期失败收成 is_final_output=False,把不可预期失败上抛成领域错误,并打上工具名与结果哈希。不要 except Exception: return ToolsToFinalOutputResult(True, "{}")。

生产禁区表

编号禁区smoke 证据替代做法
D1工具失败截成成功终稿F1失败标志为假时返回 is_final_output=False
D2半截 JSON 当业务结果F2先完整解析,再决定截取
D3无幂等二次截取F3同 call 键进 seen 集
D4密钥形态进终稿F4写前脱敏,审计另存指纹
D5与 stop 档位混配靠记忆(配置对照)单一停机权威,注释与代码同改

预发检查

  1. tool_use_behavior 的类型是 callable,仓库里能搜到返回 ToolsToFinalOutputResult 的函数。
  2. 准备失败、半截、重复、敏感四条假工具结果,断言前三条不截,第四条截完无密钥形态。
  3. 文档与注释不要再写与当前档位矛盾的再跑预期。
  4. 终稿审计字段与工具原始日志分表,敏感原始值默认不进终稿表。
  5. 升级 SDK 后先复跑 _w/final-output-fn-smoke/smoke.py。

接入顺序

新项目建议先写回调契约测试,再挂真实工具。第一步,定义何种工具结果允许成为终稿的纯函数,不碰网络。第二步,把 F1 到 F4 做成单测。第三步,才接到 Agent.tool_use_behavior。第四步,在预发用真实工具打一轮,对比终稿表与工具原始表的条数是否一一对应。条数对不上,优先查幂等键,而不是先怪模型。

若历史代码已经用 stop_on_first_tool,迁移到自定义回调时不要同一天切全量。可以先在影子流量里跑回调,只记账不截取,对比若截取会生成什么终稿。差异稳定后再打开真正的 is_final_output=True 路径。

上线第一周,把回调拒绝截取的次数按工具名分组。拒绝突然升高,常见原因是工具契约变了,或上游开始返回调试字段。此时应修工具或加脱敏规则,而不是把回调改宽。回调改宽是把生产边界往后推,边界一松,F1 到 F4 会成批回来。

多 Agent 协作时,终稿截取应发生在对用户可见的那一层。中间 Agent 的工具结果更适合当 handoff 输入,而不是提前钉死。过早截取会让后面的专责 Agent 看不到工具原文,排障只能猜。

回调里不要做的三件事

第一,不要在回调里再发一次网络请求去补全半截结果。截取回调的职责是判定与整形,不是补偿。补偿应发生在工具层或显式的重试策略里。回调里藏一次 HTTP,超时叠加超时,排障时栈会短得吓人。

第二,不要把模型提示词拼进 final_output。终稿一旦混入请用户稍等一类提示腔,下游结构化校验会失败,客服侧也会把提示词片段读给用户听。提示词留在 Agent instructions,终稿只留业务对象或业务句子。

第三,不要按工具返回的自然语言情绪词决定截取。例如看到成功二字就截,看到失败二字就放行。情绪词不稳定,工具契约字段才稳定。以 ok、错误码、JSON schema 为准。

这三条看起来像风格问题,线上却对得上真实事故。回调越胖,越像第二个 Agent,权限与超时却仍按纯函数来估,偏差会很大。

和 output_type 一起用时

若 Agent 声明了 output_type,回调的 final_output 必须能被该类型接受。更稳的做法是回调返回已经是模型实例或合法 dict,而不是再丢一段散文。声明期 schema 与截取回调是两道闸。只开一道,假成功会从另一道溜走。

双闸的代价是多一次校验。代价换来的是终稿表字段稳定。财务、工单、合规三类下游最在乎稳定,不在乎你省掉的那几毫秒。

观察指标

至少拆四条序列。

  1. 回调决定截取的次数。
  2. 回调拒绝截取的次数,按原因码分组(失败、半截、重复、敏感)。
  3. 截取后下游校验失败的次数。
  4. 与 stop_on_first_tool 配置漂移相关的告警(例如注释声明与实际类型不一致)。

第二条升高时先看工具契约。第三条升高时先看 output_type 是否变更。不要一上来改宽回调。

审计字段建议

终稿表至少留这些列。trace 或请求 id,工具名,结果哈希,是否截取,拒绝原因码,脱敏后的终稿指纹,回调版本号。不要把原始工具输出整段塞进终稿表。原始输出进受限的工具日志表,权限更窄,保留期可以不同。

原因码建议枚举。tool_failed、invalid_json、duplicate、sensitive、schema_mismatch。报表按原因码看拒绝结构,比只看拒绝率更有行动指导。某一周 sensitive 突然升高,多半是上游开始把调试头写进正文,应回头修工具,而不是在回调里加白名单。

小结

自定义终稿截取的价值,在于让这一刀停不停变成可测的纯逻辑。标题里的函数名是习惯叫法,本机 0.23.1 的真实挂载点是 tool_use_behavior 上的 callable,返回 ToolsToFinalOutputResult。假成功几乎都来自失败当成功、半截入库、无幂等、密钥进终稿。把这四面守住,再谈和 stop 档位的分工。升级后请先复跑本目录 smoke。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/q155281844/article/details/169164693

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--