BotOf TechAI / IoT / Full-Stack / 植物养护
返回首页图像 RAG 工程实战(三):可落地架构与参考实现

图像 RAG 工程实战(三):可落地架构与参考实现

·7 分钟阅读·

前两篇定了路线(范式 C:视觉文档 late-interaction)和参数(两阶段检索 + 量化)。这一篇只回答一个问题:怎么把它变成能跑、能部署、能评测的系统。 所有代码尽量贴近可直接运行的状态,关键链路标注来源。

一、整体架构

一个生产级视觉文档 RAG,逻辑上分两条流水线:离线索引在线查询

组件清单与选型:

组件推荐起步说明
文档渲染pdf2image + PopplerPDF → 每页 PNG
视觉检索编码ColQwen2.5 v0.2 (Apache 2.0)多向量 embedding
检索框架Byaldi(快速原型)/ 原生 colpali-engine(可控)见第二、三节
多向量库Qdrant 或 Milvus原生 MaxSim 多向量
重排MonoQwen2-VL-v0.1视觉文档 pointwise reranker
生成 VLMQwen2.5-VL(vLLM 服务化)读图回答 + 引用

二、最快跑通:Byaldi 三行索引

想在一小时内验证可行性,用 Byaldi(ColPali 系的高层封装):

# pip install byaldi pdf2image
from byaldi import RAGMultiModalModel

# 加载 ColQwen2(Apache 2.0,HF cookbook 默认)
RAG = RAGMultiModalModel.from_pretrained("vidore/colqwen2-v1.0")

# 索引一个目录下的所有 PDF:自动渲染成图像 + 编码多向量 + 建索引
RAG.index(
    input_path="docs/",
    index_name="my_docs",
    store_collection_with_index=True,  # 把页面图像 base64 一起存,方便后续喂 VLM
    overwrite=True,
)

# 检索:直接返回 top-k 页(含页码、文档名、可选 base64 图像)
results = RAG.search("2025 年第三季度毛利率是多少?", k=5)
for r in results:
    print(r.doc_id, r.page_num, r.score)

Byaldi 适合 PoC 和中小库。要做生产级的两阶段检索、量化、外部向量库,就需要下一节的原生路径。

三、可控路径:原生 colpali-engine + 外部多向量库

3.1 编码与 MaxSim 打分

# pip install colpali-engine torch pdf2image
import torch
from pdf2image import convert_from_path
from colpali_engine.models import ColQwen2, ColQwen2Processor

device = "cuda" if torch.cuda.is_available() else "cpu"
model = ColQwen2.from_pretrained(
    "vidore/colqwen2-v1.0",
    torch_dtype=torch.bfloat16,
    device_map=device,
).eval()
processor = ColQwen2Processor.from_pretrained("vidore/colqwen2-v1.0")

# 1) 文档侧:PDF -> 页面图像 -> 多向量
pages = convert_from_path("report.pdf", dpi=150)
with torch.no_grad():
    batch = processor.process_images(pages).to(device)
    doc_embeddings = model(**batch)          # list[Tensor[num_patches, 128]]

# 2) 查询侧:文本 -> query token 向量
with torch.no_grad():
    q_batch = processor.process_queries(["2025 年第三季度毛利率"]).to(device)
    query_embeddings = model(**q_batch)      # Tensor[num_q_tokens, 128]

# 3) MaxSim 晚交互打分(processor 内置,等价于第二篇手写的 max_sim)
scores = processor.score_multi_vector(query_embeddings, doc_embeddings)
top = scores[0].topk(5)
print(top.indices.tolist(), top.values.tolist())

这套"ColQwen2 检索 + 量化 VLM 生成"的端到端管线,HuggingFace 官方 cookbook 验证可在单张消费级 GPU(NVIDIA L4,显存 < 24GB) 跑通,并明确以视觉文档检索模型替代了传统 OCR 管线。[¹]

3.2 写入 Qdrant(原生多向量 + binary 量化)

Qdrant 原生支持多向量 MaxSim,并能直接开 binary 量化:

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="visual_docs",
    vectors_config=models.VectorParams(
        size=128,
        distance=models.Distance.COSINE,
        # 多向量 + MaxSim 晚交互
        multivector_config=models.MultiVectorConfig(
            comparator=models.MultiVectorComparator.MAX_SIM
        ),
        # binary 量化(配合 rescoring,见第二篇 5.1)
        quantization_config=models.BinaryQuantization(
            binary=models.BinaryQuantizationConfig(always_ram=True)
        ),
    ),
)

