dsh-loop-breaker
Manifest validDSH Streaming Repeat Circuit Breaker: at the llm/stream layer, evaluate as chunks arrive; when a degraded repeat is detected, cut it off on the spot (the underlying HTTP request is then cancelled, token billing stops), sanitizing the content upon cutoff to avoid polluting the context
dsh-loop-breaker
只想要“照着做一遍”的封装版本(含安装、配置与排错表),见同名技能包 llm-degeneration-loop-breaker;本仓库是代码本体。
只要检测器、不想装 DSH? 核心判定逻辑已抽成零依赖独立包 → core/README.md
DSH 的流式复读熔断器。解决 DeepSeek flash 偶发的「退化复读」——思考链或正文反复输出
同一小段内容(好的 / 好的,执行 / 好),长时间不产出有效结果,按输出速度白白烧 token。
装好之后:命中复读 → 当场掐断生成(底层 HTTP 随即取消,token 停止计费)→ 同一轮注入一句 补救提示让模型重新作答。
为什么不用现成的 dsh-thinking-loop-guard
社区已有一个 unknowbug/dsh-thinking-loop-guard
解决同类问题,但它只有 turn 边界 检测(挂在 agent/turn-stopping 上读完整 reasoning)。
问题在于:turn 边界 = 这一轮的 token 已经烧完了。它能阻止循环蔓延到下一轮,却救不回
正在烧的那一轮。
它的 README 明确断言「DSH 直连远程 API,没有中间层能检测并打断流式响应」。这个判断是错的:
DSH 的 @deepseek-ai/dsh-llm 暴露了 llm/stream 瀑布事件,包住每一次流式模型调用:
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
本插件就挂在这里,因此能做到它做不到的事:流式过程中命中,就地掐断。
工作机制
模型流式输出
→ llm/stream 瀑布(本插件包住这条流)
→ 逐块统计 reasoning / text
→ 规则任一命中
→ 停止拉取上游
└─ 适配器生成器 finally 执行 consumer.abort()
└─ 底层 HTTP 请求当场取消,token 停止计费
→ 补上合法的流收尾(闭合内容块 + finish:{kind:'stop'})
└─ L0 消毒:写回会话的正文换成「开头一小段 + 占位符」,复读原文不落盘
→ 本轮 step 正常结束(没有 tool call → 无后续动作)
→ agent/turn-stopping:注入一句补救提示,同一轮重新作答
第二层是关键体验保障:如果只掐断不补救,模型往往只产出了半截思考、没产出正文, 用户会拿到一个「只有思考、没有答案」的空轮次。
L0 消毒:为什么掐断时还要改写正文
掐断只解决了「继续烧 token」,没解决「垃圾进历史」。补 block-end 时如果把复读原文
(可能是几百遍「好的执行」)原样写回,它会被持久化进会话历史,成为下一次生成的先验——
同前缀同退化是自回归模型的固有性质,于是出现「掐断后重发、甚至新开对话仍然复读」。
因此写回前先过一遍 sanitizeBlockText():保留开头 sanitizeHeadChars 字(保住「刚才在
做什么」的语义),其余替换成占位符;若开头本身就已字面塌缩(用字单调且碎片化),
则一段都不保留。可用 sanitize: false 回退成旧行为(排查期想看模型到底复读了什么时有用)。
为什么必须补 finish 块
dsh-llm 内置 validateStream(以 prepend: true 注册,位于本插件外层)校验流协议:
- 流必须以
finish块收尾,否则LLM stream ended without a terminal finish chunk; finish.reason.kind不是error/aborted时,不允许有未闭合的内容块。
所以掐断时必须先为每个打开的内容块补 block-end,再补 finish:{kind:'stop'},顺序不能反。
用 aborted 虽然天然允许未闭合块,但 agent loop 会把它当失败抛出,用户看到报错。
开着工具调用块时不掐断
半截的工具调用参数是无意义且危险的(可能被执行、报 JSON 解析错)。因此只要还有非
text/reasoning 的块开着,就推迟掐断;等工具块关闭后若仍在复读,恢复掐断(有测试覆盖)。
检测规则与阈值依据
复读有五种真实形态,规则分别针对:
| 规则 | 抓什么 | 默认阈值 |
|---|---|---|
tight-period-loop | 碎片周期循环:做。→(执行)→好。→执行。 这类在决策点原地打转 | 尾部 200 字内存在周期 p∈[3,48]、自吻合率 ≥ 0.92,且窗口内片段种类 ≤6 |
char-collapse-loop | 字符集塌缩:整段用字单调到只有个位数种字 | 尾部窗口去重字符占比 < 0.10、片段长度中位 <10、片段种类 ≤6 |
sentence-recycle | 换着说法反复重下同一个决定 | 句子回收率 ≥ 0.80(且 ≥20 句、窗口 ≥600 字) |
gram-recycle-16 | 大段分析反复重算 | 16-gram 回收率 ≥ 0.80(窗口 ≥900 字) |
tight-phrase-loop | 好的/好/好的执行 这类短句空转 | 窗口内 3-gram 冗余度 ≥ 0.90(窗口 ≥400 字) |
max-chars / max-reasoning-chars | 兜底硬上限 | 20 万字 / 12 万字(0 = 关闭) |
判据为什么从「重复率」改成「周期长度」:前三类本质都在量「重复得多不多」,而正常的 模板化长输出重复得一样多——40 个结构相同、只有函数名不同的函数体 16-gram 回收率 0.808, 比真实复读样本 real_loop3 的 0.676 还高,光靠重复率阈值分不开这两类。
真正能分开的是重复单元的周期长度:退化复读的单元是几到几十字的无意义碎片(周期 4~48 字),正常模板化输出的单元是一个函数体、一行表格(周期几百字),差两个数量级。 所以用「短周期严格重复」当判据,能精确抓住碎片循环,同时天然放过结构性雷同的批量内容。
另有一个零信息增量信号(连续 stallMinChars 字没产生任何新的 3-gram)不单独触发,
只作为助推:命中它时放宽上面两条的闸门(片段种类 ≤10、去重字符占比 <0.16)。单独用会误杀
批量同构内容——那确实没有新 3-gram,但不是退化。
还要注意「用字单调」与「碎片循环」都必须叠加片段种类极少这一条:图表型文本(大量 #
与数字)用字同样单调,但每一行都不同,靠这道闸门排除在外。
阈值不是拍脑袋定的,是用真实样本标定的(samples/ + test.mjs):
| 样本 | 句子回收率 | 16-gram 回收率 |
|---|---|---|
| 3 份真实复读会话记录 | 1.000 / 1.000 / 1.000 | 0.68 ~ 0.78 |
| 6 份官方中文文档 + 3 份源码 + 60 行长表格 | 0.000 ~ 0.255 | 0.02 ~ 0.15 |
两类之间留了很大余量,因此阈值取中间,宁可漏过不可误杀。实测命中位置(2026-09-12 加入 短周期与塌缩规则后重测):
- 三份真实复读样本:第 626 / 636 / 1723 字命中(
sentence-recycle) - 碎片周期循环(
做/执行/好):第 135 字命中(tight-period-loop) - 短句空转(
好的,执行):第 132 字命中(tight-period-loop) - 「正常前缀 + 短句空转」:第 1403 字命中(前面的正常前缀把新规则的短窗口稀释了,回落到周期规则)
即复读开始后约 130~2000 字内被拦下,碎片型复读由原来的 400 字级提前到百字级。 作为对照,ollama 那份报告里一次失控跑了 31M token。
配置
- id: loop-breaker
name: 'dsh-loop-breaker' # 与包名一致(install.ps1 就装在这个名字下);若装在 @local\ 作用域里则写 '@local/dsh-loop-breaker'
config:
enabled: true # 总开关
recover: true # 命中后是否注入补救提示
maxRecoveriesPerTurn: 2 # 同一轮最多补救几次(按轮号归零,跨轮重新计数)
nudge: '' # 补救提示文本,留空用内置文案
# --- L0 消毒:防止复读原文落盘污染后续生成 ---
sanitize: true # 是否改写掐断时写回会话的正文
sanitizeHeadChars: 240 # 保留原文开头的字数(0 = 全丢;开头已塌缩时同样全丢)
degradePlaceholder: '' # 替换复读原文的占位符,留空用内置文案
# --- 检测阈值 ---
windowChars: 1600 # 滚动统计窗口(归一化字符数)
periodMax: 48 # 短周期检测的最大周期;超过它的重复视为长块模板重复
periodMatch: 0.92 # 短周期自吻合率阈值
periodMaxPieces: 6 # 窗口内片段种类上限(用字单调 ≠ 复读,还要碎片少)
collapseUniqueRatio: 0.10 # 去重字符占比阈值(正常中文 0.3~0.6)
stallMinChars: 400 # 零信息增量助推门槛(0 = 关闭)
sentRecycle: 0.80 # 句子回收率阈值
gramRecycle16: 0.80 # 16-gram 回收率阈值
tightRedundancy: 0.90 # 窗口字面冗余度阈值
maxChars: 200000 # 单次输出硬上限(0 = 关闭)
maxReasoningChars: 120000 # 单次思考链硬上限(0 = 关闭)
调参方向:
- 误杀正常长输出 → 调高
sentRecycle/gramRecycle16/tightRedundancy,或调大windowChars - 误杀批量同构内容(清单 / 条款 / 近似函数) → 这类靠句子回收命中,调高
sentRecycle - 误杀碎片循环判定 → 调高
periodMatch,或调低periodMax(周期越小越像退化) - 漏过复读 → 反向调低;或调小
windowChars(循环占比更高,更早命中) - 只想掐断不想要补救 →
recover: false - 排查期想看模型到底复读了什么 →
sanitize: false(让原文落盘)
安装
三种装法,按你的情况选一种。
1) 装成 DSH 插件(本项目的原始形态)
git clone https://github.com/CHIP-PHILO-GH/dsh-loop-breaker
cd dsh-loop-breaker
pwsh -File install.ps1
install.ps1 是幂等的,可重复执行。默认装到
$env:DSH_HOME\profiles\node_modules\dsh-loop-breaker(DSH_HOME 未设置时取 ~/.dsh),
也可以用 -Destination 指定别处。
装载行在 $env:DSH_HOME\profiles\<profile>\cordis.patch.yml。
⚠️ 插件市场装卸会回滚该文件,若行丢失按上面配置块补回即可。
改动后需重启 dsh web 生效——本项目不做热重载。
(用 Windows PowerShell 5.1 跑 install.ps1 会因无 BOM 的中文被按 ANSI 误读而报错,请用 pwsh 7。)
2) 只要检测器,不装 DSH
检测器本体是零依赖的独立包 core/,不依赖 DSH,也不依赖任何第三方包:
node core\cli.mjs 输出.txt # 直接判一段输出是不是复读了
node core\cli.mjs --self-test # 冒烟自检
import { createLoopDetector } from 'dsh-loop-breaker/core'
详见 core/README.md。
3) 适用形态 / 不适用
它主要抓紧邻的短周期复读:例如“做。→(执行)→好。→执行。”这类由少数短碎片连续拼装的流式退化输出。基准中的合成样本显示,长间隔重复、较短英文周期、以及长度不足的样本可能漏检;这些不是通用复读检测器应承诺覆盖的形态。
已知边界是:长技术报告、含大量近似函数的代码等正常长输出,可能因结构性重复而误杀;仓库测试和基准结果已保留这些边界。请不要把本项目当作通用复读检测器,也不要把基准数字解读为真实生产分布上的准确率。
4) 只想跑一遍测试验证它有效
不需要安装任何东西,也不需要联网:
node test.mjs # 流式正/负样本回归(24 例)
node test-false-positive.mjs # 误杀对抗测试(10 例真实长输出形态)
node benchmarks/lab/run-benchmark.mjs # 合成量化基准,详见 benchmarks/README.md
node core\cli.mjs --self-test # 核心包自检
这三条就是 CI 里跑的全部内容,任何 Node ≥18 的机器上都能跑通。
插件集成测试(33 例:流协议合法性、上游取消、工具块推迟、补救预算、L0 消毒)
本插件运行时依赖 DSH 宿主提供的 @deepseek-ai/dsh-llm 与
@deepseek-ai/schemastery(已在 package.json 里以 peerDependencies 声明,不把宿主运行时复制进自身依赖树),
只能在装了 DSH 的环境里从安装目录跑:
node "$env:DSH_HOME\profiles\node_modules\dsh-loop-breaker\tests\guard.test.mjs"
它复刻了 dsh-llm 的 validateStream,专门验证掐断后的流仍然合法。
重启前自检:
dsh --profile web --dump-config 2>&1 | Select-String -Pattern 'loop-breaker|error|failed|invalid config'
踩坑记录
- 协议收尾:不补
finish块会让内置校验器抛错;补了finish:{kind:'stop'}但留着未闭合 块同样抛错。必须先闭合块(见上)。 - 补救计数器:最初在
turn-stopping里trips.delete(key),导致每轮都从 1 开始数,maxRecoveriesPerTurn形同虚设。现在按payload.turn区分轮次,跨轮才归零。 - 测试用例本身会骗人:S4 最初构造的「正常阶段」文本是同一句话只改数字,被短句冗余规则 提前命中,看起来像插件 bug。负样本必须真正多样,否则标定出的阈值不可信。
- 硬上限不能设太低:一开始设 12 万字,把 9 万字的正常长输出拼接测试判定为超限。
单次调用的实际上限由 provider 的
max_tokens决定,硬上限只是极端兜底,已放到 20 万字。 - 测试夹具会骗人(第二次):集成测试里用
'正常思考。'.repeat(50)、'…'.repeat(4)当 「正常文本」,那本身就是退化复读的形状,新规则判它复读是对的——错的是夹具。已换成真正 多样的文本。遇到误判先怀疑夹具,再动阈值。 - 测试的日志捕获器会吞掉断言输出:集成测试把含
[loop-breaker]的 console 行收进logs, 而断言又把日志正文当作附加信息打印,于是那行断言在输出里凭空「消失」(值其实算对了)。 断言附加信息不要包含日志正文。 - 「用字单调」不等于复读:新增的塌缩规则一开始没有片段种类闸门,把图表型文本(大量
#与数字,用字同样单调但每行都不同)判成复读。必须叠加「片段种类极少」才能区分。 - 熔断没解决的另一半是「垃圾进历史」:掐断只停止烧 token;复读原文若被原样写回会话, 会成为下一次生成的先验。真正的修复在写回那一刻(L0 消毒),不在掐断那一刻。
已知边界
- 只覆盖
text-delta/reasoning-delta。模型反复用完全相同的参数调工具属于另一类循环, 由官方@deepseek-ai/dsh-repeat-tool-reminder负责(base 组合默认已启用)。 - 掐断的那次调用可能拿不到
usage(请求已取消),该轮的 token 计量会缺失,属预期。 - 判定是启发式的:它能让复读早点结束,但不能让模型变聪明。
- 同一句短句在同一窗口内连续重复 4 次以上(例如批量逐项报告「该项检查通过。」×20)会命中
tight-period-loop。它在结构上与退化复读同型,无法用统计量分开;要保这类输出请调高periodMatch或调低periodMax。 - L0 消毒会改写落盘文本:会话记录里留下的不再是模型原话,而是「开头一小段 + 占位符」。
排查期想看原文请设
sanitize: false。 - 掐断那一刻只累积到「命中时已产出的正文」,所以被丢弃的原文长度 = 触发点位置,不是整段长度。
回滚
删掉 cordis.patch.yml 里的 loop-breaker 行(或把 enabled 改 false)后重启 dsh web。
实测记录(2026-09-10 重启后核验)
端到端验证:熔断确实生效
在真实运行时里让一个子代理去复读(要求输出 600 遍「好的,执行」),然后直接读它的会话日志核对
($env:DSH_HOME\sessions\<工作区编码>\<会话 id>\session.v3.jsonl.zstd;注意是多帧 zstd 拼接,
必须按魔数 28 B5 2F FD 分帧逐帧解压,一次性解压只会得到空):
- 该 assistant 消息里「好的,执行」只写了 204 遍就被截断,没有跑满 600 遍;
- 日志中出现
agent/inbox/spliced,内容带source: {kind:'plugin', plugin:'loop-breaker', form:'notice', summary:'复读熔断(tight-phrase-loop),已要求重新作答'}; - 该轮有 2 个 step:step1 被掐断 → 注入补救 → step2 模型改口给出可用替代方案,全程约 6 秒。
这同时证明插件确实挂载成功。
日志为什么曾经是哑的
Cordis 的 logger 在当前版本不作为宿主 Service 提供(ctx.logger 为 undefined),
可选链会把全部日志静默吞掉——启动日志里能看到 [dsh-cost-meter]、[dsh-turn-cost],是因为它们走 console.log。
现已改为 console.log:启动打印 [loop-breaker] 熔断器已安装…,命中时打印规则、判定值与被截样本片段。
对正常长输出的影响(15 例真实长输出 + 3 例模板化边界,逐例实测)
零误杀:
- 官方中文文档 6 份、官方源码 3 份(句子回收率 ≤0.24,16-gram ≤0.05)
- 大表格 200 行数值 / 120 行文本(16-gram 均 0.000)
- 长清单 50 条、长 JSON 150 行、长数字序列 0..399、图表型文本
- 长报告 12 节各不相同(句子回收率 0.000)
已知边界(会被判为复读而中断):
- 结构性高度雷同的批量内容,例如 40 个结构相同、只有函数名不同的 handler、每节只换编号的条款。 实测「近似函数 40 个」的 16-gram 回收率为 0.808,而真实复读样本 real_loop3 只有 0.676—— 两者无法用任何阈值分开。这是文本层面的固有边界,调参解决不了。
- 需要这类输出时,调高
sentRecycle/gramRecycle16,或临时enabled: false。
取舍原则:宁可漏过也不误杀。上面这一节是 2026-09-10 版本的实测记录;2026-09-12 加入短周期
与字符塌缩规则后,碎片型复读在百字级即可判定,而长负样本的误杀情况与旧版持平
(test-false-positive.mjs 复测:10 例中 2 例误杀,探针确认均为旧版既已存在的误杀,
本次改动引入的新误杀为 0)。开发过程、阈值推导与踩坑的完整记录见 docs/PROJECT_LOG.md。
许可
MIT。判定逻辑与标定数据可自由取用。
Comments
Loading…
From the same category
DeepSeek Harness plugin for Reactive Resume: bridges your resumes and job applications into a Harness session over MCP.
★ 41.7k
↓ 143/wk
MIT
Aug 24, 2026
dsh plugin --profile web add dsh-plugin-reactive-resumeby Tencent
Let AI agents use your real, logged-in browser without interrupting your work. CLI + extension for browser automation across any shell-capable AI agent.
★ 8.5k
↓ 5.9k/wk
MIT
TypeScript
Oct 9, 2026
dsh plugin --profile terminal add @wxg-prc-cpg/browser-skill-dsh-pluginby yjh051108
dsh-routing-suite — injector + router-standard kit: install the runtime injector first, then the task-aware reasoning-mode router preset (measured P1-P23).
★ 7k
MIT
JavaScript
Sep 18, 2026
dsh plugin --profile web add @dsh-external/dsh-super-injectorby Q00
Agent OS: the agent gets smarter on its own. We just hold the line: Interview-gated, staged evaluation, budgeted evolution loop. MCP server, 14 runtimes: Claude Code, Codex CLI, Gemini CLI, OpenCode,
★ 6.2k
MIT
Python
Oct 7, 2026
by dsh-market
The plugin market inside DeepSeek Harness — browse, search, one-click install · DSH 可视化插件市场
★ 6k
↓ 95.5k/wk
MIT
TypeScript
Oct 9, 2026
dsh plugin --profile web add dshmarketby superdesigndev
OpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn
★ 4.9k
NOASSERTION
Python
Oct 9, 2026
dsh plugin --profile web add treg-dsh