# 中医知识库检索质量改造 · 进度与成果总结

**日期**：2026-10-02　**性质**：本机实测报告（可直接给外部评审）
**环境**：DSH 桌面端 0.2.0-rc.2 + `dsh-knowledge` 插件 0.4.1，仅监听 `127.0.0.1:19387`
**嵌入/重排**：`Qwen/Qwen3-Embedding-4B`(2560 维) / `Qwen/Qwen3-Reranker-8B`（硅基流动 API）
**总原则**：原文完整、出处可追溯 **>** 分块与检索效果；未确认前不动原始 EPUB、不动现有库、不改插件

---

## 0. 一句话结论

《经络腧穴学》的"碎块"问题已定位并解决：**不改插件**，用"结构优先文本"通道重建，**676 块**（原 6535 块），入库**逐字无损**，**36 道测试题全部第 1 位精确命中**（原库 14/36）。新库已建成（`经络腧穴学·结构优先`），**但检索开关仍指向原库**，等人工确认后切换。

---

## 1. 问题与根因（已确认，可复现）

### 1.1 现象
「中医新」库（《经络腧穴学》沈雪勇 4 版 EPUB）共 **6535 块 / 283093 字符**，平均 **41 字符/块**，1341 块不足 50 字符。问"中府 定位"，Top1 返回的是一句 **36 字符、没有穴名**的 `【定位】横平第1肋间隙…`。

### 1.2 根因（源码 + 复刻双重确认）
- 源 XHTML 本身是**一个段落一个 `<p>`**；插件 `parseEpub`（`lib/knowledge/index.js:4433`）按 ZIP 字母序取 HTML → Turndown 转 Markdown → `join("\n\n")`。
- 分块函数 `splitBlocks`（`:1992`）**只以空行和 `#` 标题行为边界**，因此"段落数 = 块数"。
- 复刻验证：按 body 顶层块元素（`<p>`/`<blockquote>`）逐个转换，得 **6535 块、283096 字符，与库内 `rawText` 逐字相同**。
- **重建索引无效**：同一套解析 + 同一套分块，仍是 6535 块。
- 字符预算 = `chunkSize 800 × charsPerToken 2.28` = **1824 字符**（此前误算为 1280，已更正）。

### 1.3 顺带查实的两个源数据事实
- **图片引用零丢失**：217 个 `![](ImageNNNNN.jpg)` 占位 ↔ 217 个 `<img>`，一一对应；另有 2 张为封面（manifest `cover-image`）与封底类页图。**但 217 个 `alt` 全为空**，图内文字没有任何文字化入口。
- **脚注锚点 1922 个，断链 0**。
- **`parseEpub` 从不读取 OPF `spine`**（全文无 `spine`/`container.xml`/`content.opf` 字样），只按 ZIP 字母序。本书恰好"字母序 = spine 序"，属**偶然正确**；凡 spine 顺序 ≠ 字母序的 EPUB，正文顺序会错。这是通用缺陷（尚未向上游提 issue）。

---

## 2. 三条候选路线与实测对比

测试集：**第一、二、三、九、十六章 + 附录**，共 **1812 个原始段落**；同一套嵌入/重排/检索参数。

| 指标 | A 现状 | **C 结构优先（采用）** | D 语义合并 | R 原库（整本） |
|---|---|---|---|---|
| 块数 | 1812 | **224** | 1552 | — |
| 块长 中位 / 均值 / 最长 | 24 / 42.2 / 509 | **266 / 377 / 1821** | 23 / 49.4 / 881 | — |
| 碎块（<50 字符） | 1341 | **9** | 1090 | — |
| 孤立标题块 | 98 | **0** | 48 | — |
| 穴名与【定位】同块 | 0/116 | **116/116** | 2/116 | — |
| 入库逐字无损 | 通过 | **通过** | 通过 | — |
| 检索：单块精确命中（21 题 TopK=4） | 11 | **21** | 14 | 11 |
| 检索：仅靠 Top4 拼装才命中 | 7 | **0** | 1 | 5 |
| 检索：完全落空 | 3 | **0** | 6 | 5 |

