news 2026/10/5 5:39:19

RAG翻车元凶在PDF解析:bbox一招解决多栏与水印难题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG翻车元凶在PDF解析:bbox一招解决多栏与水印难题

做 RAG 有一段时间后你会发现,真正拦住你的往往不是用什么模型、怎么调 prompt,而是最不起眼的文档解析。上周我处理一批多栏排版的 PDF 时,文本抽取结果直接把“引言”里的一句话接到了“相关工作”的引用上,向量化之后怎么改 chunk 都没用;另一批带水印的合同 PDF,检索出来的片段里频繁混着“仅供内部使用”和公司名称,回答质量肉眼可见地崩。这两类问题对应 RAG 文档解析里最常见的两个场景:多栏排版与水印 PDF,而解决它们的共同工具就是 bbox。

如果你刚好在搭知识库、做检索增强生成,或者已经在用本地模型跑 RAG 但总被 PDF 解析效果折磨,这篇实战笔记应该能帮上忙。我不会讲太多模型层的东西,重点放在解析层:怎么用 bbox(bounding box,边界框)把多栏文本按真实阅读顺序还原,怎么识别并清掉水印文本对向量化的干扰,以及这中间一堆容易踩坑的细节。

1. 这次要解决的问题:两个让 RAG 翻车的经典场景

1.1 多栏排版为什么是文本抽取的“秩序杀手”

绝大多数 PDF 解析库在抽取文本时,默认按页面上对象的物理位置从上到下、从左到右排列。这个逻辑在单栏文档里基本正确,可一旦遇到双栏甚至三栏论文、报纸、杂志排版,问题就来了。

我拿一个典型的 IEEE 双栏论文页举例。左栏第一行是“This paper proposes a method for...”,右栏第一行是“transfer learning has shown great performance in...”。如果按默认顺序读取,出来的文本是“This paper proposes a method for... transfer learning has shown great performance in...”,两个完全不相干的句子被硬生生拼在一起。再往下读,左栏第二行和右栏第二行又交叉,整页文本变成一条左右摇摆的乱序字符串。

对 RAG 来说这几乎是灾难级的。文本切块(chunking)是基于顺序切开的,乱序文本切出来的块语义是断裂的;向量化之后进检索库,用户问“某某方法的应用场景”,召回的可能是一段夹着右栏无关内容的片段;生成阶段更麻烦,模型拿到的上下文本来就乱,回答自然也跟着乱。更隐蔽的是,这种问题不会在单独的某一行暴雷,而是让一批 chunk 的平均质量整体下降,肉眼审查很难全部发现。

1.2 水印是 RAG 的“隐形噪音”

水印的问题则更“日常”。企业内部文档、合同、标书,很多 PDF 都会盖一个“仅供内部使用”“机密”“副本”之类的文字水印,有的还是多行多列铺满整页。这种水印在阅读时人眼会自动忽略,但解析引擎不这么干,它会把水印文本当成普通文本抽出来,混进正文。

后果分两层。第一层是向量化污染:水印文字会作为独立文本块进入 embedding 模型,生成的高维向量夹杂着大量重复、低信息量的“机密”“内部”等词,这些向量会和正文向量混在一起,干扰相似度计算。第二层是生成污染:假设用户问“这份合同的有效期是多久”,检索到的 chunk 里如果刚好被水印文本穿插,模型生成的回答可能莫名其妙带上“仅供内部使用”这类词汇,看起来非常业余。

还有一个很少有人提的点:水印会让文本长度膨胀。一份 20 页的 PDF,如果每页都有 6 行水印,抽出来的纯文本里会有 120 行无效内容,占字符比例可能达到 10% 以上。chunk 数量变多、向量库变大、检索噪声变高,成本却一点没省。

1.3 bbox 才是这局游戏的“支点”

解决上面两个问题,核心不是换个更聪明的模型,而是用 bbox 把文本的空间位置信息充分利用起来。

bbox 是一组坐标,用来表示页面元素占据的矩形区域。在 PDF 解析语境里,一个文本行、一个字符、一张图片都有自己的 bbox。pdfplumber 里的 line 对象返回的典型结构是:

  • x0:左边界坐标
  • top:上边界坐标(也叫 y0)
  • x1:右边界坐标
  • bottom:下边界坐标(也叫 y1)

