← 返回列表
未验证
实时检测模型输出重复内容并立即停止生成
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/9 · 已提供中文文档
Real-time repetition guard for DeepSeek Harness (DSH): stops model generation the moment the same string repeats ≥10 times in the streamed output. 防复读守卫:实时检测 DSH 大模型输出中的重复内容并立即停止生成。
综合分
30
GitHub 分
30
用户评分
—
★ Stars
1
周下载量
—
兼容 / 相关生态插件(非 dsh 原生,请按其对应运行时安装)
git clone https://github.com/zqh260619/dsh-dupguard.git数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
dupguard · DSH 大模型重复输出守卫
dupguard — 面向 DeepSeek Harness (DSH) 的实时重复输出守卫:当流式输出中同一字符串重复 ≥ 10 次(可配置)时,立即停止模型生成。
dupguard 是 DeepSeek Harness (DSH) 的实时重复输出守卫插件:当最新输出中同一字符串连续重复 10 次及以上(可配置)时,立即停止本次生成。
npm version
License: MIT
CI
dsh-plugin
触发后,已生成的内容会正常提交为助手消息,本轮对话干净结束——不会报错、不会丢弃输出、不会污染会话日志。
触发后,已生成的内容会正常提交为助手消息,本轮对话干净结束——不会报错、不会丢弃输出、不会污染会话日志。
特性 / Features
- 实时检测:逐 token(text-delta)检测,复读出现即停,延迟为单个增量。
- 多种复读形态:单字符循环、词语循环、带空格/换行分隔的复读均能识别(默认去空白后检测)。
- 思考守卫:默认同时检测 reasoning(思考)文本,思考中的复读同样会被截停(可通过 monitorReasoning 关闭)。
- 真正的服务端停止:提前关闭流迭代 → 适配器 consumer.abort() → 中断 HTTP 连接,模型在服务端停止生成。
- 安全停止:绝不 abort() agent 步骤信号;补发协议合规的 block-end + finish(stop),消息正常提交。
- Markdown 表格友好:默认忽略连字符与竖线(ignoredChars 白名单),表格分隔行与长分隔线不会被误判为复读。
- 图形化设置页(npm 常驻版):在 DSH 设置面板注册与「通用设置 / 模型 / 插件 / Agent 预设」并列的
「重复守卫」分节,可视化编辑白名单并持久化(dsh-dupguard 设置命名空间),修改即时生效。
- 零配置开箱即用:默认配置即可用;仅白名单可通过设置页调整。
- 双入口交付:动态插件(plugin/host.js)+ npm 组合挂载(lib/index.js + lib/client.js),行为一致、CI 防漂移。
- 内置 DSH 兼容补丁(fixStandingMountConflict,默认开启):幂等化 cordisInspect.register,
修复 DSH ≤ 0.1.1-rc.1 的 preset standing-mount 多代并存冲突(见下文"已知限制")。
快速开始 / Quick Start
方式一:动态插件(无需安装,进程内生效)/ Dynamic plugin (no install)
把 plugin/host.js 的全部内容作为 code.host 提交给 cordis_define,再 cordis_run 激活即可:
1. cordis_define:kind 选 new,idPrefix 例如 dupguard,code.host 填入 plugin/host.js 内容;
2. cordis_run:激活返回的 packageId(首次使用 mode run)。
动态插件随 DSH 进程存在;重启后需重新 define + run。
将 plugin/host.js 的全部内容作为 code.host 粘贴到 cordis_define,然后用 cordis_run 激活返回的 packageId。
方式二:npm 安装 + 组合挂载(常驻,随 DSH 启动)/ npm + composition (persistent)
插件已发布到 npm:dsh-dupguard。
1. 安装(DSH ≥ 0.1.2-rc.1,一条命令):
dsh plugin --profile web add dsh-dupguard
本包自带 bundle 补丁层(dsh.bundle.patch → cordis.patch.yml):
dsh plugin 把参数转发给 profile 目录下的 pnpm,安装后自动把声明了 dsh.bundle 的依赖
加入 dsh.profile.bundles;DSH 按 bundles 顺序应用各层补丁,本插件的层插入宿主行
{ id: dupguard, name: dsh-dupguard }。无需手改任何 YAML。
- 验证:dsh --profile web --dump-config 末尾应出现 dupguard 行;loader 日志出现
apply plugin dupguard。
- 升级:dsh plugin --profile web update dsh-dupguard;
卸载:dsh plugin --profile web remove dsh-dupguard(依赖与 bundles 层一并移除)。
2. 手工补丁层(旧版 DSH 或不想加入 bundles 时仍受支持):在 profile 的用户补丁层
cordis.patch.yml 自行插入同一行:
$DSH_HOME/profiles/web/cordis.patch.yml
- insert:
- id: dupguard
name: dsh-dupguard
用户补丁层在 bundle 层之后应用;运行中的 DSH 通过 watchUserPatches 热重载它——
保存即生效,无需重启,加载失败会事务性回滚。
⚠️ 两种方式不要同时使用:loader 对重复 entry id 直接抛
duplicate loader entry id: dupguard。从手工方式切换到 bundle 方式时,请删除手工 insert 项。
本地开发:未发布/调试时,name 也可直接用 file: URL 指向仓库内的
lib/index.js(CJS 导出 { name, apply },与 loader 的
unwrapExports 兼容,零构建)。
该插件已发布到 npm,名称为 dsh-dupguard,
并自带 bundle 补丁层(dsh.bundle.patch → cordis.patch.yml)。
用一条命令即可安装(DSH ≥ 0.1.2-rc.1):
dsh plugin --profile web add dsh-dupguard
dsh plugin 会把参数转发给 profile 目录下的 pnpm,并将任何声明了 dsh.bundle 的依赖
追加到 dsh.profile.bundles;随后 DSH 按顺序应用每个 bundle 层的补丁,本插件的层会插入
宿主行 { id: dupguard, name: dsh-dupguard }。无需编辑 YAML。可用
dsh --profile web --dump-config 或 loader 日志行 apply plugin dupguard 验证;
升级用 dsh plugin --profile web update dsh-dupguard,卸载用
dsh plugin --profile web remove dsh-dupguard。
手工层(仍受支持):在没有 bundles 条目时,将同一行插入 profile 的
用户补丁层 cordis.patch.yml;运行中的 DSH 会热重载该文件(watchUserPatches),
失败时事务性回滚。不要同时使用两种方式——loader 会抛出
duplicate loader entry id: dupguard。file: URL 形式对于针对
lib/index.js 的本地开发也仍然很方便(CJS,module.exports = { name, apply },
与 loader 的 unwrapExports 兼容,无需构建步骤)。
配置 / Configuration
修改 plugin/host.js 或 lib/index.js 顶部 CONFIG 常量(两个入口需保持同步,CI 会校验一致性):
编辑 plugin/host.js / lib/index.js 顶部的 CONFIG 块(两个入口必须保持同步;CI 会校验行为一致性)。
| 配置项 / Option | 默认 / Default | 说明 / Description |
| --- | --- | --- |
| threshold | 10 | 触发阈值:同一字符串连续重复 ≥ 该值时停止 / 当同一字符串重复 ≥ 该次数时停止 |
| minUnitLength | 1 | 最小重复单元长度 / 最小重复单元长度(1 也能捕获像 aaaaaaaaaa 这样的单字符循环) |
| maxUnitLength | 80 | 最大重复单元长度 / 最大重复单元长度 |
| detectionWindow | 8192 | 检测滚动窗口(字符,去空白后)/ 滚动检测窗口,单位为字符(去除空白后) |
| stripWhitespace | true | 检测前移除空白/换行,识别带分隔符的复读 / 去除空白,以便捕获 "x x x" 和 "x\nx\nx" |
| ignoredChars | ['-', '\|'] | 检测时忽略的字符白名单(默认值):Markdown 表格分隔行(连字符与竖线)不参与重复统计;npm 常驻版可在设置页「重复守卫」分节中可视化修改并持久化(动态版固定取此常量)/ 检测期间忽略的字符白名单(默认值):Markdown 表格分隔符不计入重复;npm 构建版在设置页「Dupguard」中暴露该项并支持持久化(动态构建版使用此常量) |
| monitorReasoning | true | 是否检测思考文本(思考中的复读同样消耗 token,默认截停;只检测可见输出时置 false)/ 同时守卫推理(思考)文本——默认开启;设为 false 则仅守卫可见输出 |
| monitorToolArguments | false | 是否检测工具调用参数 / 同时守卫工具调用 JSON 参数——默认关闭(base64/JSON 重复很常见) |
| fixStandingMountConflict | true | DSH ≤ 0.1.1-rc.1 兼容补丁:幂等化 cordisInspect.register,修复 preset standing-mount 多代并存冲突 / 针对 DSH ≤ 0.1.1-rc.1 standing-mount 冲突的幂等 cordisInspect.register 补丁 |
工作原理 / How it works
1. 拦截流式输出 / Intercept the stream
监听 llm/stream 瀑布事件(包裹每次流式模型调用),返回包装后的 AsyncIterable。与 DSH 自带
@deepseek-ai/dsh-llm invariant 插件、dsh-session-checkpoint-policy 同款接入方式。
监听 llm/stream 瀑布事件(包裹每次流式模型调用),并返回包装后的 AsyncIterable。
2. 检测算法 / Detection
- 按块索引(chunk.index)分别累积文本,多块交替输出互不干扰;
- 去空白后做尾部连续重复检测:文本以某个单元(长度 1..80)连续重复 ≥ 阈值结尾即触发。
模型一旦复读,重复必然在尾部,因此尾部检测即可实时捕获所有循环,同时避免全窗口词频的误报
(如正常中文里高频的"的")。
对去除空白后的缓冲区进行仅尾部连续重复检测:可实时捕获所有循环,
且不会像全窗口词频统计那样产生误报。
3. 停止机制 / Stopping
守卫生成器提前结束 → for await 调用上游 iterator.return() → 适配器 finally 中
consumer.abort() 中断 HTTP 连接 → 服务端真正停止生成。绝不直接 abort()
options.signal(对 loop 请求它就是 agent 步骤信号,直接中止会以 aborted 结束并丢弃消息)。
优雅提前结束:iterator.return() 传播至适配器,其 finally 在服务端中止 HTTP
连接。我们绝不直接中止 options.signal(对 loop 请求而言,它就是 agent
步骤信号)。
4. 协议合规收尾 / Protocol-compliant closure
停止时补发所有打开块的 block-end(携带完整已生成文本)与 finish{kind:'stop'},满足
llm-invariant 校验器要求;agent-loop 将已生成内容正常提交为助手消息。
发出合成的 block-end 以及 finish(stop),以满足 llm-invariant 校验器要求,从而使
agent-loop 将部分文本作为普通助手消息提交。
触发示例 / What gets stopped
| 形态 / Pattern | 示例 / Example |
| --- | --- |
| 单字符循环 / single-char loop | aaaaaaaaaa |
| 词语循环 / word loop | 哈哈 ×10 |
| 带空格复读 / space-separated | hello hello hello ... ×10 |
| 逐行复读 / line repeats | 抱歉,我无法完成。 ×10 行 |
| 前缀后循环 / loop after prefix | 好的,下面开始回答: + 循环 ×10 |
| 思考复读 / reasoning loop | 思考中 想 ×10(默认截停) |
不会触发 / Won't trigger:正常文本中的高频词(检测只针对连续重复)、重复 9 次及以下、
工具参数(默认关闭)、Markdown 表格分隔行与长分隔线(连字符与竖线在白名单中,默认忽略)。
/ 正常行文中的高频词(仅限连续重复)、≤9 次重复、工具参数(默认关闭)、
Markdown 表格分隔行与水平分隔线(默认已加入白名单)。
项目结构 / Project layout
.
├── plugin/
│ └── host.js # 动态插件形式(cordis_define 的 code.host)
├── lib/
│ ├── index.js # npm/组合常驻形式(package.json main 入口,含设置集成)
│ └── client.js # 浏览器端设置页(ModuleLoader 格式,dsh.client 入口)
├── tests/
│ ├── detector.test.js # 端到端测试:双入口防漂移 + reasoning 开关 + settings 集成
│ ├── client.test.js # 设置页组件测试:最小 React/DSH 桩驱动写路径
│ └── experiment-cancel.mjs # 诊断实验(不进 CI):验证截停不阻塞于底层流取消
├── .github/workflows/ci.yml # GitHub Actions:Node 20/22/24
├── cordis.patch.yml # bundle 补丁层(dsh.bundle.patch:插入宿主行)
├── package.json
├── CHANGELOG.md
├── LICENSE # MIT
└── README.md
测试 / Tests
npm test # 两个测试文件
node tests/detector.test.js # 检测端到端(48 项)
node tests/client.test.js # 设置页组件(6 项)
同一套 15 项用例分别驱动两个入口(plugin/host.js 经 new Function 求值、lib/index.js 经
require 加载),覆盖:透传完整性、各类复读形态、阈值边界、协议闭合、上游 return() 调用、
默认不检测 reasoning/工具参数、未闭合工具调用块的闭合、多次调用状态隔离等。client.test.js
用最小 React 与 DSH 客户端桩驱动设置页组件,断言写入走 settingsScope 控制器
(set/unset)而非已移除的 connection.api。CI 在 Node 20/22/24 上运行
(与 DSH 一致,不支持 Node 18)。
同一套 15 项测试用例驱动两个入口,防止两种形式之间发生漂移。CI 在
Node 20/22/24 上运行(与 DSH 一致;不支持 Node 18)。
已知限制 / Limitations
- 停止时若恰有未闭合的工具调用块(顺序输出块的适配器几乎不可能),该块会按已累积参数闭合并可能被执行。
- 服务端停止依赖适配器在流关闭时中止底层请求的语义(已验证 dsh-llm-deepseek;自定义适配器需自查)。
- 阈值语义为 >= threshold:第 10 次重复出现时即停止。
DSH 运行期间编辑 preset 后的 standing-mount 冲突(DSH ≤ 0.1.2-rc.1 缺陷,本插件已内置补丁)
现象:对某个会话执行模型选择等操作时报
resume failed ... preset ... failed to mount ... Host Cordis inspect provider "Service" is already registered,
此后该错误持续出现,只有重启 DSH 才能恢复。
机制:preset 以 standing mount 方式每 preset 挂载一次并常驻;当 preset 的 composition 文件在
DSH 运行期间被编辑过(mtime/size 变化),下一次对"无活跃 agent 的会话"的操作(模型切换、打开历史会话等)
会新建一代 standing mount,而旧代从不销毁(DSH 注释明示 "a superseded one is never disposed
while the process lives")。tool-cordis 在每次挂载时向进程全局的 cordisInspect 注册表注册
Service/Event/Builtin/Tool 四个 provider,新旧两代并存即冲突;失败的新代回滚、旧代残留,
重试永远重复冲突——这正是报错后"必须重启才能恢复"的原因。
本插件的修复(默认开启):apply 时把 cordisInspect.register 幂等化——同 id 已有注册时
共享既有注册并返回 no-op disposer,多代并存不再冲突。补丁进程内常驻(卸载本插件后仍生效,
重启后由本插件重新安装;HMR 重载不会叠加)。依赖 cordisInspect.providers 为可读 Map
(rc.6 / rc.7 / 0.1.1-rc.1 / 0.1.2-rc.1 实测如此);0.1.2-rc.1 上游仍留有
"reclaim the superseded generation" 的 TODO,缺陷未修复,故默认开启。
DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false 关闭。
仍建议的操作纪律:运行期间编辑已挂载 preset 后重启 DSH(补丁消除的是报错,旧代残留的
组合仍占用资源,这是 DSH 的既有行为);根治仍待上游修复。
如果你在 DSH 运行期间编辑已挂载 preset 的 agent.cordis.yml,下一次会话恢复
(例如由模型选择器在 agent 已消失的会话上触发)会挂载该 preset 的一个新的 standing-mount
代,而旧代从不销毁——此时 tool-cordis 会注册
它的进程全局 Host inspect 提供程序(Service …)被注册了两次,并且每次重试都会失败,报错
Host Cordis inspect provider "Service" is already registered,直到 DSH 重启。此插件默认会修补该问题:cordisInspect.register 被改为幂等(相同 id 的注册会共享现有注册,并获得一个 no-op 释放器),因此共存的多代不再冲突。该补丁常驻进程(在插件卸载后仍存活,重启时重新安装;HMR 重载不会叠加它);一旦修复后的 DSH 发布,将 CONFIG.fixStandingMountConflict 设为 false。在 0.1.2-rc.1 中仍未解决:上游有一个 TODO,用于回收被取代的多代。
许可证
MIT扫码进群