# 技术验证 S1–S4 结论(2026-11-03) > 依据:架构文档 `architecture.md` §10。代码在 `spikes/`(一次性代码,复现步骤见 `spikes/README.md`),由样本派生的产物都在各子目录的 `out/` 里,不进 git。 > 环境:本机 Docker 29.8.2(Apple Silicon,**arm64**),Gotenberg 8.39.0;样本是 `samples/tenders/` 的 S01–S10。 > 模型:DeepSeek 官方接口,配置名 `deepseek-chat`,接口返回实际服务的模型是 `deepseek-flash`(DeepSeek-V4.1-Flash),非思考模式(思考 token 为 0)。 ## 0. 结论速览 | # | 结论 | 关键数字 | 对架构的影响 | |---|---|---|---| | S1 | **可行,但不能直接用 Gotenberg 默认配置**:要先修正 WPS 写错的字体字符集,再换成按宋体行高调整过的开源字体 | 默认配置:页数偏差 +17%~+37%,S07 正文**全部空白**;调整后:两份有真值的文件 79/79、75/73 页,目录页码跨度误差 0%~-2.2% | §3.5 新增两步预处理和字体包 | | S2 | **可行**:在规范化 PDF 上识别「斜体且下划线」,和 DOCX 原文件逐字对得上 | 4 份文件按字符 100% 一致;片段级召回 0.993~1.0 | 不需要 Docling 来保格式 | | S3 | **正文可行,表格要改进**:原句高亮的几何位置全部正确;切句在表格里会把相邻单元格拼在一起 | 几何回读 50/50、300/300;文本保真:正文 97.5%,表格内 78% | 切句要识别表格单元格和页眉页脚 | | S4 | **可行**:两道证据校验 + 第三道「依据校验」后,抽取结果可用;★ 类条款建议用规则兜底保召回 | ★ 召回 93.5%(扣除银标准噪音后约 96.6%),斜体下划线 99.8%;采纳条目 95.2% 属实;每份文件约 0.15 元(高峰价) | §3.6 新增依据校验和规则兜底 | ## 1. S1 Word 转 PDF 的保真度 ### 1.1 环境核实 - **Gotenberg 镜像**:`gotenberg/gotenberg:8`(8.39.0)有 **arm64 版本**,镜像约 2.5GB,基础系统是 Debian 13。 - **LibreOffice 版本**:26.8.1,比参考项目踩过坑的 7.3 / 24.2 都新。 - **自带中文字体**:只有 Noto Sans / Serif CJK。宋体、仿宋、黑体全部回落到西文的 Noto Sans,衬线和黑体都分不清。 - **样本用到的字体**:宋体(约 4.8 万处)、仿宋、黑体、楷体、方正系列、Times New Roman、Arial。都是商业字体,不能随镜像分发。 - **OLE 嵌入对象**:10 份样本里都没有,所以「内嵌 OLE 的 docx 会让旧版 LibreOffice 崩溃」这一条无法用本批样本验证,留到收集到这类样本时再测。 - **耗时与稳定性**:10 份文件 × 6 种配置,全部转换成功,没有崩溃;单份 0.4~2.0 秒。 ### 1.2 真值 | 来源 | 覆盖 | 说明 | |---|---|---| | 旧版 DOC 里 WPS 保存时记录的页数 | S07(79 页)、S09(73 页) | OLE SummaryInformation | | DOCX 的 `docProps/app.xml` 页数 | **没有** | 8 份 DOCX 都是 ONLYOFFICE 7.2 生成的(苏采云平台导出),不写页数 | | DOCX 目录里缓存的页码(PAGEREF) | S03、S04、S06 | 生成文档时排版写入;比较「各章标题之间隔了几页」,消除页码起始值的影响 | **发现**:平台导出的 DOCX 没有「Word 里显示的页数」可以对照。用户在 Word 或 WPS 里看到的分页,取决于他们自己装的软件和字体。所以「出处页码以规范化 PDF 为准」这条设计是必需的,不是权宜之计。 ### 1.3 严重问题:WPS 旧版 DOC 转出来正文是空白 - **现象**:S07 默认转换后,正文渲染成空白,抽出的文字全是「第第第第」;S07 只有 61 页,真值是 79 页。 - **原因**:WPS 在字体表里把「宋体」的字符集标成了 `02`(SYMBOL,符号字符集)。LibreOffice 据此把正文当成符号,换成了 OpenSymbol 字体;转成 DOCX 后,这些字也变成了 `` 符号字符,不是普通文本。 - **修复**:先转 DOCX,把中文字体名被标成 SYMBOL 的字符集改成 GB2312(`86`),再转 PDF。修复后 S07 正文正常,**80 页**。 - **影响**: - 转换前必须加这一步预处理。 - 用 DOCX 做交叉校验时,读文本要同时读 `` 和 ``。 - 只要规范化 PDF 里出现大段 OpenSymbol 字体、或抽出的文字大量重复同一个字,就判定为「转换异常」:不扣点,提示用户。 ### 1.4 字体配置对比(页数) | 配置 | 说明 | |---|---| | A-default | Gotenberg 默认,原文件直接转 | | B-alias | 加 fontconfig 规则:宋体 / 仿宋 / 楷体 / 方正系列 → Noto Serif CJK SC,黑体 / 雅黑 → Noto Sans CJK SC | | A-fixed / B-fixed | 同上,但先做 §1.3 的字符集修正 | | **C10-fixed** | 字符集修正 + 用 fontTools 克隆 Noto CJK,**字形不变,只把行高改成 1.0em**(和宋体一致,上 0.86 / 下 0.14),改名 BD Song / BD Hei,替换规则同 B | | C13-fixed | 同 C10,行高 1.3em | | 样本 | 真值 | A-default | B-alias | A-fixed | B-fixed | **C10-fixed** | C13-fixed | |---|---|---|---|---|---|---|---| | S07(WPS) | 79 | 61(正文空白) | 61(正文空白) | 80 | 80 | **79** | 80 | | S09(WPS) | 73 | 89 | 90 | 91 | 92 | **75** | 87 | | S01 | — | 93 | 95 | 93 | 95 | 81 | 90 | | S03 | — | 145 | 145 | 145 | 145 | 104 | 131 | | S06 | — | 198 | 199 | 198 | 199 | 166 | 190 | 目录页码真值(标题之间的页数跨度误差,括号里是最大单点偏差): | 样本 | 目录项 | A-fixed | C10-fixed | C13-fixed | |---|---|---|---|---| | S03 | 7 | +36.7%(29 页) | **-1.3%(1 页)** | +24.1%(19 页) | | S04 | 8 | +32.3%(20 页) | **0.0%(1 页)** | +22.6%(14 页) | | S06 | 6 | +17.3%(26 页) | **-2.2%(11 页)**¹ | +11.5%(26 页) | ¹ S06 只匹配上 4 个目录项,最大偏差里有标题匹配不准的成分。 **结论**: - 分页偏差的主因是**行高**,不是衬线还是黑体:Noto CJK 的行高约 1.45em,宋体是 1.0em,同样的「单倍行距」在 Noto 下每行高出 45%。 - 克隆字体的行高改成 1.0em 后,5 个有真值的数据点都落在 ±3% 以内,单点偏差大多在 1 页内。 - **字体许可**:Noto CJK 是 OFL 1.1,允许修改后再分发;改名后的字体仍按 OFL 分发,许可证文件要随镜像带上。 - **仍然存在的风险**: - 这 5 个数据点都以宋体为主。仿宋、楷体为主的文件(S01、S02、S08)没有真值,偏差未知。 - 克隆字体只调了行高,西文字宽和宋体的半角宽度不同。表格密集、中英混排多的文件可能仍有偏差。 ## 2. S2 格式识别(斜体 / 下划线 / 加粗) ### 2.1 方法 - **银标准**:DOCX 原文件里「斜体且下划线」的文字。解析 XML 时完整处理样式继承:文档默认 → 段落样式链 → 字符样式链 → 直接格式。注意 python-docx 的 `paragraphs` 不包含表格里的段落,直接用它会漏掉大半。 - **PDF 侧**: - 斜体:字体名含 Italic,或字形矩阵有倾斜(中文没有斜体字形,LibreOffice 合成斜体时倾斜量是 0.35); - 下划线:字符底边附近有一条横线覆盖该字; - 加粗:字体名含 Bold。 - 下划线的判定窗口要包含「字符框内侧」:克隆字体的下行部分变小后,下划线会落进字符框里。 ### 2.2 结果(规范化 PDF = C10-fixed) | 样本 | DOCX 斜体下划线字数 | PDF 识别字数 | 按字符一致 | 片段召回 | 片段精确率 | |---|---|---|---|---|---| | S06 | 14209 | 14209 | **100%** | 0.993 | 0.871² | | S08 | 1088 | 1088 | **100%** | 1.000 | 1.000 | | S09 | 70 | 70 | **100%** | 1.000 | 1.000 | | S03 | 98 | 98 | **100%** | 1.000 | 1.000 | ² S06 的片段精确率偏低,是因为 PDF 按阅读顺序把相邻表格单元格的片段连在了一起,按片段匹配就对不上;按字符计数两边完全一致。默认字体配置(A-fixed)下结果相同,说明格式识别不依赖字体选择。 ### 2.3 Docling 对比(S06) | 输入 | 耗时 | 保留斜体下划线 | 页码和坐标 | 表格识别 | |---|---|---|---|---| | DOCX 原文件 | 2.1 秒 | **100%**(14209 字) | **没有** | 31 个 | | 规范化 PDF | 96.2 秒(CPU) | **0** | 有 | 73 个 | - `docling-serve-cpu` 有 arm64 镜像,约 7.2GB,本机拉取约 5 分钟。版本:docling-serve 1.38.2、docling 2.142.1。 - **结论**:Docling 做不到「格式 + 坐标」同时具备;我们自己从规范化 PDF 提取,两样都有,而且 4 份文件合计只要约 6 秒。Docling **不作为主解析器**。它的 PDF 表格识别可以作为候选,用来解决 S3 的表格切句问题,要再评估。 ## 3. S3 原句级坐标与高亮 ### 3.1 方法 - 从规范化 PDF 取逐字坐标,按行聚类;同一行里字间空隙大于 1.5 个字号时断开,得到「行片段」。 - 在 。!?; 处断句;段落末行或表格单元格结束时也断句。句子不跨页。 - 编号格式:`<文档>:p<页>:s<页内序号>`。规范化 PDF 不再重新生成,所以编号稳定。 - 每句生成 quads:按行合并成矩形。 - 校验分两种: - **几何回读**:用 quads 裁剪原页重新取字,和句子比对; - **文本保真**:句子能在 DOCX 原文里找到,不算 Word 自动编号和页码域。 ### 3.2 结果 | 指标 | 50 句(种子 20261103) | 300 句(种子 7) | |---|---|---| | 10 份文件总句数 | 24020 | 24020 | | 跨行句子占比 | 23 / 50 | 118 / 300 | | **几何回读正确** | **50 / 50** | **300 / 300** | | 文本保真 | 49 / 50(98%) | 269 / 300(89.7%) | 300 句按位置分类的文本保真率: | 位置 | 文本保真 | 失败原因 | |---|---|---| | 正文 | **196 / 201 = 97.5%** | 个别行内拼接 | | 表格内 | 71 / 91 = 78.0% | 相邻单元格(序号列、分值列)的文字被拼进同一句 | | 页眉页脚带 | 2 / 8 = 25.0% | 页眉文字混进了正文句 | - 高亮图在 `spikes/s3-citation/out/highlight-*.png`(8 张),样式是设计规范的蓝铅笔浅底加蓝铅笔描边。人工看过两张:跨 3 行的句子在句号处结束,同一行后面的下一句没有被带进来。 - **要改进的两点**: 1. 切句前先识别表格单元格,用 PDF 的表格线,或者参考 Docling 的表格结构,按单元格切; 2. 识别每页重复出现的页眉页脚带,单独处理,不并入正文句。 - 自动编号(「60.」「24.」)由 Word 渲染时生成,规范化 PDF 里有、DOCX 文字里没有。出处高亮以 PDF 为准,所以不影响高亮;只是做 DOCX 交叉校验时要容忍这一点。 ## 4. S4 大模型抽取实质性条款 ### 4.1 方法 - **输入**:S3 的句子,带格式标记(`` 斜体且下划线、`` 下划线、`` 斜体)。每个分块约 6000 字,10 份文件共 150 块。每个分块前附上从全文摘出的「标注规则说明」句。 - **银标准**(实质性条款句): - ★ 句:排除说明句和「★号条款」这类引用句; - 斜体下划线句:只在文件自己声明这种标注方式时才算(S06、S08、S09),否则它常常只是模板里的填空处。 - S03 正文的 ★ 句**正好 14 条**,和文件自述的「★ 项共 14 条」一致。 - **提示词**:`spikes/s4-llm/prompts/extract_substantive/`,有 v1、v2 两个版本。要求只能引用句子编号,引文必须逐字复制。 - **三道校验**: 1. 编号必须在本分块里; 2. 引文必须是原句的子串,并满足最小长度; 3. **依据校验**(v2 新增):模型声称的依据,用确定性信号核对。★ 看原句里有没有 ★,斜体下划线看有没有 ``,「写明无效」看原句和相邻句里有没有无效类字样。 - 任何一道没通过,条目都进**待确认区**,不丢弃。 ### 4.2 结果(10 份文件全量) | 指标 | v1(无依据校验) | **v2 + 依据校验** | |---|---|---| | ★ 召回(严格 / 含相邻句) | 91.3% / 92.4%(84、85 / 92) | **93.5% / 93.5%**(86 / 92) | | 斜体下划线召回(严格 / 含相邻句) | 91.3% / 99.8% | 91.7% / **99.8%** | | 采纳条目数 | 1761 | 651 | | 采纳条目属实(命中银标准,或证据句写明无效) | 36.7% | **95.2%** | | 进待确认区 | 66 | 283 | | 证据两道校验通过率 | 95.1% | 93.6% | | 输入 / 输出 token | 61.1 万 / 11.4 万 | 63.6 万 / 5.2 万 | | 费用(高峰 / 空闲) | 1.93 / 0.96 元 | **1.49 / 0.74 元** | - **漏掉的 ★ 句(v2)**:共 6 条,其中 3 条是银标准的噪音:「备注:加★为核心技术参数」「非标★、▲项为一般技术参数」「属于清单中打★品目的」。**真正漏掉的 3 条都是表格里的参数行**。扣除噪音后召回约 **96.6%**(86 / 89)。 - **v1 的主要问题是过度抽取**:合同履约、保密协议、付款等条款都被当成了「写明无效」。S09 有 208 条被标成「斜体下划线」,实际上全文只有 2 句。说明模型会**标错依据**,所以必须有第三道依据校验。 - **证据没通过的原因**:引文过短(少于 8 个字)、引文不是原句子串、编号不在本分块。都被校验拦下进了待确认区,没有一条错误出处进入正式清单。 - **耗时**:10 份文件 8 路并发,约 2 分钟;最大的 S06(199 页、28 块)单独串行累计 142 秒,并发后约 20 秒。 ### 4.3 成本(供 D2 定价参考) 价格来源:DeepSeek 官方价目页(https://api-docs.deepseek.com/zh-cn/quick_start/pricing,2026-11-03 查询)。`deepseek-flash`(DeepSeek-V4.1-Flash-1017)每百万 tokens:输入缓存命中 0.04 元、未命中 2 元、输出 8 元(高峰);空闲时段半价。 | 口径 | 每份招标文件 | |---|---| | 本次实测(只做「实质性条款」一项抽取,v2) | 输入约 6.4 万 token、输出约 0.5 万 token;**约 0.15 元(高峰)/ 0.07 元(空闲)** | | 推算:完整 M2 解读(资格、废标条款、评分、日期、参数、摘要等 5~6 项抽取) | 约 0.6~1.0 元(高峰),**推算值,未实测** | - 换成深度思考模式或 `deepseek-v4-pro`,输入单价是 Flash 的 2.25~4.5 倍,还会多出思考 token。本次没有测,D2 定价前要补测「Pro 或思考模式能把召回提高多少」。 - 价格表里没有 `deepseek-chat` 这个名字,但接口仍然接受,实际由 Flash 提供服务。正式接入时应直接用 `deepseek-flash`,并把接口返回的 model 字段记进调用流水账(架构 §3.6)。 ### 4.4 建议 1. **标注类条款用规则保召回**:★、*、文件声明了的斜体下划线,都能从规范化 PDF 上确定性地识别出来(S2)。所有这类句子都必须被某个条目覆盖;模型没覆盖的,系统自动生成一条「待确认」。模型负责归并、概括和判断依据,召回不再完全依赖模型。按 S2 的结果,这一类的召回可以做到接近 100%。 2. **「写明无效」类靠模型加依据校验**:这一类没有银标准,召回率还没测,需要人工标注 2~3 份文件。可以和样本缺口里的「人工标注 S03 / S06」一起做。 3. **数量核对**:文件写明了实质性条款数量的(S03「共 14 条」),和抽到的数量对比,不一致就提示(FR-M2-05 已有要求)。 4. 引文最小长度从 8 个字放宽到「不少于 6 个字,或整句」:表格单元格常常很短,「引文过短」是证据被拒的首要原因。 ## 5. 对需求目标值的建议 | 需求 | 建议目标值 | 依据 | |---|---|---| | FR-M2-05 ★ / 标注类实质性条款召回 | **≥ 99%**(规则兜底 + 模型) | S2 格式识别 100%;S4 模型单独约 96.6% | | FR-M2-05 「写明无效」类召回 | 先定 **≥ 90%**,人工标注后校准 | 尚无银标准 | | FR-M2-05 采纳条目准确率(进正式清单的条目属实) | **≥ 95%** | S4 v2 实测 95.2% | | FR-M2-06 出处页码正确率 | **100%** | 出处由句子编号决定,页码随编号而来;编号和引文都经过校验 | | FR-M2-06 原句高亮准确率(抽查 50 条) | 正文 **≥ 98%**,表格 **≥ 95%** | 实测正文 97.5%、表格 78%,表格切句改进后再验收 | | FR-M2-02 解析耗时(300 页) | 维持 10 分钟目标 | 转换 ≤ 2 秒,文字层约 2 秒 / 100 页,单项抽取并发后约 20 秒;完整解读按 5~6 项推算在 2~3 分钟内 | ## 6. 未覆盖的事项 - **S1**:内嵌 OLE 对象的 docx(本批样本里没有);以仿宋、楷体为主的文件的分页真值。 - **S3**:扫描件 OCR 页的坐标(S5 的范围)。 - **S4**:深度思考模式和 Pro 模型的对比;「写明无效」类的召回;资格、评分、日期等其他抽取项。 - S5(OCR)、S6(签章)、S7(任务队列)按 §10 的计划另做。