Files
bid-doctor/docs/architecture/tech-validation.md
YANG JIANKUAN 604e889ca4 docs(architecture): 完成技术验证 S1–S4,确定 FR-M2-05/06 目标值
- spikes/:Word 转 PDF(Gotenberg)、格式识别、原句坐标、DeepSeek 抽取四项验证代码与两版提示词;样本派生产物和 .env 不进 git
- tech-validation.md:完整结论。四项都可行:WPS 字体字符集要修正并改用行高调整过的字体;格式识别不需要 Docling;表格内切句待改进;抽取需第三道依据校验与规则兜底
- architecture.md:§3.5 加预处理与字体包,§3.6 证据校验改为三道,§10 回填结论
- 03 功能需求与 decisions.md(TV1):召回 ≥ 99%、采纳准确率 ≥ 95%、页码 100%、高亮正文 ≥ 98% / 表格 ≥ 95%

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:46:49 +08:00

220 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 技术验证 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 后,这些字也变成了 `<w:sym>` 符号字符,不是普通文本。
- **修复**:先转 DOCX,把中文字体名被标成 SYMBOL 的字符集改成 GB2312(`86`),再转 PDF。修复后 S07 正文正常,**80 页**。
- **影响**:
- 转换前必须加这一步预处理。
- 用 DOCX 做交叉校验时,读文本要同时读 `<w:t>` 和 `<w:sym>`。
- 只要规范化 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 的句子,带格式标记(`<iu>` 斜体且下划线、`<u>` 下划线、`<i>` 斜体)。每个分块约 6000 字,10 份文件共 150 块。每个分块前附上从全文摘出的「标注规则说明」句。
- **银标准**(实质性条款句):
- ★ 句:排除说明句和「★号条款」这类引用句;
- 斜体下划线句:只在文件自己声明这种标注方式时才算(S06、S08、S09),否则它常常只是模板里的填空处。
- S03 正文的 ★ 句**正好 14 条**,和文件自述的「★ 项共 14 条」一致。
- **提示词**:`spikes/s4-llm/prompts/extract_substantive/`,有 v1、v2 两个版本。要求只能引用句子编号,引文必须逐字复制。
- **三道校验**:
1. 编号必须在本分块里;
2. 引文必须是原句的子串,并满足最小长度;
3. **依据校验**(v2 新增):模型声称的依据,用确定性信号核对。★ 看原句里有没有 ★,斜体下划线看有没有 `<iu>`,「写明无效」看原句和相邻句里有没有无效类字样。
- 任何一道没通过,条目都进**待确认区**,不丢弃。
### 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 的计划另做。