334. AgentX:从 0 到 1 构建一个可以和 PDF 对话的 RAG 系统

2026.07.28

·fdeagentx

本文范围:只完成三本 PDF 的最小 RAG 闭环——上传、提取文字、切块、向量化、检索、大模型回答、返回页码。
不涉及 OCR、切块调优、嵌入模型选型、混合检索、重排、评测和部署,后续分别展开。

先说结论

RAG(Retrieval-Augmented Generation,检索增强生成)就是:先从 PDF 中找到相关原文,再让大模型根据原文回答。

334. AgentX:从 0 到 1 构建一个可以和 PDF 对话的 RAG 系统 图表 1

上传时把 PDF 做成可搜索的资料;提问时先找原文,再让大模型回答。

本文用三本大模型书籍,跑通这条最小主线。

案例:和三本书对话

我上传三本 PDF:

  • 《大模型应用开发极简入门》
  • 《图解大模型:生成式AI原理与实战》
  • 《从零构建大模型》

然后提问:

RAG 是怎样让大模型根据这三本书回答,而不是凭记忆乱说的?

系统先检索三本书中的相关原文,再生成答案:

上传时,系统把三本书切成带书名和页码的文本块,并建立向量索引。提问时,系统先找到相关文本块,再把问题和原文交给大模型。这次回答使用的是检索出的证据,不依赖模型记忆。
〔《大模型应用开发极简入门》,PDF 第 146—148 页〕〔《图解大模型》,PDF 第 228、244—245 页〕〔《从零构建大模型》,PDF 第 34 页〕

整个系统分成两段:

  • 上传时:提取文字 → 切块 → 向量化 → 存储。
  • 提问时:问题向量化 → 检索原文 → 大模型回答。

三本书只是案例。换成其它 PDF,流程不变。

为什么不直接把整份 PDF 交给大模型

一份很短的 PDF,只问一两次,可以直接发送全文。

三本书需要反复提问,直接发送全文有三个问题:

  • PDF 太长,可能超过模型上下文。
  • 无关文字太多,噪音大、速度慢、成本高。
  • 很难稳定返回答案对应的书名和页码。

所以 RAG 每次只把最相关的几段原文交给大模型。

向量数据库负责找原文,大模型负责组织答案。

上传 PDF:提取文字和页码

每本 PDF 上传后,系统先做三件事:

  • 保存原始文件,并生成 pdf_id
  • 逐页提取文字。
  • 给每页文字保留书名和 PDF 页码。
python
library_id:llm-study
pdf_id:book-app
title:大模型应用开发极简入门
page:146
text:这一页提取出的原文……

这里的 page 是 PDF 文件页码,可能和书上印刷的页码不同。它的作用是让用户能够跳回原文。

扫描版 PDF 没有文字层,需要先做 OCR:

text
扫描页面 → OCR → 文字和页码

为什么要切块

一本书包含很多主题。整本书只生成一个向量,系统只能得到一个模糊的“全书意思”,无法定位具体原文。

切块时只把握两点:

  • 太大:一个块混入多个主题,噪音多。
  • 太小:一句完整的话被切断,上下文丢失。

可以先这样做:

  • 每页单独处理。
  • 优先按段落切,长段落再按完整句子切。
  • 每块约 500 字,相邻块重叠约 80 字
  • 带上章节标题、书名和页码。

50080 只是起点,要用真实问题调整。

切完的一条数据是:

text
chunk_id:book-app-page-146-chunk-02
pdf_id:book-app
title:大模型应用开发极简入门
page:146
text:这一段原文……

什么是向量

向量就是一串数字。文本嵌入模型用这串数字表示一段文字的意思。

text
“RAG 怎样根据 PDF 回答?”
→ [0.12, -0.48, 0.31, ...]

意思接近的文字,向量通常也接近。系统因此可以计算:用户的问题和哪段原文最相似。

这里要分清两种向量:

  • 大模型内部的词元向量:
    • 供模型内部计算。
  • RAG 使用的文本向量:
    • 供检索系统比较问题和原文。

本文讨论第二种。还有一条硬规则:

PDF 文本块和用户问题必须使用同一个文本嵌入模型

否则两边不在同一套向量空间里,距离没有比较意义。

什么是向量数据库

向量数据库保存文本向量,并找出与问题向量最接近的记录。

它返回的是原文,不是答案。

本文使用 Qdrant。一条数据可以理解为:

js
id:book-app-page-146-chunk-02

vector:[0.12, -0.48, 0.31, ...]

payload:
    library_id:llm-study
    pdf_id:book-app
    title:大模型应用开发极简入门
    page:146
    text:这一段原文……

vector 用来搜索,payload 用来拿回原文、书名和页码。

原始 PDF 仍然保存在文件系统或对象存储中。向量数据库保存的是搜索索引,可以根据原始 PDF 重新生成。

Redis、FAISS、Qdrant、Milvus 和 Pinecone

它们都能出现在向量检索系统中,但定位不同:

  • Redis:通用数据存储,已经支持向量索引和相似度搜索
  • FAISS:向量搜索库,适合本地实验。
  • Qdrant、Milvus:专门的向量数据库。
  • Pinecone:托管的向量数据库服务。

