OpenAI Agents SDK 工程笔记:tool 调用循环、max_turns 与生产禁区

千笔-AIWritePaper · https://www.aiwritepaper.com

多轮 Agent 最容易翻车的不是「忘了挂 tools」,而是把模型决定调用、SDK 执行工具、再送回模型 当成无限循环,却从不验收 max_turns。官方 Running agents(中文:运行智能体)把循环钉死为:调模型 → 最终输出则停 / 任务转移则换 Agent / 有 tool calls 则执行并回灌 → 超过 max_turnsMaxTurnsExceeded。本文按工程笔记写法,钉死循环、可跑片段、对照表与生产禁区。示例模型名写作 gpt-4o以你账号可用快照与官方文档为准

图:上方 Runner 循环(最终输出 / handoff / tool 回灌);中部 max_turns 与 error_handlers;下方 tool_use_behavior、reset_tool_choice 与生产禁区。

目标说明

读完你应能独立完成五件事:

  1. 用一句话说清 tool 循环:模型发出 tool calls → SDK 执行并追加结果 → 再调模型;无 tool 且类型匹配才算最终输出。
  2. 解释 max_turns:一轮 = 一次模型调用 (含该次发出的工具执行);默认 DEFAULT_MAX_TURNS=10None 禁用上限。
  3. 写出可跑片段:@tool 本地函数 + max_turns=2 触发 MaxTurnsExceeded,以及 error_handlers={"max_turns": ...} 受控回退。
  4. 会用 tool_use_behaviorrun_llm_again / stop_on_first_tool / StopAtTools)与 reset_tool_choice,而不把 hosted tools 误当成可「停在第一刀」。
  5. 列出生产禁区:生产里 max_turns=None、强制 tool_choice 且关掉 reset、副作用工具无审批、把超轮当成功、无沙箱挂 Shell/Computer。

规格钉死(对照官方 Running agents / Tools / Agents):

  • 循环:LLM 调用 → 最终输出 / handoff / 执行 tools 后重入。
  • 最终输出判定 :所需类型的文本输出,且没有 tool calls。
  • max_turns :超限抛 MaxTurnsExceeded;可用 error_handlers["max_turns"] 返回受控 final_output
  • tool_use_behavior :默认 run_llm_againstop_on_first_tool / StopAtTools 只作用于 FunctionTool,hosted tools 仍会再送模型。
  • reset_tool_choice :默认 True,强制 tool 后复位,防止 tool_choice 死循环。

适用边界

适合上 tool 循环验收

  • 本地函数工具(查库存、算分、读配置):要证明「调用 → 回灌 → 终答」可复现。
  • 需要硬上限的生产原型:客服/教辅 Agent 必须在 N 次模型调用内收口。
  • 副作用工具(写库、付款、删文件)要先走 needs_approval,再谈循环。
  • 已出现「模型一直调同一工具」:先查 tool_choice + reset_tool_choice,再查提示词。
  • 要把超轮从「进程崩了」改成「对用户说收窄请求」:上 error_handlers

不该指望它单独搞定

  • 业务授权is_enabled 只控制可见与分发,不替代参数级鉴权。
  • hosted 工具的「停在第一刀」tool_use_behavior 对 WebSearch / FileSearch 等 hosted tools 不适用
  • 无限探索型研究助手max_turns=None 只适合受控实验,不能当生产默认。
  • 把 tool 超时当循环成功ToolTimeoutErrorMaxTurnsExceeded 是两类故障。
  • ComputerTool / ShellTool 无沙箱:循环正确 ≠ 执行面安全。

风险提示

默认 10 轮在「每轮都打外部 API」时足够烧钱;max_turns=None 遇上强制 tool_choicereset_tool_choice=False,就是死循环。error_handlers 的回退默认会进历史(include_in_history=True),生产要显式决定要不要落盘。并行 tool 的并发由 ToolExecutionConfig.max_function_tool_concurrency 管,和模型侧 parallel_tool_calls 不是同一开关。

步骤与机制

1. 循环与上限对照