# 每页一个 point,payload 里带页码用于引用回溯
points = [
    models.PointStruct(
        id=i,
        vector=emb.cpu().float().numpy().tolist(),  # [num_patches, 128]
        payload={"doc": "report.pdf", "page": i + 1},
    )
    for i, emb in enumerate(doc_embeddings)
]
client.upsert(collection_name="visual_docs", points=points)

# 查询:oversampling + rescoring 用 float32 救回精度
hits = client.query_points(
    collection_name="visual_docs",
    query=query_embeddings[0].cpu().float().numpy().tolist(),
    limit=10,
    search_params=models.SearchParams(
        quantization=models.QuantizationSearchParams(rescore=True, oversampling=4.0)
    ),
).points

3.3 Milvus 多向量混合检索(dense + sparse 融合)

如果要把范式 C(视觉多向量)和范式 D(OCR 文本的 sparse/BM25)做混合,Milvus 是顺手的选择:每集合默认 4 个向量字段、可改 proxy.maxVectorFieldNum 到最多 10,混合检索并行跑多路 ANN,再用 WeightedRanker(分数加权)或 RRFRanker(按排名做 Reciprocal Rank Fusion) 融合。[²]

from pymilvus import MilvusClient, AnnSearchRequest, WeightedRanker

client = MilvusClient(uri="http://localhost:19530")

# 一个集合里同时放:视觉多向量(粗排表示) + 文本稀疏向量(OCR/BM25)
req_visual = AnnSearchRequest(
    data=[pooled_query_vec], anns_field="visual_pooled",
    param={"metric_type": "IP", "params": {"ef": 128}}, limit=200,
)
req_text = AnnSearchRequest(
    data=[sparse_query_vec], anns_field="text_sparse",
    param={"metric_type": "IP"}, limit=200,
)

res = client.hybrid_search(
    collection_name="docs",
    reqs=[req_visual, req_text],
    ranker=WeightedRanker(0.7, 0.3),   # 视觉 0.7 / 文本 0.3;或换 RRFRanker(k=60)
    limit=50,
    output_fields=["doc", "page"],
)

Weaviate 也是可选项:v1.29 起原生支持 ColBERT/ColPali/ColQwen 多向量 + MaxSim,v1.30 GA,v1.31 加入 MUVERA(把多向量编码成单向量以复用常规 ANN)。[³] Vespa 则有把 ColPali 扩到十亿级的公开方案。本轮调研未对 pgvector/LanceDB 的 ColBERT-style 原生支持成熟度做核验,选型前请查最新文档。

四、重排与生成

4.1 MonoQwen2-VL 重排

对精排后的 Top-50 再做一层 VLM pointwise 重排,提升头部精度:

# MonoQwen2-VL-v0.1:首个视觉文档 reranker (Qwen2-VL-2B LoRA)
from transformers import AutoProcessor, AutoModelForVision2Seq
import torch

rr_proc = AutoProcessor.from_pretrained("lightonai/MonoQwen2-VL-v0.1")
rr_model = AutoModelForVision2Seq.from_pretrained(
    "lightonai/MonoQwen2-VL-v0.1", torch_dtype=torch.bfloat16, device_map="cuda"
).eval()

def rerank(query: str, page_images: list, top_k: int = 10):
    scored = []
    for img in page_images:                       # 对每个候选页打 relevance 分
        prompt = (f"Assert the relevance of the previous image document to "
                  f"the following query: {query}. Answer True or False.")
        inputs = rr_proc(text=prompt, images=img, return_tensors="pt").to("cuda")
        with torch.no_grad():
            logits = rr_model(**inputs).logits[:, -1, :]
        true_id = rr_proc.tokenizer.convert_tokens_to_ids("True")
        scored.append(torch.softmax(logits, -1)[0, true_id].item())
    order = sorted(range(len(page_images)), key=lambda i: -scored[i])
    return order[:top_k]

MonoQwen2-VL 论文报告 ViDoRe ndcg@5 达 90.5,是这一段重排的强基线。[⁴]

