Movingelated/DSH-LocalModels-TokenSavior ↗★ 1

@local/dsh-localmodels-tokensavior

DSH-LocalModels-TokenSavior —— 在设置里提供独立的「本地模型」栏目:面板可直接开关与选择模型(volatile Config),并把只读采集任务委派给本地 Ollama 模型执行(原文不进上下文、只回结论)。用法/前置/验收纪律见 README.md。 适合希望利用本地 Ollama 模型处理只读采集任务以节省 Token 的用户。

パッケージ
@local/dsh-localmodels-tokensavior
互換性
未検証
バージョン
1.13.0
ライセンス
MIT
最終更新
2026/09/29

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Movingelated/DSH-LocalModels-TokenSavior

ドキュメント

README 全文を読む ↗

2.5 首次在这台机器上使用前:跑一遍四步容量校准

自检通过后,先做一次「四步容量校准」(容量 / 吞吐 / 一口多少行 / 可用哪几种 kind,见 §5.7,约 5–8 分钟), 再把结果写成插件目录下的 calibration.json(一台机器一本档案册,按模型分条)。跑过一次的模型不用重复跑 —— 插件读到该模型的有效档案就把里面的参数当默认值;同机换模型只需给新模型校准,换回来直接用旧档案; 读到过期或损坏的档案则退回保守默认,并在每次结果的账本里写明原因。

3. 开启与配置

方式一(推荐,人人可用):设置 → 本地模型 →

  • 「启用本地模型子代理」按钮:开 / 关
  • 模型列表:点任意条目切换默认模型
  • 端点框:改完失焦即保存(留空 = 先读 OLLAMA_HOST,再退回 127.0.0.1:11434)

方式二(无界面时):直接改 profile 的 cordis.patch.yml:

- id: local-ollama-models
  config:
    enabled: true            # 开关
    model:    # 默认模型(必须已写进路由 models:,见 §2.3)
    baseURL: ""              # 端点覆盖
    toolName: subagent_local # 工具名(非 volatile,界面改不到)
    provider: ollama-local   # 路由名(非 volatile,需与 §2.2 一致)
  disabled: false

生效语义:enabled / model / baseURL 是 volatile 字段 —— 写进 profile patch, 即时生效、无需重启、不重挂插件。其余字段改了要重启。

关闭:把开关关掉,或在「插件」页停用本 bundle(停用会让两个工具一起从工具表消失)。

历史包袱:1.2.0 之前用过 /local-ollama-models.json 存状态,现已废弃、不再读取,可以删。

5. 最优使用法

5.1 该派 / 不该派(v1.9.0 判据重写:看"要不要用脑子",不看"文件多大")

✅ 该派(需要语义理解)❌ 不该派(确定性手段更准更快)
语义归类 / 归因("这些报错属于同一根因吗"、"这条日志说明了什么")路径 / ID / 时间 / 版本号等正则能确定性拿到的抽取 —— Select-String / -match / Group-Object 100% 准、快、零幻觉
大文档问答(读 200 页只回一句 + 行号)最终代码、架构判断、质量类交付
摘要 / 提炼、翻译安全、权限、删除类操作
图片 / 截图 → 文字计数(本地模型的计数不可信,见 §6)

两条轴:① 要不要用脑子(语义 → 派;纯机械 → 自己抽)② 值不值得(> 30 KB 或 ≥ 3 个文件 → 派得划算)。 分工原则:机械的部分交给 shell,语义的部分交给本地模型 —— 抽出来的东西"怎么归类"仍然值得派。

⚠️ 旧判据是"文件 > 30 KB 就派",已被实测证伪(见 §9.3 的复盘):对"抽取文件目录"这类正则可确定的任务, shell 更快更准;而且实测里那个 AI 用 shell 也没把 146 KB 读进上下文 —— 它没派是对的。

5.2 prompt 模板(v1.5.0 起:素材交给宿主取)

首选做法:用 collect 参数,不要让本地模型自己去 grep。

// subagent_local({ prompt, collect })
{
  "prompt": "把素材里的「告警与报错」按根本原因归类去重。\n输出格式:分类 | 次数 | 代表原文(≤80字) | 出处\n最后单独一行输出 TOTAL=。不要开场白、解释、建议。",
  "collect": { "path": "D:\\logs", "include": "app*.log", "pattern": "WARN|ERROR", "maxChars": 60000 }
}

prompt 里不用再写路径(素材已附在 prompt 末尾),只写"要什么 / 怎么归类 / 输出格式"。