这三本书的数据量不大,用 FAISS 也够。

本文选择 Qdrant,是因为它能持久保存数据,也方便按 library_id 筛选、增加和删除 PDF。工具换了,RAG 主线不变。

上传时建立索引

每上传一本 PDF,执行一次:

python
function index_pdf(library_id, pdf, title):
    pdf_id = 保存原始 PDF

    for page in 逐页提取文字(pdf):
        chunks = 切块(page.text)
        vectors = 文本嵌入模型(chunks)

        for chunk, vector in zip(chunks, vectors):
            写入 Qdrant(
                vector,
                library_id,
                pdf_id,
                title,
                page.number,
                chunk.text
            )

三本书分别建立一次索引。用户提问时,不需要重新处理 PDF。

PDF 内容变化后,要删除旧索引并重新生成。

提问时检索原文

用户提问:

RAG 是怎样让大模型根据这三本书回答,而不是凭记忆乱说的?

系统用同一个文本嵌入模型处理问题,再到 Qdrant 搜索:

python
question_vector = 文本嵌入模型(question)

chunks = Qdrant 查询(
    vector = question_vector,
    filter = {"library_id": "llm-study"},
    top_k = 3
)

library_id 把搜索范围限制在这三本书中。

一次可能返回:

text
[S1]《大模型应用开发极简入门》,PDF 第 146 页
[S2]《图解大模型》,PDF 第 244 页
[S3]《从零构建大模型》,PDF 第 34 页

Top-3 指最相关的三个文本块,不保证每本书各有一个。可以先取 3 到 5 个,再用真实问题调整。

向量数据库总能找到“最近”的文本,但最近不等于能够回答。系统还要设置相关度门槛,并检查证据是否足够;证据不足时,直接返回:

这三本书没有提供足够信息。

大模型根据原文回答

系统把问题和检索结果组成提示词

text
只能根据下面的原文回答。

原文不足时,回答:
“这三本书没有提供足够信息。”

每个主要结论都要引用证据编号。
不要编造书名和页码。

问题:
{question}

证据:
[S1,书名,PDF 页码]
{原文}

[S2,书名,PDF 页码]
{原文}

大模型只返回答案和 [S1][S2] 这些证据编号。

应用程序再把编号映射成真实链接:

text
[S1] → 大模型应用开发极简入门.pdf#page=146
[S2] → 图解大模型.pdf#page=244

书名和页码来自检索结果,不让大模型自己填写。这样才能减少编造引用。

出错时按这条线检查

不要一上来就改 Prompt。按数据经过的顺序查:

  • 提取错误:检查 PDF 实际提取出的文字;扫描件先做 OCR。
  • 切块错误:检查完整意思有没有被切断,或一个块是否混入太多主题。
  • 检索错误:直接看 Top-K 中有没有正确原文;没有就调整切块、嵌入模型和候选数量。
  • 精确词漏检:型号、缩写和专有名词可能需要关键词检索或混合检索。
  • 生成错误:正确原文已经找回,模型仍然乱说,再检查提示词和证据约束。
  • 没有答案:检索返回了相近文本,不代表 PDF 真能回答。

RAG 只能减少胡编,不能保证大模型永远正确。

怎样验证

先用三本书准备四类问题:

  • 直接问题:原文中有接近的表达。
  • 改写问题:意思相同,但不用原文关键词。
  • 跨段问题:答案需要相邻文本块
  • 无答案问题:三本书中没有相关信息。

每个问题只检查两件事:

  • 检索:正确原文和页码是否进入 Top-K
  • 回答:模型是否只根据原文回答,引用是否正确,无答案时是否停止。

正确原文没有被找回,先修检索;原文已经找回但答案仍然错误,再修生成。

总结

我会把 PDF RAG 拆成上传提问两段。

  • 上传时,逐页提取文字并保留书名和页码,再切成文本块。用文本嵌入模型生成向量,把向量、原文和页码存入 Qdrant
  • 提问时,用同一个嵌入模型生成问题向量,在当前资料库中找回相关原文,再把问题和原文交给大模型。
    • 证据不足就停止回答

引用页码来自检索结果,由应用程序映射回 PDF,不让大模型自己编。

考考你

PDF 为什么要切块?

一本书包含很多主题。切成小块后,系统才能定位与问题相关的局部原文。

向量是什么?

文本嵌入模型生成的一串数字,用来表示问题或原文的意思。

为什么问题和原文要使用同一个嵌入模型?

只有在同一套向量空间中,距离才有比较意义。

向量数据库解决什么问题?

保存文本向量,并找到与问题向量最接近的原文。

Redis 为什么只是选择之一?

Redis 能做向量检索QdrantMilvus 是专门的向量数据库,Pinecone 是托管服务。工具不同,但 RAG 流程相同

大模型为什么不是凭记忆回答?

应用程序先检索 PDF 原文,再把原文放进本次模型上下文。

为什么不能让大模型自己填写页码?

模型可能编造。页码应在解析 PDF 时保存,并由检索结果映射回来。

参考