DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

MaudieHakimi /

MaudieHakimi/dsh-entry-shaper

Verified

A Deepseek Harness Plugin to Shape The Entry of LLM.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@6a124ae1

dsh-entry-shaper

把上下文压缩从出口挪到入口:元素在被首次发送之前定型,此后永不改写 ⇒ 出网 payload 的前缀保持单调 ⇒ 不再产生断点税地缩小上下文。

状态:实验性 · 默认关闭 · 测试 82/82 · Node ≥ 20 · MIT

⚠️ 这句话的确切边界(避免过度承诺):

  • "零断点税"指的是本插件自身不引入断点 —— 它不改已发送的内容、也不在 wire 上做 有状态变换。它不能消除别处产生的断点:官方自动压缩、缓存 TTL 过期、 换 provider、手工改配置都会照旧打断前缀(见§ 实测数据与 §1b)。
  • 前提是确定性:纯 op 天然满足;不可证明为纯的 op(exec/http/inline.code/注入函数) 靠内容哈希缓存兜住。若你把它关掉(cache: false)而实现又不确定,前缀就会抖动 ✗。
  • 当前默认只塑形推理一类,其余类别保持 keep ⇒ 省幅上限由推理在 payload 中的占比决定 (实测 −21% ~ −47%:推理占比低的会话接近下限,长推理会话接近上限)。

📌 来源声明

本插件的全部源码(index.js、测试、基准、文档与工具脚本)完全由 DeepSeek-V4.1-Flash 生成,人类只负责提出需求、做出取舍决策与验收。

角色 承担者
需求、取舍决策、验收 @MaudieHakimi
代码生成(全部源码与文档) DeepSeek-V4.1-Flash

这意味着:

  • 代码里保留了大量"为什么这么做"的注释(含踩过的坑与实测数字)—— 那是生成过程中 真正得到的结论,不是事后补的说明;
  • 数字都尽量给可复现的取证方式(bench/、tools/、以及 harness 源码出处), 因为这个项目里出现过若干次"看起来对、实测否掉"的判断(见 CHANGELOG.md 的撤回记录);
  • 若发现任何事实性错误、过时描述或与代码不符的文档,请当作 bug 报告 —— 那属于需要修的东西。

实测(本机真实会话 + 真实 API,最新一次):

指标 实测值
出网 payload 缩减 −46.6%(插件自证日志 proxy-shape,长会话 03/04 两天分别 −40.1% / −46.6%)
缓存命中率 98.4% ~ 99.6%(9 天 2739 步的长会话 / 7 天 1085 步的会话)
正常回合的未命中 1,148 ~ 4,140 token(≈ 每步新增量 ⇒ 零断点)
正常回合成本 $0.019 ~ $0.038 / 回合

取证方法见§ 实测数据;日志与界面永远保留原文。


目录

  • 问题:出口侧的小步压缩,在命中价极低的 provider 上是净亏的
  • 做法:在入口定型
  • 它是什么 / 不是什么
  • 官方栈已经做了什么(先读这个)
  • 安装
  • 配置
  • 三种挂载方式
  • 统一协议:一切外部塑形都走同一套 I/O 约定
    • 链式组合:七条性质(全部实测)
  • 栈护栏
  • 实测数据
  • 现状与限制
  • 故障排查
  • 文档
  • 开发
  • 许可

问题:出口侧的小步压缩,在命中价极低的 provider 上是净亏的

这一节只针对三种具体做法,不是泛指"压缩都不好":

维度 会亏的做法 不会亏的做法
改哪里 改已发送过的历史("出口"压缩)⇒ 打断前缀缓存 改尚未发送的内容("入口"定型)⇒ 前缀从未包含旧形态
一次削多少 每次只削一小块(自动压缩按 step 触发,常见 3–9 万 token/笔) 一次打到底(如手动 /compact:753K → 23.7K)
按什么价折算 省下的是命中价 token(便宜 50 倍) 省掉的是未命中价 token(全价)

三者同时成立才会亏 —— 缺任何一条都可能反而是赚的(下表里手动 /compact 就是反例)。

亏损的机制

DSH 的 compaction-basic 在出口改历史:把已发送的区间换成摘要检查点。每一刀都会让 改动点之后的缓存作废 ⇒ 下一次请求从该点起整份按全价重算(本文称"断点税")。

而它的收益只有"实际削掉的那部分"(按命中价计)⇒ 于是:

收益 ≈ 削掉的 token × 命中价
断点税 = 压缩后整份 payload × 未命中价

