Agent Engineering 课程阅读

在 GitHub 查看原文

本页目录

Lesson 00 练习

code.pygenerate_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% 的知识库现在是瞎的」。


✅ 完成本课后,你应该能回答

  1. 扫描页为什么让 text-only 管线「全盲」?(无文本层,get_text 返回空)
  2. 表格被 get_text() 抽出来后,丢失了什么关键信息?(行列对应关系/结构)
  3. 图表里的数值为什么 text-only 管线「看不见」?(数值是图片像素,不在文本层)
  4. 三种多模态处理路线(全 VLM / 传统解析 / 分类路由)各自的成本-精度取舍是什么?为什么本课选分类路由?
  5. 为什么本课用「关键词命中」做基线评分,而不是跑真 RAG?(量化「信息有没有进库」的天花板,离线可复现)
  6. 毒文档的生成脚本为什么要随课交付、可复现?(全程 before/after 对照的锚点)
  7. (落地)kb-qa 现状的 loader 只认哪些后缀?扫描件/表格/图表在现状管线里的下场分别是什么?