这组坐标放在页面坐标系里,就相当于给页面上的每个文本块都贴了一个定位标签。多栏排版问题的本质,是解析引擎只用了“上下”的顺序,没用“左右”的聚类信息;水印问题的本质,是可以利用 bbox 的位置规律把水印从正文里区分出来。

你只要想明白这一层,后面所有逻辑都会顺起来。左边一栏文本的 x0 和右栏 x0 天然不同,按 x0 聚类就能分栏;水印文本的 bbox 在整页范围内重复、规律分布,按位置和频次就能识别。bbox 不是数据结构,它是你理解 PDF 版式的钥匙。

2. 工具选型与坐标基本功

2.1 pdfplumber:文本层的“手术刀”

这一系列实战我主要用 pdfplumber 处理文本层。选它的原因很简单:它对字符级信息的暴露足够细,而且字段命名直观。

pdfplumber 基于 PDFMiner 开发,针对每个字符(char)都能拿到 text、x0、x1、top、bottom、width、height、fontname、size、stroking_color、non_stroking_color 等属性。这些属性在做水印识别时特别有用,因为水印文本的颜色、字号、字体往往和正文有明显差异。

你可能也会问,PyMuPDF(fitz)不是更快吗?没错,PyMuPDF 在速度和渲染能力上确实更占优势,尤其是后面我要讲到的“把 PDF 页面渲染成图片做像素级分析”时,fitz 几乎是唯一选项。我的用法是两者配合:pdfplumber 负责精细的文本层分析,fitz 负责渲染和视觉定位。这不是二选一,而是组合拳。

安装依赖就四行命令:

pip install pdfplumber pymupdf pillow

注意版本,pdfplumber 0.11 之后接口比较稳定,pymupdf 建议用最新版,老版本在部分 PDF 的坐标变换上有坑。

2.2 先把坐标系踩明白

PDF 的坐标系和常见图像坐标系有一个很关键的差异:PDF 的单位是磅(pt),不是像素,1 英寸等于 72 磅。一个 A4 页面大约是 595 x 842 磅,所以你在 bbox 里看到的坐标值基本落在 0 到 900 之间,而不是几百上千的像素值。如果你做坐标映射时直接拿 PDF 坐标当像素坐标,后面渲染定位必然对不上。

坐标系原点在页面左上角,x 轴向右增大,y 轴向下增大。pdfplumber 中 top 就是 y0,bottom 就是 y1,所以 bottom 的数值一定比 top 大,千万不要像数学坐标系那样以为 y 轴向上。我在项目里见过不少同事把 top 和 bottom 弄反,最后水印区域裁剪完全错位,排查了一下午才发现是坐标方向的问题。

还有页面旋转。部分扫描 PDF 或者用某些工具生成的 PDF,page.rotation 是非 0 值(90、180、270)。pdfplumber 在解析时通常已经应用了旋转,但如果你想用 fitz 渲染同一页来做比对,最好显式检查两边的 page.rect 是否一致。如果不一致,所有 bbox 映射都会错位。

2.3 准备一份能复现的实验样张

动手之前,先准备一份适合复现的 PDF 样张。我的建议是找一份真实的双栏论文,加上一个多行多列文字水印,这样两个问题能同时验证。

如果没有现成文档,可以用 WPS 或 Word 快速生成一个双栏文档,然后在“页眉页脚/水印”里添加文字水印,比如“仅供内部测试”。注意水印要设置成多行多列铺满整页那种,不要只放一个居中的,不然 bbox 规律不够明显,复现效果打折。

样张准备好之后,先用 pdfplumber 打开,打印第一页的信息:

import pdfplumber with pdfplumber.open("sample.pdf") as pdf: page = pdf.pages[0] print(page.width, page.height) print(page.bbox) lines = page.extract_text_lines() for line in lines[:10]: print(line["x0"], line["top"], line["x1"], line["bottom"], "|", line["text"][:30])

这一步不是为了出结果,而是为了让你亲眼看到:解析库返回给我们的原始顺序就是错乱的,水印文本也会混在里面。有了这个“现场证据”,后面每一步处理你都知道是在解决什么。

3. 多栏排版实战:按 bbox 聚类还原阅读顺序