4.2 用 VLM 读图生成带引用的答案

把重排后的 Top-K 页图像直接喂给生成 VLM:

from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor

gen_proc = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct")
gen_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
    "Qwen/Qwen2.5-VL-7B-Instruct", torch_dtype="auto", device_map="cuda"
)

def generate(query: str, top_pages: list):
    content = [{"type": "image", "image": img} for img in top_pages]
    content.append({"type": "text",
                    "text": f"基于上述页面回答,并在答案末尾标注信息来自第几页:{query}"})
    messages = [{"role": "user", "content": content}]
    text = gen_proc.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    inputs = gen_proc(text=[text], images=top_pages, return_tensors="pt").to("cuda")
    out = gen_model.generate(**inputs, max_new_tokens=512)
    return gen_proc.batch_decode(out, skip_special_tokens=True)[0]

引用回溯:因为每页 point 的 payload 里存了 {doc, page},答案可以精确标注"来自 report.pdf 第 12 页"——这是视觉文档 RAG 相对"图注转文本"范式的一大可解释性优势。

五、服务化部署

生产环境不要把 VLM 塞进请求进程,用 vLLM 起独立推理服务:

# 生成 VLM 服务(OpenAI 兼容接口)
vllm serve Qwen/Qwen2.5-VL-7B-Instruct \
  --port 8000 \
  --gpu-memory-utilization 0.9 \
  --max-model-len 32768 \
  --limit-mm-per-prompt image=8        # 限制单次最多 8 张图,控显存

部署拓扑(与你工作区的单机/多容器风格一致):

落地提醒(针对单机服务器):ColQwen 编码、VLM 重排、VLM 生成都需要 GPU;纯 CPU 的云主机(如未挂卡的腾讯云 CVM)跑不动范式 C。务实方案有三:① 单独挂一台 GPU 实例只跑这三个服务,向量库和编排 API 留在现有服务器;② 编码/生成走云端 VLM API、只自建向量库与编排;③ 用 ColSmol-256M 这类极轻量模型在小显存上做降级版。

六、GPU 选型与成本权衡

配置检索模型生成 VLM显存档位适用
入门(已验证)ColQwen2-2B量化 Qwen2-VL单张 L4 < 24GB [¹]PoC、中小库
标准ColQwen2.5-3BQwen2.5-VL-7B~24–48GB(如 L40S/A100-40G)生产主力
高精度ColNomic-7BQwen2.5-VL-32B+多卡 / A100-80G 级高难文档、研究
降级(小显存)ColSmol-256M云端 VLM API< 12GB边缘 / 成本敏感

成本权衡的三条经验:

  1. 离线索引是大头:百万页逐页编码的 GPU 时,往往比在线查询贵得多。优先把索引做成可断点续跑的批处理。
  2. 存储用量化换:binary+rescoring 直接把向量存储降到 1/32(见第二篇),是省钱最快的旋钮。
  3. 生成按需降配:检索+重排定生死,生成模型可以先用 7B,质量不够再升级,不必一上来就上 32B。

注意:表中显存档位除"入门(L4 < 24GB)"为官方 cookbook 实测外,其余为按参数量估算的级别,非基准实测——上线前请以实际权重精度(bf16/量化)和 batch 实测为准。

七、离线评测:不评测就别上生产

视觉文档 RAG 的检索质量必须离线量化,沿用信息检索标准指标 NDCG@k / Recall@k。最小评测集结构:

评测集(每条样本):
  query            自然语言问题
  relevant_pages   该问题的标准答案所在页(doc_id + page_num),可多页
指标:
  NDCG@5 / NDCG@10   头部排序质量
  Recall@10 / @20    正确页是否进了候选
  分桶:文本密集 / 表格 / 图表 / 扫描退化 / 中文竖排 …

一个无依赖的 NDCG@k 参考实现:

import numpy as np

def ndcg_at_k(ranked_page_ids: list, relevant: set, k: int = 5) -> float:
    """ranked_page_ids: 检索返回的页 id 列表(已按分数降序)"""
    gains = [1.0 if pid in relevant else 0.0 for pid in ranked_page_ids[:k]]
    dcg = sum(g / np.log2(i + 2) for i, g in enumerate(gains))
    ideal = sorted([1.0] * min(len(relevant), k), reverse=True)
    idcg = sum(g / np.log2(i + 2) for i, g in enumerate(ideal))
    return dcg / idcg if idcg > 0 else 0.0