万不得已要它自己取素材时,两条死规矩:

  1. 检索式必须是最简形式(如 WARN|ERROR)——给它带括号或转义的正则,它一定会"优化"它。
  2. 强制自报口径:要求它先输出 HITS=,最后输出 TOTAL=,两个数必须相等。
  3. 一口别超过约 40 行:同一批素材行数越多,它越容易只抓大类别、丢掉只有 1~3 条的小类别。 实测同一模型同一任务:33 行 → 100% 覆盖(6/6 根因);115 行 → 59%(4/6 根因,丢的全是 ≤3 条的小类)。 对策(v1.6.0 起首选自动):给 collect 加 chunkLines: 40 —— 宿主自动切片、逐片归类、再合并, 每一口都是"小而完整"的料;先去重再喂同样有效(本例 6 份日志是同一份的累积快照,只取最新那份即从 115 行降到 33 行)。 对照数据见 §9.1。

经验:

  • 一次只派一个任务;任务边界越窄,本地模型越不容易跑偏。
  • 让它只输出表格/清单,别要散文 —— 散文既贵又难验收。

5.3 模型常驻

切换模型要重新加载(5~30 秒,显存大的要更久)。锁定一个主力模型长期用,别一次任务换一个。 ollama ps 可以看到驻留情况(默认闲置 5 分钟后卸载)。

5.4 让它"在该用的时候被用上"(主动性机制)

问题:工具描述只会说"我能做什么",不会在具体情境里主动冒出来。上线首日实测:某会话 250 次工具调用中, 本插件仅 6 次,全部是用户点名要求演示的,自发调用 0 次。工具没问题 —— 是触发条件没写进模型看得见的地方。

两个机制(v1.4.0 起):

机制位置作用
系统提示词段落 local-ollama-delegation(order 2850,紧跟 TOOL_SUBAGENT)每一次请求的系统提示词写死新版判据:先问"要不要用脑子"(纯机械抽取 → 自己抽;语义归因/归类/摘要/问答/翻译/看图 → 优先派),再用规模决定"值不值得"(> 30 KB 或 ≥ 3 文件);> 32K token 的素材必须先收窄(先用 shell 去重归一化成小文件,或用 collect.pattern)
大文件当场提示(tools/post-execute)读进主上下文的 read 结果末尾在花掉钱的那一刻提醒:"这份内容 ≈N k token 已进主上下文,下次这类活可交给 subagent_local"。每个会话只提醒一次

怎么验证生效:重启 DSH 后给一个"需要读懂内容"的任务(比如把一批日志按根因归类、或就一份 30 KB 文档提问), 看它是否先调 subagent_local;或让它读一份 > 30 KB 的文件,结果末尾应出现 ⚠ 采集提示。 ⚠ 反例也要认:纯抽取任务(抽路径/ID/时间)它就该自己用 grep 抽 —— 那不叫"没生效",那叫判据正确(见 §9.3)。 ✅ 成功长什么样:见 §9.4 —— 一个全新窗口在 1.7 MB 日志上自主完成了"shell 收敛 → 委派语义 → 复核 → 交付"。想复现这个效果, 最值得抄的是它那两步:先用 pwsh 把大素材压成小文件,再把小文件交给 collect。

怎么调:阈值与文案都在 index.js —— BIG_READ_CHARS(默认 30000 字符)、DELEGATION_POLICY。 ⚠ 政策文本必须保持静态(见 §10 第 7 条)。

5.5 三种调用模式(v1.7.0:公式化提示词)

调用方不必再手写格式:给 task + mode,插件自动套上角色、铁律、输出格式与对账要求。

模式别名素材上限分批阈值输出云端 token本地时间召回
a 极致省 tokenmax-save200 000 字符>40 行强制分批≤15 行 + 逐类穷尽最少*最长(N+1 次推理)最高
b 均衡(默认)balanced60 000>120 行才分批≤12 行少中中
c 快跑fast15 000从不分批≤8 行,小类并入「其他」省得有限*最短低(需补核)

* 云端节省主要由"素材不进主上下文"决定,三种模式其实一样;差别在召回率: a 召回高 → 调用方几乎不用补核;c 召回低 → 多半要回头补核,实际省得更少。这就是"a 省最多、c 省得有限"的机制。

subagent_local({
  "task": "把素材里的告警按根因归类",          // ← 只写任务,格式由插件套
  "mode": "a",                                  // ← a/b/c 或 max-save/balanced/fast
  "collect": { "path": "D:\\logs", "include": "app*.log", "pattern": "WARN|ERROR" }
})
  • 省略 mode → 用「设置 → 本地模型」里的默认(面板可直接切换)。
  • 省略 collect.maxChars / collect.chunkLines → 按模式取默认;显式给值可覆盖(chunkLines: 0 = 强制不分批)。
  • 模板原文在 index.js 的 MODES 与 buildPromptFor() —— 要改文案只改那一处。

