turn 的精确定义与计数逻辑(解析 Agent SDK loop 中 max_turns 的判定边界)

Agent SDK 的 turn 是”模型产出一段含 tool calls 的输出 + SDK 执行这些工具 + 把结果回传给模型”的完整一轮,它把”思考—行动—反馈”封装为一个不可拆分的执行单元。max_turns 只数包含工具调用的 turn,纯文本响应不计入计数,所以它的真实含义是”最多允许多少轮’做事’,而不是’说话'”。

一、turn 不是一个 HTTP 请求

很多第一次接触 Agent SDK 的工程师会把 turn 误读为”一次模型调用”。实际上一个 turn 内可能包含多个并行的 tool call,模型在一次响应里同时调用 Bash、Read 与 Edit 是常见现象。turn 的边界由”是否需要 SDK 介入执行外部动作”决定:只要模型希望 SDK 跑一段代码或读一个文件,循环就新增一个 turn;如果模型只输出文字回复,循环就停在当前 turn 并准备收尾。

这种切分方式让 turn 成为一个对资源敏感的单位:每个 turn 都意味着 token 重新组装、工具进程拉起、权限校验。生产环境把 max_turns 作为防御性兜底,本质是限制”做事”的次数,而不是”回复”的次数。

二、maxturns 与 maxbudget_usd 的差异

SDK 提供两个并行的兜底参数:max_turns 限定”做事”轮次,max_budget_usd 限定美元花费。两者触发后 SDK 都会返回 ResultMessage,但 error 子类型不同:命中 turn 上限是 error_max_turns,命中预算上限是 error_max_budget_usd。

维度 max_turns maxbudgetusd
计数对象 含 tool call 的 turn 累计 API 费用
纯文本 turn 不计 计入(要花钱)
触发后 error 子类型 error_max_turns error_max_budget_usd
典型取值 简单任务 5–10,复杂任务 30+ 按任务单价预估
单位敏感性 受”单 turn 工具数”影响 受”模型选择 + 单价”影响

业务选型时通常把 max_turns 视作”防死循环”的硬上限,把 max_budget_usd 视作”防账单失控”的安全网。前者过松会让 agent 在错误路径上反复尝试,后者过紧会在合理任务中途截断。

三、为什么纯文本 turn 不计数

设计上有三层考虑。其一,计费模型上纯文本 turn 仍然要花钱,但相比工具 turn 它不会执行外部动作、不会改变系统状态,对”失控”的代表性更弱;其二,循环终止条件本身就是”模型产出不含 tool call 的回复”,把终止 turn 也算进去会让 max_turns=1 永远无法跑出有效结果;其三,调用方通常关心的是”agent 还能折腾几次”,纯文本 turn 是一种”自我收敛”的信号,不应消耗预算。

一个真实任务”修复 auth.ts 失败用例”完整跑完会消耗 4 个 turn:3 个含 tool call(跑测试、读文件、编辑 + 重跑),1 个含最终纯文本总结。设 max_turns=2 会在 Edit 步骤前停下,整个修复无法完成。这正是 max_turns 只数工具 turn 带来的可预期行为。

# 捕获 turn 上限触发后的处理模板
from claude_agent_sdk import query, ClaudeAgentOptions

async def run_with_cap(prompt: str):
    options = ClaudeAgentOptions(
        max_turns=30,           # 工具 turn 上限
        max_budget_usd=1.50,    # 预算硬上限
        allowed_tools=["Read", "Edit", "Bash"],
    )
    async for msg in query(prompt=prompt, options=options):
        if msg.type == "result":
            if msg.subtype == "error_max_turns":
                return {"status": "truncated_turns", "text": msg.result}
            if msg.subtype == "error_max_budget_usd":
                return {"status": "truncated_budget", "text": msg.result}
            return {"status": "ok", "text": msg.result, "cost": msg.total_cost_usd}

四、按步骤配置兜底参数

  1. 预估合理上限:先看任务类型,文档问答设 5–10、批量代码改设 30+、长链路重构设 50+;
  2. 同时配 budget:纯文本 turn 也花钱,单靠 max_turns 防不住高单价模型;
  3. 捕获 error 子类型:在消费 ResultMessage 时先判 result.subtype,区分截断原因;
  4. 失败重试策略:若截断原因是 turn 上限,可在提示词里告诉模型”先用 plan 模式列出步骤再动手”,降低单 turn 工具数。

五、loop 实际运行的消息流

query() 是一个异步迭代器,吐出的消息类型包含 SystemMessage(会话元信息,含 init 子类型)、AssistantMessage(模型产出,含文本与 tool call)、UserMessage(工具结果回传)、ResultMessage(终态,含 usage 与 cost)。turn 与消息类型并不一一对应:单个 turn 内可能同时产生多个 AssistantMessage(并行工具调用场景)。要把 turn 数计清楚,应当监听 tool_use 事件而不是把所有 AssistantMessage 都当 turn。

六、生产环境常见误用

把 max_turns 设为 1 几乎一定失败,因为模型需要至少一个工具 turn 去读懂上下文。把 max_turns 设得过大又会让”卡死重试”行为持续数小时。生产实践通常给 max_turns 一个偏紧的初始值(如 30),用 max_budget_usd 作为兜底,并配合 hook 在 PreToolUse 阶段拦截已知会失败的命令,把循环尽早掐断在工具执行前。

另一个常见误用是把”模型每条消息”等同于”turn”。在 SDK 的消息流里,AssistantMessage 是模型响应片段,tool_use 是其中一种 block;一次 AssistantMessage 内可能既有文本又有多个 tool call,整个回合仍只算 1 个 turn。如果按消息计数来限制循环,要么过早截断、要么永远不触发上限,与 SDK 的真实执行模型错位。

七、调试循环时要看哪些字段

ResultMessage 的字段几乎全部用来事后分析 turn 与预算:total_cost_usd 看实际花费、duration_ms 看单 turn 平均耗时、num_turns 看真实工具 turn 计数、error 字段(命中上限时填充)告知截断原因。把这些字段写入结构化日志,是判断”上限设得过紧还是过松”的客观依据。

到这里,turn 的精确定义与 max_turns 的计数边界就清楚了:turn 是”做事”的单位而不是”说话”的单位,预算与上限要分开配置,捕获终态时要看 error 子类型而不是只看消息流计数。

常见问题(FAQ)

Q1:max_turns=0 会发生什么?

模型没有任何工具 turn 可用,会立即进入终态的纯文本响应,几乎拿不到任何实际工作产出。

Q2:turn 上限触发后能不能续跑?

需要外部保存 session_id 并用持久化参数续接,单次 query 不支持”再加 5 个 turn”。

Q3:并行 tool call 算一个 turn 还是多个?

算一个 turn,只要它们在同一次模型响应里被请求,循环就只前进一次。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部