Agent 终态判定:何时该停止思考、给出最终回复

一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了

Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接了搜索引擎 API,它第一轮搜了一下,觉得信息不够全,第二轮换了关键词再搜,第三轮觉得 "还有 3 篇相关文章没读",第四轮读了之后发现"这篇文章引用了另一篇——我再搜一下"……十二轮后用户早就不在了。

Agent 的终止判定是一个比想象中更复杂的问题。不是因为怎么停止很难,而是"什么情况下应该停止"需要几个维度的判断:信息完备度够不够、用户是否有明确的时间预期、本轮回答的质量是否已经"够好"。

当前大多数 Agent 框架的终止逻辑极其简单。要么是固定步数限制(最多调用 5 步就停),要么是 LLM 自己说了算(它觉得该停了就停了)。固定步数太硬——复杂任务 5 步不够,简单任务 1 步就够了。LLM 自己判断太软——它可能永远不觉得"信息够了"。

二、底层机制与原理剖析

Agent 终态判定的多维度决策模型:

四个判定维度:

信息增益(Information Gain):每一步工具调用都会带回新的信息。衡量这一步带来的信息增量——如果新信息与已收集信息的语义重复度超过 95%,说明再搜下去也不会获得新信息了(边际收益递减)。计算方法:新信息的嵌入向量 vs 已收集信息的嵌入向量之间的余弦相似度。

步数预算:不是固定上限(如 5 步),而是动态预算。简单问题(如"今天天气怎么样")1-2 步足够,复杂问题(如"分析 A 公司财报并做竞品对比")给 8-10 步。LLM 在初始阶段评估任务复杂度,生成预算值。

置信度阈值:每一步后让 LLM 自评当前答案的置信度(0-100%)。超过阈值(如 85%)就输出答案;未超过就继续。这比固定步数更弹性——有时 1 步就能高置信度回答,有时 8 步才能。

用户显式信号:最高优先级的停止信号。用户的任何"结束意图"的表达("可以了""够了""先这样吧""明白了")都应该立即停止推理并输出最终回答。

三、生产级代码实现

"""
Agent 终态判定器
四个维度:信息增益、步数预算、置信度阈值、用户显式信号
"""
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
import numpy as np
from enum import Enum

class StopReason(Enum):
    INFORMATION_SATURATED = "info_saturated"   # 信息饱和
    BUDGET_EXCEEDED = "budget_exceeded"         # 步数预算耗尽
    CONFIDENCE_REACHED = "confidence_reached"   # 置信度达标
    USER_SIGNAL = "user_signal"                 # 用户显式终止
    MAX_STEPS_FORCED = "max_steps_forced"       # 硬上限强制终止

@dataclass
class StopDecision:
    """终态判定结果"""
    should_stop: bool
    reason: StopReason
    confidence: float             # 当前置信度
    information_gain: float       # 最后一步的信息增益
    steps_used: int               # 已用步数
    steps_budget: int             # 总步数预算
    detail: str                   # 人类可读的决策说明

@dataclass
class AgentContext:
    """Agent 当前步骤的上下文"""
    steps_taken: int
    steps_budget: int
    collected_info: List[str]     # 已收集的信息片段
    last_action_result: str       # 最后一步动作的结果
    current_confidence: float     # 当前置信度 (0-100)
    user_last_message: str        # 用户最后一条消息

