乐于分享
好东西不私藏

OpenClaw升级后经常卡死+死循环,我让我的小龙虾自己修好了自己

OpenClaw升级后经常卡死+死循环,我让我的小龙虾自己修好了自己

OpenClaw 升级到 4.2 版本后一直运行稳定,没出过什么问题。后来隔了几个版本没更新,直到 4.22 发布时,我想看看这段时间 OpenClaw 到底有了多大的进步,就选择了升级。

结果升级后发现太多问题了——飞书卡死、频繁死循环。当时我不确定是飞书插件的问题还是 OpenClaw 本身的问题,于是之后 OpenClaw 一有更新就立刻升级,想看看卡死问题是否解决了。但无论怎么升级,4.23、4.26 都问题依旧。

4.2 之前真的很好用,升级之后太难用了。修改代码执行不了几次就会卡死 + 死循环,必须立马关掉 OpenClaw,不然要么耗尽调用次数,要么被 API 限制。

我的”小龙虾”项目还没完成啊,连续几天因为 OpenClaw 的问题都没怎么改动。

实在不想重新折腾 OpenClaw+ 飞书插件了,于是我选择了另一条路:用我自己的小龙虾来修改小龙虾,实现自我迭代。

之前一直都是用 OpenClaw 迭代我的小龙虾,还从来没有让小龙虾长时间大规模地修改自己的代码,有的话也只是小测试。这次实在是没办法了……

让我万万没想到的是,两天的自我迭代运行得非常非常好,远超预期。 除了几个异常处理因为流程安排不够合理,需要向大模型回复”继续”外,其他执行几乎都操作成功,而且主要是顺畅不卡。(我把失败率高的edit工具和websearch工具取消了特别是edit工具的取消应该是让调用流程更顺滑的一大功臣,避免了很多毫无意义的调用尝试,因为在openclaw中这两个工具的失败率一样高)

不过到了第三天,出现了一次跟 OpenClaw 一样的死循环问题(不知道是不是调用轮数过多,让大模型迷糊了)——执行一个命令连续 10 次错误还是不改变策略,只是 reasoning_content 有变化,tool_calls 的 arguments 一模一样,最后触发了连续失败次数限制才停止。

之前两天的顺利还一度让我以为是 OpenClaw 导致的死循环,现在看来大概率还是大模型本身的问题,只是在 OpenClaw 中发生的频率更高(这个跟提示词的大小有关——Token 数量、任务复杂度决定了大模型的反应)。

于是我给我的小龙虾加了一个重复调用守卫模块


一、模块功能

RepeatGuard 是一个重复调用守卫模块,用于防止 Agent 在同一个 turn 内无限重复执行相同的工具调用。

核心功能

功能
说明
签名追踪
为每次工具调用生成唯一签名,追踪相同调用
软警告
重复 N 次后触发警告,提示 AI 换策略
硬拦截
重复 5 次后跳过执行,防止死循环
消息注入
将警告以 user 角色注入对话流,权重最高

设计原则

  • • 纯函数,无副作用:不依赖外部状态,只负责检测
  • • 不拦截正常操作:给 AI 足够的试错空间
  • • 只检测,不执行:拦截逻辑由调用方assistant.js实现

二、配置常量

警告阈值(DEFAULT_THRESHOLDS)

各工具触发软警告的重复次数:

constDEFAULT_THRESHOLDS = {
run_command2,    // 相同命令重复 2 次就警告
read_file2,      // 相同读取重复 2 次就警告
write_file3,     // 相同路径重复 3 次才警告(允许分步修改)
edit_file2,      // 相同修改重复 2 次就警告
list_dir2,       // 相同目录重复 2 次就警告
web_fetch2,      // 相同 URL 重复 2 次就警告
default2,        // 其他工具默认 2 次
};

硬拦截阈值(BLOCK_THRESHOLD)

constBLOCK_THRESHOLD = 5;  // 重复 5 次后硬拦截,跳过执行