5.6 五种任务类型(v1.8.0:素材策略 + 输出契约 + 验收动作)

任务类型决定"怎么用素材、输出什么形状、怎么验";模式只决定"喂多少、分几口"。

kind任务素材策略输出契约验收动作(结果里会自动附上)
classify(默认)分类 / 计数按 mode 分批分类 | 次数 | 代表原文 | 出处 + TOTALgrep 复核计数 + 抽查行号
qa大文档问答禁止分片;素材 > 60k 字符直接报错Q | 答案 | 出处 + ANSWERED=n/N抽查行号;ANSWERED 的分母必须等于题数
extract结构化抽取按 mode 分批,合并成数组严格 JSON 数组,每对象带 _src + COUNTJSON.parse + 抽查 _src + COUNT==数组长度
summary摘要 / 提炼按 mode 分批要点 + 出处 + KEY=实体 + POINTS=n抽查行号;KEY 里的数字要能 grep 到
code代码只读勘查允许它自己 read/grep(不摘这两个工具)项目 | 说明 | 文件:行号 + ITEMS=n逐条 grep 回查;ITEMS 必须等于行数

三条纪律(写在代码里,不是倡议):

  1. 没有验收动作的类型不许加 —— 上表最后一列就是它存在的准入条件。
  2. 禁止分片的类型不许悄悄降级:qa 遇到超预算素材会抛错并给出两条出路(缩小素材范围 / 换 classify|summary), 而不是偷偷截断或分片 —— 分片问答 = 让它在残缺材料里找答案(§9.2 实测过)。
  3. 只读不放松:code 只放开 read/grep/glob,write/edit/pwsh 等依然被摘。

已知短板与对策(v1.8.1 真机实测出来的):

现象证据(改前 → 改后)对策
qa 会因为素材是英文而用英文作答5 题里 4 题英文(v1.8.0)→ 5 题全部中文(v1.8.1 回归,同一任务同一文档,单变量对比)语言要求嵌进输出格式那一行(模型只服从格式行;单列一条规则实测无效)
extract 过度保守,能推断的字段也写 null33 行里 20 行 root_cause=null(v1.8.0)→ 33/33 全填、零 null(v1.8.1 回归)规则改为"能推断就写简短概括,只有完全无法判断才写 null"
改后有没有"为了填满而编造"?抽查 6 条"原来空、现在填了"的行(110/154/186/219/264/229)→ 6/6 与原文对得上(含此前四轮实验全漏的"实例 20 s 无响应")COUNT 与 _src 是防编造的硬约束,改规则时不许动它们(回归里两者都保持:33/33)
extract 字段冗余任务里自定义 file 字段与模板强制的 _src 重复任务里不要再要 file 字段 —— _src 已经带了出处
完整性与计数依然不可信v1.8.1 回归:Q1 的计数这轮对了(30 = 表格行数),但列表仍是"每行一个代表"而非全部导出名;Q2 的 props 仍只给 2/5只能靠调用方复核(§6)。qa 的 ANSWERED=n/N 只保证"不跳题",不保证"答得全";想声称"稳定改善"需同一任务跑多次取分布(本轮每次各 n=1)

5.7 换机器 / 换模型:四步容量校准(v1.10.1:一台机器一本模型档案册)

为什么需要它:本插件里所有"机器相关"的数字(素材上限、一口多少行、输出几行)都取决于那台机器的模型。 其中"素材上限"能从模型声明的上下文窗口推出来,但"一口多少行"取决于模型的质量 —— 参数推不出来,只能实测。

没校准 = 按保守默认跑(素材上限 60000 字符 / 一口 40 行),并且插件会在每次结果的账本里写明"未校准"。

校准结果写成一个文件:插件目录下的 calibration.json(已进 .gitignore,不会入库/发布)。 它是一台机器一本"模型档案册":profiles 里每个模型各一条,键是 provider::模型id:

{
  "schema": 2,
  "profiles": {
    "ollama-local::qwen3:30b-a3b": { "calibratedAt": "…", "capacity": { "contextWindow": 32768, "maxChars": 60000, "chunkLines": 40, "maxRows": 12 }, "evidence": { … } },
    "ollama-local::llama3:8b":     { "calibratedAt": "…", "capacity": { "contextWindow": 8192,  "maxChars": 15000, "chunkLines": 15, "maxRows": 8  }, "evidence": { … } }
  }
}

