☰
DeepSeek-Agent-Harness-2026终极指南-第4章第18节-AgentLoop心脏-循环终止条件:模型什么时候想好了
2026/9/30 2:41:27 网站建设 项目流程

循环终止条件:模型什么时候"想好了"

循环不能无限转下去。怎么判断"模型想好了、该停"?全在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?

强制终止
输出超限提示

发生异常?

异常终止
记录错误并停

  • 自然终止:模型这轮没带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 上限截断)。我在实际项目里的判断顺序是:

  1. 有 tool_calls → 继续循环(工具优先);
  2. 没有 tool_calls 且 finish_reason 正常 → 自然结束,取 content 为答案;
  3. 没有 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 拒绝而崩溃。

教训和对应策略:

  1. 必须设 max_iters——这是第一道/也是最重要的防线。哪怕设个 20 也会避免无限烧钱。
  2. 把"完成标准"写进系统提示词——告诉模型"当你认为任务已完成就停止,不要过度追求完美"。很多"停不下来"其实是提示词没给模型退出的台阶。
  3. 提示词里明确"何时该停"——例如"如果文件已修改且测试通过,直接输出总结,不要再次调用工具"。
  4. 配合熔断(第 20 节)——token 超限、时间超限、工具调用次数超限的三重保险,把"停不下来"从灾难降级成一次中断。

后两者属于工程和提示词的双重配合,第 20 节和第 53 节(系统提示词工程)会分别展开。


小结

  1. 停机三闸:自然终止(无 tool_calls)、强制终止(max_iters)、异常终止(出错),各有分工。
  2. finish_reason 是关键信号:结合stop/tool_calls/length/content_filter判断"该不该停"。
  3. 自然终止最理想:模型带了完整答案就放心收工。
  4. max_iters 是硬基线:没有它,Agent 会"无限烧钱",这是上生产的第一道红线。
  5. "停不下来"要双管齐下:既要有熔断兜底,也要提示词给模型退出台阶。

下节预告

循环会停了,但新的敌人出现:失败。工具抛异常、网络抖动、API 限流、超时……一个稳健的 Agent 必须能优雅地吞掉这些错。下节讲生产级循环的容错设计——指数退避重试,与"哪些错该重试、哪些错必须停"的分级哲学。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询