本文范围:只完成三本 PDF 的最小 RAG 闭环——上传、提取文字、切块、向量化、检索、大模型回答、返回页码。
不涉及 OCR、切块调优、嵌入模型选型、混合检索、重排、评测和部署,后续分别展开。
先说结论
RAG(Retrieval-Augmented Generation,检索增强生成)就是:先从 PDF 中找到相关原文,再让大模型根据原文回答。
上传时把 PDF 做成可搜索的资料;提问时先找原文,再让大模型回答。
本文用三本大模型书籍,跑通这条最小主线。
案例:和三本书对话
我上传三本 PDF:
- 《大模型应用开发极简入门》
- 《图解大模型:生成式AI原理与实战》
- 《从零构建大模型》
然后提问:
RAG 是怎样让大模型根据这三本书回答,而不是凭记忆乱说的?
系统先检索三本书中的相关原文,再生成答案:
上传时,系统把三本书切成带书名和页码的文本块,并建立向量索引。提问时,系统先找到相关文本块,再把问题和原文交给大模型。这次回答使用的是检索出的证据,不依赖模型记忆。
〔《大模型应用开发极简入门》,PDF 第 146—148 页〕〔《图解大模型》,PDF 第 228、244—245 页〕〔《从零构建大模型》,PDF 第 34 页〕
整个系统分成两段:
- 上传时:提取文字 → 切块 → 向量化 → 存储。
- 提问时:问题向量化 → 检索原文 → 大模型回答。
三本书只是案例。换成其它 PDF,流程不变。
为什么不直接把整份 PDF 交给大模型
一份很短的 PDF,只问一两次,可以直接发送全文。
三本书需要反复提问,直接发送全文有三个问题:
- PDF 太长,可能超过模型上下文。
- 无关文字太多,噪音大、速度慢、成本高。
- 很难稳定返回答案对应的书名和页码。
所以 RAG 每次只把最相关的几段原文交给大模型。
向量数据库负责找原文,大模型负责组织答案。
上传 PDF:提取文字和页码
每本 PDF 上传后,系统先做三件事:
- 保存原始文件,并生成
pdf_id。 - 逐页提取文字。
- 给每页文字保留书名和 PDF 页码。
library_id:llm-study
pdf_id:book-app
title:大模型应用开发极简入门
page:146
text:这一页提取出的原文……这里的 page 是 PDF 文件页码,可能和书上印刷的页码不同。它的作用是让用户能够跳回原文。
扫描版 PDF 没有文字层,需要先做 OCR:
扫描页面 → OCR → 文字和页码为什么要切块
一本书包含很多主题。整本书只生成一个向量,系统只能得到一个模糊的“全书意思”,无法定位具体原文。
切块时只把握两点:
- 太大:一个块混入多个主题,噪音多。
- 太小:一句完整的话被切断,上下文丢失。
可以先这样做:
- 每页单独处理。
- 优先按段落切,长段落再按完整句子切。
- 每块约 500 字,相邻块重叠约 80 字。
- 带上章节标题、书名和页码。
500 和 80 只是起点,要用真实问题调整。
切完的一条数据是:
chunk_id:book-app-page-146-chunk-02
pdf_id:book-app
title:大模型应用开发极简入门
page:146
text:这一段原文……什么是向量
向量就是一串数字。文本嵌入模型用这串数字表示一段文字的意思。
“RAG 怎样根据 PDF 回答?”
→ [0.12, -0.48, 0.31, ...]意思接近的文字,向量通常也接近。系统因此可以计算:用户的问题和哪段原文最相似。
这里要分清两种向量:
- 大模型内部的词元向量:
- 供模型内部计算。
- RAG 使用的文本向量:
- 供检索系统比较问题和原文。
本文讨论第二种。还有一条硬规则:
PDF 文本块和用户问题必须使用同一个文本嵌入模型。
否则两边不在同一套向量空间里,距离没有比较意义。
什么是向量数据库
向量数据库保存文本向量,并找出与问题向量最接近的记录。
它返回的是原文,不是答案。
本文使用 Qdrant。一条数据可以理解为:
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,执行一次:
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 搜索:
question_vector = 文本嵌入模型(question)
chunks = Qdrant 查询(
vector = question_vector,
filter = {"library_id": "llm-study"},
top_k = 3
)library_id 把搜索范围限制在这三本书中。
一次可能返回:
[S1]《大模型应用开发极简入门》,PDF 第 146 页
[S2]《图解大模型》,PDF 第 244 页
[S3]《从零构建大模型》,PDF 第 34 页Top-3 指最相关的三个文本块,不保证每本书各有一个。可以先取 3 到 5 个,再用真实问题调整。
向量数据库总能找到“最近”的文本,但最近不等于能够回答。系统还要设置相关度门槛,并检查证据是否足够;证据不足时,直接返回:
这三本书没有提供足够信息。
大模型根据原文回答
系统把问题和检索结果组成提示词:
只能根据下面的原文回答。
原文不足时,回答:
“这三本书没有提供足够信息。”
每个主要结论都要引用证据编号。
不要编造书名和页码。
问题:
{question}
证据:
[S1,书名,PDF 页码]
{原文}
[S2,书名,PDF 页码]
{原文}大模型只返回答案和 [S1]、[S2] 这些证据编号。
应用程序再把编号映射成真实链接:
[S1] → 大模型应用开发极简入门.pdf#page=146
[S2] → 图解大模型.pdf#page=244书名和页码来自检索结果,不让大模型自己填写。这样才能减少编造引用。
出错时按这条线检查
不要一上来就改 Prompt。按数据经过的顺序查:
- 提取错误:检查 PDF 实际提取出的文字;扫描件先做 OCR。
- 切块错误:检查完整意思有没有被切断,或一个块是否混入太多主题。
- 检索错误:直接看
Top-K中有没有正确原文;没有就调整切块、嵌入模型和候选数量。 - 精确词漏检:型号、缩写和专有名词可能需要关键词检索或混合检索。
- 生成错误:正确原文已经找回,模型仍然乱说,再检查提示词和证据约束。
- 没有答案:检索返回了相近文本,不代表 PDF 真能回答。
RAG 只能减少胡编,不能保证大模型永远正确。
怎样验证
先用三本书准备四类问题:
- 直接问题:原文中有接近的表达。
- 改写问题:意思相同,但不用原文关键词。
- 跨段问题:答案需要
相邻文本块。 - 无答案问题:三本书中没有相关信息。
每个问题只检查两件事:
- 检索:正确原文和页码是否进入
Top-K。 - 回答:模型是否只根据原文回答,引用是否正确,无答案时是否停止。
正确原文没有被找回,先修检索;原文已经找回但答案仍然错误,再修生成。
总结
我会把 PDF RAG 拆成上传和提问两段。
- 上传时,逐页提取文字并保留书名和页码,再切成文本块。用
文本嵌入模型生成向量,把向量、原文和页码存入Qdrant。 - 提问时,用
同一个嵌入模型生成问题向量,在当前资料库中找回相关原文,再把问题和原文交给大模型。- 证据不足就停止回答。
引用页码来自
检索结果,由应用程序映射回 PDF,不让大模型自己编。
考考你
PDF 为什么要切块?
一本书包含很多主题。切成小块后,系统才能定位与问题相关的局部原文。
向量是什么?
文本嵌入模型生成的一串数字,用来表示问题或原文的意思。
为什么问题和原文要使用同一个嵌入模型?
只有在同一套向量空间中,距离才有比较意义。
向量数据库解决什么问题?
保存文本向量,并找到与问题向量最接近的原文。
Redis 为什么只是选择之一?
Redis 能做向量检索,Qdrant 和 Milvus 是专门的向量数据库,Pinecone 是托管服务。工具不同,但 RAG 流程相同。
大模型为什么不是凭记忆回答?
应用程序先检索 PDF 原文,再把原文放进本次模型上下文。
为什么不能让大模型自己填写页码?
模型可能编造。页码应在解析 PDF 时保存,并由检索结果映射回来。
参考
- 【内部:《大模型应用开发极简入门:基于 GPT-4 和 ChatGPT(第 2 版)》PDF】,PDF 第 105—107 页、第 146—152 页。
- 【内部:《图解大模型:生成式AI原理与实战》PDF】,PDF 第 42 页、第 225—235 页、第 244—245 页。
- 【内部:《从零构建大模型》PDF】,PDF 第 34 页。
- Lewis 等人:Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Redis 官方:Redis as a vector database、Vector search concepts
- Qdrant 官方:Overview、Points、Local Quickstart
- Milvus 官方:What is Milvus
- Pinecone 官方:Pinecone documentation、Semantic search