3.1 先看清从 pdfplumber 拿到的原始顺序

打开样例 PDF,跑完上面那段代码后,你会看到类似这样的输出:

68.0 102.0 250.0 120.0 | This paper proposes... 360.0 102.0 540.0 120.0 | transfer learning has... 68.0 125.0 245.0 140.0 | We evaluate our method... 360.0 125.0 530.0 140.0 | on three benchmark...

注意看 x0:第一行是 68,第二行直接跳到 360,第三行又回到 68。这说明 pdfplumber 的默认顺序是按“先来后到”的行对象存储顺序输出,并不自动按视觉阅读顺序排序。真正阅读时,人眼会先读完左栏整列,再读右栏;但机器给的是逐行交叉排列。

再往下看,如果页面顶部有标题、作者、摘要这种横跨双栏的行,它们的 x0 通常很小,x1 会延伸到页面右侧,bbox 宽度明显大于单栏文本行。这类行就是“跨栏块”,在处理排序时要特殊对待,不能简单划进任何一栏。

3.2 用加权列坐标给文本行分栏

分栏的关键是给每一行算一个“列坐标”,然后用聚类算法分成左右两组。直接取行首字符的 x0 不太稳,因为有些行首有缩进,缩进量可能让左栏某几行的 x0 和右栏某几行的 x0 接近。更稳的做法是按字符宽度做加权平均,公式很简单:

column = (sum(x0_i * width_i for 每个字符)) / sum(width_i for 每个字符)

这个加权列坐标可以理解为“这一行在水平方向上的重心”。一个文本行如果从 x=70 到 x=245,重心大概落在 150 附近;如果从 x=360 到 x=530,重心大概在 440。左右栏的重心天然分开,聚类就很干净。

我用一个简化版的一维 KMeans 来演示,避免引入额外依赖。如果你愿意装 sklearn,直接 KMeans(n_clusters=2) 效果是一样的,只是要记住输入是 2D 数组。

