交接 · 知识库分块:两项补查 + 单章 A/B 对照(2026-10-02 续)

上一轮:工作区\交接-知识库修复与EPUB碎块-20261002.md(重排修复、碎块根因、723 块候选方案)。 本轮做的是 ChatGPT 回复里要求的补查与单章对照测试。全部结论均为本机实测,命令与产物路径见文末。

一、补查 1:阅读顺序 —— 本机数据「ZIP 顺序恰好等于 spine 顺序」,但实现的正确性是偶然的

实测(脚本 _tmp\epub_spine_check.py,直接读 content.opf):


META-INF/container.xml → rootfile OEBPS/content.opf
manifest 251 项 = xhtml 29 + image/jpeg 219 + css 2 + ncx 1
spine itemref = 29,顺序 text00000.html … text00028.html
ZIP 内 29 个 html 的字母序      = text00000.html … text00028.html
两者顺序完全一致:True        集合一致:True

结论要限定范围:

项结论
本 EPUB 的阅读顺序✅ 正确。spine 与 ZIP 顺序逐项一致,29 页无缺无多
实现是否遵循 OPF spine❌ 没有。parseEpub(lib/knowledge/index.js:4433-4445)只做 Object.keys(zip.files).sort() + 扩展名过滤 + join("\n\n"),从不读 container.xml / content.opf / spine
风险属潜在缺陷:凡 spine 顺序 ≠ ZIP 字母序的 EPUB(改名、非零填充编号、spine 重排),解析出的正文顺序会错。本文件恰好看不出问题

二、补查 2:219 张图 ↔ 217 个占位 —— 差异是两个「结构性图片」,不是丢失

实测(_tmp\epub_img_refs.py):


ZIP 内 .jpg 文件        219 个,编号 Image00000–00219,其中 00218 缺号(共 219 个 = 0..219 去掉 218)
HTML 中 <img> 标签      217 个,对应 217 个不同文件名(无重复引用)
孤儿图片(存在但无 <img> 引用) Image00217.jpg、Image00219.jpg
存档文本图片占位 ![](ImageNNNNN.jpg) 217 个,使用的文件名最大到 Image00216.jpg

两个孤儿的身份(content.opf manifest 属性 + 引用关系):

文件manifest 属性说明
Image00217.jpgproperties="cover-image"封面图,由阅读器取用,正文不引用
Image00219.jpg普通 image 项,无 properties正文、toc.ncx、guide 均无引用;相邻的 00218 已被出版社删除 → 判为封底/版权类页图(同批被清理)

逐项对应:217 个占位 ↔ 217 个 <img>,文件名一一对应,无缺失、无多余、无重复。差 2 = 封面 1 + 封底类 1。

顺带核到的可访问性(此前未测):

项结果
脚注锚点 <a href="textNNNNN.html#filepos…">1922 个,断链 0 个(目标页与目标 id 全部存在);同页 840 / 跨页 1082
其他 <a href>(非 text 开头)58 个,未逐一定位
<img> 的 alt 文本全部为空(0/217)→ 图内文字确实没有任何文字化入口,只能靠视觉模型另做
图片路径全部在 OEBPS/ 同一目录,与 HTML 同目录,相对路径可直接解析

三、补查 3(顺带修正):真实字符预算是 1824,不是 1280

按插件自己的口径复算(chunkSize=800、chunkOverlap=100、smartChunk=true):


normalizeText 后长度 283093;estimateTokens(ck/1.5 + 其他/4) = 124162
charsPerToken = 2.2800  →  safeSize = round(800×2.28) = 1824 字符,safeOverlap = 228

上一轮交接里「折算 1280 字符」需更正为 1824。对结论无实质影响(现状仍是 0 个块超限),但后续拆分阈值要按 1824 判断。


四、复刻:抽取链路第一次做到与库内逐字一致

写了两版抽取器,第二版才对齐(差异全在非 <p> 结构上):

版本差异原因结果
v1 只取 <p>漏掉 text00007.html(540 个 <blockquote> 目录行)与 text00028.html(嵌套 blockquote)内的 1057 块5478 块
v3/v4 取 body 顶层块元素(<p> + <blockquote>)—6535 块,拼接后 283096 字符,与库内 rawText 逐字相同(含 29 处仅来自我复刻脚本的页眉行)

抽取器同时输出可追溯字段:page(原始 XHTML)、elemIndex(页内第几个 <p>)、id(如 filepos203143)、class、spans、images。这满足「每块能定位到版本、章节、原 XHTML/锚点」的要求。

五、单章 A/B 对照:结论是「分块必须落在插件解析层,Markdown 聚合会被入库时重新切掉」

