Pixel-Native RAG 实战:把网页、PDF 与扫描件按“像素”统一检索

本文整理自 MarkTechPost 的 Pixel-Native RAG: A Practical Guide to Visual Document Indexing

原文:https://www.marktechpost.com/2026/08/04/pixel-native-rag-a-practical-guide-to-visual-document-indexing/

配套 Notebook:https://github.com/MARKTECHPOST-AI-MEDIA-INC/AI-Agents-Projects-Tutorials/blob/main/LLM%20Projects/pixelragfromscratchtutorialMarktechpost.ipynb

来源边界:原文正文经 Jina Reader 提取并清理导航与分享组件;本文保留原文方法,补充了一个可离线运行的最小检索融合示例。扩展代码不是原文代码的逐字复制。

先说结论

传统 RAG 通常先抽取文本,再分块、嵌入和检索。Pixel-Native RAG 换了一个入口:先把文档渲染成图片,再切成图块,以图块为最小检索单元

它适合以下资料:

如果资料主要是结构简单、文本层干净的纯文本,普通文本 RAG 通常更便宜。不要为了“像素原生”而给所有文档增加截图、OCR 和视觉模型成本。

1. 整体架构

原文流程可以压缩成八步:

  1. 渲染:网页用 Playwright 截图,PDF 用 PyMuPDF 逐页转图。
  2. 切片:默认图块为 1024×1024 像素,相邻图块重叠 128 像素。
  3. 清洗:过滤空白图块,用平均哈希去掉近似重复页眉等内容。
  4. 视觉向量:使用 SigLIP、CLIP,或可选的 Qwen3-VL Embedding。
  5. OCR 稀疏索引:对图块运行 OCR,并建立 BM25 检索。
  6. 融合:用倒数排名融合(RRF)组合视觉向量结果和 OCR-BM25 结果。
  7. 文档聚合:把图块命中归并到文档,保留证据图块、坐标和 OCR 片段。
  8. 评估与服务:计算 Recall@K、MRR,可通过 FastAPI 暴露搜索接口,并可选用 VLM 根据证据截图回答。

检索链路如下:

用户问题
  ├─ 文本编码 → 视觉向量索引(FAISS) ─┐
  └─ 关键词查询 → OCR-BM25 ───────────┤
                                      ├─ RRF 融合
                                      └─ 按文档聚合 → 证据图块 → 可选 VLM 回答

2. 环境准备:优先跑原始 Notebook

原文示例依赖较多,最省事的入口是 Google Colab。打开配套 Notebook 后,按顺序运行单元格即可。

如果需要本地运行,建议先创建隔离环境;依赖及版本以 Notebook 当前安装单元为准,不要把下面的示例版本当成原文锁定版本:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pillow numpy faiss-cpu rank-bm25 pytesseract pymupdf playwright fastapi uvicorn transformers torch
python -m playwright install chromium

系统还需要 Tesseract。Debian/Ubuntu 示例:

sudo apt-get update
sudo apt-get install -y tesseract-ocr

验证标准:

python -c "import PIL, fitz, faiss; print('core imports: ok')"
tesseract --version

预期至少出现:

core imports: ok

tesseract 5.x ...

失败处理:

3. 第一步:渲染与切片

3.1 网页与 PDF

原文事实:网页优先由 Playwright 截图;浏览器不可用时,可把抽取文本渲染为图片作为后备。PDF 使用 PyMuPDF 逐页渲染。

切片的关键不是“越小越好”,而是让一个图块能覆盖完整的局部语义,同时避免内容恰好落在边界上。原文默认参数是:

# config.py(参数示意,与原文默认值一致)
TILE_WIDTH = 1024
TILE_HEIGHT = 1024
TILE_OVERLAP = 128
MIN_TILE_HEIGHT = 200
MAX_TILES_PER_DOC = 12

重叠会提高边界内容的召回机会,但也会增加图块数、嵌入时间和索引体积。先保留默认值,只有在评估显示跨块文本经常丢失时,再把重叠提高到 256。

3.2 清洗

原文做了两类低成本清洗:

这一步应在生成向量前完成。否则重复页眉会浪费嵌入、索引和检索名额。

检查点:随机查看 20 个保留图块和被过滤图块。若正文页被误删,应先降低空白阈值或收紧去重距离,不要先调检索模型。

4. 第二步:建立双路索引

4.1 视觉向量索引

