# 循环什么时候该停：模型的停止信号、步数上限，和工具出错之后怎么办

模型返回的停止原因只是三层判据中的第一层：还要有罩住真实反馈路径的确定性上限，以及区分暂时性错误、永久性错误和有副作用动作的失败处置规则

> ReAct 循环 · 可靠性 · 工具调用 · 约 7 分钟 · 10 月 13 日

## 本篇要点

1. 每次模型响应都带一个停止原因字段（OpenAI 的 finish_reason、Anthropic 的 stop_reason），它决定循环该继续还是结束。
2. 正确的写法是只在收到工具调用信号（tool_calls / tool_use）时继续，其余取值如 max_tokens、refusal、服务端工具迭代到上限，都要显式处理，不能一律当成任务完成。
3. 「模型不再调工具」不等于任务做完：模型可能提前收尾返回接近空的答案；用不带执行函数的 done 工具可以把「完成」变成可断言的结构化事件，代价是多花一轮 token。
4. 只靠模型的停止信号，循环的控制权就在模型手里，所以必须再设确定性上限：步数、墙钟超时、token 或费用预算。
5. 设步数上限有两个陷阱：一步（super-step）可能包含并行的多次工具调用，不能按工具调用次数推算；上限必须罩住真正重复触发模型调用的那条反馈路径，内层的上限管不住外层循环。
6. 上限触发时要有优雅收尾：捕获框架抛出的错误返回部分结果，或利用剩余步数提前转到收尾节点，而不是静默截断。
7. 工具失败先分两问：错误是否暂时性（429、408、5xx、网络问题可重试；4xx、权限、schema 错误重试无用），以及动作是否已有副作用。
8. 暂时性错误用指数退避加抖动重试，设次数与单次等待上限，遇 429 优先读 Retry-After；写操作要重试必须先有幂等键，且 key 只在一次逻辑操作里生成一次。
9. 有些重试该在工具内部静默完成（网络抖动、限流），有些必须把错误原样交回模型（参数错误、权限不足），因为它需要据此改参数或换工具；重试只在一层实现，避免次数叠加。
10. 重试预算耗尽后只有两条出路：返回说明卡在哪里的部分结果，或升级给人；断路器连续失败到阈值后快速失败，打开状态下应升级而不是空等。

---

前面几篇里，我们反复讲过：循环在你的代码里——模型只负责一次推理并输出 tool_call，真正发请求、执行工具、把结果塞回上下文、决定要不要再转一圈的，是你写的那个 `while`。既然循环是你的，「什么时候停」也得由你回答。模型的停止信号只答了一半。这一篇讲清三层判据：**模型给的停止信号该信到什么程度、你自己要加什么样的确定性上限、以及工具报错或超时之后，是重试、换路还是放弃。**

## 第一层：模型返回的停止原因字段