雪上加霜的是它的区间选择(packages/compaction/compaction-basic/src/region.ts:98-133): 区间头锚定 —— 从最早的表面节点起、切到"保留尾部"之前 ⇒ 检查点落在头部 ⇒ 它后面的一切都作废。所以削得越少越亏。

本机实测(一个 2739 步的长会话,见 docs/OFFICIAL-STACK.md)

做法 削掉的 token 断点税(全价重算) 比值 判定
compaction-basic 自动压缩 109 笔合计 3,021,022 6,282,630 0.48 : 1 ✗ 净亏 2 倍
其中单笔最差 64,891 547,342 0.12 : 1 ✗ 亏 8 倍
人类手动 /compact(一次打到底) 729,434 6,176 118 : 1 ✓ 大赚

同一套代码、同一个会话,只因"一次削多少"不同,结论相反 —— 这正是上表第二行的意思。

做法:在入口定型

会话是一根只进不出的栈。元素在首次回传之前定型,前缀里就从来没出现过它的旧形态:

请求 n   : [S][T][U][1'][2']…[n-1']
请求 n+1 : [S][T][U][1'][2']…[n-1'][n']
           └──────── 逐字相同的前缀(全命中)────────┘ └ 新增 ┘

本插件的代理通道把这件事放在 wire 层:拿 deriveMessages() 产出的消息数组, 按类别跑 op 链,再交给上游 provider。会话与日志完全不动,只有发出去的那一份被塑形。

它是什么 / 不是什么

✅ 它是 ❌ 它不是
代理 provider:llm.registerAdapter() 注册一个新路由,转调上游 不接管 compaction 服务(可与官方 dsh-compaction-basic、dsh-context-slimmer 共存)
原文永远在 append-only 日志里;界面与审计读原文 不改写已发送的内容(那是"出口压缩"的领域,要付断点税)
塑形逐消息确定:纯 op,或不可证明为纯的 op 走内容哈希缓存 ⇒ 同一条旧消息每次出网形态一致 ⇒ 前缀稳定 在代理通道上不自己发 HTTP(见下)—— 它把请求交回 llm 服务,凭据仍由服务按上游 id 解析 ⇒ 不需要新 API key
失败即放行(fail-open):任何异常只写日志,请求照常发 日志不记录消息内容(只记字符数、策略名与计数)
可选:exec / http / inline 三种 op 可用于"你自己接入的塑形" 默认不做脱敏(无内置密钥/隐私识别规则;脱敏需你在 redact 槽位注入)

⚠️ 最后两行的边界不要混淆:代理通道本身不发 HTTP(它复用 llm 服务), 但你显式配置 http op 时它会 POST 到你指定的 endpoint;配 exec 时会起子进程。 两者都只在你配置之后才发生(详见 SECURITY.md 的"它会访问什么")。

官方栈已经做了什么(先读这个)

DSH 自带栈里已经有一个确定性免费剪枝器和一个摘要压缩器。 装本插件之前请先确认你要的是 本插件的空位:

组件 触发条件 代价 模型视角
tool-result-pruner(默认开) 单条 tool result > thresholdChars(8192) 零(纯函数、逐条独立 ⇒ 不破前缀) 中段换成剪枝标记,保留头 4096 + 尾 1024
compaction-basic(默认开) prompt 达 thresholdRatio × 窗口(0.8),保留 retainRatio(0.16) 一次断点(全价重算) 老历史被 LLM 摘要取代(不可逆)
本插件 每次出网 零(前缀单调) 按类别塑形后的版本;原文仍在日志里

空位只有三处:① 推理内容(官方剪枝器完全不碰,而它是日志里最大宗的 payload: 本机 40,916 条 reasoning-chunks vs 20,570 条 assistant/chunk); ② 按类别的策略(官方只有一个全局字符阈值);③ 保留原文的 wire 级塑形(压缩不可逆)。

细节与出处:docs/OFFICIAL-STACK.md。

安装

# 路径换成你放本包的位置
dsh plugin --profile web add "file:D:\plugins\dsh-entry-shaper"

或手动:把目录复制进 <profile>\node_modules\,并把 dsh-entry-shaper 加进 profile package.json 的 dependencies **与 dsh.profile.bundles**(后者决定它是否被加载; dsh.bundle.patch 必须仍指向本包的 cordis.patch.yml,否则启动即抛错)。

配置

配置一律放在 ~/.dsh-entry-shaper.config.json,不要写进 cordis.patch.yml。 原因:patch 里含 config: 行会让市场无法热挂载(dshmarket/lib/hot.js:295), 只能重启生效;热配置文件则由本插件按 mtime 热读 ⇒ 改配置不用重启 ✓。

{
  "enabled": true,              // 总开关(任何类别都不动就设 false)
  "shapeReasoning": true,       // 是否塑形推理这一类(旧名 dropReasoning 仍兼容)
  "policy": {                   // 按类别各给一条 op 序列(文本 → 文本 的纯函数链)
    "reasoning":     { "ops": [{ "op": "extract", "maxChars": 240 }] },
    "toolResult":    { "ops": [{ "op": "keep" }] },
    "toolArgs":      { "ops": [{ "op": "keep" }] },
    "assistantText": { "ops": [{ "op": "keep" }] }
  },
  "cache": { "enabled": true, "path": "~/.dsh-entry-shaper.cache.jsonl", "maxBytes": 8388608 },
  "proxy": { "enabled": true, "provider": "deepseek-shaped", "upstream": "deepseek-official" },
  "opTimeoutMs": 8000,
  "logPath": "~/.dsh-entry-shaper.log"
}

打开代理只需改这里(proxy.enabled: true),本插件会惰性注册该路由 —— 不必重挂载、 不必重启 ✓。随后把会话或默认 provider 换成 proxy.provider(默认 deepseek-shaped)即生效。

⚠️ 路径里不要写 ~。配置文件里的路径是原样使用的(没有 shell 展开), 写 "logPath": "~/.dsh-entry-shaper.log" 会在当前目录下建一个名叫 ~ 的目录 ✗。 省略这些键(推荐,默认值已经是正确的绝对路径),或写绝对路径 (如 "D:/logs/shaper.log")。

免重启的三个开关

想做什么 怎么做 生效
全部停手(原样转发) 建空文件 ~/.dsh-entry-shaper.OFF 立刻,不用重启
只停某一类 把该类别的 ops 改成 [{ "op": "keep" }] 下一次请求
完全卸载 市场里关掉插件(或从 dsh.profile.bundles 移除) 立刻(热挂载)

⚠️ 改配置会改变前缀形态 ⇒ 下一次请求整份按全价重算一次(约 $0.2)。 所以请一次定好,别频繁来回调。

三种挂载方式

方式 命令 / 操作 需要重启吗 说明
profile bundle dsh plugin --profile web add "file:…" 或手改 profile package.json 需要 最稳,随 profile 长期加载
市场热挂载 在市场插件页点开关 不需要 要求 patch 是纯 insert(本包已满足);窗口期不影响已有会话
直接 mount 手工在一个 cordis 上下文里 ctx.plugin(module) 不需要 用于实验

热挂载的两条硬限制(实测):

  1. 不刷新模块缓存 —— 重挂载只新建实例,代码改动仍需重启;
  2. 热挂载条目的 id 是 mkt-<原id>,与 patch 层的 - id: <原id> 不是同一个条目 ⇒ patch 层的 disabled: true 管不住热挂载实例。

统一协议:一切外部塑形都走同一套 I/O 约定

op 名只表示"传输方式",不表示"这是 .py / 这是 LLM"。任何语言、任何命令、任何 OpenAI 兼容端点,都通过同一套信封与返回规则接入 —— 这是本插件唯一的扩展接口。

                        传输(transport)
        ┌──────────────────┬──────────────────┬─────────────────┐
   exec │ 本地进程          │ http │ 任何 HTTP   │ inline │ 进程内 │
        │ python/node/pwsh │      │ 端点        │        │ 纯算法 │
        │ /你的 exe        │      │             │        │        │
        └──────────────────┴──────────────────┴─────────────────┘
                 ↕ 数据格式(protocol)
        json │ text │ lines │ argv        (exec 用;http 用 json/text)

输入信封(每次调用必送)

{ "text": "<待塑形文本>", "kind": "reasoning|toolResult|toolArgs|assistantText",
  "meta": { }, "op": "exec", "protocol": 1 }

protocol: 1 是协议版本号 —— 你的脚本可以据此判断该怎么解析。

返回值(三种形态都接受,按序判定)

# 你输出什么 插件怎么理解
1 JSON 对象含字符串 text 用这个 text
2 JSON 对象含 "text": null 整块删除
3 其它任何字符串 取原文(trim 后)

失败只跳过该 op(退出码≠0 / 超时 / 输出空 / 抛错)⇒ 保留上一步结果。

exec:任何命令

{ "op": "exec", "command": "python", "script": "D:/shaper/mine.py" }
{ "op": "exec", "command": "node",   "args": ["D:/shaper/mine.mjs"] }
{ "op": "exec", "command": "pwsh",   "args": ["-File", "D:/shaper/mine.ps1"] }
{ "op": "exec", "command": "D:/tools/shaper.exe", "args": ["--mode", "fast"] }
参数 默认 说明
command 必填 可执行文件(python / node / pwsh / 你的 exe)
script — 便捷写法:作为第一个参数。args 里的 {} 会被替换成它
args [] 参数数组;{text} 会被替换成待塑形文本(仅 argv 协议需要)
protocol json 输入格式,见下
cwd / env — 工作目录 / 追加的环境变量(与 process.env 合并)
timeoutMs 全局值 超时即 kill 并跳过
protocol stdin 收到 适合
json 一行信封 JSON 新写的脚本(推荐)
text 纯文本 只想要原文的现成工具
lines 第一行 = kind,其余 = 文本 shell 友好的轻量写法
argv 不写 stdin,文本作为最后一个命令行参数 不接受 stdin 的现成程序(注意命令行长度上限)

⚠️ Windows 上的 Python 中文乱码:Python 往管道写非 ASCII 时默认用系统代码页 (GBK/cp936),插件按 utf8 解码 ⇒ 中文变 ????。插件已替 python 自动设好 PYTHONIOENCODING=utf-8(你在 env 里给同名键即可覆盖),但脚本里自己写一句 sys.stdout.reconfigure(encoding="utf-8") 更稳妥。注意 python -X utf8 不能解决 这个问题(那条只管源码与文件,不管管道)。

http:任何 OpenAI 兼容端点

// 标准 OpenAI 形状:system 提示词 + 待塑形文本作为 user 消息
{ "op": "http", "baseURL": "https://api.deepseek.com", "model": "deepseek-flash",
  "system": "压成 3 行要点,保留路径与数字。", "maxTokens": 200 }

// 追加一条微调指令(作为第二条 system 消息)
{ "op": "http", "url": "https://gateway/v1/chat/completions", "system": "…", "instruction": "再简短些" }

// 非 OpenAI 形状的服务:body 整包覆盖({{text}} 会被替换成待塑形文本)
{ "op": "http", "url": "https://my.example/shaper", "body": { "input": "{{text}}", "mode": "fast" } }
参数 默认 说明
url — 完整端点(给了它就原样使用)
baseURL ctx.llm.baseURL → DeepSeek 官方 自动补 /chat/completions
system 内置中文提示词 系统提示词(旧名 prompt 仍可用)
instruction — 追加的第二条 system 消息(微调用)
apiKey ctx.llm.apiKey → DEEPSEEK_API_KEY Bearer 令牌
model / maxTokens / temperature / headers / method — 常规请求控制
body — 整包覆盖请求体(接非 OpenAI 形状时用)
protocol json text ⇒ 响应体当纯文本

响应解析(json 协议)按序尝试:choices[0].message.content → text → output → content → data → 响应原文。所以大多数兼容服务不用改配置就能用。

inline:进程内算法(不必写脚本、不必起进程)

{ "op": "inline", "use": "firstLine", "maxLines": 3 }          // 只留前 3 行
{ "op": "inline", "use": "jsonField", "field": "a.b.c" }        // 从 JSON 里取字段
{ "op": "inline", "use": "regex", "pattern": "id=(\\d+)", "template": "#$1" }
{ "op": "inline", "use": "code", "code": "return text.split('\\n').slice(-5).join('\\n')" }

use: "code" 的 code 是函数体源码字符串(可用 text / op / ctx,return 结果) ⇒ 能写进热配置文件 ⇒ 改算法不用重启 ✓。它无法被证明是纯函数,因此总是走缓存。

纯 op:不改结构,只挑内容

op 参数 说明
keep — 原样(默认)
drop — 整块删除(推理块例外:内核退回短桩,见下)
stub — 用 stubTemplate,默认 [reasoning omitted: {chars} chars]
extract maxChars(240)
marker(true)
只留决策句;marker:false 去掉省略前缀
head / tail chars(512) 保留前 / 后 N 字符
truncate maxChars(512) 首尾各半 + …(省略 N 字符)…

每个 op 都可以带 timeoutMs(覆盖超时)与 cache: false(非纯 op 别关)。

复制即用的配置片段

// ① 默认:推理只留决策句(最省,也最有损)
"reasoning": { "ops": [{ "op": "extract", "maxChars": 240 }] }
// ② 保守:保留推理的真实开头(不按句式挑,模型更容易接上)
"reasoning": { "ops": [{ "op": "head", "chars": 800 }] }
// ③ 两道闸:先抽取再兜底截断
"reasoning": { "ops": [{ "op": "extract", "maxChars": 400 }, { "op": "truncate", "maxChars": 300 }] }
// ④ 完全不动推理
"reasoning": { "ops": [{ "op": "keep" }] }
// ⑤ 工具结果只留首尾(注意官方 tool-result-pruner 已在做,别重复)
"toolResult": { "ops": [{ "op": "truncate", "maxChars": 1024 }] }
// ⑥ 你自己的脚本(任何语言)
"reasoning": { "ops": [{ "op": "exec", "command": "python", "script": "D:/shaper/mine.py" }] }
// ⑦ 让 LLM 来压
"reasoning": { "ops": [{ "op": "http", "system": "压成 3 行要点,保留路径与数字。", "maxTokens": 200 }] }
// ⑧ 进程内自定义算法(免重启)
"reasoning": { "ops": [{ "op": "inline", "use": "code", "code": "return text.split('\\n').slice(-5).join('\\n')" }] }

旧名(仍可用,但不建议新写)

python 是 exec 的别名(预填 command: "python"),summarize 是 http 的别名 (prompt 即 system)。两者走同一套实现 ⇒ 缓存与失败语义完全一致。 它们存在的唯一理由是兼容旧配置;新配置请直接用 exec / http。

op 可任意链式组合,可以是 async,统一受 opTimeoutMs(默认 8s)约束:超时或抛错 ⇒ 只跳过该 op、保留上一步结果(fail-open)—— 最坏情况是"没省到",绝不会挂住会话。 未知 op 名同样被跳过(拼错名字不会让整条链失效)。

链式组合:七条性质(全部实测)

线性可组合性的意思是:每个 op 都是 文本 → 文本(或 → null), 前一个的返回值就是后一个的入参,而 op 的来源完全无关。内核里只有一个载体 —— applyOps 的局部变量 out:

let out = text
for (const op of ops) {
  const produced = await withTimeout(fn(out, op, ctx), op.timeoutMs ?? ctx.opTimeoutMs ?? 8000)
  out = produced
  if (out === null || out === undefined) return null   // ① 立即终止整条链
}
return out
# 性质 实测
1 线性:前一个的输出即后一个的输入 'a-b-c' 经 [replace(-→+), toUpperCase, replace(+→|)] ⇒ "A|B+C"
2 异构混排:纯函数 / 本地进程 / HTTP / 进程内 可插在任何位置 [head, exec(python), http, inline.regex, truncate] ⇒ "原始文本[llm]尾巴"
3 逐 op fail-open:一个坏掉不毁整条链 [A, exec(不存在的命令), AB, ABC] ⇒ "ABC"(日志:op exec 失败,跳过)
4 逐 op 超时:慢 op 被跳过,其余照常 [http(挂起, timeoutMs:50), inline] ⇒ 后续 op 的结果正常返回
5 提前终止:返回 null ⇒ 整条链立即结束 [stub, drop, 不应出现] ⇒ null(第三个 op 根本没执行)
6 逐 op 缓存:纯 op 不缓存,其余各自缓存 同一输入跑两次,结果逐字节相同且真实 HTTP 只调用 1 次 ⇒ 这就是前缀稳定的根据
7 逐 op 覆盖:同 op 名可在链上多次出现、各带参数 [head(6), tail(3), truncate(30)] 作用于 一二三四五六七八九十 ⇒ "四五六"

timeoutMs / cache 也可以每个 op 单独写。

最实用的一条结论:换实现不动架构。 同一个位置可以是纯函数、子进程、本地 LLM 或云端 —— policy 里只改那一个对象。例如把默认的决策句抽取升级为"本地 LLM 语义压缩":

// 之前
"reasoning": { "ops": [{ "op": "extract", "maxChars": 240 }] }
// 之后(其余一切照旧:缓存、超时、fail-open、代理链路)
"reasoning": { "ops": [{ "op": "http", "baseURL": "http://127.0.0.1:11434/v1",
                         "model": "qwen2.5:7b", "system": "压成 3 行要点。", "timeoutMs": 3000 }] }

⚠️ 两个实测发现的细节:

  • inline.regex 默认只替换第一处,要全局替换需显式加 "flags": "g";
  • 直接调用导出的 applyOps(不经 apply())时,inline.code 需要你自备 ctx.compileInline(插件运行时由 apply() 提供)。

上面七条可以自己跑一遍复现:node examples/compose-demo.mjs(会真实启动一个 python 子进程;无法启动时 fail-open 跳过该 op,其余性质照常验证)。

接自己的算法:三条路怎么选

方式 写法 能热改吗 适合
组合内置 op [{ "op": "extract" }, { "op": "truncate" }] ✅ 大多数场景,先用这个
exec 外部脚本 任何语言,读 stdin / 写 stdout ✅ 任意算法;可单独调试
inline.code 函数体源码写在配置里 ✅ 不想起进程的小逻辑
config.ops 注入函数 patch 里写 config.ops ❌ 需重启 需要进程内状态/依赖时

⚠️ 注入函数会牺牲热挂载:patch 出现 config: 行 ⇒ dshmarket/lib/hot.js:295 判定为"含配置行"⇒ 本插件失去热挂载能力。同一件事能用前三条做就别用第四条。 ctx.llm / ctx.redact / ctx.fetch / ctx.spawn 同理(函数与凭据写不进 JSON)。

⚠️ 自定义 op 也会进缓存:无法判断你的实现是否纯,而"结果漂移 ⇒ 前缀改变 ⇒ 付断点税" 比"多占几 KB 磁盘"贵得多。不同实现请用不同的 op 名字 —— 缓存键里只有名字与参数。

完整配置文件见 examples/config.full.json, 接入自定义算法的详解见 examples/custom-op.md。

非纯 op 必须缓存(前缀稳定性的前提)

python / summarize 不保证可复现(temperature 0 也不保证:批处理、MoE 路由、服务端版本、 并发都可能变)。同一条旧消息两次出网算出不同结果 ⇒ 前缀改变 ⇒ 付断点税。 所以非纯 op 的结果按 sha256(配置指纹 + op 参数 + 输入文本) 缓存:

性质 行为
改 prompt / model / 端点 键变化 ⇒ 自动失效 ✓
重启 DSH 仍命中 ✓(启动时把 JSONL 读进内存)⇒ 重启不打断前缀
存了什么 只存摘要,不存原文 ✓

一条硬约束(踩过)

推理块永远不会被整块删除。 thinking 模式下,当报文以 role:'tool' 结果结尾时缺少 reasoning_content 会被 API 拒绝:

400 The `reasoning_content` in the thinking mode must be passed back to the API.

换成短桩则 200 ⇒ API 要的是字段在,不是内容全。所以内核在链把它删空时会退回短桩 (test/shaping.test.mjs 有对应用例)。

栈护栏

# 护栏 实现
1 只处理原始追加事件 surfaceOp !== 'append' 一律跳过
2 替换范围恰为自己 { op: 'replace', start: seq, end: seq } ⇒ 从不触碰更早的 seq
3 元素若已不在当前表面(被别人替换/被压缩覆盖)⇒ 放弃 session.surface.nodes 成员检查
4 异步 op 超时即跳过,并量"追加 → 定型"延迟 opTimeoutMs + lagWarnMs

代理通道不依赖 1–4(它不碰会话),但继承"确定性"这一条:纯 op 天然确定, 非纯 op 靠缓存兜住 ⇒ 同一输入永远同一形态。

实测数据

全部为本机真实会话 / 真实 API 的实测。

1. wire 级塑形:−40.1%,缓存命中 99.3%

用 dsh-llm-tap 抓到的真实出网请求,把外层(deepseek-shaped,塑形前)与 内层(deepseek-official,真正出网)按 callId 配对(messageCount 完全相同 ⇒ 同一次调用):

字符数
外层(塑形前) 12,814,907
内层(塑形后) 7,678,485
省 5,136,422(40.1%),逐对 39.6%–40.4%

同一会话的 assistant/message.usage:命中 99.3%(如 hit 191,872 / miss 1,270), 且命中量每步按上一步新增量平稳递增 ⇒ 前缀在延长、没有被重建 ✓

折成钱:省下的是命中价 token($0.02/M)⇒ 按 193K token/步 估算约 $0.0015/步, 300 步的长会话约 $0.46(≈3 元)。不大,但是白拿的 —— 而且与轨迹长度成正比。

对照:一次断点付的是整份 payload 全价($1/M)。所以"省 40% 的命中价"和"招来一次断点" 完全不是一个量级 —— 这也是为什么前缀单调比"省得多"更重要。

1b. 成本结构实测:钱到底花在哪(2026-09 多会话审计)

这部分与本插件无关,但决定了它值不值 —— 实测自本机 9 天/2739 步(759,991,919 输入) 与 7 天/1085 步(380,897,683 输入)两个长会话:

事实 实测
成本 ≈ 步数 × payload 单回合 18 步 × 峰值 payload 507,255 = 输入 9,061,476($0.67);其余回合同样吻合
未命中只占输入 ~1.6%,却占 ~57% 的钱 命中价 / 未命中价 = $0.02 / $1(相差 50 倍)
最大单项支出是"断点税" 2739 步会话:182 次断点、断点税占未命中的 87%($10.59 / 总 $27.08)
断点主因是"隔太久回来"(缓存 TTL) 间隔 1060 min → miss 497,394;间隔 7158 min → miss 266,259;而间隔 0.1 min 的步 miss 仅 ~500–1,400

间隔 vs 平均命中率(同一会话 2694 步全样本,实测):

距上一步 样本 平均命中率
<1 min 2501 96.7%
1–3 min 118 94.6%
3–6 min 46 90.0%
6–15 min 13 66.2%
>15 min 17 63.2%

⇒ "隔一会儿回来"的罚金 = 整份 payload 全价,而这正是本插件能间接帮上忙的地方: payload 减半 ⇒ 同样一次隔夜回来的罚金也同比减少约 46% ✓。

另外两个与插件无关、但会瞬间把账单打穿的放大源(实测踩过):

放大源 机制 实测
搜索 web_search 一次请求 = 服务端最多 5 个模型回合(max_uses 默认 5),结果轮间累积;这些轮次的 usage 不进会话日志 ⇒ 本地账本里是零 9 个会话 7 分钟内共 221 次搜索,平台账单比本地日志多 ≈ $5
子代理递归扇出 主会话开子代理 → 子代理又各自开子代理,前缀完全不共享(缓存零复用) 1 → 3 → 9 个并行会话,全部挤在 7 分钟内

这两条的启示:本插件不会造成任何放大(它只在出网前做纯函数变换、不发请求), 但它省的量级也远小于"一次搜索风暴" —— 排障时先看这两项,再看压缩/缓存。

2. 离线基准(可复现)

固定轨迹重放:真实会话里同一轮内连续 12 次工具调用当剧本,两臂逐轮发前缀请求。

轮次 payload 字符(full → stub) 省
1 3,809 → 3,204 15.9%
4 33,769 → 21,484 36.4%
8 76,720 → 36,938 51.9%
12 100,893 → 51,617 48.8%

命令:node bench/replay.mjs <session.jsonl.zstd> --dry

3. 行为分歧(真实历史,同一前缀发两次)

档位 纯桩 stub 决策抽取 extract
动作类型(工具 vs 直接答复) 12/17(71%) 15/17(88%)
同名工具 2/17(12%) 6/17(35%)
完全一致 0/17 0/17

⇒ extract 每档都 ≥ stub,体积只多 1.8 个百分点 ⇒ extract 支配 stub(默认值)。 N=17 的配对样本不足以做强断言。

4. agentic 基准(多轮工具调用 + 确定性判分)

8 个短任务 + 3 个长轨迹任务(初始文件 + 提示 + 检查器):

臂 成功率 轮数 输入 token
full(短任务 8 个) 8/8 35 31,580
stub(短任务 8 个) 8/8 37 33,322
full(长轨迹 3 个) 3/3 21 55,092
stub(长轨迹 3 个) 3/3 21 49,074

质量未观察到退化;成本列不可信(两臂会跑出不同的命令与输出,未命中被"这轮干了什么" 污染)⇒ 成本请看固定轨迹重放。

⚠️ 在线成本 A/B 不可信:曾跑出"−77%",但反序重跑后另一臂得到完全相同的数字 ⇒ 那是"谁第二个跑谁几乎全命中"的缓存复用假象,已撤回。

现状与限制

  • 默认关闭:本包默认 enabled: false,需要你确认可接受"模型只看得到推理精要"这一点;
  • 推理是有损的:extract 只留决策句(默认 240 字符预算),探索与试错过程不进 payload ⇒ 模型可能在长轨迹上重复劳动。界面与日志永远保留原文,所以审计不受影响; 觉得太紧就换 head(保留真实开头)或放宽 maxChars(代价:省幅下降);
  • 默认只塑形推理:toolResult / toolArgs / assistantText 默认 keep。 官方 tool-result-pruner 已在做工具结果剪枝,先别重复造轮子;
  • 任务偏短:agentic 基准的任务规模仍小于真实长会话,长轨迹任务(15–30 轮 + 大体量 工具输出)还在补;
  • 基准边界:行为分歧测的是"是否被改变",不是"是否更好"。

故障排查

现象 原因 / 处理
模型选择器里看不到代理 ① 之前踩过:listModels() 未把 provider 改盖成本路由 id ⇒ 整组被判 INVALID_CATALOG 而不显示(已修 0.9.3);② 重启后的短暂窗口:ctx.get('llm') 在 apply() 时尚未就绪,提供方要等注册成功才出现(1.0.1 起启动期主动重试,日志有 proxy 等待 llm 服务就绪 / WARN …仍未注册成功)。自查:POST /api/llm.models 看 failures;选择器目录按会话缓存,刷新页面再看
日志有 proxy 注册失败:adapter.xxx is not a function 适配器缺运行时要求的成员。运行时调用 providerInfo / providerRetryPolicy / listModels / resolveModel / prepareCall / stream 全部(packages/llm/llm/src/index.ts)。已修(0.9.2)
热挂载后行为没变 热挂载不刷新模块缓存 ⇒ 代码改动必须重启;只有配置与开关能热改
看得到省了多少吗 日志里 proxy-shape #n chars=A→B(累计 …)(前 3 次 + 之后每 20 次一行)
想验证出网内容 装 dsh-llm-tap 看 ~/.dsh-llm-tap/llm-tap.jsonl,或读本插件日志的字符统计
账单突然打穿 先怀疑搜索与子代理扇出,不是本插件:web_search 一次请求 = 服务端最多 5 个模型回合(max_uses 默认 5)且这些轮次的 usage 不进会话日志;子代理又会各自开子代理 ⇒ 并行前缀不共享、缓存零复用。见§ 1b
突然行为异常 建 ~/.dsh-entry-shaper.OFF 立停(原样转发),再排查

文档

文档 内容
docs/OFFICIAL-STACK.md 官方三层(pruner / compaction / command-compact)的位置与阈值、一次 /compact 的实测账单、本插件的空位
docs/PIPELINE.md 工作管道:总图、时序(含 harness 源码出处)、元素生命周期、降级路径、验收观测点
docs/PROCESSING.md 处理层规范:统一协议、op 目录、里程碑与被否掉的方案
examples/custom-op.md 接入自定义算法的五条路径 + 函数契约 + 热挂载取舍表
tools/README.md 会话体检与排障脚本(11 个,只读、零依赖):命中率/断点/成本/搜索与子代理扇出定位
CHANGELOG.md 变更记录(含被基准否掉的方案与踩过的坑)
SECURITY.md 边界与失败模式(含"本插件不做脱敏"这一明确边界)
CONTRIBUTING.md 改动前必读(6 条硬规则 + 发版前验证清单)

开发

node test/run-all.mjs                                          # 单测(81 例,无需 DSH 运行时)
node test/shaping.test.mjs                                     # 同上(直接跑,沙箱友好)
node bench/replay.mjs "<session.jsonl.zstd>" --dry             # 固定轨迹重放
node bench/divergence.mjs "<session.jsonl.zstd>" --cases=20    # 行为分歧
node bench/agentic/runner.mjs --max-rounds=20                  # agentic 基准(需真实 shell)

node --test test/ 在受限沙箱下会因 spawn EPERM(运行器要管道)失败 —— 那是沙箱限制,不是测试问题;本地用 node test/shaping.test.mjs,CI 用 node --test。 基准与检查器需要能捕获子进程输出的真实 shell;可用 DSH_BENCH_SHELL 指定。

同理:exec op 在受限沙箱下也会遇到 spawn EPERM(fail-open 会跳过该 op, 表现为"没省到"而不是报错)—— 那是沙箱限制,不是插件问题。

体检自己的会话(只读、零依赖)

node tools/session-health.mjs "$env:USERPROFILE\.dsh\sessions\--D-DSH~0020Desktop--\<会话>\session.jsonl.zstd"
node tools/recount-miss.mjs  "$env:USERPROFILE\.dsh\sessions"          # 多时间口径 + 逐小时
node tools/today-turns.mjs   "$env:USERPROFILE\.dsh\sessions"          # 按回合看成本
node tools/today-tools.mjs   "$env:USERPROFILE\.dsh\sessions"          # 哪个工具把上下文撑大

全部用途见 tools/README.md。账单异常时先跑这几个, 再怀疑本插件 —— 实测最可能的原因是 web_search(服务端多回合、usage 不进日志) 与子代理递归扇出,两者量级都远大于本插件省下的量。

许可

MIT,见 LICENSE。

—/ 5

No ratings yet

Verified DSH bundle

Commit 6a124ae137fa

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout