1. 项目概述:多栏和水印为什么是 RAG 解析里的两座山
先说个场景。上个月我把一批技术论文和旧版产品手册塞进自建的 RAG 知识库,原以为 PDF 解析早就不是问题,结果检索结果让我怀疑人生:同一个问题的答案碎片化,有的片段左边一栏读到一半突然跳到右栏,有的片段把每页底部的“Confidential”水印当成正文喂给了向量库,最后回答里全是莫名其妙的口水话。排查了一圈,问题不是出在模型,而是出在 bbox 这一层——我当年用 PDF 解析库取文本时只看字符串,压根没想过“文字在页面哪个位置”这件事有多重要。
这个项目本质上就是一个 RAG 文档解析链路里的高发事故区:多栏排版和水印 PDF。多栏会让纯文本流式读取顺序错乱,水印会让文本内容带上大量重复噪声,两者叠加后,向量检索的召回质量会被拉低得惨不忍睹。我在这个系列的前两篇里写过 PDF 的文本抽取和表格还原,这次专门把 bbox 实战拿出来写透,用一套可复现的 Python 流程解决两类最恶心的 PDF。
适合谁看?正在搭 RAG 知识库、做文档解析管道、或者被扫描版论文和带水印的公开报告折磨过的人。这篇不会只讲理论,我会直接给出坐标体系、列检测算法、水印过滤规则、以及一套从 PDF 页面到干净文本的流水线代码。就算你之前没碰过 bbox 也没关系,读完后你自己上手改参数就能用。
1.1 一个真实场景:从论文库到检索问答
我最初接到的需求很朴素:把 2000 篇 PDF 论文和 100 多份厂商手册做成一个可问答的知识库。单栏排版的白皮书几乎没花时间,文本抽出来就能用。麻烦的是那些双栏排版论文和带官方水印的 PDF 报告。
双栏排版最常见的问题是文本流错位。很多 PDF 阅读器或解析库按“文本对象在文件里的出现顺序”抽取,但 PDF 内部对象顺序和人类阅读顺序经常不一致。你可能以为在解析输出里先看到左栏上段,实际却是左栏、右栏、左栏、右栏地交错出现。举个例子,一页双栏论文的文本被抽出来之后,如果把这段文本直接切块,第一块可能是左栏第一段的开头加右栏第一段的开头,第二块可能是左栏第一段结尾加右栏第一段结尾。嵌到一起之后,语义是碎的。
水印的问题就更隐蔽了。很多水印是文本对象,会被普通解析器当成正文;比如“Confidential”“DRAFT”或公司名,在每一页以中等字号、半透明的样式出现在页面中央或底部。抽取文本时,你会得到一份每页都重复若干次的水印词表,向量化之后它们会占据高频词位置,检索时容易命中水印而不是正文。
我当时的目标很明确:解析后的文本必须尽可能接近“人眼阅读顺序”,而且要把水印这类视觉噪声从文本流里剔除。这两件事都必须依赖 bbox。
1.2 bbox 是什么,跟 RAG 有什么关系
bbox 是 bounding box 的缩写,指文本对象在页面上的最小外接矩形。一般用四个数字表示:左上角 x、左上角 y、右下角 x、右下角 y。对 PDF 解析来说,词级 bbox 是重建阅读顺序的最重要线索;对 OCR 来说,检测框也是 bbox,只不过坐标系的单位不同。
为什么 RAG 需要它?因为文本向量化本质上只处理一维字符流,它不关心二维视觉位置。如果从 PDF 提取到的字符流顺序错了,那么后续的每个 RAG 环节都会继承错误:切块会切出语义断裂的片段,向量检索会召回乱七八糟的内容,生成回答时模型只能在坏素材上强行组织语言。换句话说,RAG 系统的天花板很大程度取决于文档解析是否保留了正确的版面顺序,而 bbox 就是修正版面顺序的锚点。
在实际项目中,我经常用 bbox 做三件事:一是多栏检测,判断一页里文本分几栏,再按视觉列重组;二是阅读顺序排序,把同一栏内按从上到下排序,栏之间按从左到右拼接;三是水印过滤,识别那些位置重复、颜色异常、透明度高、字号特殊的干扰文本并剔除。下一节开始,我会把 PDF 坐标体系和版面分析的基本概念讲清楚,然后再给实操代码。
2. 核心概念拆解:坐标、版面分析与阅读顺序
很多人用 PyMuPDF 或 pdfplumber 时,直接调page.get_text()拿字符串,认为这就完成了解析。这其实是把 PDF 里的很多信息丢掉。PDF 本身是一个二维版面模型,页面上的每个字符都有精确坐标,只是大多数文本抽取接口默认按内部对象顺序输出,而这个顺序跟人类阅读顺序不完全一致。
2.1 PDF 坐标体系:从页面坐标到词级 bbox
PDF 的坐标原点通常在页面左下角,x 轴向右,y 轴向上,单位是点(point,1 point = 1/72 英寸)。但 PyMuPDF 返回坐标时用的是左上角为原点的转换,也就是说 y 轴向下增长,0 在页面顶部,数值越大越靠下。和 PDF 原生坐标相反。pdfplumber 则保留 PDF 原生坐标,y 轴向上,0 在页面底部。
这个差异非常坑。我第一次把 PyMuPDF 和 pdfplumber 的坐标混用,做列投影时怎么都不对,后来发现两个库的 y 方向是相反的。建议在一个项目里固定只用一个坐标体系,或者统一转换成同一参考系。我自己习惯用 PyMuPDF 的坐标,因为后续要画框调试时,左上角原点跟常见的图像坐标一致,跟 OpenCV 也能无缝对接。
拿到词级 bbox 之后,每个词的形态是这样的:
# PyMuPDF 返回的 word 结构 # (x0, y0, x1, y1, word, block_no, line_no, word_no)坐标的单位是点,不是像素。如果 PDF 页面宽 612 点,对应 letter 纸的宽度;如果页面要用到 150 DPI 渲染成图片,需要做一个简单换算:像素 = 点数 * DPI / 72。这个换算是 OCR 场景的常见操作,也是后续 bbox 过滤和图像渲染对齐的前提。
2.2 版面分析:启发式规则和版面模型的取舍
版面分析不外乎两条路:一条是用规则启发式,一条是训练或调用版面模型。规则启发式基于坐标、字号、颜色、缩进这些浅层特征做判断;版面模型则是用深度学习模型检测段落、标题、表格、图片区域,并输出结构化坐标。
对多栏问题,我一开始试过基于深度学习的版面模型,比如开源版面分析工具,效果不错,但部署成本和推理延迟都比较高。在实际 RAG 管道里,每天解析几百上千页文件,离线批量解析还能忍,但如果是实时文档问答,每一页都走模型推理会拖慢链路。更简单的办法是先上规则:判断页内是否有明显的列结构,如果有,就用列投影来做重排;如果遇到特殊页面(代码块、表格、复杂示意图),再降级到版面模型或人工处理。
我的结论是:不要一开始就上模型。先用规则跑通 80% 的常规 PDF,剩下 20% 的特殊页面单独标记,定期抽检人工修复,这样工程上更稳妥。规则做不了的事,再交给模型补位。
2.3 多栏检测的底层逻辑
多栏检测的核心思路是“投影”。把所有文本的最小 y 坐标映射到 x 轴,统计不同 x 区间里出现的文本密度。如果是单栏,x 轴上的文字分布几乎是连续一段;如果是双栏,文字分布会出现中间明显空洞——那个空洞就是栏间距。
实际操作时,我会按词级 bbox 的中线(x0 + x1) / 2做投影,也可以按行级单位做。统计每个 x 区间内的字符数或词数,再用一个阈值判断空洞是否足够宽,足够宽就切分栏。这个算法简单有效,尤其适用于规则排版的论文页。
还有一种老方法是找页面里所有文字块的左上角 x 坐标,做聚类,比如 KMeans 或类似 DBSCAN 的密度聚类。聚类数大于 1 且块之间水平分布明显分离时,就判定为多栏。但聚类对噪声敏感,而且需要预设类别数。相比之下,投影直方图的解释更强,也更容易调试,所以我先用投影,不行再上聚类。
3. 工具选型与环境准备
工具选型决定了后边所有代码的写法,所以我单独开一节讲。我做 PDF 解析这几年,试过一堆库,最后在 RAG 场景里最顺手的组合是 PyMuPDF 负责版面解析,pdfplumber 做表格和高精度文本时需要时备用,PaddleOCR 负责扫描件 OCR。
3.1 工具链对比:PyMuPDF、pdfplumber、PaddleOCR
PyMuPDF 的优势是快,安装包里自带了 MuPDF 渲染引擎,能同时拿文本、坐标、字体信息,还能直接渲染页面成图片。用page.get_text("words")一行就能拿到词级 bbox,性能很强,适合批量解析。
pdfplumber 的优势是表格抽取精细,能把线框表格还原成行列结构。但它基于 pdfminer,速度偏慢,遇到几百页的大文件容易磨叽。我只在需要处理表格时调用,日常文本抽取不用它。
PaddleOCR 是目前我用下来中文扫描件识别准确率最稳的,而且能输出每个文本检测框的坐标和置信度。缺点是依赖 PaddlePaddle 环境,安装体积大。如果不想引入重依赖,也可以拿 Tesseract 顶一顶,但中文识别效果要差一些,尤其是弯曲、低分辨率的文字。
工具对比我用一个表说清楚:
| 工具 | 定位 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| PyMuPDF | 文本与版面 | 快、能拿词级 bbox、渲染页面 | 表格还原弱 | 原生 PDF、批量解析 |
| pdfplumber | 精细表格 | 表格结构化好、坐标完整 | 慢 | 表格密集的 PDF |
| PaddleOCR | OCR | 中文好、出检测框 | 依赖大、重 | 扫描件、带水印图片文本 |
| Tesseract | OCR 备用 | 轻量 | 中文略差 | 服务器环境受限时 |
3.2 环境安装与一个小坑
我先给一套最小依赖,Python 3.9 以上都行:
pip install pymupdf pip install paddlepaddle paddleocr pip install pdfplumber pip install opencv-python numpy装 PaddleOCR 的时候有个容易踩的坑:新版本默认使用paddle后端,如果本机 Python 刚好是 3.12 或者缺少某些编译库,可能会报一堆动态链接错误。我当时的解决方法是先装 CPU 版 PaddlePaddle,再装 paddleocr,并且把模型下载路径设置到有写权限的目录,避免在服务器上用 root 跑下载到/root/.paddleocr。这一步看似无关紧要,但卡住的人不在少数,尤其是公司内网环境,模型下载失败会直接中断整个解析流程。
装完之后可以做一次冒烟测试,拿一页 PDF 试提取词级 bbox,确认坐标不是全零、文字不是乱码,再进下一步。
4. 实战:用 bbox 完成多栏 PDF 的重排
现在进入正文戏。目标是把双栏论文页面重排成正确的从上到下、从左到右的阅读顺序。我用一个简化但真实可跑的流程:先提取词级 bbox,再做列检测,最后按列和行重排。
4.1 提取词级 bbox:第一手坐标长什么样
用 PyMuPDF 提取每页的词级信息非常简单:
import fitz def extract_words(page): return page.get_text("words") def draw_debug_boxes(page, words, out_path): for w in words: x0, y0, x1, y1 = w[:4] page.draw_rect(fitz.Rect(x0, y0, x1, y1), color=(1, 0, 0), width=0.5) page.get_pixmap(dpi=100).save(out_path)get_text("words")返回的每条记录包含 x0、y0、x1、y1、word、block_no、line_no、word_no。注意这里的 word 是按空白拆分的结果,中文没有问题,但英文里的连字符和跨行断词需要额外处理。
拿到坐标之后,我建议立刻做一步调试:把词级 bbox 画回页面,渲染成 PNG。这一步的重要性怎么强调都不过分。有一次我发现某页文本抽出来顺序乱,画框之后才发现那页其实是三栏,但中间栏是极窄的边注。不画框,靠肉眼很难看出这类问题。
4.2 列检测算法:投影直方图的思路
列检测我采用“x 坐标投影”的算法。核心是统计每一列 x 区间内的字符密度,找到明显空洞作为栏分隔线。实现时不需要真的生成图片,直接用词级 bbox 统计即可:
def detect_columns(words, page_width, gap_threshold=30): # 取每个词的横向中线 centers = [(x0 + x1) / 2 for x0, y0, x1, y1, *_ in words] # 按中线排序后找相邻中心之间的空隙 centers.sort() gaps = [] for i in range(1, len(centers)): gap = centers[i] - centers[i - 1] if gap > gap_threshold: gaps.append((centers[i - 1], centers[i], gap)) if len(gaps) >= 2: midpoints = [(g[0] + g[1]) / 2 for g in gaps] return midpoints # 列分隔点 return []这个简化版适合大多数双栏页面。它的逻辑是:双栏论文的两栏之间通常有一个 20 到 40 点的空白沟,足够大就能被检测到。如果文字字号大、行间距宽,阈值可以调大一些;如果表格密集或页边距混乱,阈值调小,但也要接受误报的可能。
实际生产里我不会只靠这一个算法,而是结合几个特征:栏间空洞宽度、左右两栏的文字宽度是否接近、同一 y 行内是否出现左右两栏的文字同时存在。只有同时满足这些条件,才判定为多栏。这样能避免把“标题跨栏”或“表格列间空白”误判成真正的栏。
4.3 Z 字排序与文本拼接
检测出栏位之后,接下来就是把文本按视觉顺序拼接。我用两个阶段:先给每个词分配栏号,再在栏内按 y 坐标排序。最后按左栏到右栏的顺序输出。
def reassign_columns(words, column_split_x): new_words = [] for w in words: x0, y0, x1, y1, word = w[:5] cx = (x0 + x1) / 2 col = 0 for split in column_split_x: if cx > split: col += 1 else: break new_words.append((col, y0, w)) new_words.sort(key=lambda r: (r[0], r[1])) return new_words def build_text(reordered_words): lines = [] last_col = None last_y = None line_parts = [] for col, y0, w in reordered_words: if (last_col is not None and col != last_col) or (last_y is not None and abs(y0 - last_y) > 5): lines.append(" ".join(line_parts)) line_parts = [] line_parts.append(w[4]) last_col = col last_y = y0 lines.append(" ".join(line_parts)) return "\n".join(lines)这个流程有几个细节要注意。第一,判断同一行时用的 y 坐标不能只比 y0,因为同一行文字的字号不同,基线位置会有偏差,最好允许 5 到 8 点的误差。第二,词之间的空格偶尔会丢,join的时候最好保留;但中文不需要空格,英文必须保留,所以最好根据目标语言调整拼接规则。第三,遇到跨栏的大标题时,标题 x 方向跨了整页,列检测后会把它分配到第一栏,这没问题,但需要把它单独识别为“全栏标题”,否则左栏正文会跟标题混在一起。我的经验是:如果一个文本行的宽度超过页面宽度的 80%,就先提取出来当作独立段落,不参与栏内排序。
排序完成后,文本流基本接近人类阅读顺序。再用这个文本流去做切块和向量化,RAG 的召回质量会明显好转。
5. 实战:水印 PDF 的过滤
水印比多栏更阴险。因为很多水印在页面上是半透明的,视觉上不影响阅读,但一旦被抽成文本,就成了污染源。下面按水印的形态和处理方式拆开讲。
5.1 水印在 PDF 对象层的四种形态
第一种是文本型水印:直接在页面上放置一串重复的文本对象,颜色浅、字号不定、角度可能是斜的。PyMuPDF 的get_text("dict")能返回每个 span 的文本、字体、字号、颜色和透明度。第二种是图形型水印:用矢量图形拼出来的公司 Logo 或背景底纹,文本抽取拿不到,但渲染时能看到。第三种是图片型水印:水印作为半透明 PNG 或 JPEG 图像叠加在页面上,文本抽取拿不到。第四种是复合水印:既包含文本又包含图片,多用于公文和官方声明。
对 RAG 来说,真正会破坏文本流的是第一种和第四种里的文本部分。图形和图片水印如果不影响 OCR,就不用太操心;如果影响,就需要在图像层做去水印。
5.2 用 bbox 加样式特征过滤水印
针对文本型水印,我总结了一套“四看”规则。一看重复性:水印文本通常每一页都在同一个位置附近出现,或者同一页内多次出现;二看位置:比如页面底部、页面正中央、斜穿页面;三看颜色:水印一般是非黑色或者低对比度颜色;四看透明度:PyMuPDF 在dict模式下能拿到 span 的opacity,半透明的对象很可疑。
举个例子,某 PDF 每页底部都有“DRAFT”字样,坐标基本在(page_width*0.3, page_height*0.92)附近,颜色是灰色,字号固定。只要把全页词按 bbox 聚类,出现频率高且位置集中,就能自动识别。
我用一段代码说明过滤思路:
def is_watermark_span(span, page_width, page_height): bbox = span["bbox"] if bbox[3] > page_height * 0.9: # 底部区域 if span.get("opacity", 1) < 0.5: return True if span["color"] not in [(0, 0, 0), (1, 0, 0)]: return True # 页面中央的大范围重复文字 return False这里color是 sRGB 颜色值,PyMuPDF 默认以 8 位整数表示。黑色是 0,白色是 16777215。水印常见的灰色大约在 8421504 附近。你不需要死记这些值,关键是建立“正文默认黑色且不透光”的基线,凡是偏离基线的,都放到待剔除列表里。
我踩过一个坑:有些水印是“透明墨粉”处理过的,实际字体颜色是黑色,但透明度很低;也有一些正文段落用了灰色高亮。所以“四看”规则一定要组合判断,单一规则容易误杀。比如正文里有一条灰色注释,位置不在重复区域,那就不该删。
5.3 扫描件里的水印:OCR 和图片双重处理
如果 PDF 是扫描件,文本抽取拿不到内容,只能走 OCR。水印会直接出现在图像上,OCR 识别的时候可能会把水印文字一起识别出来。更麻烦的是水印文字覆盖了正文区域,比如“官方文件”斜跨整页,导致正文被遮挡,OCR 结果里既有水印又有残缺的正文。
我的处理方法是两步走。第一步,先用 PaddleOCR 的检测框输出所有文本区域,得到每个文本框的 bbox 和置信度。第二步,根据 bbox 的位置和面积判断哪些框是水印。例如水印通常面积大、倾斜角明显、每页重复出现、识别置信度忽高忽低;正文框则位置规整、字数均衡。
不断言 OCR 能精准“去掉”水印,但至少能在文本流里把水印文本过滤掉。图像层的水印,我一般不做像素修复,因为容易把正文也修糊;只在极少数必须展示截图的场景里,我才会用 OpenCV 的 inpaint 做局部修复。
给一个 PaddleOCR 快速调用的示例:
from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=True, lang="ch", show_log=False, ocr_version="PP-OCRv4" ) result = ocr.ocr("page.png", cls=True) # result[0] 里每一项是 [box, (text, score)] # box 是四个角点坐标,可以算 bbox拿到检测框之后,把每页出现的检测框按坐标重叠度和文本内容聚类。如果同一个文本(比如“水印”或“机密”)在超过 90% 的页面里反复出现,并且位置相近,我就把它加入全局过滤词表。这个“全局重复检测”是扫描件水印过滤里最实用的一招,比单纯看位置可靠得多。
6. 完整实现:从 PDF 到可入库文本的流水线
前面几节把关键技术点拆开了,这一节我给出一个能直接抄的流水线整体结构。它把多栏重排、水印过滤、扫描件 OCR 兜底整合在一起,最终输出干净的文本文件和对应的页面信息,方便下游切片。
6.1 流水线结构与顺序
我设定的处理顺序是:先判断页面是原生文本还是扫描件,再分别走两条路。原生文本页走“提取词级 bbox -> 水印过滤 -> 列检测 -> 排序拼文本”;扫描件页走“渲染图片 -> Ocr -> 水印过滤 -> 列检测 -> 排序拼文本”。最后统一输出。
这个顺序是有讲究的。水印过滤必须放在列检测之前,因为水印通常横跨多栏或者独立于栏结构,不先删掉会干扰列投影。列检测需要干净的文字分布,否则水印的“中央大字”会让投影直方图出现伪栏位。
流程图我不用画了,直接看代码结构更实在。
6.2 核心代码:从页面到干净文本
import fitz from paddleocr import PaddleOCR class PDFParser: def __init__(self): self.ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False, ocr_version="PP-OCRv4") def parse_page(self, page): text = page.get_text("words") if not text: page_pix = page.get_pixmap(dpi=150) page_pix.save("temp_page.png") ocr_data = self.ocr.ocr("temp_page.png", cls=True) words = self._ocr_to_words(ocr_data) else: words = self._words_to_struct(text) words = self._filter_watermark(words, page) columns = self._detect_columns(words, page.rect.width) reordered = self._sort_by_columns(words, columns) return self._to_text(reordered)这段代码故意写得精简,重点在函数之间的衔接。每一个函数就是你前面看到的核心逻辑。如果你想一次性跑通,还需要补_ocr_to_words和_words_to_struct两个转换函数,把 OCR 输出或 PyMuPDF 输出统一成(x0, y0, x1, y1, text, page_id)的结构。统一结构是整条流水线最重要的设计决定,它让两种数据源可以共用后面的水印过滤和列检测逻辑。
6.3 质量评估:怎么知道自己做对了
解析完不等于解析对,必须有一个可以量化的评估办法。我常用三个指标。
第一是“阅读顺序还原率”:随机抽 20 页,人工看重排后的文本流,统计连续语义片段正确拼接的数量占比。第二是“水印残留率”:检测最终文本里是否还包含已知水印词,比如“机密”“DRAFT”等,出现次数除以总页数,理想情况是接近 0。第三是“切块语义完整性”:把重排文本切块后,让检索系统跑一组预置问题,人工判断召回内容里是否出现“一句话被硬生生截断”的块。
我还会做一个更粗暴的端到端测试:用同一个文档解析结果去做 RAG 问答,分别对比解析前和解析后的答案质量。如果修复解析后,答案的引用片段明显更完整,就说明 bbox 这一层的功夫没有白花。这个测试看起来不严谨,但对业务方最有说服力。
7. 常见问题与排查实录
这节把我在实战里遇到的高频问题整理成速查,每个问题都附上我当时的排查过程和最终解决方案。
7.1 表格、首行缩进和浮动块的误判
表格是多栏检测的头号干扰项。一个三列宽的表可能会被误判成三栏,但表格内部横向关系比列更重要。我的处理办法是:在列检测之前先用 pdfplumber 或者 PyMuPDF 的find_tables()识别表格区域,把表格 bbox 整体抠出来,不参与列投影。等到文本重排完成之后,再把表格内容按单独的结构追加到文本流里。这样既不会破坏正文的栏位逻辑,也不会丢失表格语义。
首行缩进也很坑。中文论文常见首行缩进两个字符,但 bbox 计算的是词框,缩进会让每一行的开头 x 坐标参差不齐。如果我的行合并逻辑只看 x0,就会把缩进造成的小位移误当成另起一行。解决办法是放宽行合并阈值,只看 y 坐标是否接近,不要求 x0 严格单调。这点在build_text函数里已经体现。
7.2 中文与英文混排中的 bbox 差异
中英文混排是另一个容易翻车的场景。中文词检测依赖分词结果,PyMuPDF 的get_text("words")对中文往往会把整句当作一个“词”,这会导致 bbox 很大,造成列投影和行合并的偏差。
我刚碰到这个问题时也懵了:为什么有些页面重排后列检测总是多出一栏?查了半天才发现,某段中文长句的 bbox 横跨到了旁边的栏,把栏间空洞堵住了。解决办法有两个:一是用get_text("dict")按 span 提取,因为 span 是按字体和样式切分的,中文 span 通常更合理;二是对中文文本先不做词级处理,直接按行级 bbox 走列检测。实践下来,第二种更稳,毕竟对中文来说,行才是排版的基本单元。
7.3 性能问题与缓存经验
2000 篇 PDF 动辄几千页,每页都跑 OCR 的话,时间成本很吓人。我一开始没做缓存,结果同一批文件反复解析了好几遍,CPU 全被吃掉。后来我加了一层缓存:以 PDF 文件的哈希值和页码为 key,把解析出来的 clean text 存成 JSON。第二次解析时直接读取缓存,只有当 PDF 文件变化时才重新跑。
另外,OCR 对低 DPI 图片的识别率会骤降,但也别盲目上 300 DPI,因为渲染时间和显存占用会成倍增长。我实测 150 DPI 对大多数扫描件已经够用,只有字体小于小五号或者图片模糊的才需要提升到 200 到 300。处理时按页释放对象,避免内存被几十张大图撑爆。
8. 实操体会与扩展思路
我在实际项目里最深的体会是:RAG 的文档解析质量,不取决于用了多强的模型,而是取决于有没有认真对待 bbox 这类基础信息。很多团队把大量精力花在调 embedding 模型上,却忽略了文本源头已经乱成一团。embedding 再强,也救不了顺序错乱的字符流。
关于后续扩展,我认为这套 bbox 思路还能往几个方向深挖。第一,结合版面模型把标题层级抽出来,给切块提供结构化上下文,让 RAG 的召回具备更细的段落边界。第二,把水印过滤和“图像敏感区域”检测结合,在解析时直接屏蔽含审核敏感信息的区域,减轻下游合规压力。第三,把多栏重排和表格识别整合成流水线算子,封装成服务,每次解析结果都输出一份带 bbox 的 JSON 文档,方便调试和回放。
最后再分享一个小技巧:很多问题页面不要急着加复杂模型,先用 bbox 画框把每页的真实版面看一遍。页面上一个不起眼的边注、一个跨栏表格、一段被手动旋转的文字,都会让规则失效。你用视觉排查一次,比读十次文档都管用。先把绝大多数常规页面稳定处理好,再把异常页面列入慢速兜底通道,这套工程思路比一上来就追求“万能解析”要务实得多。