按上轮 §五.2 的做法执行:第三章(手太阴经络与腧穴,text00011.html),两个临时库、同一套嵌入/重排/检索参数。

文件与库:

项A(现状策略)B(结构优先)
源文件_tmp\ch3-A.md(每段一块,183 段 / 7451 字符)_tmp\ch3-B.md(结构优先,20 片 / 7604 字符)
临时库_tmp-第三章策略A 30cea406-cc9e-4a5b-a538-9b9f7a43536e_tmp-第三章策略B b0fdc239-1b96-4d1c-8891-1578bad6ae25
入库块数183184
块长中位/均值/p90/最长22 / 38.7 / 74 / 43024 / 39.3 / 74 / 430
【定位】/【主治】/图片/脚注锚点11 / 11 / 9 / 6811 / 11 / 9 / 68
孤立标题块9 个(章、节、一、、(一)…各自成块)7 个(其中 3 级标题被并成一片)

关键实测:B 的源文件本来就是 20 片,但入库后仍变成 184 块 —— 因为插件的 splitBlocks 按空行分段,B 片内部为了保留段落结构必然留有换行分隔,于是入库时被重新切开;我为超预算穴位条目加的「章·节」来源前缀也因此被单独切成了 第三章手太阴经络与腧穴 · 第二节 手太阴腧穴 这种小片(库内块 134/142/175 实测)。

也就是说:

  1. 在 Markdown 层面聚合无法改变入库块边界 —— 只要走「导入文件」这条路,splitBlocks 就按空行把聚合拆回去。
  2. 想让「章 → 节 → 穴位条目 → 字段」真正成为块边界,只有两条路:
  1. 现状策略 A 的检索并不差:12–20 道本章问题(穴位定位、主治、循行、络脉、经别、经筋、脚注引文)两库 TopK=4 结果几乎一致,B 只在个别题上把正确条目提到第 2 位(如「天府 定位」A 的第 2 位是无关的「取法:先确定云门…」(0.700),B 是正确【定位】(0.998))。检索退步没有出现,噪声下降很轻微。

六、结构优先分块的量化效果(在同一份解析文本上,纯离线计算)

在同一 6535 块基线上按 §五 规则离线重排(_tmp\seg_v9.py):

指标A 现状B 结构优先
块/片数65351151
中位/均值/p90/最长28 / 41.3 / 75 / 951108 / 244.0 / 405 / 43371(附录歌诀那段,嵌套 blockquote 无段落边界,属解析层残留问题)
超 1824 字符06(全部是论文式长段/附录,需按句再切)
孤立标题253(含目录行 540 另计)0(标题与紧随内容同块)
无损性—去空白逐字相同;253 个标题一个不少
穴位条目401 个【定位】分散在 401+ 块401 个条目各自成单元,穴位头与【定位】同片

(注:401 是全书穴位条目数,与单章 A/B 的 11 个不矛盾。)

七、下一步建议(未动手)

  1. 先定路线:是否允许改插件 dsh-knowledge(改 parseEpub 读 spine + 按元素层级切块)。这是唯一能让结构优先真正生效的位置;插件升级会覆盖改动,需记录还原点。
  2. 若暂不改插件:可先把 A/B 测试扩到 3–5 章 + 全书离线评估,用 §六 的离线指标证明收益,再决定。
  3. 图片文字仍按上轮 §五.3 分类处理(字形/正文/注释图提字核对;经络示意图描述另存并标注;封面装饰图保留资源不生成图注);imageCaptionProvider=off,尚未启用。
  4. 原 EPUB、原「中医新」库、旧「中医」库均未改动。
  5. 临时库(_tmp-策略验证-可删、_tmp-第三章策略A/B)用完即删。

八、本轮产物(均在 工作区\_tmp\,未清理)

文件用途
epub_spine_check.py / epub_img_refs.py补查 1、2 的脚本与结论
extract_v1.js / extract_v2.js / extract_para.jsEPUB 结构抽取器(v2 = 与库内逐字一致的版本)
blocks_src.json / block_tiers.json6535 块 + 结构层级标注
seg_v9.py / B-pieces-v9.json结构优先切分引擎与结果(无损性已验)
ch3-A.md / ch3-B.md / bases.json单章 A/B 材料与临时库 id
compare_ab.py / ck_rechunk.py临时库块级对比、重切验证
db-raw.txt / doc-full.json从插件 API 取回的库内 rawText 与全量块(用于比对)
凭据提醒(上轮遗留未处理):GET /knowledge/config 与 GET /knowledge/bases 会把嵌入/重排/图注/mi​​neru 的 key 明文返回;本机环回口暂无鉴权。本轮未把任何 key 写入文件或回复。