本页目录
Lesson 00 练习
改
code.py和generate_poison_pdf.py里的代码,运行python code.py观察变化。本课依赖 PyMuPDF + matplotlib(venv 已装),毒文档在data/multimodal_docs/。
练习 1:亲手制造一个扫描页,验证「抽不到字」
扫描页是这门课的起点。亲手造一个,理解它为什么让 text-only 管线瞎掉。
打开 data/multimodal_docs/generate_poison_pdf.py,找到 make_scan_page 函数。现在的流程是「临时文档排文字 → 渲染成 150dpi 位图 → 整页嵌入图片」。改一下 dpi,观察抽到的文本量:
# 原来 150 dpi
pix = src.get_pixmap(dpi=150)
# 改成 300 dpi(更清晰的扫描)
pix = src.get_pixmap(dpi=300)
重新生成 PDF、跑 code.py:
cd data/multimodal_docs && python generate_poison_pdf.py && cd ../../doc-intelligence-lessons/00_baseline && python code.py
思考:P3 扫描页抽到的字符量是多少?改成 300 dpi 后变了吗?为什么?(提示:dpi 只影响图片清晰度,文本层始终是空的——扫描页的失败跟清晰度无关,跟「有没有文本层」有关。这正是 OCR 要解决的:给没有文本层的图片「补」上文字。)
练习 2:让表格变得更毒(加合并单元格)
现在的薪酬表有合并表头(「薪酬」跨两列、「绩效」跨两列)。再加一层合并,看 text-only 抽取乱成什么样。
在 make_table_page 里,把数据行改成「两行合并一个单元格」的效果——比如让 P3 和 P4 共享同一个部门「研发部」(模拟跨行合并)。直接在表格几何里画一个跨两行的「部门」列:
# 在 xs 列边界最前面加一列
xs = [50, 110, 210, 350, 490, 545] # 多了部门列
# 在 data 行里,给 P3/P4 画一个跨两行的「研发部」文字(只画一次)
page.insert_text((60, data_y[0]+20), "研\n发\n部", fontname="china-s", fontsize=9)
# 注意:跨行合并的单元格,文字只画一次,但视觉上占两行高度
重新生成、跑 code.py,看 P4 抽出的文本顺序。
思考:合并单元格被 get_text() 抽出来后,「研发部」这个词出现在哪里?它能正确关联到 P3 和 P4 吗?这就是表格串行化的典型灾难——合并单元格的语义(「这两行同属一个部门」)在串行文本里完全丢失。L02 会用结构化表示(markdown/HTML)解决这个问题。
练习 3(设计实验):量化「图表数值不可见」有多严重
这是本课的设计实验验证题——自己设计对照、观察指标。
图表页的杀手点是「数值只在图片里」。设计一个实验量化这个损失:
步骤 1:在 golden_questions.json 里,给图表题加几道「趋势类」问题(现在的题都是「Q3 多少」,纯数值;趋势题需要比较多根柱子):
{"id": "CHT05", "category": "chart", "question": "Q2 到 Q3 营收是涨了还是跌了?",
"answer": "跌了,从 2400 降到 2100", "answer_tokens": ["跌", "降"], "source_page": 5}
步骤 2:跑 code.py,看新题的 verdict。
步骤 3:对比「数值题」(CHT01-03)和「趋势题」(CHT05)的失败原因一样吗?
思考:数值题失败是因为「1800 这个 token 不在文本层」;趋势题失败的原因更深一层——即使数值都在,判断「涨/跌」需要把多个数值放一起比较,串行文本做不到。这说明图表不只是「把数字抽出来」,是要保留数值之间的结构关系(哪个季度对应哪个值)。L04 的 VLM 两段式(描述做索引 + 现场看图)就是为此设计。
练习 4(进阶):换一份你自己的毒文档
把生成脚本改造成能处理「你手头的真实文档」的工具——这是从教学到实战的一步。
找一份你工作/学习里的真实 PDF(脱敏后),用 code.py 里的 extract_text_layer 抽一遍:
from pathlib import Path
import fitz
doc = fitz.open("你的文档.pdf")
for i, page in enumerate(doc):
txt = page.get_text().strip()
imgs = page.get_images()
print(f"P{i+1}: {len(txt)} 字, {len(imgs)} 图")
观察:哪些页字符量异常低(< 50)?那些页大概率是扫描页或图表页。哪些页有图但文本也多?那是图文混排。
思考:如果你公司的知识库 80% 是这类文档,现状 text-only RAG 实际能服务多少查询?(用本课的基线方法估一下:随机抽 20 页,数有多少页字符量 < 50,那就是盲区比例。)这个数字就是你向老板申请做多模态升级的依据——不是「多模态很酷」,是「我们 X% 的知识库现在是瞎的」。
✅ 完成本课后,你应该能回答
- 扫描页为什么让 text-only 管线「全盲」?(无文本层,get_text 返回空)
- 表格被
get_text()抽出来后,丢失了什么关键信息?(行列对应关系/结构) - 图表里的数值为什么 text-only 管线「看不见」?(数值是图片像素,不在文本层)
- 三种多模态处理路线(全 VLM / 传统解析 / 分类路由)各自的成本-精度取舍是什么?为什么本课选分类路由?
- 为什么本课用「关键词命中」做基线评分,而不是跑真 RAG?(量化「信息有没有进库」的天花板,离线可复现)
- 毒文档的生成脚本为什么要随课交付、可复现?(全程 before/after 对照的锚点)
- (落地)kb-qa 现状的 loader 只认哪些后缀?扫描件/表格/图表在现状管线里的下场分别是什么?