前面几篇里,我们反复讲过:循环在你的代码里——模型只负责一次推理并输出 tool_call,真正发请求、执行工具、把结果塞回上下文、决定要不要再转一圈的,是你写的那个 while。既然循环是你的,「什么时候停」也得由你回答。模型的停止信号只答了一半。这一篇讲清三层判据:模型给的停止信号该信到什么程度、你自己要加什么样的确定性上限、以及工具报错或超时之后,是重试、换路还是放弃。
第一层:模型返回的停止原因字段
每次模型响应里都有一个字段,告诉你它为什么停下。OpenAI 叫 finish_reason,Anthropic 叫 stop_reason;名字不同,作用是同一个:这次响应是「还要调工具」还是「答案已经说完」2。
最朴素的循环就写在这两个值上:finish_reason == "tool_calls"(Anthropic 是 stop_reason == "tool_use")就执行工具、把结果追加进消息、再发一次请求;"stop"("end_turn")就结束,把最后那段文本当答案输出。
坑在于取值远不止这两种。Anthropic 官方列出的还有 max_tokens(撞到输出长度上限,答案被截断)、refusal(模型拒绝回答),以及服务端工具跑到迭代上限等若干种1。这些值里只有 tool_use 意味着「继续转」,其他都不该被当成「任务顺利完成」。如果代码写成「不是 tool_use 就结束」,一次被截断的回答就会被你当成完整答案交给用户,一次拒绝也会被当成正常结束。正确的写法是反过来:只在收到工具调用信号时继续,其余每个取值都显式处理。
还有一层更隐蔽的问题:「模型不再调工具」不等于「任务做完了」,它可能只是停早了。Anthropic 的文档提到一个现象:如果工具结果后面总是跟着一段文本,模型会学到「工具结果之后就该轮到我停」,于是提前结束那一轮,返回一个几乎空的响应1。这类问题不该靠反复调 prompt 去试,而是该让「完成」变成结构化事件:定义一个专门的收尾工具(比如 done),参数就是最终答案,而它没有真正的执行函数——在 Vercel AI SDK 里,一个不带 execute 的工具被调用时,循环就停在调用它的地方,参数里的答案就是结果3。代价是多花一轮 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 步345。
设上限有两个容易踩的陷阱。一是「一步」不等于「一次工具调用」:LangGraph 的一个 super-step 里可以包含并行的多个工具调用,按工具调用次数推算步数会算不准4。二是上限必须罩住真正在循环的那条路径:一篇分析智能体死循环的论文把这点当作核心结论——框架虽然都提供了上限机制,但这些机制经常没盖住实际的反馈回路,比如你在子智能体那层设了轮数上限,而外面那个「执行—评估—再执行」的循环不受它约束,照样能无限转下去5。设完上限要回头问一句:真正会重复触发模型调用的那个圈,是我刚限住的这个吗?
上限触发时该发生什么,同样要设计。静默截断最差——用户拿到一个不知从哪断掉的答案。LangGraph 的做法是抛 GraphRecursionError,你可以捕获它,返回一个「做到哪算哪」的部分结果,或者提示用户是否继续;更主动的做法是利用它暴露的剩余步数(RemainingSteps),某个节点发现只剩 2 步时主动转去收尾节点整理答案4。步数之外通常还要配一个墙钟超时和一个 token 或费用预算,三个一起才勉强算「有边界」。
第三层:工具报错或超时之后
工具失败是常态。决定怎么办之前先问两个问题。
问题一:这个错误是暂时性的吗? 网络断开、请求超时、429 限流、503 服务暂时不可用,重试有意义;400 参数错误、401/403 权限不足、404 找不到资源、schema 不匹配,重试一万次结果也一样6。判断标准应该是错误本身的语义,而不是「先试三次看看」8。
暂时性错误要重试,但不能立刻重试、也不能原地猛重试。标准做法是指数退避加抖动:每次等待翻倍,再加一点随机量,避免多个请求同时重试把对方压垮;同时设重试次数上限和单次等待上限6。真实 SDK 的默认值可以当起点:Cloudflare 的 Agents SDK 是 3 次尝试、100ms 起步、3 秒封顶、full jitter7。遇到 429 优先看响应里的 Retry-After,服务端已经告诉你该等多久9。
问题二:这个动作有没有副作用? 这条更容易被忽略,后果也更严重。一条规则总结得准:只在请求派发阶段重试,一旦产生副作用就不要再重试。读操作(查询、搜索)重试是安全的;写操作就不一定——重发一次「发邮件」或「下单」,可能真的发两封、下两单8。解决办法是幂等键:为一次逻辑操作生成一个 key,重试时原样带上,让服务端去重;关键是这个 key 只在一次逻辑操作里生成一次,不是每次尝试都生成新的9。
这里有个容易忽略的设计选择:有些重试该由代码静默完成,有些必须交回模型。 网络抖动、限流这类错误,模型知道了也没用,反而可能让它乱改参数,就该在工具函数内部消化,模型只看到最终成功的结果。但参数错误、权限不足这类错误,价值恰恰在于信息——模型看到「字段 date 格式不对」能改写参数,看到「没有权限访问这个库」能换个工具;这时把错误吞掉、由代码反复重试是最坏的选择,也正是上一篇讲的:错误消息要写给模型看。顺带提醒:如果工具内部在重试,模型发现结果不对又重试,两层的次数会相乘。AWS 的指南明确说,重试只在一层实现6。
最后是放弃。 重试预算耗尽之后,不要回到循环开头让模型再试一遍——那只是把同一个失败重新包装一次。合理的收尾只有两条:返回一个说明卡在哪里的部分结果,或者交给人处理(护栏那一篇讲的接管点)。更稳一点的做法是加断路器:连续失败 N 次之后对后续调用快速失败,不再真的发出去;断路器打开时,正确反应是升级或返回部分结果,而不是坐在那里等它恢复9。
三层合起来才叫「会停」
模型的停止字段告诉你这一轮该不该继续;你的步数、超时和预算上限保证循环总会在某处停下;错误处置规则决定失败之后是把这一步重来、把信息交回模型,还是干脆停手。三层缺一层,都会在你没盯着的时候变成一个转不完的循环,或者一个看起来正常、其实早就断了的答案。