MrWeiCodes/dsh-loop-guard ↗★ 9

@mrweicodes/dsh-loop-guard

监控并自动打断模型空转或复述的思维循环。 适合需要防止模型陷入死循环、刷屏并自动恢复任务的用户。

套件
@mrweicodes/dsh-loop-guard
相容性
待驗證
Harness 依賴範圍
>=0.1.2-rc.1 <0.1.3 || >=0.1.3-alpha.2 <0.1.4 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0 || >=0.1.7-alpha.1 <0.2.0
Cordis 依賴範圍
^4.0.2
版本
1.0.6
授權
MIT
最近更新
2026年9月26日

安裝

$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
}

常见需求(直接抄)