**结论**：C 路（结构优先文本）全面胜出；**D 路（插件配置项 `semanticChunk=true`，按嵌入相似度自动合并碎块）只能把 1812 块降到 1552 块，穴位条目同块率仍只有 2%，解决不了问题，予以排除**。

---

## 3. 关键技术发现：「Markdown 聚合不可行」需要限定条件

早期结论认为"在 Markdown 层面预先聚合无效"。**实测表明该结论只在"单元内部保留空行"时成立**：

1. 单元**内部**段落用**单换行**连接（`normalizeText` 只压缩 3 个以上连续换行，单换行保留）→ `splitBlocks` 不再切。
2. 单元**之间**用**空行**分隔 → 每个单元恰好成为一个块。
3. `#`/`##` 开头的行会被 `splitBlocks` **吃掉并转成该块的 `heading` 元数据**，入库时拼成 `context = 书名 > 章 > 节`（`:9401`）。此前一次失败的 A/B 测试，是因为前缀行与正文之间留了空行，才被切成独立小片。
4. 本方案改用**片首内联出处行**（不依赖上述机制），因此**逐字无损**。

即：**仅靠文本导入通道就能实现结构优先分块，无需修改插件。**

---

## 4. 最终采用的分块规则

- **层级边界**：章 → 节 → 一、 →（一）；**401 个穴位条目为原子单元**（编号穴名 + 后随【定位】结构信号）；论述按小节聚合、不跨小节；目录页按行聚合。
- **超限才拆**：超过 **1824 字符**时，穴位条目按【字段】边界拆，其余按句子 → 换行 → 硬切；并按**每篇文档自身**的字符预算再收口一次（插件按整篇文档计算 `charsPerToken`，各篇预算不同，不收口会被 `windowBlock` 带重叠切开）。
- **出处**：每块片首内联 `〔源：textNNNNN.html#锚点〕` 或 `〔源：textNNNNN.html 元素N〕`；被拆开的块另补「章 · 节」上下文。

### 全书离线核验（29 页 / 6535 段 → 676 片）

| 核验项 | 结果 |
|---|---|
| 片数 / 最长片 / 超预算片 | **676 / 1817 / 0** |
| 无损性 | 去标记与注入上下文后，**去空白逐字 == 库内 rawText** |
| 标题 | 253 个，**一个不少** |
| 图片占位 / 脚注锚点 / 【定位】 | 217 / 1922 / 401，**与现状完全一致** |
| 出处标记可回解 | **676 / 676 全部**可定位到原 XHTML 页 + 元素序号 / 锚点 id |
| 穴位条目纯度 | 每片最多 1 个【定位】，**0 粘连** |
| 标记与上下文开销 | 19810 字符，占原文 **7.1%** |
| 目录页处理 | EPUB 目录页 541 小片 → 18 片；NCX 导航页 3.3 万字符单块 → 20 片 |

---

## 5. 已建成的新库（检索开关尚未切换）

| 项 | 值 |
|---|---|
| 库名 | **经络腧穴学·结构优先** |
| 库 id | `8bf27782-5f61-49c8-8d9a-af2b89158239` |
| 结构 | 29 篇文档（按原 XHTML 页切分，标题取原书章名/前言/附录等） |
| 块数 | **676** |
| 字符 / token | 297000 / 128197 |
| 逐篇核验 | 块数与送入片数一致 **29/29**；入库逐字无损 **29/29** |
| 检索开关 | **仍指向原「中医新」库**（`enabledBaseIds` 未改） |

建设过程中修正了两个**我方脚本**的缺陷（与插件无关）：① 未按每篇文档自身预算收口，导致 5 篇中各有片超限；② 按篇补切时片首标记行后多出空行，入库被切成"标记 + 正文"两块（首版库 686 块 ≠ 676 片）。修正后删库重建，逐篇完全对齐。

---

## 6. 检索验证：36 题三版对照（**重点，含一处结论更正**）