原文支持以下后端:

后端 适合场景 主要限制
SigLIP 通用截图主题、布局和图形 对细小文字理解有限
CLIP 轻量基线与通用图文匹配 密集文字通常不是强项
Qwen3-VL Embedding 2B 文字密集截图 下载量大,原文提示约需 8 GB 显存

图块向量经 L2 归一化后写入 FAISS;此时内积可作为余弦相似度。小规模索引使用 IndexFlatIP 最直接。原文虽然设计了 IVF 阈值,但展示代码最终仍回到精确索引,因此不要把该 Notebook 当作已验证的大规模 ANN 实现。

4.2 OCR-BM25 索引

视觉模型能识别大致语义和布局,但具体编号、术语、金额或人名往往更依赖 OCR。对每个图块保存:

{
  "doc_id": "invoice-2026-001",
  "tile_id": "invoice-2026-001-p1-y0",
  "page": 1,
  "y0": 0,
  "y1": 1024,
  "image_path": "tiles/invoice-2026-001-p1-y0.png",
  "ocr_text": "Invoice No. 2026-001 ..."
}

BM25 使用 ocr_text,视觉索引使用对应图块向量。两路索引共享同一个 tile_id,后续才能稳定融合和回溯证据。

5. 第三步:用 RRF 融合,而不是硬拼分数

视觉相似度和 BM25 分数不在同一量纲,直接相加很脆弱。原文使用 RRF,只看各结果在各自列表中的排名:

RRF 分数 = Σ weight / (k + rank)

下面是一个只用 Python 标准库的离线最小示例。它不生成真实视觉向量,而是验证“密集排名 + 稀疏排名 → 融合 → 文档聚合”的控制流。

# rrf_demo.py(本文实践扩展,可离线运行)
from __future__ import annotations

import json
from collections import defaultdict

DENSE_RANK = ["invoice-p1", "manual-p2", "scan-p1"]
SPARSE_RANK = ["invoice-p1", "scan-p1", "manual-p2"]
TILE_TO_DOC = {
    "invoice-p1": "invoice",
    "manual-p2": "manual",
    "scan-p1": "scan",
}


def rrf_fuse(
    dense: list[str],
    sparse: list[str],
    *,
    k: int = 60,
    dense_weight: float = 1.0,
    sparse_weight: float = 1.0,
) -> list[tuple[str, float]]:
    scores: dict[str, float] = defaultdict(float)
    for rank, tile_id in enumerate(dense, start=1):
        scores[tile_id] += dense_weight / (k + rank)
    for rank, tile_id in enumerate(sparse, start=1):
        scores[tile_id] += sparse_weight / (k + rank)
    return sorted(scores.items(), key=lambda item: (-item[1], item[0]))


def aggregate_docs(tile_hits: list[tuple[str, float]]) -> list[dict[str, object]]:
    docs: dict[str, dict[str, object]] = {}
    for tile_id, score in tile_hits:
        doc_id = TILE_TO_DOC[tile_id]
        doc = docs.setdefault(doc_id, {"doc_id": doc_id, "score": 0.0, "tiles": []})
        doc["score"] = max(float(doc["score"]), score)
        doc["tiles"].append(tile_id)
    return sorted(docs.values(), key=lambda item: (-float(item["score"]), str(item["doc_id"])))


