Skip to content
dsh.fish
Bundle

@local/dsh-localmodels-tokensavior

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

Source
Movingelated
stars
1 stars
License
MIT
Updated
Updated 9 days ago

Readme

---
doc: usage-declaration
plugin: "@local/dsh-localmodels-tokensavior"
version: 1.13.0
audience: AI agent(人类也可直接阅读)
purpose: 让任何一台刚装上本插件的机器上的 AI,无需历史对话即可正确启用、使用并验收本插件
host-tools: [ollama_local_models, subagent_local]
settings-section: 设置 → 本地模型
config-namespace: local-ollama-models
hard-requirements:
  - 本机 Ollama 可访问(默认 http://127.0.0.1:11434)
  - llm-pi-ai 中存在一条 provider 路由(默认名 ollama-local)—— 见 §2.2
  - 要委派的模型 id 必须写在该路由的 models: 列表里 —— 见 §2.3
integrity: 本文件与 package.json 的 version 应当一致;不一致说明插件被改过,优先信任代码
---

# 这个插件能干啥:让你的 DSH 调用本地模型,帮你省些大白饭(token)

**DSH-LocalModels-TokenSavior**(本地模型子代理)· 使用声明 | DSH 插件 | 设置页在「设置 → 本地模型」

> 让它**调用本机 Ollama 模型当"子代理"用**,干一些它力所能及的活(读大文件、扫日志、归类、抽取、问答、看图)
> —— **原文不进你的上下文、只有结论回来**,帮你省下一些大白饭(token)。
>
> 它不万能,先记住一句:**分类与行号可信,计数必须自己数**(§6);哪些活该派、哪些别派,看 §5.1。

> **DSH (DeepSeek Harness) plugin** — delegate *read-only collection tasks* (scan logs, extract fields,
> count, dedupe, read a large file and return only the conclusion) to a **local Ollama model**:
> zero cloud token cost, zero API keys. Ships a Settings panel, a model-selection ruler (`bench.mjs`),
> and this AI-readable usage declaration. Requires DSH + Ollama ≥ 0.3 + one declared `llm-pi-ai` provider route.
> Full details below (Chinese; the plugin's UI text is Chinese as well).

> 这份文件是插件的**自带说明书**。它假设读者(AI 或人)此前对这台机器、这个插件一无所知,
> 因此把"怎么开、怎么用、怎么验收、怎么排错"全部写死在这里,不依赖任何历史对话。

## 装它(第一次来的人看这里)

这是一个 **DSH profile bundle**(不是独立程序)。两种装法,任选其一:

**A. 从 GitHub 直接装**(推荐,跟着仓库更新)

在 DSH 里让 AI 执行:`plugin_manager` → `action: install_bundle` → `target: "github:Movingelated/DSH-LocalModels-TokenSavior"`。

**B. 克隆后按路径装**

```bash
git clone https://github.com/Movingelated/DSH-LocalModels-TokenSavior.git
```

然后 `install_bundle` 的 `target` 填**克隆到的绝对路径**。

装完**重启 DSH** —— 宿主插件是模块代码,DSH 不做热替换(这条是实测结论)。重启后打开「设置 → 本地模型」。

> ⚠ **光装插件不够**:委派还需要一条 provider 路由。**没有 §2.2 那条路由,任何委派都会失败。**
> 建议顺序:装插件 → 重启 → 按 §2.1 量本机模型 → 按 §2.2 声明路由与凭据占位 → 再重启 → 用面板开关。

## 0. 给 AI 的 60 秒速览

- **我拿到什么**:两个工具 —— `ollama_local_models`(只读查状态)、`subagent_local`(把只读采集任务派给本机 Ollama 模型,零云端 token)。
- **第一步永远先调** `ollama_local_models`:它同时告诉你 Ollama 是否活着、有哪些模型可用、以及本 README 的绝对路径。
- **模型名与参数一律以本机实测为准**:本文档里出现的任何模型名、吞吐、上下文、显存数字都只是**样本**,不是推荐值。
  换一台机器,就照 §2.1 用 `bench.mjs` 重新量一遍,再决定往路由里写什么。
- **派活公式**:`subagent_local({ prompt })`,prompt 必须**自包含**(绝对路径 + 要提取什么 + 输出格式),因为子代理看不到对话。
- **三条铁律**:
  1. **只派读多写少的活**(采日志、计数去重、字段抽取、模式匹配、图转文);代码、架构、措辞交付别派。
  2. **它的数字不可信**:分类与定位基本可信,**计数必须自己机械复核**(一条 grep 的事)。
  3. **大语料先切块**:路由上下文默认 32K token,超了直接报错。
- **省钱的原理**:不是本地模型算得快,而是**主上下文不必吞下原文**。原文只进本地,回来的是结论。
- **改配置不用重启**:`enabled` / `model` / `baseURL` 是 volatile 字段,在「设置 → 本地模型」点一下即生效。
- **改代码要重启**:宿主插件是 ESM 模块,DSH 不会热替换模块代码。

## 1. 它是什么,不是什么

**是**:一台"零成本的只读采集工人"。它把一段自包含的采集任务交给本机 Ollama 模型,在**独立上下文**里执行,
只把结论拿回主对话。子代理自己会调 `read` / `grep` 等只读工具去读文件、翻日志。

**不是**:
- 不是省钱魔法 —— 本地模型烧的 token **比云端更多**(Ollama 没有提示缓存),它省的是**主上下文**,也就是钱。
- 不是万能工人 —— 它判断力弱:**归类可以,计数不准**,架构与质量类结论不可用。
- 不是写手 —— 不要让它产出最终交付文本或改代码。

## 2. 前置条件(换新机器只需照做这四步)

### 2.1 Ollama、模型与选型(**本文件不指定推荐模型**)

```bash
ollama --version                      # 需要 0.3 以上(/v1 兼容层 + tools 能力)
curl -s http://127.0.0.1:11434/api/version
ollama pull <模型>                    # 装哪个由下面的判据决定,别照抄本文档
```

本机一个模型都没有时,先随便拉一个**支持工具调用**的当起点(例如 `qwen3:30b-a3b`,约 17 GB),
再用下面的尺子实测决定要不要换 —— 起点不等于推荐。

**选型判据(六条,逐条量,别抄参数)**

| # | 判据 | 怎么得到 |
|---|---|---|
| 1 | 必须支持工具调用(`tools`) | `node bench.mjs` 直接把不支持的排除掉 |
| 2 | 别挑带内置人设的 | 同上,输出里带 ⚠ 的排除(人设会污染任务) |
| 3 | 装得进显存(要 100% GPU) | `node bench.mjs <id>` 的「显存驻留」;不是 100% 就换更小或更低量化 |
| 4 | 吞吐够用 | 同上「吞吐tok/s」;长任务低于 ~50 会很难受 |
| 5 | 上下文 ≥ 你要喂的素材 | 同上「原生上下文」;路由里的 `contextWindow` 填 `min(原生, 所需)`,保守可先填 32768 |
| 6 | 实测**真的会**调工具 | 同上「实测会调用工具」必须是 `true`(有的模型声明支持却调不出来) |

```bash
node bench.mjs                   # 第一步:列出本机可用模型 + 各自的事实(秒回,不测速)
node bench.mjs <模型id>           # 第二步:实测它(工具调用/吞吐/显存),并打印可直接粘贴的路由 YAML
node bench.mjs <模型id> --json    # 给程序或 AI 解析用
```

> 三条命令**只读**:只调 Ollama 的 `/api/*`,不拉取、不删除、不改配置。
> 它输出的 YAML **已经把本机实测值填好了** —— 抄它,别抄本文档。

### 2.2 在 profile 里声明 provider 路由

本插件不自带路由:它委派时必须有一个已注册的 provider 路由。**没有这一步,任何委派都会失败。**
把下面这段加到 profile 的 `cordis.patch.yml`(顶层数组里),路由名保持 `ollama-local`:

```yaml
- id: llm-pi-ai
  name: "@deepseek-ai/dsh-llm-pi-ai"
  config:
    providers:
      ollama-local:
        displayName: Ollama 本地(文字)
        api: openai-completions
        baseURL: http://127.0.0.1:11434/v1
        apiKeyEnv: OLLAMA_API_KEY
        reasoning: off              # 开思考会吃光 token 预算且 content 恒空
        timeoutMs: 300000
        defaultContextWindow: 32768
        defaultMaxTokens: 8192
        defaultInput: [text]        # 显式声明,防止把图片误发给纯文本模型
        models:
          - id: "<本机实测选定的模型 id>"      # ← 用 bench.mjs 生成这一段,别照抄本文档
            name: <显示名,随便取>
            contextWindow: 32768               # 建议值;bench.mjs 会按该模型的原生上下文给出
            maxTokens: 4096
            input: [text]
```

**凭据占位符**:Ollama 的 `/v1` 不校验 key,但 pi-ai 必须有非空凭据。在 `<DSH_HOME>/.credentials.yaml`
的 refs 下加一条占位值即可(按请求解析,免重启):

```yaml
OLLAMA_API_KEY: ollama-local-no-key-required
```

### 2.3 模型必须写进路由(最容易踩的坑)

- 设置面板列出的是 **Ollama 里的全部模型**;能被委派的只有**路由 `models:` 列表里声明过的那些**。
- 选到未声明的模型 → 调用以 `UNKNOWN_MODEL` 失败(`pi-ai provider "X" has no configured model "Y"`)。
- 本插件会在委派前自查并给出可执行的报错(列出该路由已声明的模型 id),但**补声明要人工做**:
  在 §2.2 的 `models:` 下加一条,然后重启 DSH。

### 2.4 自检清单

```bash
ollama ps                                   # 看模型是否 100% GPU 驻留
```
```
cordis_inspect_query host / Config / listConfigs  {name: "@local/dsh-localmodels-tokensavior"}
  → status 应为 "schema"(为 "absent" 说明 Config 没加载,设置页将不可写)
cordis_inspect_query host / Tool / listTools
  → 应能看到 ollama_local_models 与 subagent_local
cordis_inspect_query client / Slots / listSubTree {root: "settings.section"}
  → occupants 里应有 id "local-ollama"
```

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

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

## 3. 开启与配置

**方式一(推荐,人人可用)**:设置 → **本地模型** →
- 「启用本地模型子代理」按钮:开 / 关
- 模型列表:点任意条目切换默认模型
- 端点框:改完失焦即保存(留空 = 先读 `OLLAMA_HOST`,再退回 `127.0.0.1:11434`)

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

```yaml
- id: local-ollama-models
  config:
    enabled: true            # 开关
    model: <本机实测选定的模型 id>   # 默认模型(必须已写进路由 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 之前用过 `<DSH_HOME>/local-ollama-models.json` 存状态,现已废弃、不再读取,可以删。

## 4. 工具契约

### `ollama_local_models()`
- 入参:无。只读,无副作用,不启动也不下载任何模型。
- 返回:端点 / 连通性 / 版本 / 模型总数 / 可作子代理数 / 可用模型清单(带体积、量化、上下文、人设警告)+ 本 README 路径。
- 用于:委派前的环境确认;以及"这台机器上到底有什么模型"。

### `subagent_local({ task?, kind?, prompt?, mode?, model?, label?, collect? })`
- **`task`(推荐)**:只写任务本身(一句话:要什么 + 对象 + 字段/口径)。插件按 `kind` + `mode` 套上**公式化提示词**
  (角色 / 铁律 / 素材策略 / 输出契约 / 验收提示),调用方不必再手写格式。见 §5.5、§5.6。
- **`kind`(可选,v1.8.0)**:任务类型 `classify`(默认)/ `qa` / `extract` / `summary` / `code`,也收中文别名(分类/问答/抽取/摘要/代码)。见 §5.6。
- `prompt`(可选,高级用法):完整提示词原文;**给了它就不再套模板**。
- **`mode`(可选)**:`a`/`max-save`、`b`/`balanced`(默认)、`c`/`fast`;省略则用「设置 → 本地模型」里的默认。见 §5.5。
- `model`(可选):覆盖默认模型,须在 §2.3 的已声明列表内。
- `label`(可选):会话里显示的短标签。
- **`collect`(可选,v1.5.0 起强烈建议;v1.6.0 起支持自动分批)**:让**宿主侧**代取素材,直接把命中行拼进子代理的 prompt。
  `{ path, include?, pattern?, maxChars?, chunkLines? }` —— `path` 是文件或目录,`include` 是目录下的文件名 glob(如 `launcher*.log`),
  `pattern` 是 JS 正则会只保留匹配行(建议最简形式 `WARN|ERROR`),`maxChars` 默认 60000 超则截断,
  `chunkLines`(默认 0 = 不分批)超过该行数就**自动切片 → 逐片归类 → 合并**(每片 ≤N 行且 ≤8000 字符,最多 8 片)。
  用了它:**素材 100% 正确、不占主上下文、子代理的 `grep`/`glob` 会被自动禁用**(它就是靠这两个工具静默改坏过素材)。
  结果末尾会回 `[宿主预取素材] N 个文件 / M 行 / K 字符` 与 `[分批委派] X 片 × ≤N 行 → … 共 T 秒`,供调用方核对。
  ⚠ 为什么值得分批:实测同一模型 **33 行 → 100% 覆盖,115 行 → 59%**(详见 §9.1)。
- 返回:本地模型产出的**文本结论**(不是原始文件内容)。
- 错误语义(都会抛错,不会静默失败):
  | 报错关键字 | 含义 | 处置 |
  |---|---|---|
  | `关闭状态` | 开关是关的 | 打开「设置 → 本地模型」的开关 |
  | `尚未指定本地模型` | 没配 model | 面板选一个,或传 `model` |
  | `provider 路由 ... 未注册` | 缺 §2.2 的路由 | 补路由后重启 |
  | `没有写进 provider ... 的 models 列表` | 缺 §2.3 的声明 | 补声明后重启 |
  | `subagents 服务不可用` | 宿主组合异常 | 检查 dsh-base 是否完整 |
- 子代理视角:**看不到本对话**,独立上下文,跑在配置的 provider 路由上,**零云端 token**。
- **只读是机制保证的**:委派时用 `toolFilter.deny` 摘掉 `write` / `edit` / `pwsh` / `job_kill` / `plugin_manager`
  以及再派活类(`subagent` / `subagent_fork` / `subagent_local` / `workflow`)。
  ⚠ 但 `tools.restrict()` 只认"继承来的**可 restrict** 全局工具名"(`dsh-tools`: `view(scope).restrictableNames`),
  名单里只要有一个不在该集合里,**整条调用就抛错**。所以插件是"尽力而为":把报错点名的名字逐个剔除后重试,
  保住其余防护;整份名单都被拒时才退化为不过滤。
  **并且它一定会告诉你结果** —— 过滤没能完全生效时,委派结果末尾会附一句
  `⚠ 只读过滤未完全生效:<被剔除的名字>` 或 `⚠ 只读过滤未生效:…`。
  想自行核对权威证据:跑一次委派,再看那个子代理会话的 `request/header.tools` 里有没有 `write`。
- **本机实测(一次真实委派,权威证据取自子代理会话的 `request/header.tools`)**:
  工具数 **32 → 24**;被摘掉的是 `write` / `edit` / `pwsh` / `job_kill` / `plugin_manager` /
  `subagent_fork` / `subagent_local` / `workflow`;**唯一摘不掉的是 `subagent`** —— 它落在子代理 scope
  自己的那一层,而 `view()` 里"自己层"的工具只算 known、不算 restrictable,属于 `tools.restrict()` 的固有限制。
  想彻底封死"子代理再派活",可考虑给 `agentOptions` 配 `maxDepth`(本插件**未启用**,未验证;注意 `maxDepth: 0`
  会让子代理卡在启动前)。在这一天到来之前,这句 `⚠` 就是它诚实的自我声明。

## 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。**

```jsonc
// 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** 极致省 token | `max-save` | 200 000 字符 | >40 行强制分批 | ≤15 行 + 逐类穷尽 | 最少\* | 最长(N+1 次推理) | 最高 |
| **b** 均衡(默认) | `balanced` | 60 000 | >120 行才分批 | ≤12 行 | 少 | 中 | 中 |
| **c** 快跑 | `fast` | 15 000 | 从不分批 | ≤8 行,小类并入「其他」 | 省得有限\* | 最短 | 低(需补核) |

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

```jsonc
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 分批 | `分类 \| 次数 \| 代表原文 \| 出处` + `TOTAL` | grep 复核计数 + 抽查行号 |
| `qa` | 大文档问答 | **禁止分片**;素材 > 60k 字符直接报错 | `Q<编号> \| 答案 \| 出处` + `ANSWERED=n/N` | 抽查行号;`ANSWERED` 的分母必须等于题数 |
| `extract` | 结构化抽取 | 按 mode 分批,合并成数组 | **严格 JSON 数组**,每对象带 `_src` + `COUNT` | `JSON.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` 过度保守,能推断的字段也写 `null` | 33 行里 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`:

```jsonc
{
  "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` 0~500 / `maxChars` 5000~400000 / `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/s | `node bench.mjs <模型id>` | `evidence.throughputTokPerSec`(决定"愿不愿意为分批等") |
| ③ **胃口**(最关键) | "一口多少行时召回还够" | **两个探针一起跑**(只用合成素材会得到过于乐观的参数):<br>**① 规则合成素材**(同类行重复,如 4 类 / 30·60·120 行)→ 量"容量型"上限;<br>**② 真实杂乱素材**(多种措辞 + 长尾小类 + 重复行,答案要能机械核对)→ 量"真实"上限。<br>**`chunkLines` 取 ② 的结果**:取"仍 ≥90% 的最大行数";都 <90% 就压到 20,并接受"这台机器必须复核" | `chunkLines` + `evidence.appetiteProbe`(每点标注 `material: synthetic-regular` / `real-messy`) |
| ④ 能力矩阵 | 这台模型能做哪几种 kind | 跑一次最小 `kind=extract`(输出能否 `JSON.parse` —— **注意有的模型会给 ```json 围栏,那等于不合格**)+ 一次 `kind=code`:**任务要选"答案能被机械核对"的**(例如"列出某目录下所有 `export function` 的定义位置",答案可用 `Select-String` 逐条比对)。**同时看两个环节**:工具**调没调**(账本里的"自调工具"次数)与**最后写出来的东西对不对** | `evidence.kinds.*` |

> ⚠️ **两个必须知道的实测反例**(本机 `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. **换模型或改上下文后必须重校准** —— 插件检测到不一致会把它标成"过期"并退回保守默认
   (避免"大模型配小参数"这种最坏组合)

## 6. 验收纪律(本节最重要)

**本地模型的"分类"和"定位"基本可信,"计数"和"统计值"不可信。** 样本数据(某 30B 级模型的一次真实委派;换模型后偏差幅度会变,但"数字必须复核"这条纪律不变):

| 维度 | 结果 |
|---|---|
| 分类是否真实存在 | 7/7 命中 ✅ |
| 行号是否指向真实内容 | 7/7 命中 ✅ |
| 文件名 | 5/7 正确,2 条张冠李戴(行号却对)⚠ |
| 计数 | 4/7 偏差 2~3 倍(报"34 次"实际 17 次;报"6 次"实际 18 次)⚠⚠ |

因此验收动作固定为三条:
1. **先对账再信数**:让它回报 `HITS`(取到多少行)/ `TOTAL`(覆盖多少行),两者不等就有缺口;
   再用你自己的 grep 核一遍总量。
2. **数字自己数一遍**:`Select-String -Path <files> -Pattern <关键词> | Measure-Object`(或 grep -c)。成本几秒。
3. **抽查 2~3 条引用**:打开它给的行号,确认内容对得上;行号对不上就整份打回。

⚠ **口径也要对齐**:大小写、编码、是否把续行算作命中,都会改变计数。实测我自己的 PowerShell 计数
(`Select-String` 默认大小写不敏感)把一条 JSON 续行 `[Error: ...` 也算成命中,比真实标记多 1 条 ——
差点把一次"33/33 全召回"判成 97%。**数错的不只是本地模型,验收工具链也会。**

## 7. 已知限制

| # | 限制 | 说明与对策 |
|---|---|---|
| 1 | **上下文 32K** | 路由声明 32768,超了直接报错。大文件必须分块/先 grep 收敛 |
| 2 | **Ollama 无提示缓存** | 本地 token 只会更多不会更少;省钱来自零边际成本 |
| 3 | **模型 id 必须已声明** | 见 §2.3,`UNKNOWN_MODEL` |
| 4 | **凭据不能省** | 见 §2.2,删了会报 `No API key for provider` |
| 5 | **思考模式默认关** | 路由里 `reasoning: off`;开了会吃光预算且 content 恒空 |
| 6 | **去审查底模** | 名字带 `heretic` 的模型是被去审查版本。缓解:只给只读工具、任务本身无争议、输出过 schema 校验 |
| 7 | **写权限靠 deny 名单** | 默认 deny 掉写/执行/再派活类工具;不可 restrict 的名字会被逐个剔除、并在结果里如实告知(见 §4),全部被拒时只读只靠任务约束 |
| 8 | **改宿主代码要重启** | 模块代码不热替换(loader 不做 import 缓存击穿) |
| 9 | **弱模型会篡改工具参数** | 实测:给它 `\[(WARN\|ERROR)`,它自作主张补成 `\[(WARN\|ERROR)\]`,**22/34 行被静默滤掉**,而输出行号真、原文真、格式规整,唯一破绽是最大的一整类凭空消失。对策:用 `collect` 让宿主取素材(§5.2),或给最简正则 + 强制自报 HITS/TOTAL |

## 8. 故障排查

| 症状 | 根因 | 处置 |
|---|---|---|
| 设置页开关点不动 / 红框说"写不了配置" | 宿主行没有 Config schema,或命名空间没暴露 | 确认 §2.4 里 status 为 `schema`;仍不行则重启 DSH |
| 面板说"当前 settings 暴露的命名空间:(无)" | 客户端误读 `remote.settings` 的返回信封(应为 `res.ok ? res.value : res.error`) | 属插件 bug,按此修 client.js |
| 面板"无法连接 / 0 个模型" | Ollama 没起、端口不对、或浏览器跨域 | 起 Ollama;改端点;确认 `OLLAMA_ORIGINS` 允许页面来源 |
| 委派报 `UNKNOWN_MODEL` | 模型没写进路由 | 见 §2.3 |
| 委派报 `No API key` | 缺凭据占位符 | 见 §2.2 |
| 委派很慢(>30s) | 模型冷加载 / 语料太大 | 预热一次;切块;锁定常驻模型 |
| 会话"卡死"几分钟 | 工具表变化导致 prompt 前缀缓存失效,几十万 token 重新 prefill | 别急着按停止(provider 默认 5 分钟空闲超时会自动重试);长期对策:别在会话中途增删工具 |

## 9. 实测样本(**一台具体机器**的数字,只示范方法,切勿照抄)

> 下表来自一台 24 GB 显存的机器,**不是推荐清单**。换机器后"装不装得下、快不快"都会变。
> 量你自己的:`node bench.mjs`(清单)→ `node bench.mjs <id>`(实测 + 打印路由 YAML)。判据见 §2.1。

### 9.1 对照实验:素材"怎么喂"决定召回率(同一模型、同一任务)

任务:把 6 份启动器日志里的告警按根因归类。裁判:宿主侧机械分组(大小写敏感、按消息去重,见 §6 口径提醒)。

| 组 | 素材 | 取材方式 | 子代理工具调用 | 覆盖 / 总量 | 召回 |
|---|---|---|---|---|---|
| A 基线 | 115 行(6 份快照) | 它自己 grep(**检索式被它改坏**) | 1 次,只拿到 12 行 | 46 / 115 | **40%** |
| B | 33 行(单文件) | 它自己 grep(同样被改坏) | 1 次,只拿到 12 行 | 12 / 33 | **35%** |
| C | 33 行 | 它自己 grep(改坏)+ 强制先清点 | 1 次,只拿到 12 行 | 12 / 33 | **35%** |
| D | 33 行 | 它自己 grep(**检索式换成最简形式**) | 1 次,拿到 33 行 | 33 / 33 | **100%** |
| **E** | **115 行(6 文件)** | **宿主 `collect` 代取(100% 正确)** | **0 次**(素材内联) | 68 / 115 | **59%** |
| **E2** | **115 行(6 文件)** | 宿主 `collect` + **自动分批(3 片 + 1 次合并)** | **0 次** | 根因 **6 / 7**,`浏览器`/`进程崩溃`/`环境标志` 全部找回 | **86%**(根因口径) |

四条结论:

1. **取材错 = 灾难**:A/B/C 三组全都漏掉最大的一类(`node.exe`,26 条),因为它的 grep 把 `[WARN ]`
   (WARN 后带空格)整类滤掉了 —— 而输出**行号真、原文真、格式规整**,看起来毫无破绽。
2. **取材对了,还有规模门槛**:E 组素材 100% 正确、`node.exe` 找回来了(25/26),但 115 行一口吃下只剩 59%,
   丢的全是 ≤3 条的小类。**同一模型:33 行 100% vs 115 行 59%** —— 小模型的"胃口"是真有上限的。
3. **分批把"丢小类"基本治好了**:E2(E + `chunkLines: 40`)把 `浏览器 2` / `进程崩溃 2` / `环境标志 3`
   这些小类全找回来了,根因覆盖 4/7 → **6/7**(只差 `20 秒无响应` 那 3 行)。
   代价:本地时间 56.9s → **137.6s**(3 片 + 1 次合并;本地 token 35,967,零成本);
   4 次推理**全部零工具调用**、工具表 22 个(`grep`/`glob` 被摘)。**分批买的是召回率,付的是本地等待时间。**
4. ⚠ **合并这一步会让计数膨胀**:E2 报 `node.exe 54 次`,逐行数只有 26;报 `TOTAL=75`,素材实际 115 行。
   所以"数字自己数一遍"这条纪律,**分批之后更不能省**。

### 9.2 对照实验二:大文档问答(另一种任务形状)

素材:`dsh-client-ui-primitives` 的 README(34.5 KB / 207 行)。A 组宿主代取(单批、不分片);B 组把全文读进主上下文。
5 道题(2 定位 + 2 多跳 + **1 个幻觉陷阱**:问一个文档里根本没写的 Vue 支持)。

| 指标 | A 组(插件) | B 组(纯云端) |
|---|---|---|
| 云端 token(进主上下文) | **1,253 字符 ≈ 392 token** | **32,746 字符 ≈ 10,233 token** |
| 任务时间 | 62.8 s | 17.1 s |
| 正确率(5 题) | 5 / 5 ✅(引用行号全部真实) | 5 / 5 ✅ |
| **完整性** | **2 / 5 完整**(3 题只答一半:漏计数、漏 4/5 个 props、漏主题约束)⚠ | 5 / 5 |
| 幻觉陷阱 | 答「没有」✅ **零编造** | 答「没有」✅ |

两条结论:

1. **它不编造,它"答薄"** —— 每条都真、只是漏了你问的那一半,而且**看起来完全正确**。
   所以 `qa` 类型必须有"逐题作答不得跳题 + `ANSWERED=n/N` 对账"(v1.8.0 已内置)。
2. **问答不能分片**:三个模式的分批阈值对问答全是负作用(答案可能在另一片里),
   所以 `qa` 强制关掉分片、超预算就报错(v1.8.0 已内置)。
3. 顺带:这次 A 组**没被自己改坏检索式**(零工具调用),说明 `collect` 那条防线在问答场景同样有效。

| 模型类型 | 体积 | 吞吐 | 工具调用 | 结论 |
|---|---|---|---|---|
| MoE 30B 级 / Q4(每 token 只激活 ~3B) | ~17 GB | ~230 tok/s | ✅ | 本样本里最快;思考关不掉,但不污染 content |
| MoE 35B 级 / Q4_K_S | ~18.5 GB | ~200 tok/s | ✅ | 可关思考 |
| 稠密 27B / Q4(带视觉) | ~16 GB | 快 | ❌ | 视觉专用,走 `ollama-vision` 路由 |
| 推理型(reasoning)14B / Q4 | ~8 GB | ~88 tok/s | ✅ | 能当备胎,质量一般 |
| 推理型 32B / Q4 | ~18.5 GB | **~15 tok/s** | ❌ | 不可用:关思考必空输出,且稠密大模型慢一个数量级 |
| 推理型 70B、以及同尺寸的 Q8 量化 | 35–40 GB | — | — | ❌ 装不进本样本那台 24 GB 显存的机器 |

**一次真实委派的账本**(一批 82.1 KB 的日志,997 行 / 118 条告警):

| 指标 | 数值 |
|---|---|
| 子代理动作 | 1 次 grep + 2 步 LLM,**64 秒**,100% GPU |
| 本地 token | 输入 22,262 + 输出 8,062(**零成本**) |
| 云端 token | **0** |
| 回到主上下文 | **692 字符**(vs 原文 82.1 KB,压缩 ≈147×) |

### 9.3 复盘:一次"该派却没派"的真实案例(v1.9.0 判据重写的由来)

任务(另一个对话窗口,用户实测):「把 `C:\Windows\DirectX.log` 里出现的文件目录归类一下」。
素材:**146 KB / 2,101 行 ≈ 46,058 token** —— 旧政策第①条(> 30 KB)明确该触发,但它**没有派**。

| 证据(全部来自该会话日志) | 结论 |
|---|---|
| 该会话日志里含 `本地模型子代理` 政策文本 = **true** | 段落注册没坏,政策**确实进了**它的系统提示词 |
| 它的第 2 个工具调用就是 **`ollama_local_models()`** | 它**想到了**这个插件,还查了本地模型状态 |
| 之后:`pwsh` 查文件大小 → `read` 前 30 行 → **4 次 `pwsh` 用 PowerShell 抽路径** → 自己归类 | 它选了 shell 路线,**并且没把 146 KB 读进上下文** |

**判定:它是对的,错的是旧政策。**

1. 「抽取文件目录」是**正则可确定性完成**的任务 → `Select-String` / `-match` / `Group-Object` **比本地模型更准更快**
   (本地模型的计数与完整性本来就不可信,见 §6 与 §9.1)。
2. 它用"只读 30 行 + shell 抽取"同样达成了我们宣称的核心价值(原文不进上下文)——
   **插件在这一局是与 shell 竞争,而 shell 该赢。**
3. 旧政策的错在于**拿规模当扳机**:文件大小只决定"值不值得",决定"该不该"的是**这件事要不要用脑子**。

**顺带发现的真实陷阱**:该文件 ≈ 46K token > 本地 32K 上下文 —— 就算派了,整份塞进去也会被 `collect` 的
60k 字符上限**截成 41% 的残料**。正确姿势是 `collect.pattern` 预筛(例如只取含路径的行)—— 这条已写进政策。

### 9.4 成功案例:一次完全自主的委派(v1.9.0 判据生效)

任务(另一个对话窗口,用户实测):「把 `ArmouryCrate.UserSessionHelper_2026-09-29.log` 里出现的报错归类一下」。
素材:**1,707.6 KB / 65,805 行**。该窗口**不知道本插件的任何历史对话**,只看到系统提示词里那 600 字政策。

它做了这些(共 22 次工具调用,序列全部来自会话日志):

| 步 | 动作 | 说明 |
|---|---|---|
| 1–14 | **`pwsh` × 14**:签名归一化(`0xHEX` / GUID 占位)、去重计数、按小时分布 | 机械的活交给 shell —— 政策原话 |
| — | 把 1.7 MB 收敛成 **84 行 / 9,946 字符** 的签名文件 | **压缩 99.4%**,本地模型才吃得起 |
| **15** | **`subagent_local({kind:"classify", mode:"b", collect:{…}})`** | ✅ **语义的活主动派出去** |
| 16–19 | `pwsh` × 4:验证"ServiceManager CRITICAL 是否全是插件加载清单"、"报错涉及的目录是否真实存在" | ✅ 自己执行了 §6 的复核纪律 |
| 20–22 | 写报告 + 明细 + `present` | 交付 |

插件返回的账本(原文):

```
TOTAL=91
[任务类型] classify 分类 / 计数(默认)
[宿主预取素材] 1 个文件 / 84 行 / 9946 字符
[验收建议] 先 grep 复核总数与各类计数,再抽查 2~3 条出处行号 —— 计数一向不可信
[耗时] 56.0 秒(1 次本地推理)
```

子代理账本:22 个工具、**自调工具 0 次**、本地 token 17,389(零成本)。

**它写的 task 值得抄**(比我 README 的示例更好):

> …按「根因」归类成若干类别(**不是按插件分**)…并甄别哪些签名**实际上不是错误**(例如插件加载成功清单);
> 最后给出:真正需要处理的报错次数合计,以及被误标的次数合计。

**最佳实践(本次验证出来的)**:大素材不必硬塞给 `collect.pattern` ——
**先用 shell 把它收敛成小文件(去重 / 归一化 / 筛选),再把这个小文件交给 `collect`**。
理由:shell 能做正则做不到的**签名归一化去重**,收敛比远高于 pattern 过滤(本例 99.4%);
而本地模型只需要那份"小而完整"的料(§9.1 的结论)。

### 9.5 三个本地模型的实测画像(v1.12.0:这就是"档案册"存在的理由)

同一台机器(RTX 4090 / 20 GB 可用)、同一套协议、同一批探针素材(30/60/120 行、答案已知)、同一套提示词:

| | `qwen3:30b-a3b`(MoE 30B) | `qwen3.8:27b`(稠密 27B) | `deepseek-r1:14b` |
|---|---|---|---|
| 吞吐 / 显存 | **233.3 tok/s** / 20.2 GB | 60.3 tok/s / 16.2 GB | 85.4 tok/s / 14.2 GB |
| classify 30 行 | ✅ 4/4 计数全对 | ✅ 4/4 | ⚠️「连接被拒」报 11(真值 8),各行合计 33 ≠ 自己写的 `TOTAL=30` |
| classify 60 行 | ✅ 4/4 | ✅ 4/4 | ❌ 四类全错(40/18/13/4),合计 75 ≠ `TOTAL=60` |
| classify 120 行 | ✅ 4/4 | ✅ 4/4(40.6 s) | 未测 |
| extract | ✅ 裸 JSON | ✅ 裸 JSON(最紧凑,14.7 s) | ⚠️ 字段全对,但被 ```json 围栏包住 → 直接 `JSON.parse` 失败 |
| **code(工具 + 汇总)** | ⚠️ `glob`+`grep` 调成功、**汇总崩了**(`ITEMS=0` + 编造一处出处) | ✅✅ **7/7 行号机械核对全对**,另 2 行诚实标注"未找到",`ITEMS=9` 自洽 | ❌❌ **0 次工具调用**,8.7 秒内编造 4 行假数据(`file1.js:10` / `file2.mjs:5` / `file3.js:20`) |
| 档案里的结论 | 一口 **40** 行 | 一口 **120** 行(不必分批) | 一口 **20** 行 |
| 一句话定位 | 最快,能力够用 | **质量最高,最慢** | **只能贴标签 / 抽取**;不能计数、不能用工具 |

**三条结论**:

1. **"本地模型"不是一类东西**:同样十几秒的活,27B 会老实调工具、照着真实结果写;14B 会连看都不看直接编。
2. **校准的产出不是"参数",是"可信度"** —— 档案里最值钱的字段其实是 `evidence.kinds.*`,它告诉后面的 AI
   "**这个模型只配干哪几种活**"。
3. **"看起来更快"可能是"干得更少"**:14B 单次 7.4~31.6 秒(比 30B 的 21.4~56 秒快),但它吞吐只有 85 tok/s(30B 是 233)
   —— 快的原因是**它生成的 token 少**,也就是**它跳过了该干的活**。

## 10. 维护铁律(要改这个插件之前必读)

1. **工具集合恒定**:只在 `apply()` 里注册一次,不因配置变化增删。工具表一变,整条 prompt 前缀缓存作废,
   几十万 token 的会话要重新 prefill,用户会看到"发一句话卡住几分钟"(实测 166 秒零 token)。
2. **配置走 volatile**:只有 `Schema.volatile()` 字段才会出现在设置表单并被写入接口接受;
   改完即时生效、不重挂插件。非 volatile 字段只在 patch 里改。
3. **零裸 import**:插件以 `link:` 安装,模块真实路径在工作区,Node 从真实路径向上找不到 profile 的依赖树
   (实测 `ERR_MODULE_NOT_FOUND`)。需要 schemastery 时用 `createRequire` 以 dsh 安装目录 / profile 目录为基准解析。
4. **前端护栏**:React Hook 只能在组件函数体内(在 `factory` 里调用组件会让整棵前端树崩掉);
   面板要包 ErrorBoundary;`remote` 的命名空间必须逐个显式 `inject`。
5. **异步流程不许在模块加载期跑**:加载期零副作用,全部进 `apply()`。
6. **toolFilter 名单要跟着部署走**:`tools.restrict()` 只认"可 restrict 的继承全局工具名",名单里有一个不在就整条抛错
   (allow 与 deny 一样)。`startChild()` 已把这一步包成"按报错点名逐个剔除 → 重试 → 结果里如实告知",
   改名单时**务必保留这条自适应路径**,否则换台机器就可能静默失去防护(真机踩过一次静默降级)。

7. **政策段落文本保持静态**:`DELEGATION_POLICY` 里不要塞 enabled/model 这类会变的状态 —— 它位于系统提示词最前端,
   文本一变整条 prompt 前缀缓存作废,长会话要重新 prefill(实测踩过 166 秒零 token)。要降级就用文案里的"报已关闭就自己读"。
8. **主动性靠"看得见的触发条件",不靠工具描述**:光有工具不给触发规则,实测自发调用率是 0(见 §5.4)。
   想让它被用上,就把"什么时候用"写进系统提示词或工具结果,而不是写进 README 等模型来读。
9. **取材不交给弱模型**:凡是"它自己选参数"的环节(正则式、路径、扫描范围)都是**静默失败**的来源 ——
   错误不会写在输出里,只会让某一整类凭空消失,而报告看起来毫无破绽。要么给死参数,要么由宿主代劳(`collect`)。
10. **验证自己的工具链**:判分用的 grep/PowerShell 也要声明口径(大小写、编码、续行),否则你会拿被污染的
   分母去评判别人(实测差点把 100% 判成 97%)。
11. **弱模型的"胃口"有上限,而且只能靠分批解决**:同一模型同一任务 33 行 100% / 115 行 59%(§9.1)——
   提高 prompt 质量、强制先清点,都**没能**改善(B/C 组实测无效)。`collect.chunkLines` 就是为这条存在的:
   切片 → 逐片归类 → 合并,全程在宿主侧,主上下文不受影响(E 组:零工具调用、只回结论)。

12. **要加"新策略"时:先问它该走哪条通道** —— 静态文本最贵,工具输出免费。

    | 通道 | 改它的代价 | 适合放什么 |
    |---|---|---|
    | 系统提示词段落 `DELEGATION_POLICY` | **最贵**:一变整条 prompt 前缀缓存作废,长会话要重新 prefill(实测 166 秒零 token) | 只在**判据本身**变化时改(该不该派、什么算语义任务) |
    | 工具描述 / 参数说明 | 同样在请求前缀里,同样砸缓存 | 契约类信息(有哪些 kind、参数语义) |
    | **工具返回的账本**(`[验收建议]`、`[耗时]`、`[分批委派]`) | **免费**(在对话尾部,不进前缀) | 每类任务的验收方法、不断进化的经验 |
    | **`tools/post-execute` 当场提示** | **免费** | "在花掉钱那一刻"给出的建议(已这么用) |
    | README | **免费**(按需阅读) | 全部细节、样本、复盘 |

    ⇒ 新策略**默认先写进免费通道**;只有当它改变**判断逻辑**时才动静态文本,而且要**攒着一起改**(一次重启付一次缓存代价)。

13. **改代码要重启,改配置不用**(2026-09-29 两种都真机钉死过):

    | 改什么 | 生效方式 | 实测证据 |
    |---|---|---|
    | **patch 里的配置**(volatile 字段、别的插件的 config,例如 llm 路由的 `models:`) | **存盘即生效,不用重启** | 给路由补上 `qwen3.8:27b` 后**没重启就直接派成功** |
    | **`index.js` 的代码** | **必须重启**(loader 不重新 import 模块) | 新加的代码指纹只在重启后才出现 |

    判定方法:调 `ollama_local_models`,看输出首行 `[版本] vX | 代码指纹 xxxxxxxx`(v1.11.3 起),
    指纹 = 该文件 sha256 前 8 位 —— 跟磁盘上的对一下,就知道宿主跑的到底是不是最新代码。

## 11. 文件清单

| 文件 | 作用 |
|---|---|
| `index.js` | 宿主半身:两个工具 + Config schema + 前置自检 |
| `client.js` | 客户端半身:「设置 → 本地模型」面板(开关 / 模型选择 / 端点 / 连接状态) |
| `cordis.patch.yml` | bundle 行声明(插入 `local-ollama-models` 这一行) |
| `README.md` | 本文件(使用声明;模型与参数一律"自己量",见 §2.1) |
| `bench.mjs` | 选型尺:列本机模型 / 实测吞吐与工具调用 / 打印可直接粘贴的路由 YAML(只读) |
| `package.json` | 包信息;`dsh.bundle.patch` 与 `dsh.client` 声明 |

Install

dsh plugin --profile web add github:Movingelated/DSH-LocalModels-TokenSavior

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source