dsh-loop-guard
给 agent 的干活循环加两道刹:同一个动作反复撞墙就停下来告诉你,长任务跑久了中间说一句进展。
它治什么
两种真实发生过的失败形态(不是设想):
| 现象 | 实测 |
|---|---|
原地打转:写脚本 → 跑 → 改一点再写 → 再跑,撞十几遍 |
362 秒、16 次工具调用,用户只看到"它一直在思考" |
| 闷头干:一路跑工具,全程不说一句话 | 847 秒、59 次工具调用,中间零输出 |
两个都不是上下文太长的问题。装上下文压缩插件治不了这个 —— 上下文再短, 它也会在同一个坑里继续转。
它做什么
一、撞墙叫停
每次工具调用之前记账,命中任一条件就拒绝这次调用,并把一句话当作工具结果 喂回去:
- 同一个目标被动了 N 次(默认 6)—— 判据是文件路径或命令,不是参数全等。 这是关键:改写循环里每一版脚本内容都不同,参数指纹永远不同,但目标没变。
- 一模一样的调用重复 N 次(默认 3);
- 连续失败 N 次(默认 5),换工具也算 —— 说明这条路本身不通;
- 单轮工具调用总数超硬顶(默认 40)。
叫停时给的话是刻意写的,包含一条别的出路:
停一下:file:D:/x.py 被动了 12 次,这条路已经走不通了。不要再试同一件事的 另一个版本。现在直接回复用户,说清楚三件事:已经试过什么、卡在哪、需要他 决定什么。办不成就说办不成——这比再转十分钟有用。
最后一句是有实证的:卡住时模型的默认行为是继续试,以为再试一次就通了。 不给它"放弃"这个选项,它只会换个姿势继续撞。
二、长任务播报
一轮跑过 N 秒(默认 180)还没说过一句正文,就在下一步注入一句提醒。判据是 本轮开始到现在多久,不是"上次播报多久之前"——后者会被一个超长的工具调用 整个跳过。
已经说过话就不催:她自己已经在同步进展,插件不该多嘴。
装
dsh plugin add dsh-loop-guard --profile <你的 profile>
或者手动:仓库 clone 到任意目录,在 profile 的 package.json 里加
"dependencies": { "dsh-loop-guard": "link:D:/Plugins/dsh-loop-guard" }
并把 dsh-loop-guard 加进 dsh.profile.bundles。
零依赖,不碰任何 QQ / 人格 / 渠道相关的代码 —— 它挂在宿主的通用钩子上
(agent/pre-step 与 ctx.tools.guard()),对所有会话一视同仁,包括网页、
subagent、以及以后任何新渠道。
配置
全部有默认值,不配也能用。
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关 |
maxSameTarget |
6 |
同一个文件/命令被动几次算撞墙 |
maxSameCall |
3 |
完全相同的调用几次算撞墙 |
maxFailures |
5 |
连续失败几次算这条路不通 |
maxCallsPerTurn |
40 |
单轮工具调用硬顶 |
muteAfterTrip |
true |
叫停后同一轮不再重复吼 |
narrateAfterMs |
180000 |
跑多久没出声就提醒(0 = 不播报) |
narrateEveryMs |
240000 |
两次提醒的最小间隔 |
观测
只读接口 GET /loop-guard,返回累计统计与当前轮账本。没有"停用/重启"之类
的一键开关:这个插件是让它别把人晾着,不是给人一个遥控器。
边界(说清楚它不做什么)
- 不改 system prompt。注入走
agent/pre-step的消息追加,前缀缓存不受影响。 - 不跨轮记仇。每轮清账。昨天改过十次文件,今天一提文件就挨骂,那不是看住 它,是记仇。
- 守自己出错一律放行。宁可让它多转两圈,也不能把工具整个卡死。
- 不判断任务该不该做。它只管"你在原地打转"和"你太久没出声",不管做的是什么。
和其它插件的边界
这条界限不是「渠道 vs 通用」,是谁拥有状态:
| 插件 | 拥有什么 | 归它的判据 |
|---|---|---|
dsh-onebot-companion |
人格、情绪、亲密度、风格、日记、约定 —— 那份按 chat 存的 JSON | 任何写这份状态的代码都留在它那儿,不管消息从哪个渠道进来 |
dsh-loop-guard(本插件) |
只拥有「本轮」的指纹账本,轮末即焚 | 只需要知道「agent 在循环里干了什么」、不需要知道对面是谁的 |
dsh-second-opinion |
只拥有 sessions.state 里的顾问槽位 |
一次性、同步、不写别人状态的 |
所以本插件不认识 chat、人格、亲密度,也不该去认。 它的通用性恰恰来自 「它什么都不懂」—— 一旦让它知道「谁在跟它说话」,它就变成了另一个 companion。
单独装任何一个都必须完整工作。两个插件之间可以配合,不能依赖:
要配合就走可选的 ctx.inject,对方不在就退化成当前行为。写成「本插件读
companion 的配置文件」,那叫耦合,而且 companion 一改字段名这边会静默坏掉
(守卫是全 try/catch 放行的,坏起来不报错)。
支持的 DSH 版本
>=0.2.1-rc.2。用到的两个钩子(agent/pre-step waterfall 的
{global: true} 选项、ctx.tools.guard())在 0.2.1 系列里都有。
开发
node test/selftest.mjs
测试只覆盖纯函数(判据与文案),不依赖真实宿主 —— 真挂没挂上要靠实测。 其中一组断言直接复现了 2026-10-26 那次 turn 33 的调用序列,钉住"这种循环一定会 在十几步之内被叫停"。
No comments yet. Be the first to write one.