15 评估、恢复与停止条件:让 Agent 不盲目重试
·1316 字·3 分钟
结论先说:停止不等于失败,恢复也不等于重试 #
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"
这段逻辑已经说明了三件事:
stop不只代表成功,也可能代表预算边界recover不是继续原路,而是进入补救策略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"] = "查询成功但无匹配,需要调整搜索策略"
也就是说:
- 执行成功 不等于 问题得到回答
- 返回空 也不等于 可以结束
在安全场景里,“没有查到”常常意味着:
- 搜索条件太窄
- 数据源不对
- 当前技能不适合这个问题
所以零结果更像是一种恢复信号,而不是成功信号。
四、恢复为什么不是“再试一次” #
恢复的本质是:
- 诊断当前失败原因
- 选择一种不同的策略
- 重新进入循环
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