机制 谁停循环 失败时发生什么 典型用途
最终输出(无 tool calls) Runner 判定 返回 RunResult 正常收口
tool calls 不停,执行后重入 计 1 轮(含本次模型调用) 查数/写库
handoff 换 Agent 后重入 仍占轮次预算 分诊
max_turns 超限 MaxTurnsExceeded 可用 error_handlers 改成受控终答 生产硬上限
stop_on_first_tool 第一把 FunctionTool 输出当终答 hosted tools 仍会回模型 只要工具原值

2. 可跑:本地 tool + 超轮 + 受控回退

pip install openai-agents,并导出 OPENAI_API_KEY。下面用一个故意可被连调 的计数工具:模型每轮都会看到「还没完成」,直到撞上 max_turns=2

python 复制代码
import asyncio
from agents import Agent, Runner, RunErrorHandlerInput, RunErrorHandlerResult
from agents.decorators import tool
from agents.exceptions import MaxTurnsExceeded

MODEL = "gpt-4o"  # 占位:以账号可用快照为准

@tool
def bump_counter(n: int) -> str:
    """Increment a demo counter. Always ask the model to call again."""
    return f"counter={n+1}; not done yet; call bump_counter again with n={n+1}"

agent = Agent(
    name="Counter",
    instructions="You MUST call bump_counter repeatedly until the user says stop.",
    model=MODEL,
    tools=[bump_counter],
)

async def main():
    try:
        r = await Runner.run(agent, "Start from n=0 and keep going.", max_turns=2)
        print("unexpected finish:", r.final_output)
    except MaxTurnsExceeded as e:
        print("caught MaxTurnsExceeded:", e)

    def on_max_turns(_data: RunErrorHandlerInput[None]) -> RunErrorHandlerResult:
        return RunErrorHandlerResult(
            final_output="turn_limit_reached: narrow the task or raise max_turns",
            include_in_history=False,
        )

    r2 = await Runner.run(
        agent,
        "Start from n=0 and keep going.",
        max_turns=2,
        error_handlers={"max_turns": on_max_turns},
    )
    print("handled:", r2.final_output)

asyncio.run(main())

烟测期望:第一次打印捕获异常;第二次打印受控短句,且该短句 写入 session 历史。完整勾选见 _w/tool-loop-smoke-checklist.md

3. tool_use_behavior 与 reset_tool_choice

python 复制代码
from agents import Agent, ModelSettings, Runner
from agents.agent import StopAtTools
from agents.decorators import tool

@tool
def lookup_sku(sku: str) -> str:
    """Return a stub inventory line."""
    return f"{sku}: units=42"

# 只要工具原值,不再让模型改写
stop_agent = Agent(
    name="Inv",
    tools=[lookup_sku],
    tool_use_behavior="stop_on_first_tool",
    model="gpt-4o",
)

# 只在指定工具名上停
stop_named = Agent(
    name="Inv2",
    tools=[lookup_sku],
    tool_use_behavior=StopAtTools(stop_at_tool_names=["lookup_sku"]),
    model="gpt-4o",
)

# 强制调用后必须复位,否则会无限 tool
forced = Agent(
    name="Forced",
    tools=[lookup_sku],
    model_settings=ModelSettings(tool_choice="lookup_sku"),
    reset_tool_choice=True,  # 默认就是 True;生产禁止改 False
    model="gpt-4o",
)

reset_tool_choice=True 的工程含义:强制 tool_choice 用过一次后回到 auto,避免「结果回灌 → 仍被强制再调」的死循环。文档把这写成防无限循环的默认保护;关掉它必须有书面理由。

4. 执行面与审批(生产前先填)

开关 管什么 注意
max_turns 模型调用次数上限 默认 10;生产禁止裸 None
error_handlers["max_turns"] 超轮时的受控终答 默认会进历史,按需 include_in_history=False
tool_use_behavior FunctionTool 是否回模型 hosted tools 始终回模型
reset_tool_choice 强制 tool 后是否复位 默认 True
needs_approval 执行前暂停 RunState.approve/reject 续跑
ToolExecutionConfig.max_function_tool_concurrency 本地函数并发 parallel_tool_calls
@tool(timeout=...) 单次函数超时 默认把超时当结果回模型