36 题覆盖《经络腧穴学》的第一、二、三、五、六、七、八、九、十、十一、十二、十三、十四、十五、十六、十七章及附录——其中 **12 章未参与任何规则调参**，用于防止过拟合。全部问题限定到"本书"这一个文档，TopK=4。

| 版本 | 块数 | 一块多大 | 单块精确命中 | 仅拼装命中 | 完全落空 |
|---|---|---|---|---|---|
| 中医新（EPUB 直导） | 6535 | **41 字符** | **14 / 36** | 11 | 11 |
| 中医 旧库（md 版） | 175 | ~1200 字符 | **36 / 36** | 0 | 0 |
| **新库 结构优先** | 676 | 一条一块 | **36 / 36** | 0 | 0 |

**必须更正的一点**：不能声称"前两个库都远不如新库"。实测表明**旧「中医」库里那一版（md，1200 字一块）同样是 36/36**，与新库打平。真正的差异不在"能否检索到"，而在下面四项：

| 维度 | 中医 旧库 | 新库 结构优先 |
|---|---|---|
| 一块里含几个穴位 | **多个**（1200 字一大段，需自行在其中定位） | **一个**（穴名＋定位＋主治＋操作，可整块引用） |
| 正文结构标记 | 401【定位】/401【主治】/401【解剖】（与 EPUB 完全一致） | 同左 |
| 图片占位 | **0（全丢）** | **217（全在）** |
| 脚注锚点 | **0（全丢，无法跳转）** | **1922（全在）** |
| 每块出处 | 无 | 有（原 XHTML 页 + 锚点） |

即：新库相对旧库的增量是「**一条一块、可整块引用、图片与脚注未丢、出处可回解**」，而不是"能查到 vs 查不到"。**若只要求"能检索到"，其余那批 1200 字一块的资料不需要改造。**

---

## 7. 其余 46 份资料的体检结果（决定后续工作量）

