dsh-cot-anchor
思考锚点:从模型上一段思考中提取已确立结论,在工具结果后注入,减少重复推导;并可识别打转与伪工具调用
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 5, 2026
- Updated
- Oct 6, 2026
Introduction
dsh-cot-anchor
思考锚点(COT Anchor)—— 让"思考着思考但没真正推进"的模型闭嘴,把已经得出的结论重新递回上下文。
一个 DeepSeek Harness(DSH)插件。
出发点:为什么会有这个插件
本插件诞生于对一类特定模型的真实困扰——以 DeepSeek-V4-Flash(v4.1-flash 变体)为典型代表。具体症状在一次实际会话里被硬数字刻画过:
| 指标 | 数字 | 含义 |
|---|---|---|
| 思考 : 文本输出 | 14 : 1 ~ 24 : 1 | 模型每写一个字给用户,就要消耗 14~24 个字的"思考" |
| 单条带 reasoning 的消息平均宣告"接下来要做什么" | 1.6 ~ 3.6 次 | 但兑现率显著偏低——说要做 X,下一条却在做别的 |
| reasoning 块平均长度 | ~7,000 字 | 极度啰嗦 |
| "宣布决定性证据齐全" | 几乎每轮思考一次 | 然后继续宣称"接下来要验证 XXX" |
也就是说:这个模型思考着思考,但其实没在推进。它会在一段看似有逻辑的 CoT 里,把上一轮甚至上几轮已经得出过结论的证据,又重新推一遍;它会把工具调用写成纯文本(<seed:tool_call>...);它会原地重复同一段话;它会语义空转(措辞不同但反复"重新分析");它会输出无意义的递增数字流(639, 640, 641, ...)。
如果让这种模型长时间跑长任务,会发生两件事:
- token 烧得极快——每 1 字有效输出要 14~24 字思考,照它的思考规模换算,1MB 的思考里有效推进只占极小比例。
- 结论被反复"重新发现"——上一轮已经明确写过的结论,下一轮思考里又当新问题重新推导一遍,浪费 token 的同时让模型注意力被陈年分析占据,看不清真正新出现的信息。
dsh-cot-anchor 就是用来对付这种行为的:把上一轮已经确立的结论重新递回上下文,让模型不必重新推一遍;同时在生成过程中实时打断打转/重述/伪调用/数字流等失控形态。
它不解决一切长任务问题,只解决这一类特定的"思考链极度冗余、结论反复重新推导"问题。
它做什么(结论视角)
把上面那段拗口的症状翻译成"做什么",就是这两条:
- 结论回流:每次工具结果返回后,从最新一段思考里抽取已成立的结论(最多 3 条、每条不超过 220 字),作为用户消息插回上下文。下一步思考起手就能看到这些结论,不必再推一遍。
- 失控形态打断:在生成过程中按 32 字符一次的频率检查,一旦发现模型开始打转/原地重复/伪工具调用/数字流退化,立即切断当前请求、把已生成部分作为正常消息落盘、把结论锚点排进下一步、让回合续跑。模型不必因为一次误判而丢掉整轮工作。
第一项默认开启,插件装上就生效。第二项默认开启,但需要 DSH 0.1.5-rc.1 / 0.1.5-rc.2 内核提供的 agent/soft-cut 钩子——0.1.5-rc.3 起的内核不再提供该钩子,请参看 docs/softcut-kernel-port.md 恢复。
能力边界:它治不了什么(含实测反例)
本插件的两类机制都是单步边界内的,理解这一点比理解它能做什么更重要:
- 结论注入发生在工具结果之后,作用于"下一步";软切只观察当前一次生成流内的文本(每 32 字符征询一次),进入下一个 step 后一切计数归零。
- 因此本插件治得了单流内退化(逐字节重复、伪工具调用、数字流、同一块思考内打转),治不了跨步骤折返跑:同一结论在十几个步骤里被重新推导、同一取证目标反复换路径尝试——没有任何一次单流能看见这个模式。
- 软切触发后,内核把已生成前缀正常落盘并发起一次全新请求,该请求照常重读全部缓存上下文。软切不减少回合步数;在长任务里,账单的大头是"步数 × 每步膨胀的上下文重读",不是思考长度,也不是插件注入。
- 模型可能知道自己在空转却停不下来。实测出现过思考中自述"I'm burning turns, let me stop"之后、同一段思考内继续生成下一条探索路线;被注入"立即停止重新分析"后,后续步骤再次触发打转判定。注入的前提是模型会读且会照做,自知不构成刹车。
- 每个回合软切上限 6 次,打满后当回合剩余生成完全放行。
一次"插件全程在场仍未约束住"的实测
下列数字来自单个真实长任务会话(N=1):同一 v4.1-flash 变体、思考强度调至 low、结论注入与软切全程开启且软切实际触发 4 次(打转判定 2 次、转折句判定 2 次;以无 usage 的助手消息结算点为准)。
| 指标 | 数值 | 口径 |
|---|---|---|
| 思考 : 正式文本 | 37.8 : 1 | reasoning 与 text 块的字符数之比 |
| "接下来要做 X"类宣告 | 9.2 次 / 思考块 | 关键词正则计数,口径宽于开头基线表的 1.6–3.6,两者不可直接换算 |
| 会话总量 / 助手步骤 | 约 449 万 token / 42 步 | 逐步骤 usage 四字段求和 |
| 其中缓存读取占比 | 95.4% | cacheReadTokens / totalTokens |
| 零产出思考块 | 4 个、合计约 3 万字符(约占全部思考量 25%) | usage 缺失的 assistant 消息,即软切结算点 |
| 首次实际改代码发生在 | 第 28 / 42 步 | 此前 68% 思考量用于反复重推同一根因与失败的取证支线 |
这组数字不与开头的基线表横比(任务类型、思考强度设置、统计口径均不同,N=1),它只证明一件事:插件在场且机制真实开火,不保证思考密度达标。如果你的场景以跨步骤折返跑为主,不要期待调阈值能根治——需要的是步数/预算层面的治理,那超出本插件的职责范围。
复算口径(任何人可在自己的会话日志上复核):解包 session.v*.jsonl.zstd 后,逐条统计 assistant/message 的 usage(inputTokens / outputTokens / cacheReadTokens / totalTokens)与 content 块类型(reasoning / text)的字符长度;软切结算点表现为该消息无 usage 字段且紧随一条 agent/inbox/spliced(target: next-step)。
用户能直接看到的 flash 失控形态(举例)
下面这些是从真实会话里抓出来的典型表现——用户读聊天窗口就能感觉到,不依赖任何内部诊断:
- 重复推送:同一段话说几遍还不停,每一遍措辞微调但推进为零。"让我重新看一下…" → "再确认一下…" → "确保没遗漏…" → "实际上让我重新…" 五轮之后还在原地。
- 伪工具调用:模型不再真正调用工具,而是把工具调用格式化成普通文本(常见形态
<seed:tool_call>...</seed:tool_call>),直接拼进正文。用户读起来像在调工具,实际上什么都没发生。 - 递增数字流:思考里突然开始输出无意义的递增数字(
639.640) 640.641)… 641.642)…),像是某个内部计数器被泄露进了输出。 - 自吹"决定性证据":每思考一次就来一次"我已经得到了决定性证据" / "真相大白" / "证据链齐全",然后紧接"接下来我要验证 XXX"——下一条往往又回到重分析起点。
- 结论重新发现:上一轮明明已经在正文里写明白的事("X 文件里函数 Y 是关键"),下一轮思考里又被当作新信息重新推导一遍。
如果上面任何一项在你用某些模型跑长任务时出现过,本插件就是为它而生的。
快速开始
从 GitHub Releases 拉 tarball 安装(推荐):
# 直接拉指定 tag
dsh plugin --profile <你的profile> add github:JiewiW/dsh-cot-anchor#v0.1.5
# 或先下载再装
gh release download v0.1.5 --repo JiewiW/dsh-cot-anchor
dsh plugin --profile <你的profile> add ./dsh-cot-anchor-0.1.5.tgz
安装后重启实例,设置页出现「COT 锚点」标签即已生效。
工作方式
模型思考 → 工具执行 → [插件] 摘出结论、插回上下文 → 下一步思考
↑
只在工具边界注入
注入的消息体积很小(默认最多 3 条、每条不超过 220 字),以 user 角色追加在工具结果之后、下一次请求之前。
在设置页开启相应开关后,插件还会在生成过程中按间隔询问内核是否需要中断当前生成("软切断"):检测到问题就切断当前请求、把已生成部分作为正常消息落盘、插入锚点,然后从断点继续。模型不会因为一次误判而丢失整轮工作。
成本
| 场景 | 额外成本 |
|---|---|
| 默认(结论注入开启,其余关闭) | 每个工具结果后一条短消息,上界 660 字符(3 条 × 220 字);无额外模型调用 |
| 命令执行纪律提醒(随锚点同车) | 仅在锚点实际注入时同车携带一条,与推理锚点共用同一推理指纹去重节奏,不单独排队、不无限堆叠;单条全文固定约 380 token(字符估算) |
| 开启 LLM 提炼结论 | 每个助手回合多一次小模型调用 |
| 开启打转判定 | 纯本地字符串计算,无 token 成本;可能增加少量回合数 |
| 开启 CoT 静默采集 | 每个助手回合留一条本地样本;默认关闭 |
注入消息会随之后续请求的上下文一同发送,因此持续占用少量上下文窗口。按中英混排估算,660 字符约合 200–500 token 量级——相对于一次工具往返的上下文可以忽略,但它在每一步都会累积。注入以尾部追加方式写入会话日志,不改写已有内容,因此不会打穿前缀缓存:除新追加的一块外,此前内容始终是稳定前缀。新追加块在其首次出现的那次请求上按未命中计费一次,此后转为缓存命中——这一点与文本是否固定无关。纪律块与锚点的排列顺序仅为内容组织上的一致性,不影响缓存行为。
实测成本占比
在一个 95 次助手请求、累计约 924 万 token 的真实长会话上按消息级口径统计(字符→token 为启发式估算,非 tokenizer 实测;任何人可在自己的 DSH 会话日志上按同口径复算:逐条统计 user/message 中 source.summary 为 cot-anchor: exec-discipline(固定纪律)与其他 cot-anchor:*(动态锚点)的文本,并对照各次助手请求 usage 的 inputTokens / cacheReadTokens):
| 指标 | 数值 |
|---|---|
| 注入新增估算总量 | 21,436 token,占全会话总 token 约 0.23% |
| 其中固定纪律文本 | 17,190 token,占注入估算 80.2%(45 条,单条全文固定约 380 token;仅表示体积占比,不表示缓存命中) |
| 其中动态锚点 | 4,246 token,占注入估算 19.8%(51 条) |
| 注入块首现带来的未命中输入 | 21,436 token,占会话未命中输入约 5.6%(21,436 / 379,969;每个注入块在其首现的那次请求上未命中一次) |
| 该会话缓存读取占全部输入比 | 95% 以上 |
结论:插件带来的新增输入在全会话 token 总量的 0.3% 以内。每个注入块在首现时按未命中计费一次,此后随上下文增长参与后续每一次请求。纪律块体积约占注入量五分之四,但"文本固定"不带来额外缓存收益——该占比只反映体积,不反映命中。
兼容性
| 能力 | DSH 版本 |
|---|---|
| 结论注入、设置页 | 0.1.5 起全部版本 |
| 生成中途打断(软切断) | 仅 0.1.5-rc.1 / 0.1.5-rc.2 |
0.1.5-rc.3 起内核不再提供生成中途打断所需的钩子,该能力会静默失效。插件会在启动日志和设置页顶部明确提示当前内核是否具备该能力——请以设置页显示为准,不要凭"开关是开的"就认为功能在生效。
如果你希望在新版上保留这项能力,可用随包附带的工具把软切机构移植回内核:
node node_modules/dsh-cot-anchor/tools/apply-softcut-port.mjs --check # 看状态
node node_modules/dsh-cot-anchor/tools/apply-softcut-port.mjs # 应用(幂等)
移植后需重启实例。完整步骤、原理、风险与回滚见 docs/softcut-kernel-port.md。
本插件不会中止进行中的回合;它只在模型自己写坏输出时切断当前这一次生成请求,随后继续。
设置项
设置页「COT 锚点」共 52 项,按用途分组。下列"建议值"是针对多数场景的推荐;不同模型差异较大,可据实调整。
总开关
| 键 | 含义 | 建议 |
|---|---|---|
enableToolInject | 工具结果后注入锚点 | 保持开启,这是本插件的主要价值 |
enableSoftCut | 允许生成中途打断 | 内核不支持时无效;支持的版本建议开启 |
打断判定
这五项决定"什么情况算问题"。全部关闭时插件只做结论注入。
| 键 | 含义 | 能力边界 | 建议 |
|---|---|---|---|
enablePseudoTool | 识别把工具调用写成文本的输出 | 只能识别已知的文本形态,无法穷举 | 开启 |
enableRepeat | 识别同一段内容被反复吐出 | 要求逐字节相同;换一种措辞的重复抓不到 | 开启 |
enableChurn | 识别措辞不同但反复"重新分析" | 启发式判定;遇到未闭合的代码块会自动让路 | 开启 |
enableTransition | 识别"接下来我要……"这类转折句 | 转折句不等于打转,正常思考里也会出现 | 见下方说明 |
enableNumberRunaway | 识别无意义的递增数字流 | 要求数字流足够长 | 开启 |
enableTransition是五项中最容易误判的一项:正常的分阶段思考同样会出现转折句。误判代价较低(只会多一次切断,模型从断点继续),但如果你经常看到它在正常思考中切断,可将其关闭。
锚点内容
| 键 | 含义 | 建议 |
|---|---|---|
minReasoningChars | 思考短于此长度不提取结论 | 250 |
maxPoints | 最多保留几条结论 | 3 |
maxPointChars | 单条结论字数上限 | 220 |
抽取质量边界(v0.1.5 已加过滤): 结论由规则从思考文本中摘句得到(开启 LLM 提炼时由小模型改写),不理解句子的语义角色。v0.1.5 起,抽取器在入池前剔除两类无命题内容:①纯话语标记句——剥掉 Actually / wait / Hmm / hold on / 让我重新 等标记后剩余实词不足 6 个字符("Actually, wait."、"Hmm."、"其实,等等。");②路线意图宣告句——"the decisive route: ask the user…"这类下一步动作宣告。标记词领起但带事实命题的句子("Wait, the kernel never marks in verbose mode.")不受影响。
这是抽取质量问题,不是思考标记词本身的问题——Wait / Actually / Hmm 等转折词在推理链中承担结构控制作用,本插件不从模型输出中删除它们,只阻止它们被当成结论回灌。
重复循环参数
判定"同一段内容被反复吐出"的阈值。调低更激进,调高更保守。
| 键 | 含义 | 建议 |
|---|---|---|
repeatMinCount | 长块重复几次判定打转 | 2 |
repeatMinCountShort | 短周期需重复几次 | 5 |
repeatUltraShortMinCount | 极小循环(如 er4er4…)需重复几次 | 30 |
repeatMinPeriod | 周期下限(字) | 12 |
repeatShortPeriod | 长块与短块的分界(字) | 96 |
repeatMaxPeriod | 超过此周期不再视为循环 | 8000 |
repeatWindowChars | 只在文本尾部这段长度内做预筛 | 16000 |
repeatProbeChars | 周期探针长度 | 128 |
repeatMinTextChars | 文本短于此长度不判定 | 800 |
语义空转参数
| 键 | 含义 | 建议 |
|---|---|---|
churnMinHits | 窗口内"让我想想/重新分析"类用语命中几次即判定 | 18 |
churnWindowChars | 空转统计窗口 | 3000 |
churnMinTextChars | 文本短于此长度不判定 | 800 |
转折句参数
| 键 | 含义 | 建议 |
|---|---|---|
softCutScanTail | 转折句扫描窗口 | 160 |
softCutTransitionWindow | 转折词距句末的最大距离 | 48 |
softCutMinFollowChars | 转折词后至少展开多少字才允许切断 | 6 |
pseudoToolScanTail | 伪调用扫描窗口 | 4000 |
softCutMinFollowChars是防误切的关键:它保证不会在句子刚开头就切断、截出半句结论。调低会明显增加误切。
LLM 提炼
用一次小模型调用把整段思考压成摘要式结论,通常比摘句更准。默认关闭。
| 键 | 含义 | 建议 |
|---|---|---|
enableLlmRefine | 启用 LLM 提炼 | 按需 |
llmRefineProvider | 提炼用 provider | 留空则跟随会话 |
llmRefineModel | 提炼用 model | 留空则跟随会话 |
llmRefineMaxTokens | 提炼输出上限 | 256 |
llmRefineTimeoutMs | 提炼超时(毫秒) | 20000 |
llmRefineMaxInputChars | 提炼输入截断字数 | 12000 |
CoT 采集 / CoT 分析 / CoT 增补
这三组用于归纳"现有检测器看不见的打转形态"。默认全部关闭;开启会在本地留存你的思考文本,请自行评估隐私影响。
| 键 | 含义 | 建议 |
|---|---|---|
enableHarvest | 启用静默采集 | 关闭;开启前先读隐私说明 |
harvestIncludeToolTrace | 记录工具调用轨迹 | 跟随 enableHarvest |
harvestMaxRecords | 样本条数上限 | 200 |
harvestRetentionDays | 样本保留天数 | 14 |
harvestMinTextChars | 存全文的最小思考字数 | 3000 |
harvestMaxTextChars | 单条全文上限 | 8000 |
harvestFlushDebounceMs | 落盘去抖(毫秒) | 5000 |
enableAutoAnalyze | 达到阈值自动分析 | 关闭 |
analyzeTriggerSamples | 自动分析触发条数 | 50 |
analyzeProvider / analyzeModel | 分析用模型 | 留空则跟随样本 |
analyzeMaxTokens | 分析输出上限 | 4000 |
analyzeTimeoutMs | 分析超时(毫秒) | 180000 |
analyzeMaxInputChars | 分析输入截断字数 | 8000 |
analyzeMaxSamples | 单批样本数 | 3 |
analyzeRunBudgetMs | 单次分析总时长上限(毫秒) | 300000 |
enableLearnedPatterns | 加载已采纳的学习模式 | 保持默认 |
learnedMaxPhrases | 学习模式条数上限 | 60 |
learnedMaxShift | 学习层对出厂阈值的最大调整幅度 | 20 |
learnedShadowRounds | 影子期命中次数(0=直接生效) | 20 |
learnedShadowRounds默认为 20,含义是学习出的模式不会立刻生效,需累计命中 20 次才转正——这是防止一次误学就永久改变判定。如果你希望它立即生效,可设为 0;这会提高误判风险。
隐私
- 结论注入与打转判定全部在本地完成,不发送任何数据到外部服务。
- LLM 提炼开启时,会把当前思考文本发给你指定的 provider(留空则发给你正在用的模型对应的服务)。
- CoT 采集开启时,会在本地留存思考文本与工具调用轨迹。
常见问题
设置页显示"当前内核没有掐断能力" 该版本的 DSH 内核不提供生成中途打断所需的钩子。结论注入不受影响。若需要该能力,可停留在 0.1.5-rc.1 / rc.2。
模型好像没被提醒到
确认 enableToolInject 已开启,且思考长度超过 minReasoningChars。思考过短时本来就不会提取结论。
打断太频繁
优先关闭 enableTransition;其次调高各判定阈值。enableChurn 是启发式判定,最容易偏激进。
开发
node test-repeat.mjs # 重复循环检测
node test-number-runaway.mjs # 数字流退化检测
node test-softcut.mjs # 软切判定
测试以 lib/index.js 为源,剥离 import/export 后用 new Function 求值被测函数,因此新增测试无需改动模块结构。
许可
MIT