用 Qdrant 和 Qwen3-VL 搭一个多模态文档 RAG 流水线

这篇教程讲的是一个文档 RAG 的常见升级方向:不要只把 PDF 文本抽出来喂给 LLM,而是把文本检索和页面图像一起交给 VLM。原文用 Qdrant 做向量库,用 PyMuPDF 渲染 PDF 页面,再把检索到的文本块和页面图片交给 Qwen3-VL 4B 做回答。

这个思路挺适合论文、报告、财报、模型技术文档。因为很多信息不只在文本里,还在表格、图、坐标轴、页面布局里。文本抽取一旦把这些结构打散,LLM 很容易丢掉关键证据。

为什么要让 VLM 看页面图像

传统文档 RAG 一般流程是:加载 PDF,抽文本,切块,向量化,检索 top-k,然后把文本块交给 LLM。这个方法很成熟,但有明显短板。

比如表格会被拉平成一串文字,图注和图片可能对不上,柱状图或坐标轴根本无法用纯文本表达。如果用户问的是"图 3 里哪个方法最高",文本-only RAG 就会很吃力。

原文的方案把向量库当成"定位器":先用文本检索找到相关段落和页码,再根据页码把原 PDF 页面渲染成图片,最后让 Qwen3-VL 同时看文本和页面图像。

这样模型回答时就不只是根据 chunk 猜,而是可以看到原始视觉证据。

技术栈

教程里用到的主要组件有:

  • Qdrant:本地持久化向量数据库
  • LangChain:加载文档和切分文本
  • PyMuPDF / fitz:把 PDF 页面渲染为图片
  • sentence-transformers/all-MiniLM-L6-v2:生成文本 embedding
  • Qwen/Qwen3-VL-4B-Instruct:做多模态理解和回答
  • transformers:加载 VLM 和 processor

测试环境包括 Windows + RTX 3060 12GB,以及 Ubuntu + RTX 3080 Ti 12GB。这个配置对中文用户有参考价值,因为它不是只在 H100 上跑的演示。

建库流程

第一步是加载 PDF,并把页码元数据修正成从 1 开始。这个细节很重要,因为后面要把检索结果映射回用户能理解的页码。

PDF_PATH = "Dataset/QwenVL2.5.pdf"
loader = PyPDFLoader(PDF_PATH)
documents = loader.load()

for doc in documents:
    doc.metadata["page"] = doc.metadata.get("page", 0) + 1

然后用 RecursiveCharacterTextSplitter 切块,原文设置的是 chunk_size=256chunk_overlap=50。这个粒度比较细,适合做页面定位。

接着初始化 Qdrant。这里用的是本地路径持久化,不需要额外起服务。

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2"
)
client = QdrantClient(path="qdrant_storage")

向量维度是 384,距离函数用 cosine。通过 QdrantVectorStore 把文本和 metadata 一起写入,这样检索结果能带回页码。

查询时怎么取页面图

查询阶段先把用户问题 embed,然后在 Qdrant 里搜索 top-k。原文还做了一个很实用的处理:如果多个 chunk 来自同一页,只保留一次页码,避免后面重复把同一页图像喂给 VLM。

拿到页码后,用 PyMuPDF 把 PDF 页面渲染成图片。

doc = fitz.open(pdf_path)
page = doc[page_0b]
pix = page.get_pixmap(dpi=200)
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)

这里 DPI 设为 200,是为了保留表格和小字。太低的话 VLM 看不清,太高则显存压力变大。

最后怎么喂给 Qwen3-VL

最终 prompt 里会同时放两类内容:检索到的页面图片,以及带页码的文本 chunk。

消息结构大致是这样:

messages = [
    {
        "role": "user",
        "content": [
            *[{"type": "image", "image": img} for img in images_extracted],
            *[{"type": "text", "text": f"[Page {res['page']}] {res['text']}"} for res in filtered_list],
            {"type": "text", "text": "Summarize with page citations."}
        ]
    }
]

这相当于把 Qdrant 检索变成"找证据页"的步骤,把 Qwen3-VL 变成"看证据并组织答案"的步骤。

这个方案的优点和代价

优点很明显:它能处理视觉证据,回答时也可以引用页码。对于包含图表、公式、截图、表格的 PDF,这比纯文本 RAG 更稳。

代价是工程复杂度和显存要求更高。你需要维护 PDF 原文件、文本 chunk、页码 metadata、页面渲染逻辑和 VLM 推理。查询时也不再只是一次 LLM 调用,而是多了页面图像处理。

我觉得比较适合的使用方式是:先用纯文本 RAG 做第一版,如果发现表格/图形问题很多,再升级到这种多模态 RAG。不要一开始就把所有文档系统都做得很重。