千笔-AIWritePaper · https://www.aiwritepaper.com
把专科 agent 做成工具(agent.as_tool)时,最容易踩的坑不是「调用语法」,而是控制权与状态边界:manager 仍掌握回合,但嵌套 run不会自动继承父对话状态;若把session乱共享、把needs_approval绕开、或把嵌套final_output当成已审计真相,账单与合规会一起炸。官方 Tools · Agents as tools 写清:相对 handoff,as_tool 让中心 agent 编排专科网络而不交出控制;as_tool支持max_turns/run_config/session/needs_approval/parameters/custom_output_extractor/is_enabled/on_stream等。本文按工程笔记钉死对照表、可跑 smoke 与生产禁区。示例模型名写作gpt-4o,以你账号可用快照与官方文档为准。
图:上方 as_tool vs handoff;中部嵌套选项与状态不继承;下方生产禁区与 fail smoke。
目标说明
读完你应能独立完成五件事:
- 用一句话说清:as_tool 时manager 保持控制;handoff 会交出对话控制权。
- 列出
as_tool常用选项:max_turns、run_config、session、needs_approval、parameters、custom_output_extractor、is_enabled、on_stream。 - 写出可跑片段:orchestrator + specialist.
as_tool,并说明嵌套 run不继承父 conversation state。 - 钉死生产禁区:静默嵌套花费、错误共享 session、审批旁路、把嵌套 final_output 当审计真理。
- 留下 fail smoke 笔记:无 key 至少能构造工具;有 key 时验证嵌套输出与审批中断路径可观测。
规格钉死(对照官方 Agents as tools):
- 控制权:as_tool = 中心 agent 编排;handoff = 移交控制。
- 状态:嵌套 agent 的 state 选项配置该次嵌套 run;父 run 会话状态不自动继承。
- 共享历史:要共享 client-managed 历史,须显式把同一
session传给父与嵌套;并与previous_response_id/conversation_id三选一策略,勿混用。 - 审批:
needs_approval与 function_tool 同流;中断进result.interruptions,经to_state()+approve/reject再 resume。 - 输出:默认把嵌套 final 回给 manager;可用
custom_output_extractor抽取/校验。 - 条件启用:
is_enabled可为 bool 或(ctx, agent)->bool;禁用则对 LLM 完全隐藏。
适用边界
适合用 as_tool
- 需要中心编排多个专科(翻译、检索、计费问答),且回合后仍要由 manager 汇总。
- 希望专科失败时 manager 仍可换工具或降级,而不是整段对话被 handoff 带走。
- 需要在专科入口挂
needs_approval或结构化parameters。
更适合 handoff(或单独 Runner.run)
- 专科必须接管后续多轮用户对话(客服转专家坐席)。
- 你其实要「换一个 agent 成为当前对话主体」,而不是「调一次工具拿结果」。
不该指望 as_tool 单独搞定
- 嵌套自动继承父消息历史:默认不继承;要共享须显式 session/continuation。
- 嵌套 final_output = 已审计业务事实:只是模型产物,除非你另有核验与审批。
- is_enabled=False 等于鉴权:它管可见性/调度,不替代工具内授权与资源级检查。
- on_stream 开了 = 已记账清晰:流式事件便于观察,不自动产生成本审计表。
风险提示
嵌套 run 会单独消耗 turns 与 tokens;manager 若在循环里反复调贵专科,成本呈乘法。把同一可变 session 在无隔离场景乱传,可能串话。审批工具若在测试里永远approve,等于没测拒绝路径。把 extractor 写成「缺失则编造默认 JSON」会把空结果洗成成功。
步骤与机制
1. as_tool vs handoff 对照
| 维度 | as_tool | handoff |
|---|---|---|
| 控制权 | manager 保持 | 交给目标 agent |
| 典型用途 | 编排专科、汇总答案 | 转交会话主体 |
| 状态继承 | 嵌套不自动继承父会话 | 按 handoff/会话策略走 |
| 审批挂载 | needs_approval可挂工具入口 | 另见 HITL/中断模式 |
| 失败后 | manager 可继续选工具 | 对话已在专科侧 |
2. as_tool 选项速查
| 选项 | 做什么 | 不做什么 |
|---|---|---|
tool_name/tool_description | 暴露给模型的工具面 | 不改专科内部 instructions |
max_turns | 限制嵌套回合 | 不限制父 run |
run_config | 嵌套 RunConfig | 不自动合并父 config 全部字段(以官方为准实测) |
session | 嵌套 client-managed 历史 | 不隐式等于父 session |
needs_approval | 调用前可暂停 | 不替代工具内鉴权 |
parameters | 结构化入参 schema | 默认仍是{"input": str} |
custom_output_extractor | 改写回传内容 | 不自动校验业务真伪 |
is_enabled | 运行时显隐 | 不是授权系统 |
on_stream | 监听嵌套流事件 | 不代替成本账本 |
3. 可跑 smoke:orchestrator + specialist.as_tool
先pip install openai-agents,并导出OPENAI_API_KEY(无 key 时至少应能 import 与构造as_tool)。
importasynciofromagentsimportAgent,Runner# 专科:短指令,便于观察嵌套输出specialist=Agent(name="CitationCheck",instructions=("你只做一件事:根据输入指出引用核验要点。""不要编造 DOI;不确定就写「需人工打开核验」。"),model="gpt-4o",# 占位:以账号可用快照为准)orchestrator=Agent(name="PaperOrchestrator",instructions=("你是论文助手编排者。需要引用核验时必须调用 citation_check 工具;""工具失败或不确定时,向用户披露,禁止把猜测写成已核验。"),model="gpt-4o",tools=[specialist.as_tool(tool_name="citation_check",tool_description="对给定引用片段做核验要点检查",max_turns=4,# 嵌套默认不继承父会话;此处故意不传 session,便于对照)],)asyncdefmain()->None:result=awaitRunner.run(orchestrator,"请核验:Smith 2021 JournalX 卷3 页12-20,DOI 未提供。",)print("final:",result.final_output)# 观察嵌套痕迹:new_items / raw_responses(以 SDK 版本为准)print("items:",len(getattr(result,"new_items",[])or[]))if__name__=="__main__":asyncio.run(main())验收:进程可构造工具;有 key 时最终输出应承认「需人工打开」类不确定性,而不是伪造「DOI 已验证通过」。把一次成功日志路径写入_w/as-tool-smoke-checklist.md。
4. Fail smoke:误共享 session / 审批旁路笔记
Fail A — 误以为嵌套继承父历史
# 反例说明(不要当最佳实践):# 父 run 已有多轮上下文,但 as_tool 未传 session / continuation。# 期望:专科「记得」刚才用户纠正过的主张编号。# 实际:嵌套从自己的输入起步,可能丢掉纠正 → 输出漂移。# 记 fail:nested_state_inherit=failFail B — 审批永远自动通过
# 若 as_tool(..., needs_approval=True) 却在测试夹具里无条件 approve,# 则从未验证 reject / 超时未审批路径。# 门禁:至少 1 次 reject 样本落盘,否则 approval_path=failFail C — extractor 把空结果洗成成功
asyncdefunsafe_extract(run_result):# 反例:缺失时返回伪造 JSONreturn'{"status":"ok","verified":true}'# 正确方向:缺失则返回明确失败标记,让 manager 降级asyncdefsafe_extract(run_result):text=str(getattr(run_result,"final_output","")or"").strip()ifnottext:return'{"status":"empty","verified":false}'returntext把 A/B/C 三行 fail 记入 checklist;零 fail 样本不得宣称生产禁区已测。
5. 结构化 parameters 与输出抽取(可选加强)
frompydanticimportBaseModel,FieldclassCheckInput(BaseModel):cite_key:str=Field(description="正文引用键,如 [12]")snippet:str=Field(description="待核验片段")tool=specialist.as_tool(tool_name="citation_check_struct",tool_description="结构化引用核验",parameters=CheckInput,include_input_schema=True,custom_output_extractor=safe_extract,)门禁:结构化入参仍要在专科 instructions 里禁止编造;schema 合法 ≠ 事实正确。
生产禁区(对标 90/91)
| 禁区 | 为什么危险 | 替代 |
|---|---|---|
| 静默嵌套花费 | 专科×循环=账单乘法 | max_turns + 成本审计 + 调用次数上限 |
| 以为嵌套继承父会话 | 纠正丢失、串话 | 显式 session 或声明无共享 |
| 同一 session 无隔离乱传 | 用户/租户串数据 | 按请求隔离;写清策略 |
| 审批夹具永远 approve | 拒绝路径未测 | 强制 reject smoke |
| 把嵌套 final_output 当真理 | 幻觉进入业务 | 抽检/二次核验/人工闸 |
| extractor 缺失即「成功 JSON」 | 空失败被洗白 | 空=失败标记 |
| is_enabled 当鉴权 | 仍可能被错误装配 | 工具内授权 + guardrail |
| 与 handoff 混用却无 runbook | oncall 无法判断控制权 | 对照表进仓库 |
可验证清单
- 能用一句话区分 as_tool 与 handoff 的控制权
- orchestrator + specialist.as_tool smoke 可跑或可构造
- 文档写明:嵌套不自动继承父 conversation state
- 至少 1 条 fail:无共享却期望继承 / 或错误共享
- 若启用 needs_approval:approve 与 reject 各 ≥1 样本
- 成本:记录嵌套调用次数与 max_turns
- 禁止把嵌套 final_output 直接写入「已核验」业务字段
踩坑
- 把 handoff 示例代码改两个字就当 as_tool,导致控制权误解。
- 父 instructions 写「你记得全部历史」,却给嵌套空 input。
on_stream只打印不落盘,排障时只剩 final 字符串。- 生产用贵模型做专科,却在 smoke 用极短 prompt 掩盖超时与花费。
- 多专科并行时假设「一个失败全体停」——须对照 runner 取消与工具错误策略实测。
与 sessions / RunConfig / HITL 的分工
| 面 | 管什么 | 不管什么 |
|---|---|---|
| as_tool | 嵌套调用面与选项 | 自动继承父历史 |
| session | client-managed 历史策略 | 替代审批 |
| run_config | 嵌套追踪/模型等配置 | 自动合并父全部语义 |
| needs_approval | 人机闸 | 工具内业务鉴权 |
| custom_output_extractor | 回传整形 | 事实核验 |
常见误配:只开 tracing 观察嵌套很忙,却不设max_turns;或只设审批,却在 resume 时无条件放行。
当天最小实验(30–40 分钟)
- 复制 orchestrator smoke,无 key 验证可构造;有 key 跑通一次。
- 造 Fail A:期望继承却不传 session,记 nested_state_inherit=fail。
- 若启用审批:造一次 reject。
- 给 extractor 空结果,确认不会洗成 verified=true。
- 把四行结果写入
_w/as-tool-smoke-checklist.md。
没有第 5 步落盘,不得自称「as_tool 生产禁区已钉死」。
多服务落地建议
- 只读专科(格式检查、提纲):可 as_tool + 短 max_turns。
- 写操作/发信/扣费:默认 needs_approval 或外层事务;嵌套成功 ≠ 已提交。
- 长对话专家坐席:优先 handoff,而不是假装 as_tool 能接管多轮。
- 多租户:session 与日志按租户隔离;禁止全局单例 session。
失败含义速查
| 现象 | 含义 | 下一步 |
|---|---|---|
| 专科「忘了」用户纠正 | 状态未共享或 input 未带纠正 | 显式传入或改 handoff |
| 账单陡增 | 嵌套循环/无 max_turns | 限次 + 审计 |
| final 很顺但业务未核验 | 把模型输出当真理 | 加抽检闸 |
| 审批日志全是 approve | 拒绝未测 | 补 reject smoke |
| is_enabled 关了仍被调用 | 装配/缓存旧 tools | 查构建时 tools 列表 |
Runbook 片段(可直接贴进仓库)
[as-tool-policy] control = manager_keeps_control nested_inherits_parent_conversation = false session_strategy = explicit_only max_turns_default = 4 approval_write_tools = required extractor_empty = fail_not_ok smoke_required = construct_or_run, nested_no_inherit_fail, reject_once发版检查官只问三句:嵌套是否声明了状态策略?写操作有没有拒绝样本?嵌套输出进业务前有没有第二道核验?
对照:编排观感 vs 生产应有行为
| 场景 | 编排观感 | 生产应有行为 |
|---|---|---|
| 翻译专科 | 「调一下工具」 | max_turns + 输出语言抽检 |
| 引用核验专科 | 「已经 check 过了」 | 空 DOI→需人工;禁 verified 洗白 |
| 计费问答 | 「答案很完整」 | 金额字段二次校验 |
| 多轮专家 | 「再 as_tool 一次」 | 评估是否该 handoff |
嵌套花费审计字段(建议落盘)
嵌套 run 至少记录:
parent_run_id/nested_tool_name/nested_max_turnsnested_turns_used(若可从结果推断)session_strategy:none|shared_explicit|server_continuationapproval_decision:n/a|approve|reject|timeoutextractor_status:passthrough|empty_fail|custom_ok
把五行打进_w/as-tool-smoke-checklist.md。没有花费与审批列,只写「调用成功」,等于没做生产验收。
条件启用 is_enabled 的正确用法
is_enabled适合:环境开关(预发关计费专科)、租户能力包、A/B 工具面。不适合:把「用户是否有权扣款」塞进可见性函数却不在工具内校验参数。
| 用法 | 可接受 | 危险 |
|---|---|---|
| 预发隐藏写操作专科 | 是 | 生产仍靠隐藏当鉴权 |
| 按语言偏好显隐翻译专科 | 是 | 把偏好当安全边界 |
| 异步函数查配额再显隐 | 可 | 配额通过后不再校验参数 |
门禁:工具实现内仍要做资源级授权;is_enabled只减暴露面。
与多 agent 模式文档的对齐提示
官方多 agent 模式常并列 handoff、agents-as-tools、LLM-as-router。工程验收上要求写清:本服务默认哪一种控制权模型;切换时变更记录在哪。本文不复述全部模式图,但 runbook 必须能回答「失败时谁还握着对话」。
流式 on_stream 最小约定
若传入on_stream:
- 事件类型对齐
raw_response_event/run_item_stream_event/agent_updated_stream_event(以 SDK 为准)。 - 处理器同步或异步均可,但应有序处理。
- 生产建议:采样写入日志,避免把全量 token 明文塞进无加密存储。
- 打开
on_stream会走嵌套 streaming 并在返回 final 前排空流——延迟与费用预期要写入容量计划。
对照:handoff 误用 as_tool 的症状
| 症状 | 可能误用 | 纠正 |
|---|---|---|
| 用户后续问题「专科不接话」 | 本该 handoff 却 as_tool | 改 handoff 或显式多轮 session |
| 专科答完 manager 又改口 | as_tool 正常 | 用 extractor/指令约束改口 |
| 账单暴涨 | 循环 as_tool | max_turns + 断路器 |
| 审批从未出现 | 未设 needs_approval | 写操作强制挂闸 |
当天加强:结构化失败注入
在预发造三条:
- 专科 instructions 故意要求编造 DOI → 看 manager 是否照抄进 final。
max_turns=1逼出截断 → 看是否被叙述成「已完整核验」。needs_approval=True后 reject → 看业务是否仍写入「已通过」。
三条日志路径进 checklist;缺一则 as-tool 闸门未完整。
安全笔记(短)
- 嵌套 agent 的 tools 列表应最小权限;不要把生产写库工具挂到「只读核验」专科。
custom_output_extractor里不要执行未校验代码路径。- 日志中的
tool_call参数可能含 PII:与 tracing 敏感开关一并治理。
总结
as_tool的工程核心不是语法糖,而是控制权仍在 manager,状态与花费在嵌套边界重新结算。相对 handoff,它适合编排;相对「嵌套自动记得一切」,它默认不继承。可跑 smoke + fail 样本 + 审批拒绝路径 + 禁止把嵌套 final 当审计真理,才对得起 tool 超时/guardrails 91、RunConfig/sessions 90 那一档强度。
参考
- Tools(Agents as tools):https://openai.github.io/openai-agents-python/tools/
- 质量要点:
/workspace/csdn-posts/quality/LATEST.md