def kmeans_1d(coords, k=2, max_iter=100): sorted_coords = sorted(coords) centers = [sorted_coords[i * len(sorted_coords) // k] for i in range(k)] for _ in range(max_iter): clusters = [[] for _ in range(k)] for c in coords: idx = min(range(k), key=lambda i: abs(c - centers[i])) clusters[idx].append(c) new_centers = [sum(cl) / len(cl) for cl in clusters] if all(abs(a - b) < 0.5 for a, b in zip(centers, new_centers)): break centers = new_centers return centers, clusters

对整页所有非跨栏行算柱坐标,跑这个函数,你就能得到左右栏的聚类中心。之后给每一行打上“栏标签”,左栏为 0,右栏为 1。

补充一个判断跨栏行的经验阈值:如果一行 bbox 的宽度超过页面宽度的 60%,基本可以认定是跨栏块。这个 60% 是我在多篇论文上试出来的,页面宽度接近 A4 的常规双栏文档都适用,但遇到三栏或非对称版式时要做微调。

3.3 每栏内部再按 y 排序,还原阅读流

分好栏之后,就是组装阅读顺序了,这一步逻辑很直白:

  1. 取出所有跨栏块(标题、作者、摘要、页眉页脚),保留它们在页面上的原始相对顺序。
  2. 左栏所有文本行按 top 从小到大排序。
  3. 右栏所有文本行按 top 从小到大排序。
  4. 按“先跨栏块,再左栏,再右栏”的顺序拼接。

代码长这样:

import pdfplumber from statistics import median def restore_reading_order(pdf_path, page_index=0, full_width_ratio=0.6): with pdfplumber.open(pdf_path) as pdf: page = pdf.pages[page_index] lines = page.extract_text_lines() full_width = (page.width * full_width_ratio) spans = [] # 跨栏块 normal_lines = [] # 普通行 for line in lines: text = line["text"].strip() if not text: continue if line["x1"] - line["x0"] >= full_width: spans.append((line["top"], text)) else: normal_lines.append(line) # 计算加权列坐标 def col_coord(line): chars = line["chars"] total_w = sum(c.get("width", 0) for c in chars if c.get("width")) if total_w == 0: return (line["x0"] + line["x1"]) / 2 return sum(c["x0"] * c.get("width", 0) for c in chars if c.get("width")) / total_w coords = [col_coord(line) for line in normal_lines] threshold = median(coords) left = [line for line, c in zip(normal_lines, coords) if c <= threshold] right = [line for line, c in zip(normal_lines, coords) if c > threshold] left.sort(key=lambda line: line["top"]) right.sort(key=lambda line: line["top"]) spans.sort(key=lambda item: item[0]) result_lines = [] for top, text in spans: result_lines.append(text) result_lines.extend(line["text"] for line in left) result_lines.extend(line["text"] for line in right) return "\n".join(result_lines)

如果你遇到三栏甚至更多栏,可以把“阈值分割”替换成 KMeans,把 k 设成实际栏数。核心思路不变,还是先加权列坐标,再按 y 轴排序。

这里有个容易忽略的细节:跨栏块内部的顺序也要按 top 排序,尤其一页里有多个标题级别的跨栏行时,比如“2. Related Work”出现在页面上方,“3. Methodology”出现在页面下方,如果不排序,两个标题的相对位置会乱。

3.4 验证数据并优化切块策略

还原阅读顺序之后,不要急着接 RAG,先做一轮人工抽查。我习惯的做法是:随机抽 5 页,把还原后的文本和原 PDF 对照阅读,重点检查三个位置:页面首行、分栏交界处、跨栏块之后。

如果发现某一行仍然错位,先打印这一行的 x0、top、weighted_column,看是被分到了错误的栏,还是栏内排序不对。多数情况下问题出在跨栏块的判定阈值上,调一下 full_width_ratio 就能解决。

阅读顺序正确之后,切块策略也要跟着调。我的经验是:在栏与栏的交界处强制断开,不要让一个 chunk 横跨左右两栏;跨栏块(标题、摘要)单独成块。你可以把分栏坐标传到后面切块的逻辑里,凡是一段文本内部包含“右栏起点 x0”突变的位置,就自动切成新块。这样切出来的 chunk 语义更内聚,向量检索的命中率会有非常明显的提升。

4. 水印 PDF 实战:识别与清洗

4.1 水印的“指纹”:位置 + 内容 + 颜色

水印清洗比多栏复杂,因为水印可能是文本对象,也可能是图片,还可能半透明地压在正文下面。我先说文本水印的情况,这也是最常见的一种。

文本水印有三个很明显的“指纹”:

第一,内容高频重复。同一个文本字符串在单页内出现多次,或跨页反复出现。比如“仅供内部使用”在一页里出现 6 次,这基本就是水印。第二,坐标规律分布。多行多列水印的 bbox 在 y 方向上往往等差排列,在 x 方向上也是均匀铺开。第三,样式和正文有差异。水印通常字号较大或颜色较淡,字体也可能不同。

光是“内容重复”这个特征,已经能筛掉绝大多数误判。我用一个非常轻量的统计方法:

from collections import Counter def find_repeated_text(page, min_len=4, min_count=2): lines = page.extract_text_lines() texts = [line["text"].strip() for line in lines if line["text"].strip()] counter = Counter(texts) repeated = {t for t, n in counter.items() if n >= min_count and len(t) >= min_len} candidates = [ (line["x0"], line["top"], line["x1"], line["bottom"], line["text"].strip()) for line in lines if line["text"].strip() in repeated ] return candidates

注意,这里要排除页眉页脚。页眉页脚也符合“跨页重复”,但它们的坐标通常在页面顶部或底部固定位置。一个简单规则是:如果某文本在同一页的不同 y 坐标上出现多次,优先判为水印;如果只在页面顶部或底部固定位置出现,要单独标记为页眉页脚而不是水印。

颜色信息也可以叠加进来。pdfplumber 的 char 对象里有 non_stroking_color 属性,水印如果是灰色,这个字段会返回类似 (0.5, 0.5, 0.5) 的灰度值,而正文通常是纯黑或其他明确色彩。把颜色特征和位置特征放一起判断,能显著减少误删。

4.2 用渲染图层辅助定位水印区域

文本水印还算是好处理的,最麻烦的是图片水印或者半透明叠加的水印。这种水印在 pdfplumber 的文本层里根本不存在,它是一张覆盖在页面上的图片或矢量对象,只有渲染成图像才能看到。

这种时候我会用 PyMuPDF 把页面渲染成高分辨率图片,然后用 PIL 做像素级分析。

import fitz from PIL import Image def render_page(pdf_path, page_index, zoom=2): doc = fitz.open(pdf_path) page = doc[page_index] mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat, alpha=False) pix.save(f"page_{page_index}.png") return Image.open(f"page_{page_index}.png")

渲染出来之后,你可以肉眼观察水印区域在页面上的大致位置,也可以写一个简单的像素统计脚本:把整页图片按 Y 方向投影,看看哪些带状区域亮度均匀偏低或偏高,再结合文本层的 bbox 数据,把水印影响区域映射成一组 PDF 坐标系下的矩形。

关键点在于坐标换算。假设渲染图片宽度是 1190 像素,PDF 页面宽度是 595 磅,缩放因子就是 2。图片中一个水印文字中心点的像素坐标 (px, py),对应 PDF 坐标就是 (px / 2, py / 2)。因为两边都是左上角原点,x、y 方向一致,不需要翻转,这点和调色板坐标系不一样,别弄反。

有了水印区域的 PDF 坐标后,你可以画一个简单的可视化,把水印 bbox 用矩形框标注出来,叠加在渲染图上,肉眼检查这些框是否正好框住水印文字。这一步值得做,因为后面所有清洗逻辑都依赖这些区域是否准确。

4.3 三种清洗策略:过滤、遮挡、重建

拿到水印区域之后,有三种策略可以选,实际项目里我用得最多的是第一种。

第一种:在文本抽取层过滤。

对文本水印,直接在解析时把落进水印 bbox 的字符剔除。可以用 pdfplumber 的 crop 和 filter 组合:

def clean_page_text(page, watermark_boxes): lines = page.extract_text_lines() clean_lines = [] for line in lines: in_watermark = any( not (line["x1"] < box[0] or line["x0"] > box[2] or line["bottom"] < box[1] or line["top"] > box[3]) for box in watermark_boxes ) if not in_watermark: clean_lines.append(line["text"]) return "\n".join(clean_lines)

这个策略的好处是原 PDF 不被改动,只是抽取出来的正文文本变干净了,对 RAG 链路完全透明。缺点是对“正文和水印重叠”的情况无能为力,如果水印正好盖在正文文字上方,bbox 重叠区域会把正文也一起过滤掉。

第二种:渲染后遮挡再做 OCR。

当水印覆盖正文时,我会把页面渲染成图,在水印区域用周围像素修复(inpaint)或用白色矩形遮挡,然后交给 OCR 识别正文。这种方式能避免误删正文,但成本高、速度慢,而且 OCR 会引入新的识别误差,一般只在扫描版 PDF 里用。

第三种:重建 PDF。

用解析出的文本重排一个干净 PDF,或者直接把原始 PDF 里的水印对象删除后另存。这个方案最“完美”,但风险也最大:原 PDF 里的字体、图片、表格结构可能被破坏,重建后排版错乱。做 RAG 的话我基本不建议走这条路,因为向量化只需要语义正确的文本,不需要排版完全复刻。

4.4 和多栏问题联动的完整抽取管线

水印清洗和多栏还原往往要同时处理,我建议在同一个解析函数里做,减少中间文件的读写。

管线顺序如下:先打开页面,提取所有文本行;再识别水印 bbox;接着把水印区域内的行标记为“跳过”;然后对剩余行做加权列坐标聚类,还原阅读顺序;最后输出干净的正文。

我之前在一个内部合同项目里跑过一套完整的联动管线,有一次连续处理了几百份带水印的 PDF,效果稳定在 95% 以上的水印文本被正确剔除,误删正文的情况在抽样检查里没有出现。这里有个补充条件:管线里我还会对每页的最后输出做一次 round-trip 验证——把过滤前后的文本做 diff,凡是变化超过预期比例的页面,单独标记出来人工审。这个方法也不复杂,但非常实用。

5. 常见问题与排查技巧实录

5.1 速查表:症状、原因与解决方案

我把自己在实战里反复遇到的坑整理成表,方便你直接对照排查。

症状可能原因排查思路推荐处理
抽取文本左右栏内容串行未按栏聚类,直接用行存储顺序组装打印每行 x0/top,观察左右交替用加权列坐标做分支,每栏内按 top 排序
水印文本仍然进入向量库只做了文本过滤,未考虑水印与正文重叠渲染页面,检查水印区域是否覆盖正文用水印 bbox 做掩码,不在文本层硬过滤
bbox 坐标与渲染图对不上页面旋转不一致核对 page.rotation 和渲染分辨率统一坐标系,显式处理旋转角度
聚类时跨栏块被扔进某一栏阈值设得太小,跨栏行未独立看跨栏块宽度占页面比例将宽度超过页面 60% 的行剥离,单独排序
页脚页码被当成水印删除重复文本判断未排除页眉页脚检查文本出现的 y 坐标是否固定按固定位置区域单独标记,归为页眉页脚
旋转水印完全检测不到倾斜文本的 bbox 不遵循普通行规律渲染页面肉眼观察用图像区域匹配,而非文本行统计
半透明水印颜色接近正文颜色特征不明显,文本层不可见比较同一页不同区域的像素差异用像素级投影,标记均匀差异区域

5.2 我踩过的坑和私有避坑清单

先提醒一个最容易被低估的问题:pdfplumber 默认输出的行顺序并不代表阅读顺序。不要依赖 extract_text_lines 的顺序来做文本组装,尤其是在处理双栏或三栏文档时。前面多栏的例子已经很能说明问题。

另一个坑是非嵌入字体。有些 PDF 生成工具为了压缩体积,不嵌入字体,只保留字体名称和字符码。pdfplumber 解析时能拿到文本内容,但一旦做渲染对比,系统会用本地字体替换,渲染结果显示的字体样式和 PDF 原样差别很大。如果你发现渲染出来的页面和文本层坐标对不上,先检查字体嵌入状态。

还有水印文字被拆成多个对象的情况。很多 PDF 生成器不会把一整句水印保存成一个连续字符串,而是逐字或逐词生成独立对象,导致你统计重复文本时根本匹配不上。对付这种情况,得把字符按 bbox 位置先归组,再做句子级文本提取,否则水印的“指纹”就是碎的。

最后提一句颜色判断的复杂度。PDF 里的颜色可能是 RGB 三元组、灰度值、CMYK,甚至可以是透明色。你拿到的 non_stroking_color 可能是 (0.2, 0.2, 0.2),也可能是 [0.2],还有可能是 None(默认黑)。写判断逻辑时要兼容这些类型。

5.3 用 bbox 还能顺手解决哪些解析问题

bbox 的用途远远不止分栏和水印。实际做解析管线时,我发现很多问题都能靠位置信息解决。

比如图文关联。如果知识库需要同时存文本和图片,图片周围的 bbox 能帮你把 caption 和引用了这张图的正文段落关联起来。常见的规律是:图片上方紧邻的文本大概率是图题,图题后面的段落可能是对图片内容的解释。按 bbox 距离聚类,能自动建立“正文-图片”的引用关系,这对多模态 RAG 非常有用。

再比如表格的识别。PDF 里的表格线在解析层返回的是线对象,而单元格里的文本是 char 对象。把线和文本的 bbox 放在同一个坐标系里,你可以推断每个文本落在哪个单元格范围内,这样比纯文本规则提取表格要稳健得多。

还有页眉页脚的去除。前面提到页眉页脚会在每页重复,根据 bbox 在页面上的固定位置,可以把它们和普通正文分割开。不要等到向量化之后再去降噪,解析阶段就该处理掉。

6. 把 bbox 玩成 RAG 基础设施

多说几句和 RAG 的整体关系。很多项目一开始会在模型层花大量时间调 prompt、调 embedding,但文档解析的质量反而是检索效果的上限。如果喂进来的文本是乱的、带噪音的,后面再好的检索排序也补不回来。

我在实际项目里的习惯是,把 bbox 相关的解析逻辑沉淀成一个小工具库,输入是 PDF 路径,输出是结构化的页面对象,每个对象都带原始 bbox、栏标签、是否属于水印区域等信息。这样不管是后续做切块、向量化,还是接本地知识库(比如用 Ollama 搭的离线检索环境),都能基于干净的文本继续处理。

这里的重点是“结构化”三个字。不要只输出纯文本,而是把每个文本块的坐标和语义标签都保留下来。这样当你发现某个坑位效果不对时,你能随时回到坐标层面做调试,而不是面对一堆已经拼接成字符串、无法回溯的文本。

另外,bbox 数据本身也可以进入向量库。比如把图片的 bbox 和文本的 bbox 关联后,存储的是图文位置关系,检索时根据用户问题的语义找到相关文本块,再通过坐标关联拉出对应的图片或表格。这比单纯把图片塞进向量库要高效得多,因为图片的向量维度高、存储成本大,没必要给每张图都建索引,只对用户真正关心的图建索引就够了。

如果你正在搭知识库,我建议把解析层当成一个独立的子项目来对待,不要和模型层混在一起改。先跑通文本抽取、bbox 聚类、水印清洗,再接入 embedding 和检索。顺序对了,后面能少走很多弯路。

讲讲我自己的取舍吧。最初我也迷信大模型能容忍乱序文本,直到一批多栏 PDF 进来之后,检索效果直接掉了一半。后来把 bbox 聚类和水印清洗写进解析流程,用本地 Ollama 跑轻量 embedding 做验证,召回明显改善。现在这套思路我沿用至今:先统计、再定位、最后才决定是过滤还是重建。不是每份 PDF 都需要上 OCR 上版面分析,但多栏和水印这两关,在 RAG 文档解析里值得优先打通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 5:38:44

Brainstorm中MEG预处理的六步物理逻辑与避坑指南

1. 这不是“点几下就出图”的流程&#xff0c;而是重建大脑信号可信度的起点如果你刚接触脑磁图&#xff08;MEG&#xff09;数据&#xff0c;看到“Brainstorm”这个软件名&#xff0c;可能会以为它是个轻量级、图形化友好的入门工具——毕竟名字里带着“头脑风暴”&#xff0…

作者头像 李华
网站建设 2026/10/5 5:38:18

Embedding与向量化实战:构建企业级智能问答系统

1. 先把 Embedding 和向量化这件事想明白做企业级智能问答系统&#xff0c;你早晚会撞上那一面墙&#xff1a;关键词匹配搜不到同义表述&#xff0c;正则规则写到你手软&#xff0c;明明文档里写了&#xff0c;用户换个问法就答不出来。真正把这个问题拆掉的&#xff0c;不是后…

作者头像 李华
网站建设 2026/10/5 5:35:18

AST 语法守卫升级:结合大模型拦截前端组件内部的内存泄漏隐患

AST 语法守卫升级&#xff1a;结合大模型拦截前端组件内部的内存泄漏隐患在现代前端单页应用&#xff08;SPA&#xff09;的大型工程中&#xff0c;如果问哪一类线上缺陷最让架构师头皮发麻&#xff0c;“内存泄漏&#xff08;Memory Leak&#xff09;”绝对名列前茅。 它不像普…

作者头像 李华
网站建设 2026/10/5 5:35:09

TipDM:面向教学与产业的Java+Python混合架构机器学习平台

1. 项目概述&#xff1a;TipDM不是“另一个AI平台”&#xff0c;而是机器学习工程落地的脚手架TipDM这个名字在开源社区里常被误读为“Tip Data Mining”或“Tip Deep Learning”&#xff0c;但实际它全称是TipDM —— Teaching & Industrial Practice Data Mining Platfor…

作者头像 李华
网站建设 2026/10/5 5:34:59

C# + MySQL房屋租赁管理系统开发实战:从环境配置到代码落地

简介&#xff1a;基于C#MySQL的房屋租赁管理系统是一份面向计算机、软件工程、通信工程等专业学生的课程设计与毕业设计参考项目&#xff0c;核心代码围绕Windows窗体界面、业务逻辑与MySQL数据库交互展开&#xff0c;适合具备一定编程基础、正在完成综合实践任务的大三及以上学…

作者头像 李华
网站建设 2026/10/5 5:33:22

XXL-AI:面向生产的AI工程化底座与Agent编排实践

1. XXL-AI不是又一个“玩具框架”&#xff0c;而是面向交付的AI工程化底座你有没有遇到过这样的场景&#xff1a;团队花两周时间用LangChain搭了个RAG问答Demo&#xff0c;演示时效果惊艳&#xff0c;可一上线就卡在三个地方——知识库更新要手动跑脚本、用户问“上个月销售报表…

作者头像 李华