Skip to content

14|停止条件:什么时候结束

上一篇让循环能扛住错误。但循环还有一个问题:什么时候停。

模型回答完了该结束,步数太多该结束,预算用完该结束。如果没有明确的停止条件,循环可能一直转下去——模型不断调工具、不断"再查一下",永远不收尾。本篇讲停止条件怎么设计。

停止条件不是越多越好

一个倾向是把所有能想到的停止条件都加上:步数上限、预算上限、时间上限、连续无进展上限、连续失败上限。加完发现条件之间会冲突——步数上限和预算上限可能同时触发,谁优先?过多条件增加判断复杂度,反而让"何时停"变模糊。

保持少量核心停止条件,每个都有明确优先级和处理逻辑。比"覆盖所有情况"更重要。

四个核心条件

python
def should_stop(state, settings) -> tuple[bool, str]:
    # 1. 回答完成:模型本轮没有工具调用,给出最终回答
    if state.last_response_has_tool_call is False:
        return True, "completed"

    # 2. 步数上限:防止无限循环
    if state.turn_count >= settings.max_turns:
        return True, "max_turns"

    # 3. 预算耗尽:token 用完
    if state.usage["total_tokens"] >= settings.token_budget:
        return True, "budget_exhausted"

    # 4. 连续被拒:权限连续拒绝,触发降级
    if state.consecutive_rejections >= 3:
        return True, "rejection_cascade"

    return False, ""

AIOps 概念图:四个停止信号

第一个是正常停止:模型本轮没有工具调用,说明它认为查完了,直接给出回答。这是循环的正常出口。

后面三个是保护性停止。步数上限防无限循环——模型可能陷入"查一下→再查一下"的死循环,到上限强制停。预算耗尽防成本失控。连续被拒触发降级——模型连续被权限系统拒绝,说明它在不断换方式尝试危险操作,该停而不是继续。

两层循环各自的停止判断

回到第 12 篇的两层循环:queryLoop 判断单轮是否结束,QueryEngine 判断整个会话是否结束。

queryLoop 层判断的是"本轮要不要继续调模型"。本轮模型返回了工具调用,执行完工具后继续下一轮 API 调用——这是单轮内的继续。本轮模型没有工具调用,给出最终回答——单轮结束。

QueryEngine 层判断的是"整个会话要不要结束"。单轮结束后,会话可能还要继续——用户可能还有后续问题,或者上一个问题没完全解决。会话级停止看的是更宏观的条件:任务完成、会话超时、用户主动结束。

两层不能混。把会话级停止条件写到 queryLoop 里,会让单轮循环承担不该有的判断;把单轮继续判断写到 QueryEngine 里,会让会话管理陷入执行细节。

max_turns 不是唯一保障

max_turns 是最常用的停止条件,但它不是安全性的唯一保障。

即使只设 3 步,如果每步都调用高成本的工具(全表扫描的数据库查询、大规模日志检索),3 步也可能对生产系统造成冲击。停止条件必须和工具权限、速率限制、成本预算配合使用,形成多层保护。max_turns 防的是循环不收敛,不是防单次工具调用的破坏性。

工具失败不一定要停

工具执行失败时,要不要停止?要看失败类型。

python
def handle_tool_failure(error, state) -> bool:
    """返回是否应该停止循环。"""
    if isinstance(error, NetworkTimeout):
        return False   # 网络超时,可能是临时抖动,值得重试
    if isinstance(error, PermissionDenied):
        return True    # 权限拒绝,配置问题不会自己恢复,停
    if isinstance(error, InvalidArgument):
        return True    # 参数非法,再试也是一样错,停
    return False       # 其他失败,记录后继续

网络超时可能是临时抖动,重试有价值。权限被拒是配置问题,不会自己恢复,重试没用。工具逻辑错误(参数非法)再试也是一样错。不同失败类型对应不同策略——一刀切"失败就停"或"失败就重试"都不对。

工具失败的信息要回填给模型(第 7 篇讲过),让模型知道这条路走不通,换思路。但如果模型反复尝试同一个失败工具,连续失败计数达到上限就该停。

停止之后

停止条件让循环能收敛。但有一种情况停止条件覆盖不了:模型没出错,也没回答完,只是被截断了——输出长度到了上限,任务没写完。

这不是错误(上一篇讲过,输出 token 耗尽是 incomplete 状态不是异常),也不是正常完成(任务没做完)。下一篇讲继续工作:被截断时怎么让模型把剩下的写完,又不陷入无限继续。