def main() -> None:
    tile_hits = rrf_fuse(DENSE_RANK, SPARSE_RANK)
    docs = aggregate_docs(tile_hits)
    assert docs[0]["doc_id"] == "invoice"
    assert docs[0]["tiles"] == ["invoice-p1"]
    print(json.dumps(docs, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

运行:

python rrf_demo.py

预期输出的首项:

[
  {
    "doc_id": "invoice",
    "score": 0.03278688524590164,
    "tiles": [
      "invoice-p1"
    ]
  }
]

实际系统还应把视觉余弦分数、OCR 片段、页码和坐标作为诊断字段保留下来,但不要误把这些原始分数当成 RRF 分数。

6. 第四步:评估检索,而不是只看一次演示

原文使用 7 条预设查询,报告 Recall@1、Recall@3、Recall@5 和 MRR,并比较混合检索与纯视觉检索。不过原文没有给出足以证明通用效果的完整结果数字,样本也很少。

你的评估集至少应保存:

query,expected_doc_id
"发票编号 2026-001 对应的金额",invoice-2026-001
"哪份手册说明了紧急停机流程",safety-manual
"扫描合同里的终止通知期",contract-scan-04

建议按以下顺序做对照:

  1. 只跑视觉向量检索;
  2. 只跑 OCR-BM25;
  3. 跑 RRF 混合检索;
  4. 对每条失败查询检查 OCR、图块边界、重复过滤和模型召回;
  5. 只有确定是向量适配问题后,再考虑训练残差适配器。

验收建议(本文实践扩展):不要照搬固定阈值。先记录当前文本 RAG 或人工检索基线,再要求 Pixel-Native 方案在版式敏感子集上有明确收益,同时控制索引体积和查询延迟。

7. 三个可落地案例

案例 A:带表格的发票 PDF

目标:按发票号、金额、供应商或表格字段检索。

执行:

  1. 用 PyMuPDF 把每页渲染为图片;
  2. 切成重叠图块;
  3. OCR 提取精确字段,视觉向量保留表格结构;
  4. 用 RRF 融合;
  5. 返回命中页码、图块坐标和截图。

验证:准备 20 个已知答案,检查 Recall@3,并人工确认截图确实包含答案。若 OCR 识别金额错误,VLM 也不能把错误 OCR 自动变成可靠证据。

案例 B:扫描合同

目标:检索没有文本层的扫描合同中的期限、责任和终止条款。

执行:先检查分辨率和旋转方向,再做 OCR 与视觉嵌入。把合同编号和页码保存在图块元数据中。

验证:除了文档命中,还要检查条款是否完整落在一个证据图块或相邻图块中。若句子经常跨块,优先调整重叠或合并相邻证据,不要让生成模型猜缺失半句。

案例 C:网页、PDF、截图混合知识库

目标:用同一接口检索产品文档、PDF 手册和故障截图。

执行:统一为图块元数据协议,但保留 kind、原始 URL、页码和截图坐标。网页截图失败时,可以使用文本渲染后备,但必须在元数据里标记提取路径。

验证:按来源类型分别计算指标。总 Recall 上升可能掩盖扫描件表现很差;不要只报告一个汇总数字。

8. 可选进阶:适配器、FastAPI 与 VLM

以下能力在原文 Notebook 中出现,但不应成为第一阶段的上线前提:

[实践扩展] 即使使用 VLM,也应把“检索成功”和“答案受证据支持”分开验证:

  1. Schema 是否有效;
  2. 返回的证据图块是否存在;
  3. 页码和坐标是否正确;
  4. 答案是否由图块中的可见内容支持;
  5. 失败时重新检索、扩大相邻图块,或拒答。

9. 常见失败与排查顺序

症状 先检查 不要先做
精确编号搜不到 OCR 文本、分词、图块是否含完整编号 换更大的视觉模型
表格主题能搜到但字段错误 证据图块、OCR、VLM 是否越过可见内容 调高生成温度
大量结果是重复页眉 平均哈希阈值与去重范围 增加 top_k
句子经常被截断 图块重叠、相邻证据合并 训练适配器
小语料适配后变差 训练样本、正负例与基线指标 继续增加训练轮数
查询很慢 图块数量、批大小、精确索引规模、OCR 开销 直接上 IVF 却不做召回对照

10. 最小上线清单

原文方法覆盖清单

原文方法 是否纳入 落点 若遗漏原因
网页截图与文本渲染后备 第 1、3、7 节
PDF 逐页渲染 第 1、3、7 节
1024×1024 图块与 128 重叠 第 1、3 节
空白过滤与平均哈希去重 第 3 节
SigLIP、CLIP、Qwen3-VL Embedding 第 4 节
OCR 与 BM25 第 4 节
RRF 混合检索 第 5 节
FAISS 与持久化元数据 第 4、10 节
按文档聚合证据图块 第 1、5 节
Recall@K、MRR 与混合/密集对照 第 6 节
伪查询与残差适配器 是,进阶说明 第 8 节 未在本文重写训练实现;直接指向原始 Notebook
FastAPI 服务 是,生产边界说明 第 8 节 未重复原文完整服务代码
Qwen2.5-VL 证据回答 是,进阶说明 第 8 节 真实模型调用未执行

验证状态与边界

原文给出的是完整实验管线,不是大规模生产性能证明。正确的落地顺序是:先用少量版式敏感文档证明像素检索的增量价值,再决定是否承担 OCR、视觉模型和更大索引的成本。