47 份资料的原始格式**全部为 EPUB**，原始文件位置：
`DSH云端备份\4-原始底本\epubtest\00-原始EPUB\`（10 个 EPUB，含 2 个合集）
已转换的 md：`…\epubtest\03-入库\`（10 个）、`…\epubtest\04-合集拆分\`（38 个，由两个合集拆出）

按"块是否碎"分类（以库内块数 ≈ md 段落数、块长 ≈ 段落长为等价关系）：

| 类别 | 本数 | 特征 | 建议 |
|---|---|---|---|
| **A 已较粗** | **34 本** | 一段约 1200 字，不碎（等同旧库那版） | **不用重切** |
| B 偏碎 | 1 本 | 《脈經校注》，一段约 590 字 | 顺手做 |
| **C 真碎** | **11 本** | 一段 200–370 字，近一半段落不足 50 字 | **优先做，受益最大** |

C 类 11 本（合计约 400 万字符）：证治准绳（五）幼科、千金翼方校释、重订医学衷中参西录（上/下）、中药学、证治准绳（四）疡医、黄帝内经素问语译、针灸大成、刺法灸法学、单玉堂针灸配穴通俗讲话、黄帝内经素问（典藏版）。

### 工作量估计（若把 C+B 类做完）

| 步骤 | 时间 |
|---|---|
| 4 套"单元规则"（医经校注类 / 方书类 / 教材讲稿类 / 针灸专著类） | 4–8 小时 |
| C 类 11 本 + B 类 1 本：跑通＋逐本无损与出处核验＋导入＋抽测 | 5–7 小时 |
| 全库检索验证 | 2–3 小时 |
| **合计** | **约 11–18 小时 ≈ 2–3 个工作日** |
| A 类 34 本若也要按章细化 | 追加 8–12 小时（收益小，建议暂不做） |

复用性：EPUB 元素级抽取、结构优先切分、无损自检、出处回解、入库与检索对比脚本**均已具备**，对全部书目通用；每本真正需要人工介入的是"什么算一个单元"的规则确认。

**注意**：《中药学》那份文件名标注为 `中药学 .pdf_by_PaddleOCR-VL-1.6.md`，是**扫描 PDF 经 OCR** 得到的，文字质量需单独核对，不宜与其他书批量处理。

---

## 8. 知识库设置审计（全局 vs 库级）

**关键机制**：`库级配置优先于全局配置`（曾因此踩坑：重排模型短名写在库级，全局正常但库级报 HTTP 400）。

| 项 | 全局 | 「中医新」库级（实际生效） |
|---|---|---|
| 嵌入模型 | Qwen3-Embedding-4B | 继承（一致） |
| 重排模型 | Qwen3-Reranker-8B | 同（已修好） |
| topK | 4 | **6** |
| similarityThreshold | 0 | **0.15** |
| siblingChunks | 1 | 继承 1 |
| semanticChunk | false | false（显式） |
| 文档处理器 | builtin | **mineru**（带 key） |
| 图片识别 | off | **openai / Qwen2.5-VL-72B**（带 key） |
| chunkSize / overlap / smartChunk | 800 / 100 / true | 继承 |

**不要改动**：
1. 嵌入模型（一改，三库 1460 万字符全部需重新向量化，且新旧库向量互不兼容）；
2. 重排模型（当前是正确的全名，改动会重演 HTTP 400）；
3. `smartChunk=true` 与空行分隔（本方案生效的前提）。

**建议调整**（均在「中医新」库级，属可选）：
1. `siblingChunks` 1 → **0**：现在会把命中块的前后邻居一并塞进结果。对碎块库是补救（邻居里可能有穴名），对"一条一块"的新库则是噪声（邻居是别的穴位）。旧「中医」库本身即设为 0。
2. `topK` 6 → **4**（可选）：一条一块时 4 条足够。

**MinerU**：只对 PDF 生效。现有三库内容全部是 EPUB/md（唯一 PDF 衍生的《中药学》已在外部用 PaddleOCR 转好），**MinerU 当前完全用不上、也不会被触发**，无需改动。注意它设在「中医新」库级，新库继承全局 `builtin`；将来若向新库导入扫描 PDF，才需要另行配置。

**图片识别**：「中医新」库级其实**已开启**（`openai / Qwen2.5-VL-72B-Instruct`；全局是 off，被库级覆盖），但**这本书一张图注都没生成**——库内 217 个图片占位全部为空。说明内置 EPUB 解析路径不会把图片交给视觉模型，该设置对本库是**空转**。图片提字应作为独立工作线，且机器输出必须标注「机器识别/待核」。

**关于嵌入模型 4B vs 8B**：同家族 MTEB 多语言成绩 0.6B ≈64.3 / **4B ≈69.5** / 8B = 70.58，即 **4B → 8B 仅约 +1.1 分（相对约 1.6%）**，真正的大台阶是 0.6B → 4B。而本次仅改变分块方式（同一 4B 嵌入）就把命中率从 14/36 提到 36/36。因此**暂不更换嵌入模型**；若将来要试 8B，最省的做法是**只在新库上试**（676 块，分钟级、成本极低），不动旧库与全局。

---

## 9. 对早期外部建议的逐条核对

| # | 早期建议 | 核对结果 |
|---|---|---|
| 1 | 碎块源于源 XHTML 短段落 + 插件按空行切，重建索引无效 | **成立**，已复现 |
| 2 | 本 EPUB 顺序/图片未丢；插件从不读 spine 是潜在缺陷 | **成立**，源码复核确认 |
| 3 | Markdown 预聚合不可行，须改插件或用"单元内无空行"的文本 | **需限定**：前半句只对"保留空行的 Markdown"成立；后半句已验证可行，**因此不必改插件** |
| 4 | 按章→节→穴位单元组织，超 1824 再拆，保留章节/XHTML/元素序号/锚点 | **成立**，已实现并量化 |
| 5 | 暂不改全书，先扩到 3–5 章测试再定 | **已执行**，并已按此结论建成全量新库（未切换） |
| 6 | 图片单独处理、模型输出只作待核 | **同意**，尚未开始 |
| 7 | 接口明文返回 key，须轮换 | **同意**，尚未执行（见 §10） |
| — | **该清单遗漏**：插件配置项 `semanticChunk`（自动合并碎块） | 已补测，**解决不了问题，排除** |
| — | **需更正**：不宜声称"旧库远不如新库" | 旧库 md 版 36/36 与新库打平，真正差异见 §6 |

---

## 10. 未决事项

1. **是否把检索启用范围切到新库**（`knowledge-toggle` 的 `enabledBaseIds`，目前仅 `中医新`）。一步可回退。
2. **图片提字**：217 张图仍只有占位，`alt` 全空；字形图/正文图/注释图需分别提取并人工核对，机器输出标注「机器识别/待核」。经络示意图的模型描述建议另存。
3. **插件 spine 缺陷**：建议向上游提 issue（通用缺陷，与本机数据无关），尚未提交。
4. **凭据安全**：`GET /knowledge/config` 与 `GET /knowledge/bases` 会**明文返回**嵌入/重排/图注/MinerU 的 key，而本机接口无鉴权；历史上会话记录中亦曾出现明文 key。建议**轮换密钥**并给这两个 GET 加鉴权（后者属改动插件本体，未执行）。本报告与全部脚本均**不含任何密钥**。
5. **临时库清理**：本轮为对比共建了 5 个 `_tmp-` 临时库，尚未删除（见 §12）。
6. **C 类 11 本是否开工**（§7）。

---

## 11. 复现方式

全部脚本位于 `工作区\_tmp\`，可用随附 Python 运行；关键产物：

| 文件 | 用途 |
|---|---|
| `gen_struct.py` | 结构优先文本生成器（内置无损自检 + 按每篇预算收口） |
| `extract_v2.js` | EPUB 元素级抽取器（按 OPF spine + body 顶层块元素，输出 page/elemIndex/id/images） |
| `import_full.py` / `reset_full.py` | 全量建库 / 删库重建 |
| `verify_c_full.py` / `check_pieces.py` | 出处回解、单元纯度、片内空行与预算复核 |
| `retrieval_full.py` / `retrieval_old.py` / `deep_compare.py` | 36 题检索对比 / 旧库补测 / 三维度深挖 |
| `five\C-pieces-all.json` | 全书 676 片（含 pre/core 与元素 ids），全量建库之源 |
| `five\full_base.json` | 新库 id 与 29 篇文档 id 台账 |
| `five\retrieval_full.json` / `retrieval_3way.json` | 检索结果原始数据 |
| `five\md_inventory.json` / `worklist.json` | 47 份资料的结构体检与分类 |

主要 API 端点：`POST /knowledge/bases`、`POST /knowledge/bases/{id}/documents`、`GET /knowledge/documents/{id}?includeChunks=true`、`POST /knowledge/search`。

---

## 12. 现状快照：什么被改了，什么没动

**未改动**：
- 原始 EPUB（`DSH云端备份\4-原始底本\epubtest\00-原始EPUB\`）
- 「中医新」库（1 文档 / 6535 块）
- 旧「中医」库（47 文档 / 15995 块）
- 插件文件 `lib\knowledge\index.js`（**未修改**；历史改动仅存在于 `lib\tool-knowledge\index.js:1929`，有备份）

**新增**：
- 新库「经络腧穴学·结构优先」（29 文档 / 676 块），**未启用检索**
- 5 个 `_tmp-` 对比临时库：`_tmp-第三章策略A`(183 块)、`_tmp-第三章策略B`(184 块)、`_tmp-五章-现状A`(1812 块)、`_tmp-五章-结构C`(224 块)、`_tmp-五章-语义D`(1552 块)
- 工作区报告与脚本（上表）

**本报告全部数字均可在本机用 §11 脚本复现。**

---

*安全声明：本报告未包含任何 API key、token 或其他凭据；配置审计中密钥一律以掩码形式呈现。*
