万工-规范速查(规范库 → 可查证 Skill)
把一堆 PDF 变成 agent 能精确引用的资产,而不是塞进 prompt 的摘要。
第一性原理
法规/标准类内容的价值密度不在"讲了什么",而在"哪本第几页讲了什么,原文怎么写的"。
| 抄进 skill | 做成可查证 skill | |
|---|---|---|
| 版本更新 | 全部失效,且会误导 | 改版后重跑取证即可 |
| 编造风险 | 高(复述即失真) | 零(原文直出) |
| 体积 | 膨胀,挤占上下文 | 恒定 |
| 法律效力 | 无 | 可抗辩 |
判断标准:内容里的数字/措辞如果错了会有实际后果(法律责任、工程事故、合规驳回),就走"可查证"路线,不抄内容。
流程
第 0 步:语料体检(必做,别跳)
import pymupdf, os
for fn in pdfs:
doc = pymupdf.open(path)
n = doc.page_count
# 抽样前 12 页判断文字层
chars = sum(len((doc.load_page(i).get_text("text") or "").strip())
for i in range(min(12, n)))
has_text = chars / min(12, n) > 80
toc = doc.get_toc() # PDF 书签,最准
输出一张表:文件名 / 页数 / 体积 / 是否有文字层 / 是否有书签。
决策点:
- 有书签 → 直接用,页码精确
- 有文字层 → 可直抽全文,毫秒级
- 纯扫描 → 必须先确认有无 OCR 能力。没有就装:
python -m venv <venv>; <venv>/Scripts/pip install rapidocr-onnxruntime pymupdf
约 4 秒/页,中文识别质量够用。
时间预算:OCR 一页 4 秒 × 每本 10~16 页(只做目次+总则)× 扫描件本数。55 本规模约 25 分钟,放后台跑。
第 1 步:按坐标重建目次(关键技术)
中文国标目次的编号、标题、页码被排在不同行,用单行正则解析失效率约 80%:
1
总则…………………………………………………( 1 )
2
基本规定
…………………………………………………( 3 )
解法:按 y 坐标聚类成行,行内按 x 排序。
def rows_from_boxes(boxes, tol=6):
"""boxes: [(x0, y_center, text)]"""
boxes = sorted(boxes, key=lambda b: b[1])
rows, cur, cy = [], [], None
for b in boxes:
if cy is None or abs(b[1] - cy) <= tol:
cur.append(b); cy = sum(x[1] for x in cur) / len(cur)
else:
rows.append(sorted(cur, key=lambda x: x[0])); cur, cy = [b], b[1]
if cur: rows.append(sorted(cur, key=lambda x: x[0]))
return rows
- 文字层:
page.get_text("dict")取 span 的 bbox - 扫描件:OCR 返回 (box, text, score),取 box 的 x_min 与 y 均值
解析率可从 8% 提到 82%。
再对每行:行尾最后一个数字 token 是页码(注意全角数字 0123 要先转半角),前面拼起来是标题。
第 2 步:印刷页码 → PDF 页码
目次里的页码是印刷页码,PDF 打开看到的不是同一个数。必须校准:
# 严格正则,防止 "11.0.10" 被误判为 "1.0.1"
pat = re.compile(r"(?<![\d\..])1\s*[..]\s*0\s*[..]\s*1(?![\d])")
# 从目次最后一页往后探 10 页,找正文 1.0.1 出现的 PDF 页
offset = body_start - 1 # 印刷页 1 = 正文起始
pdf_page = printed_page + offset
三个坑:
1. 目次后面可能还有英文目次 / Contents,所以不能简单用 max(目次页)+1,要往后探。
2. 正则不加前后断言会把「11.0.10」当成「1.0.1」,offset 直接错十几页。
3. 验收方法:抽 3-5 个章节页码,OCR/抽原文核对。残余 1-2 页漂移可接受,作为路由锚点够用(取证时连取前后各一页)。
第 3 步:主题词层(决定好不好用)
用户查的是「钢结构防火涂料」「消防电梯前室」,目录里可能根本没有这个连续字符串。必须加一层主题词:
"topics": "钢结构 防火保护 耐火极限 防火涂料 厚型 薄型 等效热阻 截面系数"
匹配时按 token 匹配并统计命中数,不要拼成整串做子串匹配(norm() 吃掉空格后「消防电梯 前室」会变成「消防电梯前室」,反而匹配不上)。
再给最核心的 1-2 本加 hints:主题 → 精确页码,路由时能直接给锚点。
第 4 步:取证脚本
一个 verify.py,五个子命令足够:
| 命令 | 作用 |
|---|---|
list |
全表:规范号 / 页数 / 取证方式 / 域 |
route <关键词> |
主题路由 → 哪本哪页(按效力层级加权) |
get <规范> <起> [止] [--ocr] |
抽原文;文字层直抽,扫描件自动 OCR 并缓存 |
search <关键词> |
有文字层的部分做全文检索 |
toc <规范> |
章节目录 |
要点:
- OCR 结果必须持久化缓存(ocr_cache.json),否则每次重复烧时间
- 缓存 key 用 文件名 + PDF页码
- 输出必须带 源:文件名 第 X-Y 页,方便上层拼引用块
第 5 步:SKILL.md 里写死红线
## 第一性原理:不背条文
1. 路由:问题 → 哪本规范哪一页
2. 取证:跑脚本抽原文
3. 引用:规范号 + 条文号 + PDF页码 + 原文
任何一次给出具体数值之前,必须已经跑过取证。
还要写:效力层级与冲突规则、能力边界声明、输出引用格式。
第 6 步:压力测试
至少包含:诱饵题(诱导凭记忆作答)、超出范围题、高频混淆题、边界题。
交付清单
<skill>/
├── SKILL.md 机制 + 层级 + 工作流 + 取证操作 + 边界
├── test-prompts.json 压力测试
├── references/
│ ├── _index.json 机器可读索引(路径/页码/目录/主题词/锚点)
│ ├── 01-路由表.md 人可读的全表
│ └── 02-xx.md 按主题分册
└── scripts/
├── verify.py
└── .cache/ OCR 缓存 + 文字层全文
踩过的坑(清单)
| 坑 | 症状 | 解法 |
|---|---|---|
| 单行正则解析目次 | 解析率 8% | 按 y 坐标聚类重建行 |
| 正则匹配 1.0.1 | 匹配到 "11.0.10",offset 错 10+ 页 | 加 (?<![\d.]) (?![\d]) 断言 |
| 目次后还有英文目录 | offset 少 2 页 | 目次后向后探 10 页找正文 |
| 超大页面 OCR | DecompressionBombError(3 亿像素) |
Image.MAX_IMAGE_PIXELS = None + 按面积自适应降 dpi(上限 3000 万) |
| norm() 吃掉空格 | 主题词匹配失效 | 先 split 再 norm,token 级匹配 |
| 效力层级高的规范空命中也上榜 | 路由结果污染 | 加权只在已命中时生效 |
doc.page_count 在 close() 后调用 |
ValueError: document closed | 先把 page_count 存变量 |
| 先识别目录页再解析 | 把图集扉页/说明页误当目录,解析结果全 0 | 改为全页扫描 + 每页有效条目数 ≥5 才算目录页 |
| 切列阈值用 PDF 点算 | 双栏目录切不开(文字层是 PDF 点,OCR 是图像像素,单位不同) | 阈值按本页实际 x 跨度 × 比例计算,不传 page_w |
| 「3-31」章-页编号 | to_num("3-31") → 331,页码全错 |
正则 ^\d{1,2}[-—]\d{1,3}$ 单独识别为 label,再按目录顺序线性估算 PDF 页 |
PDF 书签是 img011 噪声 |
目录变成 img011/img012… | 先过滤 ^(img|image|page|p)[_\-\s]*\d+$ 再采用 |
| 路由被高效力条目抢排序 | 查"图示"返回通用规范,查"防火分区"返回旧专业标准 | 分层加权:效力最高层 +12;查询含"图示/图集/做法/详图"时图集类额外 +16 |
| 分卷 PDF 重复入库 | 同一文档的原版与压缩版各建一条 | 先按文件名归一化去重,只保留一份 |
| 多栏目录:行尾数字取页码 | 标题与页次分处不同栏,解析全空 | 改「标题 × 最近右侧页码 token」配对:行内从左到右扫描,遇到页码类 token 就与左侧累积的标题配对 |
| 「3-31」章-页编号误判 | 18J811-1 里的 11-1 被当成章-页 |
正则加断言 (?<![\dA-Za-z])(\d{1,2})[-—-](\d{1,3})(?![\d]) |
| 页脚裁切过扁 | 底部整条(2921×290)OCR 检测失败 | 控制宽高比,裁右下角区域(约 4:1),1.8s/页 |
| 扫描件为校准偏移跑全文 OCR | 单本烧 40~120 页 OCR,几小时卡死 | 扫描件只 OCR 前 10 页取目录;偏移直接用印刷页并标 est,不跑全文校准(取证时 ±几页浏览即可) |
| 无关国标被 OCR | 扫描件分支对语料里无关的国标规范也跑全页 OCR | 非核心/rank 0 条目跳过 OCR,只列标题占位 |
| 企业标准在 docx/xlsx | 技术标准、评审表无 PDF,纯文本层索引漏掉 | python-docx 抽 heading 段落(记段落序号 i)、openpyxl 抽 sheet;get docx 按 i 抽段、xlsx 按表名抽 |
doc 的 pages 用标题计数 |
取证锚点下溢(get X 327 326) |
doc 的 pages 取「最大段落序号 + 缓冲」,不用 len(secs)(标题数 < 段落号) |
| 企业标准无权威层级 | 总部/城市/厂商图册混排,路由乱 | rank:国标>总部技术标准>大区>城市技术标准>城市图集>做法参考(审核/缺陷/评审)>厂商图册;加权 总部+12、城市+属地优先、做法/图集+16 |
| 长短语消防查询无命中 | 应急照明疏散指示 不匹配 toc 应急照明及疏散指示系统 |
fire 模式加兜底:查询含条目的 fire_kw 即命中(子串匹配) |
| 判据 skill 与做法 skill 互污染 | 消防企业做法塞进规范 skill,或反过来 | 分离两 skill,各自 SKILL.md 写双向指针;做法 skill 建 fire 交叉入口,声明「企业标准非判据,阈值回判据 skill」 |
| 同语料多视角派生 | 同一批资料既要查做法又要查审查,重新蒸馏浪费且易漂移 | fork verify.py 只反转 rank 基础权重(审查 skill 把 rank 7 审核/缺陷/评审置顶、城市差异 rank5/6 优先、强条/消防必现);_codes.json 完全复用,不重新蒸馏;缺陷库(QXK)是经验提示不是判据,审查 skill 须声明"判据回规范" |
章-页编号 → PDF 页
图集常用「章-页」(3-31)而非连续页码,目录解析出来无法直接跳转。做法:
- 裁页面右下角(x 55%~100%、y 84%~100%)OCR,读出该页的章-页编号
- 每 4 页采样一点,转成
章*1000+页的排序键,过滤成单调序列 - 目录里的每个标签,在单调序列上插值定位 PDF 页
采样 + 插值即可把"估算页"变成精确页;276 页的图集约 70 次 OCR(2 分钟)。
分卷 PDF
大部头常被切成 _1-150.pdf、_151-234.pdf。做法:
# 建索引时
parts = [{"rel": rel, "offset": 累计页数, "pages": n}, ...] # 全局连续编号
# 取证时
def part_of(code, pno):
for p in code["parts"]:
if p["offset"] < pno <= p["offset"] + p["pages"]:
return p["rel"], pno - p["offset"]
用户看到的是连续页码,脚本内部定位到具体分卷。输出里顺带打印分卷构成,方便人工核对。
去重:同一文档可能同时存在原版、压缩版、单文件版。先按标题归一化比对,只保留一份(优先有文字层/体积适中的)。
适用信号
该用:国家标准/行业规范汇编 · 法规条例合集 · 企业内部技术标准 · 产品手册库 · 图集 · 任何"数字错了要出事"的参考资料
不该用:单本书的方法论(用 aidu-distill)· 小说散文 · 需要逐字精读的短文档 · 内容本身没有引用价值