循环终止条件:模型什么时候"想好了"
循环不能无限转下去。怎么判断"模型想好了、该停"?全在
finish_reason这一个小字段里,以及一个你迟早会撞上的大坑——“模型停不下来”。
本文导航
- 为什么终止条件如此要命
- 三种终止条件全景
- finish_reason:官方给你的停机信号
- 自然终止:没有 tool_calls 就是答案
- 强制终止:max_iters 兜底
- "模型永远不想停"案例与对策
- 小结
- 下节预告
第 18 节。前两节我们搞定了 ReAct 循环和工具调用协议,循环能转了。但一个更基础的问题浮出水面:什么时候停止循环?
看起来 trivial,实则是 Agent 里最容易翻车的地方之一。无限循环不只会烧掉你的 API 预算(第 13 节算过价),还会让整个程序卡死。这一节,我们把"停机"这件事讲得明明白白。
为什么终止条件如此要命
想象一个没有停机设计的 Agent Loop:
循环: 调用模型 解析 tool_calls 执行工具 回填结果 # —— 没有退出条件!——它会怎样?模型说"我要查天气",你查了;模型拿到结果说"再查一下湿度",你又查了;模型说"顺便看看明天",你还查……理论上它可以永远查下去。每转一圈就是一次 API 调用、一次 token 计费。几个小时不盯着,账单就爆了。
所以终止条件不是"锦上添花",是 Agent 的基础设施——没有它,系统根本不能上线。
三种终止条件全景
Agent Loop 的停机信号来自三个层面,我把它们统称为"停机三闸":
- 自然终止:模型这轮没带
tool_calls,说明它"想好了",直接输出答案收工。这是最理想的停机。 - 强制终止:循环次数达到上限
max_iters,即使模型还想调工具,也必须停(兜底)。 - 异常终止:执行中抛错(工具失败、网络断了、参数校验不过),按错误的性质决定是重试(第 19 节)还是直接停机。
三种条件不是互斥的,而是共同组成一道防线:自然终止保证"正常完事",强制终止保证"不会无限烧钱",异常终止保证"出错时不裸奔"。
finish_reason:官方给你的停机信号
聊停机,绕不开finish_reason这个字段。它是每次模型响应返回的一个字符串,官方用它告诉你"这轮为什么停了"。四类取值(第 14 节流式里也出现过,这里从循环角度再看一遍):
| finish_reason | 含义 | 对 Agent Loop 的意义 |
|---|---|---|
stop | 模型自然把话说完 | 一般意味着这一轮结束,可判断有无 tool_calls |
tool_calls | 模型要调工具 | 循环继续,执行工具后回填再问 |
length | 达到 max_tokens 被掐了 | 输出不完整,多数是异常信号,要处理 |
content_filter | 内容被安全策略拦截 | 不能当正常结果,需降级处理 |
对 Agent Loop 来说,判断"该不该继续循环"的核心逻辑其实很干净,我常写成这样:
finish_reason=resp.choices[0].finish_reason has_tool_calls=bool(msg.tool_calls)ifhas_tool_calls:# 模型明确要调工具 —— 继续循环...eliffinish_reason=="length":# 输出被截断,通常要提示模型,或至少不能直接当答案...else:# 模型想好了,输出最终答案,循环结束...把finish_reason和tool_calls结合起来看,停机的判断就不会错。
自然终止:没有 tool_calls 就是答案
自然终止是最爽的停机方式——模型这轮既不调工具,也没话说,直接把最终答案放进了content。你的循环看到"没有 tool_calls"就可以放心收工了。
这里有个细节值得留意:“没有 tool_calls"不等于"模型把话说完了”。要兼容finish_reason == length的情况(被 token 上限截断)。我在实际项目里的判断顺序是:
- 有 tool_calls → 继续循环(工具优先);
- 没有 tool_calls 且 finish_reason 正常 → 自然结束,取 content 为答案;
- 没有 tool_calls 但 finish_reason == length → 输出可能不完整,走异常/降级(第 19 节讲)。
强制终止:max_iters 兜底
自然终止能解决 90% 的情况,但剩下 10%——模型钻牛角尖,反复调用同一个工具不撒手。这时候你需要一道"硬闸门":最大迭代次数max_iters。
"""max_iters_demo.py —— max_iters 兜底停机的循环骨架(第18节) 运行环境:Python 3.12 用法: uv run python max_iters_demo.py """MAX_ITERS=10# 硬上限:最多转 10 圈iters=0whileTrue:iters+=1ifiters>MAX_ITERS:# 硬闸门兜底 —— 累计费用已控制住print(f"[停机] 达到 max_iters={MAX_ITERS},强制终止循环")break# resp = call_model(...) # 注释:真实项目里这里调 DeepSeek# 假设模型一直想调工具(模拟"停不下来"场景)has_tool_calls=Truemsg=Noneifhas_tool_calls:print(f"[第{iters}圈] 模型又想调工具,继续执行工具并回填…")# 执行工具、回填结果continueelse:print(f"[第{iters}圈] 模型想好了,输出最终答案")break# 运行输出(示意):# [第1圈] 模型又想调工具,继续执行工具并回填…# [第2圈] 模型又想调工具,继续执行工具并回填…# ...(一直循环到第 10 圈)# [停机] 达到 max_iters=10,强制终止循环这就是max_iters的意义:不管模型多"执着",循环最多转 10 圈,钱最多烧固定的量。真实生产里,MAX_ITERS还会和 context 长度、累计 token、总时间联动(第 20 节讲熔断)。
"模型永远不想停"案例与对策
光讲理论不够,讲个真实翻车案例,你会更有体感。
案例:早期我写的一个 Agent,让模型"帮我重构这个函数的命名"。结果模型进入了一种循环:read_file→ 看到代码 → 想改 →edit_file→ 改完又想看效果 →run_bash(pytest)→ 又发现问题 →read_file→ ……
它不是恶意,它就是觉得自己"还没做完",永远有下一个动作。我的初版循环没设 max_iters,跑了 47 圈,账单烧了几百块,程序才因为 context 超长被 API 拒绝而崩溃。
教训和对应策略:
- 必须设 max_iters——这是第一道/也是最重要的防线。哪怕设个 20 也会避免无限烧钱。
- 把"完成标准"写进系统提示词——告诉模型"当你认为任务已完成就停止,不要过度追求完美"。很多"停不下来"其实是提示词没给模型退出的台阶。
- 提示词里明确"何时该停"——例如"如果文件已修改且测试通过,直接输出总结,不要再次调用工具"。
- 配合熔断(第 20 节)——token 超限、时间超限、工具调用次数超限的三重保险,把"停不下来"从灾难降级成一次中断。
后两者属于工程和提示词的双重配合,第 20 节和第 53 节(系统提示词工程)会分别展开。
小结
- 停机三闸:自然终止(无 tool_calls)、强制终止(max_iters)、异常终止(出错),各有分工。
- finish_reason 是关键信号:结合
stop/tool_calls/length/content_filter判断"该不该停"。 - 自然终止最理想:模型带了完整答案就放心收工。
- max_iters 是硬基线:没有它,Agent 会"无限烧钱",这是上生产的第一道红线。
- "停不下来"要双管齐下:既要有熔断兜底,也要提示词给模型退出台阶。
下节预告
循环会停了,但新的敌人出现:失败。工具抛异常、网络抖动、API 限流、超时……一个稳健的 Agent 必须能优雅地吞掉这些错。下节讲生产级循环的容错设计——指数退避重试,与"哪些错该重试、哪些错必须停"的分级哲学。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~