zhuto666/dsh-compact-agents ↗★ 1
dsh-compact-agents
DeepSeek Harness 会话上下文强制压缩插件:模型可调用的 compact_agents 工具,忽略自动压力阈值,覆盖进程内所有活会话(主会话/子代理/AgentTeams 成员),忙则排队、本轮结束补压,逐目标回报遮蔽节点数与估算 token 数。Session-context force-compaction plugin for DeepSeek Harness: a model-callable compact_agents tool that ignores the automatic pressure threshold and covers every live session in the process (main session, sub-agents, AgentTeams members alike), queues a busy target and compacts it when its turn ends, and reports shadowed node counts and estimated tokens per target. 适合长对话或多代理任务,通过强制压缩上下文避免超出模型Token限制。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:zhuto666/dsh-compact-agentsdsh-compact-agents
DeepSeek Harness 会话上下文强制压缩插件(模型可调用的 compact_agents)
强制压缩忽略自动阈值 · 覆盖进程内所有活会话(主会话 / 普通子代理 / AgentTeams 成员一视同仁) · 没有子代理时主会话也能压自己 · 忙的目标自动排队、本轮结束立即补压 · 逐目标回报被遮蔽节点数与估算 token 数 · 只有顶层 agent 能扫描、子代理越权被拒 · 工具调用串行不并发 · 压缩过程在对话区可见 · 被输出上限截断时自动续写 · 压缩阈值等参数可在「设置」里直接改 · 零网络、零依赖、浏览器 half 手写无构建步骤
v0.4.0:所有压缩相关参数搬进「设置」界面。压缩触发阈值、保留比例、受控阶段输出预算、提示开关、自动续写次数 —— 五项都能在 设置 → 插件 里改,不用再编辑 preset 的 YAML;顺带补上「被输出上限截断时自动续写」,让对话不再停在"已达到输出 token 上限"。详见设计说明。
English | 中文
功能总览
| 能力 | 入口 / 参数 | 说明 |
|---|---|---|
| 强制压缩(忽略自动阈值) | compact_agents(scope) | 走 ctx.compaction.compactNow,契约原文是*"Explicitly compact useful history even below automatic pressure thresholds"*;自动压缩要等阈值,本工具不等 |
| 覆盖所有活会话 | scope: "others"(默认) / "all" | 覆盖范围是 ctx.agents.list()——主会话、普通子代理、AgentTeams 成员一视同仁,不是只挑团队成员 |
| 主会话压自己 | scope: "self" | 没有子代理时也有效:调用者永远在 turn 中,必然走排队路径,本轮结束自动补压 |
| 指定目标 | scope: "ids" + ids: [...] | 只压列出的会话;找不到的 id 单独回报,不静默吞掉 |
| 忙则排队 | whenBusy: "queue"(默认) | 目标正在跑 turn 时不放弃:监听其 agent/status → idle,这一轮一结束就补压;"skip" 则直接报 busy 不动它 |
| 逐目标回报 | 返回值 results[] | 每个目标给出 compacted / queued / noop / busy / error,以及被遮蔽的节点数与估算 token 数 |
| 越权保护 | — | 只有顶层 agent 能扫描他人;子代理只能 scope: "self",成员无法互压或压队长 |
| 串行调度 | — | 注册为 fail-closed 的 exclusive,一次扫描不会和另一个可能压同一会话的调用并行;超时 30 分钟(排队部分不计入,它在 turn 之后跑) |
| 自动压缩阈值(配套) | compaction-basic 配置 | 本插件不改自动策略;安装脚本顺带核对 thresholdRatio(DSH 默认 0.8×1M=800K 等于永不触发;默认 0.35,即 350K 触发) |
| 压缩过程在对话区可见 | 默认开启;notice: false 关闭 | 订阅 session/event,compaction/start 一落地就往会话尾追加一条插件来源的 user/message,客户端渲染成「上下文注入 · dsh-compact-agents」折叠行:压缩中显示 正在压缩上下文…(当前 213,400 tokens)· 触发线 ×0.35,结束时显示 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点。compaction/start 是在摘要模型调用之前写的,这段提示正好盖住原本什么都看不见的等待。提示里会报出本会话实际生效的触发线;万一热同步没成功,折叠行与正文会直接点明两个值与"新开一条对话才生效" |
| 被输出上限截断时自动续写 | 默认开启;maxAutoContinues(默认 2,0/false 关闭) | 一轮以 turn/end{reason: 'max-tokens'} 结束时,替用户发一句"继续"(agent.followup,与人在界面上发言同一条路),让对话自己走下去。连续次数有上限,任一轮正常结束即清零,避免无止境烧 token |
| 设置界面里能改 | 默认开启;settings: false 关闭 | 注册 settings 命名空间 compact-agents,浏览器 half 在「设置 → 插件」里提供卡片:压缩触发阈值、保留比例、受控阶段输出预算、压缩提示开关、自动续写次数,五项都能在界面上改,不用再去编辑 preset 的 YAML |
为什么需要它
DSH 的手动压缩入口只有一个人机命令 /compact(@deepseek-ai/dsh-command-compact 用 ctx.commands.register 注册,只服务交互式 UI 适配器)。于是:
- headless 的子代理 / AgentTeams 成员没有命令面,执行不了
/compact; - 队长也没有任何工具能替成员压缩——
packages/compaction/下没有任何registerTool; - 而自动压缩(
compaction-basic)是按阈值触发的:阈值没到就不压。
结果就是"会话越跑越贵"过去只能靠换人(退役成员、新建成员)解决。本插件补上这个缺失的模型侧入口。
真实教训:一个成员会话曾以每次调用重发 约 40 万 tokens 的上下文跑到 348 次调用,累计 1.4 亿 cacheRead、¥10.87;而它只是"没人能替它压缩"。
安装
需求:Node.js ≥ 20 + DeepSeek Harness(带
agent-presets的版本)。
一键安装(推荐)
git clone https://github.com/zhuto666/dsh-compact-agents.git
cd dsh-compact-agents
node scripts/install.mjs --dry-run # 先预览要改什么(不改盘)
node scripts/install.mjs # 确认后执行(profile 默认 web,可用 --profile 指定)
脚本做这几件事,且幂等:
- 建三个目录联接(junction),让插件能解析到
@deepseek-ai/dsh-tools、@deepseek-ai/cordis与@deepseek-ai/schemastery——本项目刻意零依赖(不装 node_modules),Node 会把 junction 解析到真实路径,因此拿到的是和宿主同一个模块实例,没有双实例问题; - 往 preset 的
compaction隔离组里追加一行挂载(compact_agents工具、压缩提示、自动续写都靠它):
- id: compact-agents
name: '/absolute/path/to/dsh-compact-agents/index.js' # 安装脚本会自动填成你的真实绝对路径
- 把本插件登记进宿主组成(浏览器 half、设置页那张卡片靠它):在
/profiles/ /node_modules/下建一个指向本仓库的dsh-compact-agents联接,并把dsh-compact-agents加进该 profilepackage.json的dsh.profile.bundles—— 宿主 Loader 于是多出一行compact-agents-client-host(由本包的dsh.bundle.patch→cordis.patch.yml注入,入口client-host.js)。profile 用--profile指定,默认web;改这个package.json前会留一份.bak-compact-agents。
装完请重启一次 dsh:宿主组成变了(profile 多了一个 bundle),重启后「设置 → 插件」里才会出现那张卡片。之后只改 preset 里的参数就不必重启(见「生效」)。
自动发现 $DSH_HOME/.agent-presets/*/agent.cordis.yml 与 $DSH_HOME/profiles/*/node_modules/@linxin666/*/presets/*/agent.cordis.yml;也可以用 --preset 指定要处理的 preset。改文件前会留 .bak 备份,没有 compaction 组的 preset 直接跳过。
DSH 检出位置同样是自动探测的,脚本和文档里不写死任何盘符,依次尝试:
--dsh/$DSH_CHECKOUT/$DSH_HARNESS;- 本项目
node_modules里已有的联接目标(装过一次就连带记下了检出在哪儿); - PATH 上
dsh启动器的真实入口(包管理器生成的 shim 里写着dsh在哪儿)—— 全新克隆、什么线索都没有时,靠的就是这一条; - 各 profile 的
node_modules; - 家目录下的常见克隆位置。
全都落空才报错,并提示用 --dsh 指定。
那行路径不是写死的,是安装时算出来的 —— install.mjs 用自己所在目录推导 PLUGIN_ENTRY,所以:
- 你在哪儿克隆/放这个项目,那行就指向哪儿;
- 项目被移动或改名后,重跑一次
install.mjs就会自动改正:发现已有行指向一个不存在的路径时直接repaired;若旧路径仍然有效(例如另存了一份副本),则只WARN不动手,加--force才重新指向。
node scripts/install.mjs --dry-run # 预览(不改盘)
node scripts/install.mjs # 执行;自动修复失效路径
node scripts/install.mjs --force # 旧路径还有效时也强制重新指向
为什么不能像普通包那样只写包名? 这不是偷懒,是 DSH 的既定语义:preset 行里的裸包名是从 harness 安装位置解析的(
agent-presets/src/mount.ts的PresetTree.import注释原文 "a package name resolves from the harness base"),不是从用户目录;装在工作区/用户目录的包根本解析不到。所以第三方插件在这里只有两条路——写绝对路径,或把插件文件放进 preset 目录跟着走。本项目选前者:单一真源,不给每个 preset 留副本。
为什么要挂两处(宿主组成 + preset)
两个挂载点缺一不可,各管一半:
| 挂载点 | 载体 | 负责 |
|---|---|---|
| preset 行 | /.agent-presets/*/agent.cordis.yml 里 compaction 组的 compact-agents 行(绝对路径指向本仓库 index.js) | compact_agents 工具、压缩进度提示、被输出上限截断后的自动续写。必须待在 compaction realm 内:cordis 的隔离按服务名生效,realm 外解析不到 ctx.compaction |
| 宿主组成里的 bundle 行 | profile 的 dsh.profile.bundles 登记本包 → 本包 dsh.bundle.patch 指向 cordis.patch.yml → 插入 id: compact-agents-client-host / name: 'dsh-compact-agents/client-host'(入口 client-host.js) | 让 DSH 的客户端模块表扫到本包的 dsh.client 声明(从而把 lib/client.js 下发给浏览器),并在宿主根上注册 settings 命名空间 |
只挂 preset 是不够的:DSH 的 ClientModuleRegistry(packages/client/modules/src/index.ts) 只遍历宿主 Loader 的 entries 来决定给浏览器下发哪些客户端 bundle —— 它监听 internal/plugin 的那段里有一行 const entryName = fiber.entry?.options.name; if (entryName === undefined) return,注释明说"fiber.entry 为空的是子插件或手动挂载",直接丢弃;构造时也只做 for (const entry of ctx.loader.entries())。而 preset 里的行是 agent-presets 用 internal.import 手动挂载的、不是 loader 行。所以只挂在 preset 里,浏览器 half 永远不会被下发,症状是设置页里既没有命名空间也没有卡片、且毫无报错。根因与源码位置见设计说明 §8.4。
client-host.js 这个根入口刻意 inject = []:宿主根上没有 compaction 服务(它由 preset realm 内的 compaction-basic 提供),声明依赖只会让这一行永远 pending。它只做两件事:让客户端模块表扫到本包、在宿主根上注册 settings 命名空间;两处入口都调用 registerSettings,靠模块级缓存保证进程级只注册一次(真实的 SettingsProvider.register 对重复命名空间会抛错)。
这不是自创形态:已装的站外插件
@a9i5k4/dsh-auto-memory同样声明了dsh.bundle.patch、dsh.client与exports['./client'],patch 内容就是- insert: [ { id, name } ]。本插件采用与它相同的形态。
生效
改动落在哪一层,决定它怎么生效:
| 改了什么 | 生效方式 |
|---|---|
宿主组成(首次安装新增的 bundle 行、client-host.js、cordis.patch.yml、package.json 的 dsh.* 声明) | 必须重启 dsh —— 重启后设置页里才会出现那张卡片 |
| preset 的压缩阈值 / 保留比例 | 保存即生效:本插件把新值热同步进正在运行的会话(下一次步边界就用新值),同时写进 preset 文件供新会话读取 |
preset 的受控阶段输出预算(tool-bootstrap)与挂载行 | 新开一条对话即可,不必重启 |
插件本体 .js | 必须重启 dsh(ESM 模块缓存,理由见「开发者本地调试」) |
preset 改动靠 standing mount 的文件戳热重载(戳 = stat 的 mtimeMs + size):戳变了,下一条新会话重新挂载一代,就带上工具;已经 composed 的会话因 agent-preset/locked 拿不到,属预期。
但阈值与保留比例不必等那一步:本插件订阅设置面的保存事件,读回 preset 里的新值后直接写进正在运行的 compaction-basic 实例 —— 该实例的 config 是普通自有属性(对象本身被冻结,但引用可换),而压力判定每次调用都重新读它,所以下一次步边界就按新阈值判,不用新开对话、更不用重启。bootstrapMaxTokens 属于另一个插件(tool-bootstrap.mjs 在 apply() 里把值捕获进闭包),改不动,仍然只对新会话生效。
压缩提示里会报出本会话实际生效的触发线;只有热同步真的进不去时(配置形状变了/写不进去),才会多一行说明两个值与出路:
⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。
行配置 livePresetParams: false 可关掉热同步(那就回到"新会话生效"的老行为)。
它还会自我反证(v0.7.1):光"写进去了"不等于"引擎吃了"。热同步之后,插件拿策略自己决定的那次压缩反过来验算 —— 达线判定是 token 数 >= 窗口 × 阈值,所以把阈值从 0.2 抬到 0.5 之后,若 1M 窗口下仍在 250K 就压,说明引擎还按 0.2 判。这时它不再自称生效,而是按 0.2 报触发线,并给出"新开一条对话"这条正路。手动压缩(/compact,或 compact_agents 工具触发的那次)在任意 token 数上都可能发生,一律不当作证据,免得冤枉引擎。
想让现有的成员也被压,就别重启 DSH —— 重启会丢掉所有成员/子代理会话(
ctx.agents.list()只覆盖活着的会话),那就没东西可压了。新开一条对话不影响它们。于是首次安装有个次序问题:"看到设置卡片"和"别丢成员会话"不能同时满足 —— 先压完再重启,或者重启后重新开成员。
compact_agents工具本身不依赖这次重启:preset 行装好,新开一条对话就能用。
校验安装
node scripts/validate-presets.mjs
逐份报告挂载行位置、引用的文件是否存在、以及 compaction-basic 的 thresholdRatio / retainRatio。期望输出:
.agent-presets/liangshen/agent.cordis.yml
rows = compaction-basic,command-compact,compact-agents,tool-result-pruner
thresholdRatio = 0.35 retainRatio = 0.05
compact-agents -> /absolute/path/to/dsh-compact-agents/index.js (存在)
ALL OK (4 preset mounted)
设置页将显示哪些初值,可以用只读脚本核对(一个文件都不写):
node scripts/inspect-presets.mjs
更新 / 卸载
git -C dsh-compact-agents pull # 更新:拉取后重新执行 install.mjs(幂等)
node scripts/uninstall.mjs --dry-run # 卸载:先预览
node scripts/uninstall.mjs # 移除挂载行 + 摘掉 profile bundle 登记 + 删除自己建的 junction
卸载只删自己加的东西:preset 的注释、!!js 表达式、其它行一律不动(实测安装→卸载后文件逐字节回到原状),profile package.json 里的 bundle 登记同样按行摘除、保留原排版。插件目录与 .bak / .bak-compact-agents 备份不删,自行处理。卸载同样改了宿主组成,所以重启 dsh 后设置卡片才会消失。
开发者本地调试
node scripts/integration-test.mjs # 真机加载测试(真 cordis Context + 真 ToolRuntime)
node scripts/deferred-test.mjs # 忙→排队→idle 补压 的行为测试(假 ctx)
node scripts/selftest.mjs # 模块导入 + defineTool 规格自检
改完 index.js 后必须重启 dsh 才能真正生效 —— 这一点很容易踩坑:
- Node 的 ESM 模块缓存按 URL 命中,preset 重新挂载不会清它(DSH 自己的 HMR 插件是靠显式清
internal.loadCache才能热重载的,而它在 profile 里默认disabled); - 所以"新开一条对话"只会重新挂载 preset,插件的模块本身仍是进程里已缓存的旧代码;
scripts/*.mjs是每次直接执行的脚本,不受影响,改完立即是新的。
只有改了 preset 时,新开对话就够了;改了插件
.js(含client-host.js/settings.js)或宿主组成相关的文件(cordis.patch.yml、package.json的dsh.*声明)都必须重启dsh。
用法
模型侧调用(不是人机命令):
compact_agents(scope = "others" | "all" | "self" | "ids", ids?: string[], whenBusy = "queue" | "skip")
| scope | 含义 |
|---|---|
others(默认) | 除调用者以外的所有活会话 |
all | 所有活会话,含调用者自己 |
self | 只压调用者自己 —— 没有子代理时也有效 |
ids | 只压 ids 里列出的会话 |
典型说法:
- 「把其他会话都压一遍」→
scope: "others" - 「我这条对话也一起压」→
scope: "all"(你自己会被排队,本轮结束补压) - 「只压我自己」→
scope: "self"
模型没调用它? 工具描述里已写明"用户要求压缩上下文时立即调用、不要反问",但仍可能遇到模型选择先确认一下。最稳的说法是把工具名说出来:
调用 compact_agents,scope=all,把所有会话压一遍工具本身与模型行为无关——只要它在工具清单里,点名调用必定执行。
返回每个目标一行,例如:
compact_agents: 3 compacted, 1 queued, 0 skipped, 0 failed (of 4 selected).
- sess_ab12: compacted, ~213,400 → ~49,800 tokens — shadowed surface 12-107
- sess_cd34: compacted, ~52,100 → ~9,040 tokens — shadowed surface 3-33
- sess_ef56: noop, ~0 tokens shadowed — nothing safely compactable (empty session, or one oversized retained unit)
- sess_gh78: queued — mid-turn; queued and will be compacted as soon as it goes idle
beforeTokens / afterTokens 是压缩前后用 ctx.tokenMeter 实测的表面估算;测不到时为 -1。
对话区的压缩提示
工具之外,任何压缩(包括阈值触发的自动压缩)都会在对话区留下一条可见提示 —— 形态就是框架自己 注入上下文时用的那种折叠行:
▸ 上下文注入 · dsh-compact-agents · 正在压缩上下文…(当前 213,400 tokens) · 触发线 ×0.35
▸ 上下文注入 · dsh-compact-agents · 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点
compaction/start 是在摘要模型调用之前写入会话日志的,所以第一条提示正好出现在原本那段
什么都看不见的等待里。行配置 notice: false 可关闭(工具不受影响)。
折叠行末尾那个 触发线 ×0.35 是本会话实际生效的值。正常情况下它总是跟 preset 文件一致 ——
保存设置时插件会把新值热同步进运行中的会话;只有热同步进不去时才会多一行:
⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。
被输出上限截断时自动续写
一轮以 turn/end{reason: {kind: 'max-tokens'}} 结束时(DeepSeek 的 finish_reason: 'length'),
插件会替用户发一句"继续",让对话自己走下去,而不是停在那里等人手动发:
▸ 上下文注入 · dsh-compact-agents · 上一轮被输出上限截断,已自动续写(1/2)
继续 ← 插件以用户身份发出的(和用户手打的一样)
- 次数上限由行配置
maxAutoContinues控制,默认 2;0或false关闭。用满后不再续写, 改为提示"已停止自动续写",避免"截断 → 续写 → 又截断"无止境烧 token。 - 任一轮正常结束即清零,所以额度是"连续"次数,不会长期耗尽。
- 发出的是标准
user/message(source.kind: 'user'+ 冻结 + 唯一 id),走的就是人在界面上发消息 的同一条路(agent.followup)—— 不是往会话表面塞一条不唤醒模型的消息。
为什么需要它:压缩后 preset 常会把下一个请求的输出预算压到很小的窗口来"重新锚定"。 若模型开着高推理,思考 token 与正文共享这份预算,很容易整份被思考吃光 → 正文 0 字被判截断。 详见设计说明 §7。
在「设置」里改这些参数
打开 设置 → 插件,会看到一张「压缩与自动续写」卡片:

这张卡片是纯参数卡片:本插件在界面上没有自己的按钮 —— 压缩与自动续写都是自动发生的, 卡片上的「重置」只是设置框架自带的还原入口,不会触发压缩。 DSH 里唯一的手动压缩入口是自带的人机命令
/compact(不是本插件);本插件提供的是模型侧工具compact_agents,由模型调用,不是给人点的。
| 字段 | 含义 | 生效时机 |
|---|---|---|
| 压缩触发阈值比例 | 0.35 = 上下文用到 350K 就自动压缩 | 新建会话生效 |
| 压缩后保留比例 | 压缩后按该比例留下最近的历史 | 新建会话生效 |
| 受控阶段输出预算 | 每次压缩后会重新进入的"受控阶段"里,单个请求的输出预算 | 新建会话生效 |
| 压缩进度提示 | 是否在对话区播报「正在压缩上下文…/压缩完成」 | 立即生效 |
| 自动续写次数 | 被输出上限截断时最多自动发几次"继续"(0 = 关闭) | 立即生效 |
卡片上还会标出哪些字段是你覆盖过的(可以单独"重置"回 preset 里的值)。
卡片本身长什么样,见上面那张标注图;图里的编号与右侧图例一一对应。docs/images 记录了这批图的来源与做法(真实界面截图 + 标注,不含任何个人信息),将来若要补别的截图,命名与插入位置也在那里写清。
卡片能出现的前提是宿主组成里有那一行(本包作为 profile bundle 被登记、进而插进宿主 Loader):只在 preset 里挂载的话,浏览器根本收不到 lib/client.js,症状是设置页里既没有命名空间也没有卡片、且毫无报错。所以首次安装后、以及任何改动宿主组成之后,都要重启 dsh(根因见设计说明 §8.4)。
设计要点(为什么分成两种生效时机):
- 前三个值属于 preset 里的其它插件(
compaction-basic的thresholdRatio/retainRatio、tool-bootstrap的bootstrapMaxTokens),插件没法替它们改运行时策略,所以改的是 preset 文件本身。preset 的挂载会记录文件 stamp,stamp 变了就给之后新建的会话开新一代 ——所以不用重启 DSH,但已经在跑的会话不受影响。写盘前会落一份.bak-compact-agents备份,并用临时文件 + rename 原子替换;只改目标那一行,preset 里的注释与排版原样保留。 - 后两个值是本插件自己的,会话事件发生时才读,所以改完立即生效。
settings: false可以整体关掉这个设置面(工具与提示不受影响)。
每个参数到底在管什么
上面那张表只说了"字段是什么意思"。这一节说清这个参数到底在管什么、往哪边调会怎样。 机制层面的理由(为什么这样设计)见设计说明 §9。
先看一次压缩从头到尾发生了什么(以 1M 窗口 ≈ 100 万 tokens、preset 用 0.35 / 0.05 为例):
压之前 系统提示 1 万 + 历史 34 万 = 35 万
└─ 撞到 thresholdRatio 0.35 的触发线 → 开始压缩
压缩中 把"最近 5 万"以外的部分【遮蔽】掉(不是删除,是移出发送内容)
└─ 换成一段摘要;压缩提示播报里报的"遮蔽节点数"就是它
压之后 系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万 ≈ 6.3 万
└─ 这"最近 5 万"由 retainRatio 0.05 × 窗口 100 万 决定
随后 进入【受控阶段】:接下来每次请求最多输出 bootstrapMaxTokens tokens
三个数各管一段,互不重叠:thresholdRatio 管什么时候压,retainRatio 管压完留下多少原文,
bootstrapMaxTokens 管压完那几轮最多让它说多少。没有一个参数管"摘要写得好不好"——那是摘要模型的职责。
把上面这条链路画成图,就是一次压缩的全过程(数值与文字版一致):
flowchart TD
A["压缩前:系统提示 1 万 + 历史 34 万
≈ 35 万 tokens"] --> B["撞到 thresholdRatio 0.35 的触发线
(1M 窗口 × 0.35 = 35 万)"]
B --> C["压缩:把「最近 5 万」以外的部分
遮蔽成一段摘要(不删除,只移出发送内容)"]
C --> D["压缩后:系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万
≈ 6.3 万 tokens"]
D --> E["「最近 5 万」= retainRatio 0.05 × 1M 窗口"]
D --> F["进入受控阶段:
此后每次请求最多输出 bootstrapMaxTokens"]
主线只有一条:达线 → 遮蔽成摘要 → 进入受控阶段。下面按段拆开讲。
压缩后保留比例 retainRatio
本质是保真的边界:这条线以内的内容原文一字不差保留,线以外只剩摘要。
- 它的单位是窗口比例,不是消息条数:
0.05× 1M = 保留最近 5 万 tokens 原文。 - 为什么必须留一块原文:摘要一定丢细节;而最近发生的内容恰恰最可能马上要用(刚贴的代码、刚提的 需求、刚纠正的错误)。被总结掉就会出现"我刚说过的它当没看见"。
- 调小的症状:
0.01= 只留 1 万,一个几百行的代码文件差不多就 1 万 tokens,压完立刻被总结掉 —— 模型转头就"忘"。 - 调大的症状:
0.2= 留 20 万,压完还剩约 25 万,很快又撞触发线 → 反复压缩、反复打断。 - 建议:一般对话
0.05够用;经常贴大文件可调到0.08~0.1。
受控阶段输出预算 bootstrapMaxTokens
它是输出预算,不是输入预算:压缩结束后那一小段时间里,每次请求最多让模型输出多少 tokens。
- 最容易踩的坑:
max_tokens把思考(reasoning)token 也算在内。 所以1024时可能光思考就 吃满预算、正文一个字都出不来 —— 表现为"空回复"或话说到一半硬截断。(这正是本插件存在的起因之一: 此前观测到的 4 次截断全是outputTokens=1024、正文 0 字。) - 为什么要"受控":刚压缩完,模型拿到一段崭新摘要,很容易一口气写一大篇,把刚清出来的空间又塞满, 前一次压缩就白做了。所以 preset 在每次压缩结束后把会话打回受控状态、强制卡住输出;等会话"晋升" (解除受控)后恢复模型本身的大预算。它是压缩之后的一段临时限流,不是永久设置。
- 调小:
16384 → 1024省,但极易截断。 - 调大:
32768不容易截断,但受控阶段每轮都可能很贵。 - 与「自动续写」配套:预