三、签名生成机制

签名规则

不同工具按不同维度判断”是否重复”:

工具
签名规则
说明
read_file 路径:offset:limit
包含分页参数,避免误杀分页读取
write_file 路径
只追踪路径,允许分步修改
edit_file 路径:oldTextHash:newTextHash
追踪修改内容 Hash,精准识别不同修改
list_dir 目录路径
只追踪目录
web_fetch URL
只追踪 URL
run_command 完整命令
追踪完整命令文本
其他工具
工具名:参数Hash
通用 Hash 比对

签名示例

read_file:C:\test.txt:1:2000
write_file:C:\output.md
run_command:echo hello
web_fetch:https://example.com

Hash 函数

使用 djb2 变种算法,将任意长度字符串映射为短字符串:

functionsimpleHash(str) {
if (!str) return'0';
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // 转为 32 位整数
  }
return hash.toString(16);
}

四、核心函数

checkRepeat(guardMap, toolCall, options?)

功能:检查是否重复调用,返回警告或拦截标记。

参数

参数
类型
说明
guardMap Map
守卫状态 Map,由调用方维护
toolCall object
OpenAI 格式的工具调用对象
options.warnThreshold number
可选,覆盖默认警告阈值

返回值

情况
返回值
未触发
null
软警告
{ signature, count, warning, block: false }
硬拦截
{ signature, count, warning, block: true, blockMessage }

五、工作流程

完整流程

AI 调用工具
    ↓
生成签名(getToolSignature)
    ↓
guardMap 计数 +1
    ↓
计数 >= 5?
    ├── 是 → 返回硬拦截标记(block: true)
    └── 否 → 计数 >= 警告阈值?
              ├── 是 → 返回软警告标记(block: false)
              └── 否 → 返回 null(正常)

行为对照表

重复次数
行为
tool 结果
user 警告
1 次
正常执行
真实结果
2~4 次
正常执行
真实结果
警告
≥5 次
硬拦截
拦截提示
警告

六、与 assistant.js 的集成

集成位置

repeat-guard 在 assistant.js 的工具执行循环中被调用:

// 1. 先保存工具调用请求
this.memory.push({ role'assistant'contentnulltool_calls: [toolCall] });

// 2. 调用守卫检查
const repeatAlert = checkRepeat(guardMap, toolCall);
if (repeatAlert) {
// 3. 保存警告到 memory
this.memory.push({ role'user'content: repeatAlert.warning });
    repeatWarnings.push(repeatAlert.warning);

if (repeatAlert.block) {
// 4. 硬拦截:跳过执行
        results.push(repeatAlert.blockMessage);
this.memory.push({ role'tool'tool_call_id: ..., content: repeatAlert.blockMessage });
continue;
    }
}

// 5. 正常执行
const result = awaitthis.executeToolCall(toolCall);
results.push(result);
this.memory.push({ role'tool'tool_call_id: ..., content: result });

guardMap 生命周期

  • • 创建:每个 turn 开始时创建新的 Map
  • • 使用:循环内每次工具调用前调用 checkRepeat
  • • 销毁:turn 结束后自动回收

七、常见问题

Q1: 为什么警告用 user 角色?

user 角色在 API 对话流中权重最高,AI 会将其视为”指令”而非”数据”,警示效果最强。

Q2: 硬拦截后为什么还要返回 tool 结果?

OpenAI API 规范要求:每次 tool_call 必须有对应的 tool 结果。如果不返回,对话流会断裂,AI 会报错。

Q3: 硬拦截的 tool_call_id 为什么用 call_blocked_ 前缀?

因为拦截时没有真实的 toolCall.idcontinue 跳过了后续逻辑),用时间戳生成唯一 ID,避免冲突。

Q4: 为什么 write_file 阈值是 3 次?

允许分步修改文件(比如先写头部、再写内容、最后写尾部),3 次足够覆盖常见场景。