
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 种植的四类假成功。
目标说明
读完你应能做到四件事。
- 正确把自定义回调挂到
tool_use_behavior,并返回ToolsToFinalOutputResult。 - 在失败、半截、重复、敏感四类输入上拒绝截取。
- 分清自定义回调与
stop_on_first_tool、stop_at_tool_names的停机语义。 - 把终稿脱敏做成写前硬步骤,而不是事后补日志。
本机 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 档位混配靠记忆 | (配置对照) | 单一停机权威,注释与代码同改 |
预发检查
tool_use_behavior的类型是 callable,仓库里能搜到返回ToolsToFinalOutputResult的函数。- 准备失败、半截、重复、敏感四条假工具结果,断言前三条不截,第四条截完无密钥形态。
- 文档与注释不要再写与当前档位矛盾的再跑预期。
- 终稿审计字段与工具原始日志分表,敏感原始值默认不进终稿表。
- 升级 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 与截取回调是两道闸。只开一道,假成功会从另一道溜走。
双闸的代价是多一次校验。代价换来的是终稿表字段稳定。财务、工单、合规三类下游最在乎稳定,不在乎你省掉的那几毫秒。
观察指标
至少拆四条序列。
- 回调决定截取的次数。
- 回调拒绝截取的次数,按原因码分组(失败、半截、重复、敏感)。
- 截取后下游校验失败的次数。
- 与
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




