1. SecurityClaw 学习笔记/

15 评估、恢复与停止条件:让 Agent 不盲目重试

结论先说:停止不等于失败,恢复也不等于重试 #

Agent 系统最容易做假的地方,不是 planning,而是 ending。

很多实现会把所有结果都压成两种:

  • 成功
  • 失败

但真实系统至少还需要两种中间状态:

  • 证据不足
  • 计划耗尽,需要恢复

如果没有这两层,你的 Agent 只会两种坏行为:

  • 明明该停却一直转
  • 明明该换路却只会重试

一、先看最小判断系统 #

def should_loop(state: AgentState) -> Literal["continue", "stop", "recover"]:
    evaluation = state.get("evaluation", {})
    steps = state.get("steps", 0)
    max_steps = state.get("max_steps", 5)

    if steps >= max_steps:
        return "stop"
    if evaluation.get("evidence_sufficient"):
        return "stop"
    if evaluation.get("plan_exhausted"):
        return "recover"
    return "continue"

这段逻辑已经说明了三件事:

  1. stop 不只代表成功,也可能代表预算边界
  2. recover 不是继续原路,而是进入补救策略
  3. continue 是最保守的选择,不是默认答案

二、为什么评估层必须先于恢复层存在 #

恢复不是“多试一次”,恢复的前提是:

你得先知道自己为什么没成功。

所以评估层真正要做的是把结果翻译成一个结构化判断:

evaluation = {
    "evidence_sufficient": False,
    "plan_exhausted": False,
    "reasoning": "查询成功但无匹配,需要扩大证据来源",
}

这一步的作用是把执行结果从“原始输出”提升成“决策输入”。

如果没有评估层,恢复层根本不知道:

  • 是工具报错了
  • 还是结果为空
  • 还是证据不足
  • 还是预算耗尽

三、空结果为什么不能直接算成功 #

这是很多 Agent 系统最容易偷懒的一步。

错误思路 #

if tool_result.status == "ok":
    return success

正确思路 #

if tool_result.status == "ok" and not records:
    evaluation["plan_exhausted"] = True
    evaluation["reasoning"] = "查询成功但无匹配,需要调整搜索策略"

也就是说:

  • 执行成功 不等于 问题得到回答
  • 返回空 也不等于 可以结束

在安全场景里,“没有查到”常常意味着:

  • 搜索条件太窄
  • 数据源不对
  • 当前技能不适合这个问题

所以零结果更像是一种恢复信号,而不是成功信号。


四、恢复为什么不是“再试一次” #

恢复的本质是:

  1. 诊断当前失败原因
  2. 选择一种不同的策略
  3. 重新进入循环
def recover_node(state: AgentState) -> dict:
    reason = state["evaluation"]["reasoning"]
    if "无匹配" in reason:
        return {"recovery_action": "expand_search_scope", "next_skill": "query_alternative_db"}
    elif "覆盖率不足" in reason:
        return {"recovery_action": "broaden_evidence", "next_skill": "query_broader_source"}
    elif "执行异常" in reason:
        return {"recovery_action": "fallback_tool", "next_skill": "fallback_skill"}
    return {"recovery_action": "restart_plan"}

这和简单重试的差别 #

简单重试 #

同样的输入,再跑一遍

恢复 #

改变路径、改变工具、改变搜索范围、改变策略

所以恢复是“重规划的一种特化形式”,不是“重复执行”。


五、什么时候必须强制停止 #

一个诚实的 Agent 必须承认三类边界:

5.1 步数耗尽 #

if steps >= max_steps:
    return "stop"

5.2 token / 预算耗尽 #

if token_state == "exhausted":
    return "stop"

5.3 没有可恢复路径 #

if evaluation["plan_exhausted"] and not has_fallback_path(state):
    return "stop"

这三类边界说明了一件事:

停止不是系统认输,而是系统拒绝继续在低价值路径上消耗资源。


六、Token 与恢复为什么要耦合 #

这点常被低估。

如果恢复系统不知道 token 或预算状态,就可能出现:

  • 恢复动作本身非常贵
  • 还没恢复成功,预算先烧光
  • 最终系统在失败路径里消耗比成功路径更多资源

所以恢复层至少要能读取:

class TokenUsageHandler:
    def consume(self, tokens: int) -> bool:
        self.used_tokens += tokens
        if self.used_tokens >= self.max_tokens:
            self.state = "exhausted"
            return False
        return True

这不是性能优化,而是停止条件的一部分。


七、设计 tradeoff:恢复策略应该集中还是拆开 #

方案 A:一个 recover_node 统一处理所有恢复 #

优点:集中、简单 缺点:逻辑容易膨胀成大 if-else

方案 B:每种失败类型对应独立恢复策略 #

优点:清晰、可扩展 缺点:模块更多,调度更复杂

如果系统继续长大,我会倾向 B。但在 SecurityClaw 当前规模下,A 是可以接受的阶段性设计。


八、这一篇真正要记住的框架 #

评估、恢复、停止的关系可以压成:

执行结果
  -> 评估:结果说明了什么
  -> 恢复:下一步该换什么路径
  -> 停止:什么时候必须承认边界

真正可靠的 Agent,不是永远努力,而是知道什么时候:

  • 继续
  • 转向
  • 停止

九、验证命令 #

pytest tests/ -k "recover or evaluate or zero_records or max_steps" -v