MrWeiCodes/dsh-loop-guard ↗★ 9
@mrweicodes/dsh-loop-guard
Thinking-loop guard for dsh: observes the llm/stream waterfall and breaks a model that degrades into a loop. Detects two per-call shapes (reasoning-only calls, restated-material calls) and re-fires instead of latching; plus two mid-stream breakers that end a single degenerate call (maxRepeatedText / maxRepeatedCycleChars for visible output, maxRepeatedReasoningCycleChars for a reasoning bleed), which no post-call detector can reach — the reasoning rule is what ends the never-finishing turn of issue #5976. Ships an offline session analyzer that replays a session jsonl through the same detector. Compatible with every published dsh line from 0.1.2-rc.1 through 0.1.6-alpha.2. 适合需要防止模型陷入死循环、刷屏并自动恢复任务的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:MrWeiCodes/dsh-loop-guard使用
开箱即用
装好就能用,不需要任何配置。 默认值已按真实数据标定:
- 纯推理空转、复述上一步、推理内部周期循环 → 自动打断回合;
- 单次调用刷屏式重复可见输出 → 自动从流内部切断;
- 正常的长推理、正常的重复性输出(表格、日志、CSS、JSON)→ 不误伤。
熔断后会发生什么
会话继续,任务可以往下走——不需要你手动重启或重新发指令。
| 项 | 结果 |
|---|---|
| 本次调用 | 被切断,已产生的推理作为正常消息落盘 |
| 回合 | 以 turn/end 结束,原因是 { kind: 'completed' }——和正常完成一样 |
| 会话 | 存活,agent 回到 idle,可直接继续 |
| 已执行的工具调用 | 保留(tool/result 不回滚) |
| 注入的提示 | 一条 notice,说明「已连续重复 N 字符,响应被中途截断,不要重复,继续完成任务」 |
| 终止 chunk | 协议合法:切断前先闭合所有 block,再用 stop 结束,已用 DSH 自己的 @deepseek-ai/dsh-llm/invariant 验证 |
所以体验是:循环 → 在几百字符处被切断 → 提示它回到任务 → 继续干活。
为什么回合是 completed 而不是报错:早先的版本用 error finish,那会让 DSH 的会话渲染器拿不到 assistant/message(只产生 assistant/attempt),于是抛出
conversation Definition "assistant-step" withdrew materialized target "chat",而且 turn() 会在 throwError 处提前抛出、永远走不到「是否再开一个回合」那一行——既崩界面又无法续跑。改为闭合 block + stop finish 后走的是正常路径,两个问题一起消失。
代价是回合结束原因不再有辨识度(completed 与正常完成无法区分)。痕迹留在两处:注入的 notice,以及宿主日志里的 warn。这是刻意的取舍——熔断的目的是让会话继续可用,不是制造告警。
插件只结束这次调用,从不结束 agent——它永远不会调用 agent.cancel()。
自动续跑(resumeAfterBreak)
通常不需要开。 熔断本身不会结束会话,任务已经能继续;这个选项只是让它在切断后不等你发话就自己往下走:
- id: loop-guard
config:
resumeAfterBreak: true
关键在于「等」,这不是实现细节而是整个方案成立的前提:切断发生在流包装器里,此时 agent 还处于 running,而 DSH 在这个阶段刻意压制唤醒——wakeDriver() 只肯为 maintenance 或 aborted 挂起唤醒,所以此刻发 steer() / followup() 都不会置 wakeRequested,kick() 的 finally 找不到唤醒依据,会话就此停下。唤醒只有在 agent 回到 idle 后才有效,插件用 whenIdle() 等这个时刻。
续跑消息带纠正文本,从不是空的。空消息等于把同一段退化历史原样再喂一遍、不带任何新信息——那正是产生循环的输入。
该选项默认关闭:它是在模型已经证明「不能自主行动」之后、不经过你同意就再进一次模型。无人值守的长任务才建议打开。
配置
默认配置就能用,通常不需要动它。 只有想调整灵敏度或开启自动续跑时才需要改。
全部选项
interface Config {
// ── 跨调用判定(调用结束后) ──────────────────────────────
/** 连续多少次「停滞调用」后反应。默认 3。 */
maxThinkingSteps?: number
/** 单次调用的推理至少这么长才参与判定。默认 2048 字符。 */
minReasoningChars?: number
/** 跨调用相似度:上一步的推理有多少重现才算「复述」。0 关闭。默认 0.8。 */
similarityThreshold?: number
/** 命中后做什么:'warn' | 'steer'(默认) | 'cancel'。 */
escalate?: 'warn' | 'steer' | 'cancel'
/** 同一个 agent 最多反应多少次。默认 4。 */
maxFires?: number
/** escalate 为 'cancel' 时的取消原因。默认 'thinking-loop'。 */
cancelCause?: string
// ── 流内切断(调用进行中) ────────────────────────────────
/** 连续多少个相同的可见输出 chunk 就切断。0 关闭。默认 60。 */
maxRepeatedText?: number
/** 可见输出尾部最长重复周期(字符)。0 关闭。默认 512。 */
maxRepeatedCycleChars?: number
/** 可见输出尾部至少要重复多长才判定。默认 256。 */
minRepeatedCycleChars?: number
/** 推理尾部最长重复周期(字符)——**这条终止 #5976 的永不结束回合**。0 关闭。默认 512。 */
maxRepeatedReasoningCycleChars?: number
/** 推理尾部至少要重复多长才判定。默认 512。 */
minRepeatedReasoningCycleChars?: number
/** 推理里「重复行」累计到多少字符就切断——**这条抓没有周期的短语池重排**。0 关闭。默认 2048。 */
maxRepeatedReasoningLineChars?: number
/** 重复行占比要达到多少才判定。默认 0.6。 */
minRepeatedReasoningLineCoverage?: number
/** 行词汇的集中度:平均每行重复几次。低于此值不算短语池。默认 4。 */
minRepeatedReasoningLineConcentration?: number
/** 可见输出里「重复行」累计到多少字符就切断——**推理侧那条的文本侧镜像**。0 关闭。默认 2048。 */
maxRepeatedTextLineChars?: number
/** 可见输出重复行占比要达到多少才判定。默认 0.6。 */
minRepeatedTextLineCoverage?: number
/** 可见输出行词汇的集中度。默认 4。 */
minRepeatedTextLineConcentration?: number
// ── 切断后的行为 ──────────────────────────────────────────
/** 日志里标注的错误码。默认 'REPETITIVE_OUTPUT'。(切断本身走 `stop`,不产生错误) */
breakCode?: string
/** 切断时注入一条纠正提示,让模型回到原任务。默认 true。 */
breakCorrection?: boolean
/** 切断后等回合收尾,不等你发话就自动续跑。默认 false。 */
resumeAfterBreak?: boolean
}
常见需求(直接抄)