# 告别暴力熔断：Agent 死循环的分级治理阶梯（Warn → Block → Halt）

Source: https://blog.ferstar.org/posts/agent-graded-loop-guard-warn-block-halt/





跑复杂任务时，Agent 最容易出现的一种糟糕状态就是原地打转。

比如某个正则没搜到内容，模型就换着花样连续调 3~4 次完全相同的 `grep_search`；或者某个文件不存在，它依然反复用同一个路径去调用 `view_file`。每一次调用返回的都是空结果或错误，但模型依然固执地在原地重试。

以前处理死循环的手段一般很粗暴：
1. **硬跑到 max_turns 耗尽**：白白烧掉几十轮 API 调用，最后报个“超出最大轮次”崩溃；
2. **检测到重复直接 throw 抛异常**：立刻中断任务。但前面辛辛苦苦跑出来的所有中间状态和排查上下文，全部跟着一起泡汤了。

这两种做法都不理想。我们在运行时里加了分级工具循环守卫（Graded Loop Guard），用三级阶梯来处理。

```mermaid
flowchart TD
  subgraph Ingestion[工具调用入参分析]
    A[收到模型工具调用] --> B[计算参数哈希 canonical_args_hash]
    B --> C[区分工具类型: 只读幂等 vs 状态变更]
  end

  subgraph Ladder[三级处置阶梯]
    C --> D{连续相同调用次数}
    D -->|第 1 次重复 Count=2| E[Warn: 执行工具并在结果追加纠偏引导]
    D -->|第 2 次重复 Count=3| F[Block: 拦截物理执行, 返回合成错误]
    D -->|第 3 次重复 Count=4| G[Halt: 终止当前 Turn, 透传 repeated_tool_calls]
  end

  subgraph Outcome[执行与反馈]
    E --> H[模型读到提示主动修正思路]
    F --> I[省去无效 IO 开销, 迫使更换工具]
    G --> J[保留完整上下文, 明确告知打转原因]
  end

  Ingestion --> Ladder
```

---

## 1. 为什么不能一刀切？

判断模型是不是在“打转”，有几个细节必须区分：

- **只读工具 vs 变更工具**：只读工具（如 `grep`、`view_file`）重复查同一个文件很多时候无害，容忍度可以高一点；但变更工具（如 `write_to_file` 或执行写命令）如果重复调用且入参完全一致，往往极度危险。
- **模型自己其实有纠错能力**：很多时候模型只是陷入了某个局部死胡同。如果能在工具结果里明确提醒它一句“*你已经用相同参数查了 2 次且没结果，请换个思路*”，大多数现代模型在下一轮都能主动换个关键词或改用其他工具。

所以守卫的设计逻辑是：**先提示纠偏，再物理拦截，最后才熔断。**

---

## 2. 三级阶梯的具体行为

在主循环执行工具前的 `before_tool_call` 阶段做拦截判定：

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopGuardAction {
    /// 放行：首次或正常调用
    Allow,
    /// 警告：注入引导文本到 tool result，工具照常执行
    Warn,
    /// 拦截：阻止物理执行，合成错误结果返回
    Block,
    /// 终止：结束本轮交互
    Halt,
}
```

### 第一级：Warn（引导注入）

当检测到相同工具、相同入参在同一任务中出现第 2 次时：
- 工具照常执行；
- 工具返回结果后，系统在 `ToolResult` 末尾追加一段系统提示：
  > `[系统提示] 检测到你已连续以相同参数调用该工具且无新进展。请勿重复调用，请调整搜索词、修改路径或改用其他工具。`
- 绝大部分模型读到这句提示后，下一轮就会主动改策略。

### 第二级：Block（合成拦截）

如果模型无视警告，发起了第 3 次完全相同的调用：
- 守卫直接短路，不再跑底层真实的文件 IO 或命令执行；
- 在协议层合成一个标准的错误结果（`ToolResultContent::Error`）返回给模型，告知底层拒绝重复执行；
- 大模型协议要求每个 `tool_use` 必须有对应的 `tool_result` 闭合，合成错误既维持了协议格式，又省去了无效开销。

### 第三级：Halt（安全熔断）

如果重复调用达到第 4 次（`HALT_AFTER = 4`），说明模型彻底卡死了：
- 运行时主动结束本 Turn，触发 `RepeatedToolCalls`；
- 前端和日志明确显示具体的工具名称与参数，告诉用户因为重复调用某个命令而暂停；
- 之前的完整会话历史全部保留在本地，用户可以在当前进度上补一句话继续引导，不用从头重跑。

---

## 3. 入参等价性判定

为了防止因 JSON 键值顺序不同导致误判，守卫在计算 `canonical_args_hash` 时，会先对 JSON 的 Key 进行递归排序后再做哈希，保证 `{"a": 1, "b": 2}` 和 `{"b": 2, "a": 1}` 算出来的指纹完全一致。

同时将工具分为只读（`ReadOnly`）和变更（`Mutating`），对写操作赋予更严格的拦截策略。

---

## 4. 总结

在实际跑任务时，大部分边缘打转在 `Warn` 阶段就被模型自行纠正过来了。

把粗暴的强行中止，改成由浅入深的警告、拦截与熔断阶梯，既保护了 API 预算，也让长任务的完成率有了明显改善。