可验证清单

  • 同工具两轮:new_items 里能看到 tool call 与 tool output。
  • max_turns=2 在「故意连调」提示下抛 MaxTurnsExceeded(或被 handler 收口)。
  • handler 回退句可复述,且 include_in_history 选择已写进报告。
  • stop_on_first_tool 只验证 FunctionTool;未对 WebSearch 宣称「停在第一刀」。
  • reset_tool_choice 保持 True;若改 False,变更单写明死循环对策。
  • 副作用工具有 needs_approval 或等价闸门。
  • 报告写明 SDK 版本、模型快照、日期(Asia/Shanghai);不编造未协议化的「工具准确率」榜。

踩坑

  1. 把一轮理解成「一次用户发言」 :官方定义是一次 AI invocation,用户一句话可能烧掉多轮。
  2. max_turns=None 当「更聪明」:只是去掉保险丝。
  3. 强制 tool_choice + reset_tool_choice=False:经典死循环。
  4. 以为 stop_on_first_tool 能停 hosted 搜索:文档写明只作用于 FunctionTool。
  5. 超时当成功error_as_result 只是让模型看见超时句,不是业务完成。
  6. 审批中断后另起一个新 Runner.run :应 to_state() 续跑,否则轮次与副作用对不上。
  7. is_enabled=False 当授权:隐藏工具 ≠ 拒绝越权参数。

生产禁区

  • 对含付款/删库/发信的 Agent 设置 max_turns=None,又无审批、无并发上限。
  • 生产默认 reset_tool_choice=False,却把「模型很勤快」当成功能。
  • MaxTurnsExceeded 吞掉后仍向调用方返回 200 成功体,且不打超轮指标。
  • ShellTool / ComputerTool / ApplyPatchTool 不设工作区与网络策略,却对外承诺「工具循环已验收」。
  • stop_on_first_tool 处理 hosted MCP / web search,并据此写 SLA。
  • 在文档未覆盖的自定义工具执行器上静默丢写,仍宣称「强一致 tool 回灌」。

工程落地顺序(建议当天跑完)

  1. 先单测循环语义 :一个无副作用 @tool,证明 call → output → 终答;打印 new_items 类型。
  2. 再测保险丝 :故意连调 + max_turns=2,必须看到异常或 handler 短句。
  3. 再测行为开关stop_on_first_tool 与默认 run_llm_again 对照;确认终答来源不同。
  4. 再测强制 tooltool_choice=工具名 且 reset=True,第二轮应能不再强制。
  5. 最后才上副作用needs_approval=True,走中断/续跑;换空 Runner 续跑视为事故演练失败。

与 sessions / guardrails 的交接

Session 管对话 items;guardrails 管输入输出与工具护栏;本篇的循环管「还要不要再调模型」。三者正交:不要指望 max_turns 自动记住「护栏已拒绝」------拒绝文案与是否写入 session 要单独设计。也不要把超轮 handler 的合成句当成模型真实推理。

文档口径如何写进变更单

  • 「一轮 = 一次模型调用(含该次 tool 执行);默认上限 10。」
  • 「生产禁止 max_turns=None;超轮必须有指标与用户可见收口。」
  • reset_tool_choice 保持 True;关闭需变更单。」
  • 「FunctionTool 的 stop 语义不得套到 hosted tools。」
  • 「副作用工具默认 needs_approval;续跑走 RunState。」

常见误读对照

误读 正确读法
max_turns 限制的是工具次数 限制的是模型调用次数
关掉上限 = 更稳 关掉的是保险丝
stop_on_first_tool 对搜索也生效 只对 FunctionTool
捕获 MaxTurnsExceeded 后当成功 必须记失败/降级
隐藏工具 = 已做鉴权 鉴权要看参数与资源

最小报告模板

一次 tool 循环验收报告固定六段:环境(SDK、模型快照)、循环步骤(call/output/终答)、超轮(异常或 handler)、行为开关、审批续跑、禁区自检。不要用「整体良好」。

超轮之后怎么回溯