def evaluate(eval_set, retrieve_fn, k=5):
    scores = [ndcg_at_k(retrieve_fn(s["query"]), set(s["relevant_pages"]), k)
              for s in eval_set]
    return float(np.mean(scores))

强烈建议:中文文档务必自建中文评测集,不要照搬英文 ViDoRe 的排名结论(第一篇已强调)。按文档类型分桶评测,才能定位是表格、扫描质量还是版式拖了后腿。

八、生产上线清单

[ ] 选型:已按第一篇决策树确认范式(C / C+D / B)
[ ] 评测集:≥ 数百条 query,按文档类型分桶,含"无答案"负例
[ ] 索引:可断点续跑的批处理;记录每页向量数与存储量
[ ] 量化:binary + rescoring 已开;抽样比对量化前后 NDCG
[ ] 两阶段:mean-pool 粗排 + 全分辨率精排(确认用 mean 不是 max)
[ ] 重排:MonoQwen2-VL 在高精度场景已接入并 A/B
[ ] 服务化:VLM 走 vLLM 独立服务;限制单请求图像数控显存
[ ] 引用:答案带 doc_id + page_num,可回溯原页
[ ] 监控:检索延迟 P95、召回、生成 token 成本、GPU 利用率
[ ] 降级:GPU 不可用时的云端 API / ColSmol 降级路径

九、踩坑与时效性提醒

把本系列调研中被对抗式核验否决、不可引用的说法集中标注,避免你被网上流传的数字误导:

  • ❌ Jina v4 的 ViDoRe 90.17 / Jina-VDR 84.11 nDCG@5(被否,0-3);
  • ❌ HPC-ColPali"注意力剪枝削减 60% 计算、<2% nDCG 损失"(被否,0-3);
  • ❌ HPC-ColPali"HNSW 下降低 30–50% 延迟"(被否,0-3);
  • ❌ ColVision 各模型 ViDoRe leaderboard 的精确分数(如 ColQwen2.5 89.4,1-2 未通过),不要当精确选型依据。

以及两条全局注意:

  1. 时效性:本系列锚定 2025 年中至 2026-06 的快照。视觉文档检索迭代极快(ViDoRe 已出 V3、新骨干层出不穷),落地前务必核对各模型与向量库的最新版本与基准。
  2. 厂商自报基准:Qdrant 的 13x、ColNomic 的对比表、Jina 的 7–10% 多向量优势、32x/64x 压缩与 90%/95–96% 精度保留,多为 best-case 或单基准自报。方向上有同行评审佐证,但绝对值不要外推到你的数据——以自建评测集复测为准。

系列结语

三篇下来,我们走完了图像 RAG 的完整工程链路:

第一篇 把"图像 RAG"拆成两类场景、四种范式,给出选型决策树;第二篇 拆解范式 C 的成本难题,用两阶段检索(13x)和量化(最高 64x)把它压到可生产;第三篇 把方案落成可运行代码、可部署架构、可量化评测。

一句话收尾:视觉富文档 RAG 已经不需要把 PDF "拆成文本"了——直接让模型"看页面",用 late-interaction 多向量检索,再用工程手段把成本压下来,是 2026 年最值得落地的一条路线。 但任何选型都要回到同一个铁律:先有评测集,再谈最先进。

参考资料

  1. HuggingFace Cookbook: Multimodal RAG(ColQwen2 + MonoQwen2-VL + 量化 VLM,单张 L4 跑通)
  2. Milvus 多向量混合检索(WeightedRanker / RRFRanker,最多 10 向量字段)
  3. Weaviate 多向量 Embeddings(ColBERT/ColPali/ColQwen + MaxSim)
  4. MonoQwen2-VL-v0.1(视觉文档 reranker,ViDoRe ndcg@5 90.5)
  5. Qdrant: Optimizing ColPali for Production
  6. Vespa: Scaling ColPali to Billions
  7. ColPali GitHub(colpali-engine / Byaldi 生态)
  8. HuggingFace: Embedding Quantization