Chapter 07

端到端多模态 RAG

检索拿到相关页面之后,传统 RAG 还得把文字抽出来喂给 LLM——等于把问题又抛回 OCR。既然 VLM 能看,就把图像直接喂:Claude / GPT-4o / Qwen-VL / Gemini 都支持。这一章讲怎么串起来。

端到端流程

用户 query │ ▼ ColPali 检索 → top-3 页面(仍是图像) │ ▼ VLM 生成:prompt + [query, page1.png, page2.png, page3.png] │ ▼ 结构化答案(带页面级引用)

Claude Vision 实战

import anthropic, base64

client = anthropic.Anthropic()

def image_block(path):
    with open(path, "rb") as f:
        data = base64.b64encode(f.read()).decode()
    return {
        "type": "image",
        "source": {"type": "base64", "media_type": "image/png", "data": data},
    }

def answer_with_colpali(query, top_pages):
    msg = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": [
                *[image_block(p.image_path) for p in top_pages],
                {"type": "text", "text": f"""
仅根据以上 {len(top_pages)} 张页面图作答,每个事实给出页号引用。
问题:{query}
"""},
            ],
        }],
    )
    return msg.content[0].text

引用:精确到页 vs 精确到区域

页级引用(简单)
prompt 里要求模型以 "[p.3]" 格式输出引用。检索已经给了页号,直接让模型用即可。够 90% 场景。
区域引用(高级)
让 VLM 输出 bounding box(Claude Opus/Gemini 2 已支持)。前端把框叠在原页面上——用户能"看到"答案出自哪块。
prompt = """
Answer the question. For each fact cite the page number AND the pixel-space
bounding box [x1, y1, x2, y2] (0-1000 scale) on that page.
Output JSON: {"answer": "...", "citations": [{"page": N, "bbox": [..]}]}
"""

分层解耦:检索 vs 生成选不同模型

阶段推荐模型原因
检索ColPali / ColQwen2便宜、专精、可微调
生成(短答)Claude Haiku / GPT-4o-mini快且便宜,适合 Q&A
生成(复杂 reasoning)Claude Opus / GPT-4o多图推理强
生成(本地)Qwen2.5-VL-72B数据不出境

成本估算

一次查询:检索 ~$0 + 3 页图像作为输入 + 200 token 生成

成本优化
入参图先 resize 到 1024px 长边——Claude/GPT 对超大图会按 tile 计费,降尺寸能省 30%+。且对 ColPali 已选中的页面,视觉信息已经足够。

对比:OCR 流程 vs 视觉流程

维度OCR → 文本 RAGColPali → 视觉 RAG
索引时间(500 页)~10 分钟(OCR 为主)~12 秒
检索 nDCG@50.550.82
生成引用准确度易"幻觉"页号原图直喂,错引概率低
表格/图表问题
生成成本低(纯文本)中(图像 token 更贵)
总体成本索引+维护高查询略高,维护低

混合模式:视觉检索 + 文本生成

极端省钱的方案:

  1. ColPali 检索到 top-3 页
  2. 对这 3 页 按需 OCR(只做命中页,不预处理所有)
  3. OCR 文本 + 原图一起喂 LLM

索引阶段零 OCR 成本,查询阶段只处理命中的几页——兼顾。

Streamlit 快速 demo

import streamlit as st
from byaldi import RAGMultiModalModel
import anthropic

model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")
model.load_index("./index")
claude = anthropic.Anthropic()

q = st.text_input("问题")
if q:
    hits = model.search(q, k=3)
    cols = st.columns(3)
    for col, h in zip(cols, hits):
        col.image(h.base64, caption=f"p.{h.page_num} score={h.score:.2f}")
    answer = answer_with_colpali(q, hits)
    st.markdown(answer)

本章小结