两条规矩(插件不强制,但靠它才不出错):

  1. 写入时先读再合并 —— 只新增/更新本模型那一条,不要覆盖别的模型(ollama_local_models 的输出里会列出本机已有哪几份档案)
  2. 换模型要重校准、换回来不用 —— 切到 B 校一次,切回 A 直接用 A 的档案 ✓(这正是"一本档案册"的意义)

插件会读它、校验它(JSON / schema / 该模型的 contextWindow 对不上就判过期,只废掉那一条), 但它只是自述,不是证明。

档案会不会"造假"或"从别的机器拷来"?(v1.11.0)

会 —— 把别人的 .dsh 整个拷过来、或手写一份 JSON,都可能让插件误判。插件能自动识别的:

情况判定依据
拷来的档案,本机没有该模型missing → 重新校准档案册里查不到该 provider::模型id
拷来的档案,本机有同名模型但内容不同(换过量化 / 重新 pull)stale → 重新校准env.modelDigest 不一致 —— 那是模型文件的 SHA256,比"模型名字照应"可靠得多
拷来的档案,本机有同名同 digest 的模型calibrated + 提示"档案来自另一台机器"env.machineHash 不一致。但参数仍然适用、不重跑 —— 参数是模型的属性,不是机器的属性(同"换模型要校准、换回来直接用"一个道理)
档案里是手写/伪造的离谱数字invalid → 重新校准值域校验:chunkLines 0500 / maxChars 5000400000 / maxRows 1~30
伪造的看起来合理的数字⚠️ 识别不了只能靠抽样复核:拿档案里的参数跑一个小任务(30~60 行、答案已知),看召回是否达标

所以云端 AI 的职责不是"校验模型名字照应"(名字可以相同而内容不同),而是两条: ① 看 ollama_local_models 输出的校准状态;② 当它提示"档案来自另一台机器"或"没记 digest"时,做一次抽样复核。

四步(合计约 5–8 分钟,云端 token 0)
步量什么怎么量落进 capacity 的哪个字段
① 容量模型声明的上下文窗口插件自动读(ollama_local_models 会显示);读不到就问那台机器的人contextWindow;再算 maxChars ≈ (contextWindow − 8192) × 2.4
② 吞吐tok/snode bench.mjs evidence.throughputTokPerSec(决定"愿不愿意为分批等")
③ 胃口(最关键)"一口多少行时召回还够"两个探针一起跑(只用合成素材会得到过于乐观的参数):
① 规则合成素材(同类行重复,如 4 类 / 30·60·120 行)→ 量"容量型"上限;
② 真实杂乱素材(多种措辞 + 长尾小类 + 重复行,答案要能机械核对)→ 量"真实"上限。
chunkLines 取 ② 的结果:取"仍 ≥90% 的最大行数";都 ⚠️ 两个必须知道的实测反例(本机 qwen3:30b-a3b 校准过程中踩到的):
  1. 合成素材 ≠ 真实难度:合成 120 行(4 类规则重复)100% 全对;真实杂乱 115 行只有 59%(长尾小类被丢)。 所以 ① 只用来量"容量上限",定 chunkLines 必须用 ②。本机最终取保守的 40 行。
  2. 工具调用"成功"不等于任务成功:kind=code 那次,子代理确实调了 glob + grep×2 且参数没坏, 但最终汇总崩了(ITEMS=0、输出退化成一行、还编造出处 plugins/local-ollama-models:0)→ 该 kind 判 unusable。 账本里的"自调工具次数"只说明它动了手,不说明它做对了事。
判据:什么叫"召回够"

用合成素材时先把答案写死(例如"7 类、共 N 条"),跑完把它的输出与答案对齐 —— 分类与行号可信, 计数必须自己数(§6)。三点里选"仍 ≥90% 的那个最大行数"。

⚠️ 校准是 n=1 的带噪声估计:本项目实测同一模型、同一任务出现过 39% / 35% / 59% / 100% 的召回波动。 所以校出来的是保守起点,不是"最佳参数";想更准就在同一尺寸点多跑 2 次。

校准完要做的三件事
  1. 把结果写成 calibration.json 里本模型那一条档案(模板:调一次 ollama_local_models,输出里直接带一份可抄的 JSON; 先读再合并,别覆盖其它模型)
  2. 插件此后自动用 capacity.maxChars / chunkLines / maxRows 当默认值(调用时显式传参仍可覆盖)
  3. 换模型或改上下文后必须重校准 —— 插件检测到不一致会把它标成"过期"并退回保守默认 (避免"大模型配小参数"这种最坏组合)