class TerminationJudge:
    """Agent 终态判定器"""
    
    def __init__(
        self,
        embed_fn,
        confidence_threshold: float = 85.0,
        information_gain_threshold: float = 0.05,
        max_steps_fallback: int = 15,     # 绝对硬上限
    ):
        self.embed_fn = embed_fn
        self.confidence_threshold = confidence_threshold
        self.information_gain_threshold = information_gain_threshold
        self.max_steps_fallback = max_steps_fallback
        
        # 用户终止信号词
        self.stop_signals = [
            "可以了", "够了", "先这样", "明白了", "清楚了",
            "不用了", "够了谢谢", "就这样吧", "好的谢谢",
            "ok", "got it", "that's enough", "stop",
        ]
    
    def judge(self, context: AgentContext) -> StopDecision:
        """
        判定是否应该停止
        
        判定优先级:用户信号 > 信息饱和 > 置信度达标 > 步数预算 > 硬上限
        """
        
        # === 优先级 1: 用户显式信号 ===
        for signal in self.stop_signals:
            if signal in context.user_last_message.lower():
                return StopDecision(
                    should_stop=True,
                    reason=StopReason.USER_SIGNAL,
                    confidence=context.current_confidence,
                    information_gain=0,
                    steps_used=context.steps_taken,
                    steps_budget=context.steps_budget,
                    detail=f"用户发出了终止信号: '{signal}'",
                )
        
        # === 优先级 2: 信息饱和(最后一步的信息增益太低)===
        info_gain = self._calculate_information_gain(
            context.last_action_result,
            context.collected_info,
        )
        
        if context.steps_taken >= 2 and info_gain < self.information_gain_threshold:
            return StopDecision(
                should_stop=True,
                reason=StopReason.INFORMATION_SATURATED,
                confidence=context.current_confidence,
                information_gain=info_gain,
                steps_used=context.steps_taken,
                steps_budget=context.steps_budget,
                detail=f"信息增益 {info_gain:.3f} 低于阈值 {self.information_gain_threshold},继续搜索收益递减",
            )
        
        # === 优先级 3: 置信度达标 ===
        if context.current_confidence >= self.confidence_threshold:
            return StopDecision(
                should_stop=True,
                reason=StopReason.CONFIDENCE_REACHED,
                confidence=context.current_confidence,
                information_gain=info_gain,
                steps_used=context.steps_taken,
                steps_budget=context.steps_budget,
                detail=f"置信度 {context.current_confidence}% >= 阈值 {self.confidence_threshold}%",
            )
        
        # === 优先级 4: 步数预算耗尽 ===
        if context.steps_taken >= context.steps_budget:
            return StopDecision(
                should_stop=True,
                reason=StopReason.BUDGET_EXCEEDED,
                confidence=context.current_confidence,
                information_gain=info_gain,
                steps_used=context.steps_taken,
                steps_budget=context.steps_budget,
                detail=f"已用 {context.steps_taken}/{context.steps_budget} 步,预算耗尽",
            )
        
        # === 优先级 5: 绝对硬上限 ===
        if context.steps_taken >= self.max_steps_fallback:
            return StopDecision(
                should_stop=True,
                reason=StopReason.MAX_STEPS_FORCED,
                confidence=context.current_confidence,
                information_gain=info_gain,
                steps_used=context.steps_taken,
                steps_budget=self.max_steps_fallback,
                detail=f"达到绝对上限 {self.max_steps_fallback} 步,强制终止",
            )
        
        # === 继续 ===
        return StopDecision(
            should_stop=False,
            reason=StopReason.INFORMATION_SATURATED,  # 实际未触发
            confidence=context.current_confidence,
            information_gain=info_gain,
            steps_used=context.steps_taken,
            steps_budget=context.steps_budget,
            detail="继续下一步推理",
        )
    
    def _calculate_information_gain(
        self, new_info: str, existing_info: List[str]
    ) -> float:
        """
        计算新信息相对于已有信息的信息增益
        
        方法:计算新信息向量与已有信息向量的最大余弦相似度。
        1 - 最大相似度 = 信息增益。
        新信息与已有信息越相似,增益越低。
        """
        if not existing_info or not new_info:
            return 1.0  # 没有旧信息,新信息的增益是 100%
        
        try:
            new_embed = self.embed_fn(new_info)
            
            # 计算与每条已有信息的相似度,取最大值
            max_similarity = 0.0
            for info in existing_info[-5:]:  # 只比较最近 5 条,降低计算量
                try:
                    info_embed = self.embed_fn(info)
                    sim = self._cosine_sim(new_embed, info_embed)
                    max_similarity = max(max_similarity, sim)
                except Exception:
                    continue
            
            # 增益 = 1 - 最大相似度
            gain = 1.0 - max_similarity
            return max(0.0, gain)
            
        except Exception:
            return 0.5  # 计算失败时返回中性值
    
    @staticmethod
    def _cosine_sim(a, b):
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)
    
    def estimate_budget(self, user_query: str) -> int:
        """
        根据用户问题复杂度估算步数预算
        
        简单启发式(生产环境应用 LLM 估计复杂度):
        - 短问题(< 20 字):2-3 步
        - 中等问题(20-50 字):4-6 步
        - 长问题(> 50 字):7-10 步
        """
        length = len(user_query)
        if length < 20:
            return 3
        elif length < 50:
            return 6
        elif length < 100:
            return 8
        else:
            return 10
    
    def build_stop_message(self, decision: StopDecision) -> str:
        """根据终止原因构建给用户的说明"""
        if decision.reason == StopReason.BUDGET_EXCEEDED:
            return (
                f"(已进行 {decision.steps_used} 步分析,"
                f"当前置信度:{decision.confidence:.0f}%。"
                f"如需更深入的分析,可以提出更具体的问题。)"
            )
        elif decision.reason == StopReason.CONFIDENCE_REACHED:
            return ""
        elif decision.reason == StopReason.INFORMATION_SATURATED:
            return (
                f"(搜索到的信息开始重复,"
                f"当前置信度:{decision.confidence:.0f}%。"
                f"如需更多信息源,可以指定方向。)"
            )
        else:
            return ""

四、边界分析与架构权衡

信息增益的准确性

用 embedding 向量计算语义相似度作为信息增益的代理变量,会产生误判。两段文字在语义上相似但包含了关键的差异信息(如两个不同城市的人口数据——结构相同但数据不同)——语义相似度高但信息增益应该高。这是当前方法的盲区。

置信度评分的可靠性

让 LLM 自己评估自己的置信度,存在"过度自信"或"不自信"的偏差。模型的置信度评估本身就是不可靠的——它可能对错误答案给出 90% 的置信度。可以用 Self-Consistency 方法(让模型多次回答同一问题,统计答案的一致程度)来增加置信度评分的可信度。

适用边界

最适合多步推理的 Agent(如研究助手、数据分析 Agent、代码调试 Agent)。任务步数在 3-15 步之间、需要多轮信息检索或分析的场景。

禁用场景

不适合单轮问答的 Agent——不需要多步推理的场景不存在终止判定的问题。也不适合实时对话——用户应该在任意时刻都能打断 Agent。

五、总结

Agent 终态判定不是简单的最大步数限制。四个维度的综合决策——用户显式信号(优先级最高)、信息增益(边际收益判断)、置信度阈值(答案质量判断)、步数预算(资源边界)。当任一维度触发时,Agent 应在回答中附上置信度和停止理由,让用户知道"这个回答是经过几轮推理得出的,可信度如何"。关键是让 Agent 能在"过度推理"和"草率给出答案"之间找到平衡。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