若从未看到 tool output:先查 tools 是否挂上,再查 tool_choice="none",再查 is_enabled。若一直 tool 不停:先查 instructions 是否要求「必须连调」,再查 reset_tool_choice,再查是否把错误结果写成「请再试」。若 max_turns 似乎无效:确认传的是 Runner.run(..., max_turns=2) 而不是只写在 Agent 注释里。把回溯树写进 runbook,比在群里猜「模型傻了」更快。

循环问题优先当控制面故障处理,不要先改提示词形容词。

轮次预算怎么估

不要用「用户说了几句」估预算。用「最坏路径上的模型调用次数」:

  1. 一次用户问题里,模型先规划、再连调 3 个工具、再总结 = 至少 5 轮(每次模型出场算 1)。
  2. 若中间有 handoff,接手前的调用与接手后的调用共用 同一 max_turns 预算------分诊 Agent 把预算花光,专家 Agent 会直接超轮。
  3. 嵌套 agent.as_tool 的内部 max_turns子运行自己的预算,不自动继承父运行;父运行仍把这次 tool 当作自己的一轮模型调用。

建议在 runbook 写:「父运行 max_turns=8;每个 tool-agent max_turns=3;任一处超轮都要有独立指标」。不要只给父运行一个很大的数字。

副作用工具的最小审批剧本

  1. 工具声明 needs_approval=True(或按参数返回 True)。
  2. 跑到中断后,把 result.interruptions 打印成表:tool 名、参数、call_id。
  3. 人类 state.approve / state.reject;拒绝时应走 tool_error_formatter,让模型看见「人类拒绝」而不是空失败。
  4. 同一 RunState 续跑,禁止 Runner.run 另起炉灶。
  5. 续跑后核对:轮次计数连续、session 未重复写入同一 call。

缺第 4 步,前面的循环验收全部作废------你测到的是两个互不相干的短跑。

并发与超时不要混为一谈

ModelSettings.parallel_tool_calls 决定模型能不能在一条回复里发出多个 toolmax_function_tool_concurrency 决定 SDK 一次执行几把本地函数。生产上常见误配:模型一次发 8 个写库调用,SDK 也照单全开,把下游打满。应先在预发环境用合成 8 调用压一次,再决定并发上限。

超时默认 error_as_result:模型会看到超时句并可能再试,这会继续消耗 max_turns 。对不可重入的扣款工具,应改 timeout_behavior="raise_exception",让运行失败而不是再扣一次。

变更单里的指标名

至少独立打点:agent.max_turns_exceededagent.tool_timeoutagent.approval_pendingagent.tool_not_found。不要把它们折进「Agent 成功率」。成功率好看、超轮在涨,是典型的生产假绿。

总结

Agents SDK 的 tool 循环解决的是模型---工具---再模型 的托管重入,不是「工具越多越智能」。先把循环语义、max_turnstool_use_behaviorreset_tool_choice 与审批续跑写成可勾选协议,再谈生产。可审计产物:本文对照表 + _w/tool-loop-smoke-checklist.md。去掉任何产品名后,你手里仍应剩下可跑的超轮与回灌验收。

相关推荐
`流年づ1 小时前
人工智能学习笔记 - 自动微分
人工智能·笔记·学习
zcmodeltech1 小时前
能源电力沙盘模型控制系统设计——基于STM32与Modbus RTU的“发—输—变—配—用”全链条动态展示方案
数据库·stm32·单片机·嵌入式硬件·能源
djjjx.1 小时前
Linux工具篇 (三):make与Makefile
linux·服务器·makefile·make
时代的凡人1 小时前
【无标题】科样KSMART服务器(Kohyoung AOI的集中数据Hub)访问方案
服务器
李兆龙的博客1 小时前
从一到无穷大 #91:从 Habitat 看存储平台的整合与分工
数据库·人工智能·架构
团子股股东峥哥2 小时前
day36-RHEL-管理基本存储
linux·运维·服务器
荣合技术服务2 小时前
山西太原服务器集群服务商——荣合科算
服务器·超算集群·荣合科算
༄久梦༒长醉༻2 小时前
技术笔记——Git Worktree工作树
笔记·git
惜分飞2 小时前
记录一次0丢失的ORA-00354: 损坏重做日志块标头故障恢复---惜分飞
数据库·oracle