# 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/pixelrag_from_scratch_tutorial_Marktechpost.ipynb  
> 来源边界：原文正文经 Jina Reader 提取并清理导航与分享组件；本文保留原文方法，补充了一个可离线运行的最小检索融合示例。扩展代码不是原文代码的逐字复制。

## 先说结论

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

它适合以下资料：

- 表格、图表、公式、代码排版会影响含义；
- PDF 没有可靠文本层，或来源是扫描件；
- 网页结构复杂，HTML 抽取经常丢失布局信息；
- 同一知识库同时包含网页、PDF、截图和扫描文档。

如果资料主要是结构简单、文本层干净的纯文本，普通文本 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 根据证据截图回答。

检索链路如下：

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

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

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

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

```bash
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 示例：

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

**验证标准：**

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

预期至少出现：

```text
core imports: ok

tesseract 5.x ...
```

**失败处理：**

- `playwright` 找不到浏览器：运行 `python -m playwright install chromium`。
- `pytesseract` 可导入但执行失败：检查系统是否安装 `tesseract-ocr`。
- ARM 或无 AVX 环境安装 `faiss-cpu` 失败：先在 Colab 验证流程，或改用平台支持的向量库；不要在环境问题未解决时把检索错误归因于模型。
- 内存或显存不足：先使用 SigLIP/CLIP、小批量和少量文档，不要直接启用 Qwen3-VL Embedding。

## 3. 第一步：渲染与切片

### 3.1 网页与 PDF

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

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

```python
# 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。对每个图块保存：

```json
{
  "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，只看各结果在各自列表中的排名：

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

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

```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()
```

运行：

```bash
python rrf_demo.py
```

预期输出的首项：

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

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

## 6. 第四步：评估检索，而不是只看一次演示

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

你的评估集至少应保存：

```csv
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 中出现，但不应成为第一阶段的上线前提：

- **残差适配器**：从 OCR 文本和标题生成伪查询，以对比学习调整查询和图块向量；小样本时可能无收益，样本不足会跳过。
- **FastAPI**：暴露健康检查和批量搜索接口。生产环境还需补鉴权、限流、超时、请求大小限制和审计日志。
- **Qwen2.5-VL 回答**：读取检索到的截图证据，并被要求只依据可见内容回答。

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

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

## 9. 常见失败与排查顺序

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

## 10. 最小上线清单

- [ ] 用 20–50 份代表性文档建立小样本集；
- [ ] 固定图块尺寸与重叠参数并记录版本；
- [ ] 抽样检查空白过滤和近似去重；
- [ ] 同时保存视觉向量、OCR 文本和可追溯元数据；
- [ ] 对比 dense-only、sparse-only 和 RRF；
- [ ] 按网页、原生 PDF、扫描件分别报告 Recall@K 与 MRR；
- [ ] 保存证据截图、页码、坐标和 OCR 片段；
- [ ] VLM 只作为证据回答层，失败时允许拒答；
- [ ] API 上线前补鉴权、限流、超时与审计；
- [ ] 只有基线证明需要时才训练适配器或切换更大模型。

## 原文方法覆盖清单

| 原文方法 | 是否纳入 | 落点 | 若遗漏原因 |
|---|---:|---|---|
| 网页截图与文本渲染后备 | 是 | 第 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 节 | 真实模型调用未执行 |

## 验证状态与边界

- `static_publish_ok`：由发布流程验证 Markdown、HTML、目录与代码复制控件。
- `mock_code_run_ok`：本文 `rrf_demo.py` 将从教程代码块提取后离线执行。
- `real_backend_contract_ok`：未验证；真实视觉嵌入、OCR、FAISS 和服务接口以原始 Notebook 为准。
- `real_backend_smoke_ok`：未执行；本文没有下载大型模型或调用真实 VLM。

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