每次模型响应里都有一个字段，告诉你它为什么停下。OpenAI 叫 `finish_reason`，Anthropic 叫 `stop_reason`；名字不同，作用是同一个：这次响应是「还要调工具」还是「答案已经说完」[[2]](#ref2)。

最朴素的循环就写在这两个值上：`finish_reason == "tool_calls"`（Anthropic 是 `stop_reason == "tool_use"`）就执行工具、把结果追加进消息、再发一次请求；`"stop"`（`"end_turn"`）就结束，把最后那段文本当答案输出。

坑在于取值远不止这两种。Anthropic 官方列出的还有 `max_tokens`（撞到输出长度上限，答案被截断）、`refusal`（模型拒绝回答），以及服务端工具跑到迭代上限等若干种[[1]](#ref1)。这些值里只有 `tool_use` 意味着「继续转」，其他都不该被当成「任务顺利完成」。如果代码写成「不是 tool_use 就结束」，一次被截断的回答就会被你当成完整答案交给用户，一次拒绝也会被当成正常结束。正确的写法是反过来：**只在收到工具调用信号时继续，其余每个取值都显式处理**。

```mermaid
flowchart LR
  A["模型响应"] --> B{"stop_reason"}
  B -->|"tool_use / tool_calls"| C["执行工具，结果塞回上下文"]
  C --> A
  B -->|"end_turn / stop"| D["输出最终答案"]
  B -->|"max_tokens / 拒绝回答等"| E["按对应原因单独处理，不能当成完成"]
```

还有一层更隐蔽的问题：**「模型不再调工具」不等于「任务做完了」**，它可能只是停早了。Anthropic 的文档提到一个现象：如果工具结果后面总是跟着一段文本，模型会学到「工具结果之后就该轮到我停」，于是提前结束那一轮，返回一个几乎空的响应[[1]](#ref1)。这类问题不该靠反复调 prompt 去试，而是该让「完成」变成结构化事件：定义一个专门的收尾工具（比如 `done`），参数就是最终答案，而它没有真正的执行函数——在 Vercel AI SDK 里，一个不带 `execute` 的工具被调用时，循环就停在调用它的地方，参数里的答案就是结果[[3]](#ref3)。代价是多花一轮 token，换来的是「结束」这件事从一段自然语言变成一个可以断言的字段。

## 第二层：步数上限

第一层的判据有个共同点：**控制权在模型手里**。它一直调工具，你就一直转。主流框架全都为此备了保险丝：LangChain 的 `max_iterations`，LangGraph 的 `recursion_limit`（1.0.6 起默认 1000 个 super-step），OpenAI Agents SDK 的 `max_turns`（超出抛 `MaxTurnsExceeded`），CrewAI 的 `max_iter`，Vercel AI SDK 默认 20 步[[3]](#ref3)[[4]](#ref4)[[5]](#ref5)。

设上限有两个容易踩的陷阱。**一是「一步」不等于「一次工具调用」**：LangGraph 的一个 super-step 里可以包含并行的多个工具调用，按工具调用次数推算步数会算不准[[4]](#ref4)。**二是上限必须罩住真正在循环的那条路径**：一篇分析智能体死循环的论文把这点当作核心结论——框架虽然都提供了上限机制，但这些机制经常没盖住实际的反馈回路，比如你在子智能体那层设了轮数上限，而外面那个「执行—评估—再执行」的循环不受它约束，照样能无限转下去[[5]](#ref5)。设完上限要回头问一句：真正会重复触发模型调用的那个圈，是我刚限住的这个吗？

上限触发时该发生什么，同样要设计。静默截断最差——用户拿到一个不知从哪断掉的答案。LangGraph 的做法是抛 `GraphRecursionError`，你可以捕获它，返回一个「做到哪算哪」的部分结果，或者提示用户是否继续；更主动的做法是利用它暴露的剩余步数（`RemainingSteps`），某个节点发现只剩 2 步时主动转去收尾节点整理答案[[4]](#ref4)。步数之外通常还要配一个墙钟超时和一个 token 或费用预算，三个一起才勉强算「有边界」。

## 第三层：工具报错或超时之后

工具失败是常态。决定怎么办之前先问两个问题。

**问题一：这个错误是暂时性的吗？** 网络断开、请求超时、`429` 限流、`503` 服务暂时不可用，重试有意义；`400` 参数错误、`401`/`403` 权限不足、`404` 找不到资源、schema 不匹配，重试一万次结果也一样[[6]](#ref6)。判断标准应该是错误本身的语义，而不是「先试三次看看」[[8]](#ref8)。

暂时性错误要重试，但不能立刻重试、也不能原地猛重试。标准做法是指数退避加抖动：每次等待翻倍，再加一点随机量，避免多个请求同时重试把对方压垮；同时设重试次数上限和单次等待上限[[6]](#ref6)。真实 SDK 的默认值可以当起点：Cloudflare 的 Agents SDK 是 3 次尝试、100ms 起步、3 秒封顶、full jitter[[7]](#ref7)。遇到 `429` 优先看响应里的 `Retry-After`，服务端已经告诉你该等多久[[9]](#ref9)。

**问题二：这个动作有没有副作用？** 这条更容易被忽略，后果也更严重。一条规则总结得准：**只在请求派发阶段重试，一旦产生副作用就不要再重试**。读操作（查询、搜索）重试是安全的；写操作就不一定——重发一次「发邮件」或「下单」，可能真的发两封、下两单[[8]](#ref8)。解决办法是幂等键：为一次逻辑操作生成一个 key，重试时原样带上，让服务端去重；关键是这个 key 只在一次逻辑操作里生成一次，不是每次尝试都生成新的[[9]](#ref9)。

```mermaid
flowchart LR
  A["工具执行失败"] --> B{"是暂时性错误吗"}
  B -->|"是，且尚无副作用"| C["退避重试，次数有上限"]
  C -->|"仍失败"| D["放弃重试"]
  B -->|"否：4xx 或 schema 错"| E["不要重试"]
  B -->|"是，但可能已有副作用"| E
  D --> F["把错误交回模型或升级给人"]
  E --> F
  F --> G["原样写进 observation，让模型改参数或换工具"]
```

这里有个容易忽略的设计选择：**有些重试该由代码静默完成，有些必须交回模型。** 网络抖动、限流这类错误，模型知道了也没用，反而可能让它乱改参数，就该在工具函数内部消化，模型只看到最终成功的结果。但参数错误、权限不足这类错误，价值恰恰在于信息——模型看到「字段 `date` 格式不对」能改写参数，看到「没有权限访问这个库」能换个工具；这时把错误吞掉、由代码反复重试是最坏的选择，也正是上一篇讲的：错误消息要写给模型看。顺带提醒：如果工具内部在重试，模型发现结果不对又重试，两层的次数会相乘。AWS 的指南明确说，重试只在一层实现[[6]](#ref6)。

**最后是放弃。** 重试预算耗尽之后，不要回到循环开头让模型再试一遍——那只是把同一个失败重新包装一次。合理的收尾只有两条：返回一个说明卡在哪里的部分结果，或者交给人处理（护栏那一篇讲的接管点）。更稳一点的做法是加断路器：连续失败 N 次之后对后续调用快速失败，不再真的发出去；断路器打开时，正确反应是升级或返回部分结果，而不是坐在那里等它恢复[[9]](#ref9)。

## 三层合起来才叫「会停」

模型的停止字段告诉你这一轮该不该继续；你的步数、超时和预算上限保证循环总会在某处停下；错误处置规则决定失败之后是把这一步重来、把信息交回模型，还是干脆停手。三层缺一层，都会在你没盯着的时候变成一个转不完的循环，或者一个看起来正常、其实早就断了的答案。

## 术语表

- 停止原因字段（stop_reason / finish_reason）：每次响应里说明模型为什么停下的字段，是判断循环该继续还是结束的第一手依据，取值不止「调用工具」和「结束」两种。
- 确定性上限：不依赖模型输出的硬边界，比如最大步数、墙钟超时、token 或费用预算，用来保证循环一定会在某处停下。
- super-step：框架计数的执行步，一步里可能包含并行的多次工具调用，所以不能拿工具调用次数当作步数。
- 收尾工具（done tool）：一个没有真正执行函数、只用来声明任务完成的工具，把「结束」从一段自然语言变成可检查的结构化事件。
- 指数退避与抖动：重试前等待时间逐次翻倍并加入随机量，避免大量请求在同一时刻重试，把已经吃力的服务压得更狠。
- 幂等键：为一次逻辑操作生成、重试时原样重发的标识，让服务端能识别并去掉重复请求，使写操作也变得可安全重试。
- 断路器：连续失败达到阈值后直接快速失败、不再真的发起调用，冷却后再放一个探测请求试探服务是否恢复。

## 来源

1. [Anthropic: Handling stop reasons — 官方列出的全部 stop_reason 取值及各自处理方式](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
2. [Anthropic: How tool use works — 以 stop_reason 为条件的客户端工具循环写法](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works)
3. [Vercel AI SDK: Loop Control — 循环停止条件、默认 20 步上限，以及不带 execute 的工具作为终止信号](https://ai-sdk.dev/docs/agents/loop-control)
4. [LangGraph: GRAPH_RECURSION_LIMIT — recursion_limit、默认步数与 RemainingSteps 主动收尾做法](https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT)
5. [When Agents Do Not Stop: Uncovering Infinite Agentic Loops in LLM Agents — 各框架的上限机制经常没覆盖真正的反馈路径](https://arxiv.org/html/2607.01641)
6. [AWS Well-Architected: Limit retries — 指数退避加抖动、限制重试次数、不在多层重复实现重试](https://docs.aws.amazon.com/wellarchitected/2023-04-10/framework/rel_mitigate_interaction_failure_limit_retries.html)
7. [Cloudflare Agents SDK: Retries — 默认 3 次尝试、100ms 起步、3 秒封顶、full jitter](https://developers.cloudflare.com/agents/api-reference/retries/)
8. [Agent SDK: Retries & transient failures — 「只在请求派发阶段重试，产生副作用后即视为致命」的错误分类表](https://github.com/nucel-dev/agent-sdk/blob/main/docs/tutorials/retries.md)
9. [Retry and Recovery Patterns — 幂等键、断路器、Retry-After 与可重试错误的判定清单](https://agentsurface.dev/docs/error-handling/retry-patterns)

---

原文：https://pangzhengboyin.com/articles/when-to-stop-agent-loop-1c07c53e

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
