阶段 3:RAG 基础与 PDF 知识库问答项目学习笔记

学习目标:理解 RAG 的完整链路,掌握文件检索、向量化、文档切块、向量数据库、来源引用等核心概念,并能完成一个可演示、可写进简历、可在面试中讲清楚的 PDF RAG 项目。


1. 阶段 3 学习目标

阶段 3 的重点是从“会调用大模型”升级到“能构建一个带外部知识库的问答系统”。

你需要掌握的不只是 LangChain 或 Chroma 的调用方法,更重要的是理解:

  • RAG 为什么存在;
  • RAG 解决了大模型的什么问题;
  • 文档为什么要切块;
  • embedding 为什么可以用于语义搜索;
  • vector store 存储了什么;
  • top-k 怎么影响召回效果;
  • metadata 为什么对来源追踪很重要;
  • 一个 PDF RAG 项目怎么从 0 到 1 落地;
  • 面试时如何解释你的设计取舍。

本阶段最终要完成的项目:

基于 RAG 的论文 / PDF 知识库问答系统。

最低功能要求:

  1. 支持多个 PDF 文档;
  2. 自动解析 PDF 内容;
  3. 自动切块;
  4. 自动生成 embedding;
  5. 存入向量数据库;
  6. 用户提问时先检索相关片段;
  7. 大模型基于检索结果生成答案;
  8. 返回答案时附带来源文件、页码和原文片段。

2. RAG 是什么

2.1 RAG 的定义

RAG 全称是:

Retrieval-Augmented Generation
检索增强生成

可以拆成两个部分:

部分含义
Retrieval检索,从外部知识库中找到和问题相关的内容
Augmented Generation增强生成,把检索到的内容作为上下文交给大模型生成答案

普通大模型问答流程是:

用户问题 → 大模型 → 答案

RAG 问答流程是:

用户问题 → 检索知识库 → 得到相关文档片段 → 拼接上下文 → 大模型 → 答案 + 来源

RAG 的核心思想是:

让大模型在回答前先查资料,再基于资料回答。


2.2 RAG 和普通 Prompt 的区别

普通 Prompt:

请解释这篇论文的核心方法。

如果你没有把论文内容给模型,模型只能依赖它已有的知识和推测。

RAG Prompt:

请根据下面检索到的论文片段回答问题。
如果资料中没有答案,请回答无法确定。

同时把检索到的内容提供给模型。

区别在于:

对比项普通问答RAG 问答
知识来源模型参数中的知识外部知识库 + 模型能力
是否可更新较难更新可以更新文档库
是否能查私有文档不能可以
是否能返回来源通常不能可以
幻觉风险较高相对较低
适合场景通用问答企业知识库、论文问答、合同问答、客服知识库

3. RAG 为什么能减少幻觉

3.1 大模型幻觉是什么

大模型幻觉指的是:

模型生成了看起来合理,但实际上不真实、不准确或没有依据的内容。

例如你问:

这篇 PDF 第三章提出了什么算法?

如果模型没有看到 PDF,它可能会根据论文标题或常见论文结构编造一个答案。

这不是模型“故意撒谎”,而是因为大模型本质上是在根据上下文预测下一个 token,它并不会天然知道自己是否掌握事实依据。


3.2 幻觉产生的常见原因

常见原因包括:

  1. 没有足够上下文
    用户问题依赖外部资料,但模型没有看到资料。

  2. 模型参数知识过时
    大模型训练数据有时间边界,可能不知道最新内容。

  3. 问题过于具体
    比如某个内部文档、某篇论文、某个公司制度。

  4. 模型过度泛化
    模型会根据相似场景生成“看起来像”的答案。

  5. Prompt 限制不够
    没有要求模型“只根据资料回答”,模型就可能补充无依据内容。


3.3 RAG 减少幻觉的方式

RAG 通过三个机制降低幻觉:

1. 外部知识注入

先从知识库中检索真实资料,再让模型基于资料回答。

问题 → 找资料 → 基于资料回答

2. 限制模型回答范围

通过 prompt 约束模型:

请只根据参考资料回答。
如果参考资料中没有答案,请回答“根据当前资料无法确定”。

3. 返回来源,方便验证

回答中附带:

来源:paper1.pdf,第 5 页

用户可以检查答案是否真的来自原文。


3.4 RAG 不能完全消除幻觉

需要注意:

RAG 能降低幻觉,但不能完全消除幻觉。

原因是 RAG 仍然可能在这些地方出错:

环节可能问题
PDF 解析表格、公式、图片解析错误
文档切块关键信息被切断
embedding语义表示不准确
检索找到的 chunk 不相关
top-k召回过少或过多
prompt没有限制模型只能基于资料回答
生成模型过度总结、补充不存在的信息

所以 RAG 系统的质量取决于整条链路,而不是只看大模型本身。


4. RAG 的完整工作流程

RAG 通常分为两个阶段:

  1. 离线入库阶段;
  2. 在线问答阶段。

4.1 离线入库阶段

离线入库就是提前把 PDF、Word、网页等文档处理好,存入知识库。

流程:

PDF 文件

文本解析

文档清洗

文档切块 Chunking

生成 Embedding

存入 Vector Store

形成可检索知识库

对应步骤说明:

步骤作用
文档解析从 PDF 中提取文本
文档清洗去除空行、页眉页脚、乱码
文档切块把长文本切成多个 chunk
向量化把每个 chunk 转成 embedding
入库把 chunk、embedding、metadata 存入向量数据库

4.2 在线问答阶段

在线问答就是用户输入问题后,系统实时检索并生成答案。

流程:

用户问题

问题向量化

向量数据库相似度检索

取回 Top-k Chunk

构造上下文 Context

Prompt 拼接

大模型生成答案

返回答案和来源

简化理解:

用户问问题
系统先从知识库查资料
把查到的资料给大模型
大模型基于资料回答
最后返回答案和引用来源

5. RAG 的核心模块

一个完整 RAG 系统可以拆成这些核心模块:

模块作用
Loader加载 PDF、网页、Markdown、Word 等文档
Parser解析文档内容
Cleaner清洗文本
Splitter文档切块
Embedding Model生成文本向量
Vector Store存储和检索向量
Retriever根据问题召回相关 chunk
Prompt Builder构造上下文和提示词
LLM基于上下文生成答案
Source Tracker返回来源文件、页码和片段
API Layer提供上传、检索、问答接口

面试时可以这样总结:

RAG 系统本质上是一个“检索系统 + 生成系统”的组合。检索系统负责找到相关知识,生成系统负责把知识组织成自然语言答案。


6. PDF 文档处理流程

PDF RAG 项目的第一步是处理 PDF。

6.1 PDF 解析的难点

PDF 和普通文本文件不一样,它不是天然结构化的。常见问题:

  1. 文字顺序可能错乱;
  2. 页眉页脚会干扰内容;
  3. 表格解析困难;
  4. 公式可能无法提取;
  5. 图片中的文字需要 OCR;
  6. 双栏论文可能解析顺序错误;
  7. 页码、脚注、参考文献会影响检索。

所以 PDF RAG 的质量很大程度取决于 PDF 解析质量。


6.2 PDF 解析后需要保留页码

每一页解析出来后,应该保留 metadata:

{
  "source": "paper1.pdf",
  "page": 3
}

因为后面需要引用来源:

来源:paper1.pdf,第 3 页

如果解析阶段没有保存页码,后面很难补救。


6.3 文档清洗

PDF 解析后通常需要清洗:

  1. 删除多余空行;
  2. 删除重复页眉页脚;
  3. 合并断行;
  4. 去掉无意义字符;
  5. 保留标题和段落结构;
  6. 对参考文献部分可以选择保留或降低权重。

示例:

原始解析结果:

Retrieval-Augmented
Generation is a method

Page 3

for improving language models.

清洗后:

Retrieval-Augmented Generation is a method for improving language models.

7. Chunk 文档切块策略

7.1 什么是 chunk

chunk 是文档被切分后的一个小文本片段。

例如一篇论文有 30 页,系统不会把整篇论文一次性放入 prompt,而是拆成多个 chunk:

chunk 1:摘要部分
chunk 2:引言部分
chunk 3:方法部分第一段
chunk 4:方法部分第二段
...

每个 chunk 会单独向量化,并作为检索的基本单位。


7.2 为什么必须切块

原因主要有四个:

1. 模型上下文有限

长文档可能有几万字,无法每次全部塞给模型。

2. 检索需要细粒度

如果整篇论文作为一个 chunk,用户问一个具体问题时,召回结果太粗。

3. 降低成本

只把相关 chunk 放入 prompt,可以减少 token 成本。

4. 提高答案准确性

相关片段越聚焦,模型越不容易被无关内容干扰。


7.3 chunk size 太大有什么问题

chunk 太大时:

  1. 一个 chunk 中包含多个主题;
  2. 检索结果不够精准;
  3. prompt token 消耗增加;
  4. 模型容易被无关内容干扰;
  5. 来源片段不够精确。

例如一个 chunk 同时包含:

研究背景 + 方法设计 + 实验设置 + 结论

用户只问“实验设置”,但系统把其他内容也带进来,会影响回答质量。


7.4 chunk size 太小有什么问题

chunk 太小时:

  1. 语义不完整;
  2. 上下文断裂;
  3. 检索到的片段不够回答问题;
  4. 需要更大的 top-k 补充上下文;
  5. 模型可能误解片段含义。

例如:

chunk A:该方法包含三个模块。
chunk B:第一个模块是视觉编码器。
chunk C:第二个模块是文本编码器。
chunk D:第三个模块是跨模态注意力模块。

如果只召回 chunk A,模型不知道三个模块是什么。


7.5 overlap 重叠窗口

overlap 指相邻 chunk 之间保留一部分重复内容。

例如:

chunk 1:A B C D E
chunk 2:D E F G H
chunk 3:G H I J K

D、E 和 G、H 就是 overlap。

overlap 的作用:

  1. 避免重要信息被切断;
  2. 保持上下文连续;
  3. 提高召回质量;
  4. 减少“上一段提到的 this / it / they 指代不清”的问题。

7.6 常见切块策略

策略说明优点缺点
固定长度切块按字符或 token 数切简单稳定可能切断语义
段落切块按自然段切语义较完整段落长度不稳定
标题 / 章节切块按 Abstract、Method 等切适合论文实现复杂
递归切块先按章节,再按段落,再按句子效果较好参数较多
语义切块根据语义边界切质量高实现成本高

学习项目推荐:

第一版:固定长度切块 + overlap
第二版:递归切块
第三版:章节 / 标题感知切块

7.7 推荐参数

对于中文文档:

chunk_size = 500 ~ 1000
chunk_overlap = 100 ~ 200

对于英文论文:

chunk_size = 800 ~ 1200
chunk_overlap = 150 ~ 250

第一版可以使用:

chunk_size = 800
chunk_overlap = 150

面试解释:

我选择 chunk size 800、overlap 150,是为了在语义完整性和检索精度之间做平衡。chunk 太小会导致上下文不足,chunk 太大又会引入噪声。overlap 可以避免重要内容被切断。


8. Embedding 向量化

8.1 什么是 embedding

embedding 是把文本转成一组数字向量。

例如:

文本:RAG 是检索增强生成
向量:[0.12, -0.35, 0.78, ...]

这些数字不是随机的,而是表示文本的语义特征。

语义相近的文本,向量距离也更近。

例如:

句子 A:RAG 可以减少幻觉
句子 B:检索增强生成可以降低模型编造答案的概率

这两个句子字面不同,但语义接近,所以 embedding 后的向量也会接近。


8.2 为什么 embedding 能做语义搜索

关键词搜索依赖字面匹配:

用户问:如何降低幻觉?
文档写:减少模型编造事实。

如果没有“幻觉”这个词,关键词搜索可能找不到。

embedding 搜索看语义:

降低幻觉 ≈ 减少编造事实

因此可以召回相关内容。


8.3 embedding 在 RAG 中的作用

RAG 中有两类文本需要 embedding:

  1. 文档 chunk;
  2. 用户问题。

入库时:

chunk 文本 → embedding → 存入 vector store

查询时:

用户问题 → embedding → 和数据库中的 chunk 向量比较相似度

8.4 embedding 模型选择

不同 embedding 模型会影响检索效果。

选择时关注:

  1. 是否支持中文;
  2. 向量维度;
  3. 速度;
  4. 成本;
  5. 本地部署还是 API 调用;
  6. 对长文本的支持程度。

学习项目可以先使用 API embedding 或开源 embedding 模型。


9. Vector Store 向量数据库

9.1 Vector Store 是什么

Vector Store 是专门用于存储和检索向量的数据库。

它一般保存:

  1. 原始文本 chunk;
  2. chunk 对应的 embedding;
  3. metadata 元数据;
  4. 文档 ID。

示例:

{
  "id": "paper1_page3_chunk2",
  "text": "本文提出了一种基于 Transformer 的多模态融合方法...",
  "embedding": [0.12, -0.35, 0.78],
  "metadata": {
    "source": "paper1.pdf",
    "page": 3,
    "section": "Method"
  }
}

9.2 常见向量数据库

工具适用场景
Chroma学习项目、本地小型知识库
FAISS高性能本地向量检索
Milvus大规模生产级向量检索
Weaviate支持丰富 schema 和混合检索
Qdrant开源、性能好、工程友好
Pinecone云服务,使用方便

你的学习项目推荐:

Chroma

原因:

  1. 本地运行简单;
  2. LangChain 支持好;
  3. 适合多 PDF 小型知识库;
  4. 不需要复杂部署;
  5. 便于演示。

10. Semantic Search 与 Keyword Search

10.1 Keyword Search 关键词搜索

关键词搜索基于字面匹配。

例如用户问:

Transformer 的注意力机制是什么?

系统会找包含:

Transformer
注意力机制

这些词的文档。

常见技术:

  1. 倒排索引;
  2. BM25;
  3. Elasticsearch;
  4. OpenSearch。

优点:

  1. 对专有名词、编号、公式、代码很准;
  2. 可解释性强;
  3. 查询速度快;
  4. 不需要 embedding。

缺点:

  1. 不理解同义表达;
  2. 用户说法和文档说法不同可能搜不到;
  3. 对自然语言问题不够友好。

10.2 Semantic Search 语义搜索

语义搜索基于 embedding 相似度。

它不只看字面词,而是看语义接近程度。

例如:

用户问:如何降低模型胡说?
文档写:RAG 可以减少 hallucination。

关键词不同,但语义相关,语义搜索可以召回。

优点:

  1. 能处理同义表达;
  2. 适合自然语言问题;
  3. 对总结、解释类问题效果好;
  4. 是 RAG 中最常见的检索方式。

缺点:

  1. 对公式、编号、精确术语可能不如关键词搜索;
  2. 可能召回“看起来语义相似但事实不相关”的内容;
  3. 依赖 embedding 模型质量。

10.3 Hybrid Search 混合检索

实际项目中,常结合:

关键词搜索 + 语义搜索

即 Hybrid Search。

适合:

  1. 论文问答;
  2. 技术文档问答;
  3. 法律合同问答;
  4. API 文档问答;
  5. 企业内部知识库。

升级路线:

第一版:向量检索
第二版:关键词检索 + 向量检索
第三版:混合检索 + rerank

11. Top-k、相似度和检索质量

11.1 什么是 top-k

top-k 指检索时返回最相关的前 k 个 chunk。

例如:

top_k = 5

表示返回最相关的 5 个片段。


11.2 top-k 太小的问题

如果 top-k 太小:

  1. 可能漏掉关键信息;
  2. 对综合性问题回答不完整;
  3. 多文档对比问题效果差。

例如用户问:

这几篇论文的方法有什么区别?

如果只返回 3 个 chunk,可能覆盖不到所有论文。


11.3 top-k 太大的问题

如果 top-k 太大:

  1. 无关内容变多;
  2. prompt 变长;
  3. token 成本上升;
  4. 模型容易被噪声干扰;
  5. 响应延迟增加。

11.4 top-k 推荐设置

问题类型推荐 top-k
事实型问题3
普通解释型问题5
总结型问题6-8
多文档对比8-12
长文档综述先分阶段检索,不建议一次 top-k 过大

第一版项目默认:

top_k = 5

11.5 相似度分数

向量检索通常会返回相似度分数。

你可以用它判断检索结果是否可靠。

例如:

[
  {"content": "...", "score": 0.88},
  {"content": "...", "score": 0.82},
  {"content": "...", "score": 0.41}
]

如果最高分都很低,说明知识库可能没有相关内容。

可以设置阈值:

score_threshold = 0.5

如果低于阈值:

根据当前知识库无法找到足够相关的资料。

这样可以减少模型胡编。


12. Metadata Filter 元数据过滤

12.1 metadata 是什么

metadata 是每个 chunk 附带的结构化信息。

常见字段:

{
  "source": "paper1.pdf",
  "page": 12,
  "section": "Experiment",
  "chunk_id": "paper1_p12_c03",
  "created_at": "2026-04-24"
}

12.2 metadata 的作用

1. 来源追踪

回答时显示:

来源:paper1.pdf,第 12 页

2. 限定检索范围

例如只查某个文件:

filter = {"source": "paper1.pdf"}

3. 文档管理

可以删除某个 PDF 对应的所有 chunk:

delete where source = "old_paper.pdf"

4. 高级筛选

例如:

只查 Method 章节
只查 2024 年后的论文
只查某个作者的文档

12.3 面试回答

面试官问 metadata 有什么用,可以回答:

metadata 用来解决过滤和追溯问题。我在每个 chunk 入库时保存 source、page、chunk_id 等信息。检索时可以根据文件名、章节等字段过滤范围;生成答案后,也可以把对应文件名和页码返回给用户,保证答案可验证、可追溯。


13. 引用来源与可追溯性

13.1 为什么 RAG 必须返回来源

一个合格的 RAG 系统不应该只返回答案,还应该返回依据。

原因:

  1. 用户可以验证答案;
  2. 系统更可信;
  3. 方便调试检索效果;
  4. 避免模型无依据总结;
  5. 面试时能体现工程完整性。

13.2 推荐返回格式

{
  "answer": "根据论文内容,该方法主要包括三个模块:视觉编码器、文本编码器和跨模态注意力模块。",
  "sources": [
    {
      "file": "paper1.pdf",
      "page": 5,
      "content": "The proposed method consists of three components..."
    },
    {
      "file": "paper1.pdf",
      "page": 6,
      "content": "The cross-modal attention module is used to..."
    }
  ]
}

13.3 来源展示原则

好的来源展示应该包含:

  1. 文件名;
  2. 页码;
  3. chunk 片段;
  4. 相似度分数;
  5. 必要时包含章节名。

例如:

来源 1
文件:rag_survey.pdf
页码:第 8 页
相似度:0.86
片段:Retrieval-augmented generation combines retrieval with generation...

14. PDF RAG 项目设计

14.1 项目名称

推荐名称:

基于 RAG 的论文知识库智能问答系统

或者:

PDF Knowledge Base QA System Based on RAG

14.2 项目目标

实现一个支持多 PDF 的知识库问答系统:

  1. 用户上传 PDF;
  2. 系统解析 PDF;
  3. 文档切块;
  4. 生成 embedding;
  5. 存入 Chroma;
  6. 用户输入问题;
  7. 系统检索相关片段;
  8. 大模型生成答案;
  9. 返回答案和引用来源。

14.3 系统架构

用户

FastAPI 服务

PDF 上传模块

PDF 解析模块

文本清洗模块

文档切块模块

Embedding 模块

Chroma 向量数据库

问答接口

Retriever 检索模块

上下文构造

Prompt Builder

大模型

答案 + 来源


14.4 技术选型

模块推荐技术
后端服务FastAPI
PDF 解析PyMuPDF / pypdf
文本切块LangChain TextSplitter
EmbeddingOpenAI Embedding / BGE / M3E
向量数据库Chroma
大模型调用OpenAI API / 其他兼容接口
配置管理python-dotenv
数据校验Pydantic
日志logging
部署Docker / docker-compose

15. FastAPI 接口设计

15.1 上传 PDF 接口

POST /documents/upload

功能:

  1. 接收 PDF 文件;
  2. 保存到本地;
  3. 解析文本;
  4. 切块;
  5. 向量化;
  6. 写入 Chroma。

返回示例:

{
  "message": "PDF uploaded and indexed successfully",
  "filename": "rag_paper.pdf",
  "chunks": 128
}

15.2 批量上传接口

POST /documents/batch-upload

功能:

  1. 支持多个 PDF;
  2. 每个文件单独解析;
  3. 统一入库;
  4. 返回每个文件的 chunk 数量。

15.3 问答接口

POST /qa

请求:

{
  "question": "这篇论文的核心方法是什么?",
  "top_k": 5,
  "source": "paper1.pdf"
}

返回:

{
  "answer": "根据检索到的论文内容,核心方法是……",
  "sources": [
    {
      "file": "paper1.pdf",
      "page": 3,
      "content": "本文提出了一种……",
      "score": 0.87
    }
  ]
}

15.4 检索接口

POST /search

只返回检索结果,不调用大模型。

作用:

  1. 调试检索质量;
  2. 观察 top-k 结果;
  3. 判断是检索问题还是生成问题。

请求:

{
  "query": "RAG 如何减少幻觉?",
  "top_k": 5
}

返回:

{
  "results": [
    {
      "file": "rag_intro.pdf",
      "page": 2,
      "content": "Retrieval augmented generation reduces hallucination by...",
      "score": 0.89
    }
  ]
}

这个接口很重要,因为它能让你定位问题:

如果检索结果不相关,是检索问题;
如果检索结果相关但答案错误,是生成问题或 prompt 问题。

16. 项目代码结构建议

推荐目录结构:

pdf-rag-demo/
├── app/
│   ├── main.py                 # FastAPI 入口
│   ├── config.py               # 配置文件
│   ├── schemas.py              # Pydantic 请求和响应模型
│   ├── services/
│   │   ├── pdf_loader.py       # PDF 解析
│   │   ├── text_cleaner.py     # 文本清洗
│   │   ├── splitter.py         # 文档切块
│   │   ├── vector_store.py     # Chroma 入库与检索
│   │   ├── rag_service.py      # RAG 主流程
│   │   └── llm_service.py      # 大模型调用
│   ├── utils/
│   │   ├── logger.py           # 日志
│   │   └── file_utils.py       # 文件工具
│   └── prompts/
│       └── rag_prompt.py       # Prompt 模板
├── data/
│   ├── uploads/                # 上传 PDF
│   └── chroma/                 # Chroma 持久化目录
├── tests/
│   ├── test_splitter.py
│   ├── test_retriever.py
│   └── test_rag.py
├── requirements.txt
├── .env
├── README.md
└── Dockerfile

17. 核心代码骨架

以下代码是学习用骨架,重点理解流程,不要求一开始完全照搬。


17.1 PDF 解析

from pathlib import Path
import fitz  # PyMuPDF


def load_pdf(file_path: str) -> list[dict]:
    """
    解析 PDF,每一页返回一个 dict。
    保留 source 和 page,方便后续引用来源。
    """
    pages = []
    path = Path(file_path)

    doc = fitz.open(file_path)

    for page_index, page in enumerate(doc):
        text = page.get_text()

        if not text or not text.strip():
            continue

        pages.append({
            "text": text.strip(),
            "metadata": {
                "source": path.name,
                "page": page_index + 1
            }
        })

    return pages

17.2 文本清洗

import re


def clean_text(text: str) -> str:
    """
    简单文本清洗:
    1. 去除多余空白
    2. 合并连续空行
    3. 保留基本段落结构
    """
    text = text.replace("\r", "\n")
    text = re.sub(r"\n{3,}", "\n\n", text)
    text = re.sub(r"[ \t]+", " ", text)
    return text.strip()

17.3 文档切块

from langchain_text_splitters import RecursiveCharacterTextSplitter


def split_pages(pages: list[dict]) -> list[dict]:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=150,
        separators=["\n\n", "\n", "。", ".", " ", ""]
    )

    chunks = []

    for page in pages:
        text = page["text"]
        metadata = page["metadata"]

        split_texts = splitter.split_text(text)

        for idx, chunk_text in enumerate(split_texts):
            chunks.append({
                "text": chunk_text,
                "metadata": {
                    **metadata,
                    "chunk_index": idx
                }
            })

    return chunks

17.4 向量化入库

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings


def build_vector_store(chunks: list[dict]):
    embeddings = OpenAIEmbeddings()

    texts = [chunk["text"] for chunk in chunks]
    metadatas = [chunk["metadata"] for chunk in chunks]

    vector_store = Chroma(
        collection_name="pdf_knowledge_base",
        embedding_function=embeddings,
        persist_directory="./data/chroma"
    )

    vector_store.add_texts(
        texts=texts,
        metadatas=metadatas
    )

    return vector_store

17.5 检索

def retrieve(vector_store, question: str, top_k: int = 5, source: str | None = None):
    """
    根据问题检索 top-k 个相关 chunk。
    如果指定 source,则只检索某个 PDF。
    """
    search_kwargs = {"k": top_k}

    if source:
        search_kwargs["filter"] = {"source": source}

    retriever = vector_store.as_retriever(
        search_kwargs=search_kwargs
    )

    docs = retriever.invoke(question)
    return docs

17.6 构造上下文

def build_context(docs) -> str:
    context_parts = []

    for i, doc in enumerate(docs):
        source = doc.metadata.get("source", "unknown")
        page = doc.metadata.get("page", "unknown")
        content = doc.page_content

        context_parts.append(
            f"[来源 {i + 1}] 文件:{source},页码:{page}\n{content}"
        )

    return "\n\n".join(context_parts)

17.7 Prompt 模板

RAG_PROMPT = """
你是一个严谨的论文知识库问答助手。

请只根据下面的参考资料回答用户问题。
如果参考资料中没有足够信息,请回答:
“根据当前资料无法确定。”

用户问题:
{question}

参考资料:
{context}

回答要求:
1. 使用中文回答;
2. 回答要准确、简洁、有条理;
3. 不要编造参考资料中没有的信息;
4. 如果资料不足,请明确说明;
5. 最后列出引用来源。
"""

17.8 RAG 主流程

def answer_question(vector_store, llm, question: str, top_k: int = 5, source: str | None = None):
    docs = retrieve(
        vector_store=vector_store,
        question=question,
        top_k=top_k,
        source=source
    )

    if not docs:
        return {
            "answer": "根据当前知识库无法找到相关资料。",
            "sources": []
        }

    context = build_context(docs)

    prompt = RAG_PROMPT.format(
        question=question,
        context=context
    )

    answer = llm.invoke(prompt)

    sources = []
    for doc in docs:
        sources.append({
            "file": doc.metadata.get("source"),
            "page": doc.metadata.get("page"),
            "chunk_index": doc.metadata.get("chunk_index"),
            "content": doc.page_content[:300]
        })

    return {
        "answer": answer,
        "sources": sources
    }

18. RAG 效果优化方法

18.1 优化 PDF 解析

检查:

  1. 是否有乱码;
  2. 是否解析出页眉页脚;
  3. 双栏论文顺序是否错乱;
  4. 表格是否丢失;
  5. 公式是否被破坏。

优化方式:

  1. 换 PDF 解析工具;
  2. 对页眉页脚做规则过滤;
  3. 对表格单独处理;
  4. 对扫描版 PDF 使用 OCR;
  5. 把图片、表格、公式作为后续升级点。

18.2 优化 chunk

可以实验:

chunk_size = 500
chunk_size = 800
chunk_size = 1200

观察:

  1. 检索结果是否完整;
  2. 片段是否包含太多无关内容;
  3. 回答是否准确;
  4. token 成本是否可接受。

18.3 优化 top-k

可以实验:

top_k = 3
top_k = 5
top_k = 8

观察:

  1. top-k 太小时是否漏信息;
  2. top-k 太大时是否引入噪声;
  3. 回答是否变得啰嗦;
  4. 响应速度是否下降。

18.4 增加 score threshold

如果检索分数低于阈值,拒绝回答:

当前知识库中没有找到足够相关的内容。

这样可以防止模型基于弱相关资料胡编。


18.5 增加 rerank

基础流程:

向量检索 top-5 → 生成答案

升级流程:

向量检索 top-20 → rerank 重排序 → 取 top-5 → 生成答案

rerank 的作用:

对初步召回结果重新排序,把真正和问题最相关的内容放到前面。

面试中提到 rerank 是加分点。


18.6 增加 Hybrid Search

升级前:

只用向量检索

升级后:

BM25 关键词检索 + 向量检索 + rerank

适合处理:

  1. 公式;
  2. 表格编号;
  3. 专有名词;
  4. 算法名称;
  5. API 名称;
  6. 论文中的 Figure、Table、Equation。

18.7 优化 Prompt

差的 prompt:

根据资料回答问题。

更好的 prompt:

请只根据参考资料回答。
如果参考资料中没有答案,请明确说明无法确定。
不要使用外部知识补充。
回答后列出引用来源。

Prompt 的核心目标:

  1. 限制模型不要编造;
  2. 明确回答格式;
  3. 要求引用来源;
  4. 允许模型在资料不足时拒答。

19. 常见问题与排查思路

19.1 答案不准确,怎么排查?

按照顺序排查:

1. 原始 PDF 解析是否正确?
2. chunk 是否切得合理?
3. 检索结果是否相关?
4. top-k 是否合适?
5. prompt 是否限制了模型?
6. 模型是否基于资料回答?

最重要的是先看检索结果。

如果检索结果本身不相关:

问题在检索阶段。

如果检索结果相关,但答案错误:

问题在 prompt 或生成阶段。

19.2 检索结果不相关怎么办?

可能原因:

  1. chunk 太大;
  2. chunk 太小;
  3. embedding 模型不适合中文 / 领域文档;
  4. 用户问题表达太模糊;
  5. PDF 解析质量差;
  6. top-k 参数不合适;
  7. 文档库中确实没有相关内容。

优化方式:

  1. 调整 chunk size;
  2. 增加 overlap;
  3. 换 embedding 模型;
  4. 加 metadata filter;
  5. 加 hybrid search;
  6. 加 rerank;
  7. 增加检索结果展示接口方便调试。

19.3 为什么回答有来源,但内容还是错?

可能原因:

  1. 来源片段不够支持答案;
  2. 模型过度总结;
  3. prompt 约束不够;
  4. 多个来源之间存在冲突;
  5. 模型引用了来源,但补充了来源中没有的信息。

优化方式:

  1. 要求模型逐条依据来源回答;
  2. 要求“不确定就说不确定”;
  3. 来源片段按相关性排序;
  4. 限制模型不能加入外部知识;
  5. 对答案做事实一致性检查。

20. 面试讲法与高频问题

20.1 一句话介绍项目

我做了一个基于 RAG 的论文知识库问答系统,支持多 PDF 上传。系统会解析 PDF 文本,按重叠窗口切成 chunk,然后生成 embedding 存入 Chroma 向量数据库。用户提问时,系统先进行语义检索,召回 top-k 个相关片段,再把这些片段作为上下文交给大模型生成答案,并返回文件名、页码和原文片段作为来源,保证答案可追溯。


20.2 面试官问:RAG 的完整流程是什么?

回答:

RAG 分为离线入库和在线问答两个阶段。离线阶段包括文档解析、文本清洗、chunk 切块、embedding 向量化和写入向量数据库。在线阶段中,用户输入问题后,系统把问题向量化,在向量数据库中做相似度检索,取回 top-k 个相关 chunk,然后把这些 chunk 拼接成上下文交给大模型生成答案,最后返回答案和来源引用。


20.3 面试官问:为什么 RAG 可以减少幻觉?

回答:

普通大模型回答问题时主要依赖参数知识,遇到未知或私有文档内容时容易编造。RAG 在生成前增加了检索步骤,让模型先从外部知识库中找到相关资料,再基于资料回答。同时通过 prompt 限制模型只根据参考资料回答,并返回来源供用户验证,所以可以降低幻觉风险。但 RAG 不能完全消除幻觉,如果检索结果不准或 prompt 约束不够,仍然可能出现错误。


20.4 面试官问:chunk size 怎么设计?

回答:

chunk size 需要在语义完整性和检索精度之间平衡。chunk 太小会导致上下文不完整,chunk 太大又会包含太多无关信息。我当前使用固定长度切块加 overlap,比如 chunk size 800、overlap 150,保证每个 chunk 有相对完整的语义,同时避免重要信息在切分边界丢失。后续可以升级为按章节、标题或段落进行语义切块。


20.5 面试官问:top-k 怎么选?

回答:

top-k 表示检索时返回最相关的前 k 个 chunk。top-k 太小容易漏掉关键信息,top-k 太大又会引入噪声、增加 token 成本并影响生成质量。我一般从 3 到 5 开始调试,事实型问题可以小一些,总结类或多文档对比问题可以适当增大。最终会根据召回结果相关性、答案完整性和响应延迟综合选择。


20.6 面试官问:metadata 有什么用?

回答:

metadata 用于过滤、管理和来源追踪。每个 chunk 入库时我会保存 source、page、chunk_id 等信息。用户如果只想查询某篇 PDF,可以通过 source 做过滤。生成答案时,也可以根据 metadata 返回文件名和页码,让答案可验证、可追溯。


20.7 面试官问:如果检索不准,你怎么优化?

回答:

我会先看原始检索结果,判断是检索问题还是生成问题。如果检索结果不相关,我会检查 PDF 解析质量、chunk 切分是否合理、embedding 模型是否适合当前文档、top-k 是否过大或过小。然后可以尝试调整 chunk size 和 overlap,引入 metadata filter,或者增加 BM25 + 向量检索的混合检索。进一步还可以在召回后加入 rerank,提高最终上下文质量。


20.8 面试官问:RAG 和微调有什么区别?

回答:

RAG 是在推理时检索外部知识,把相关资料放进上下文中,让模型基于资料回答。微调是改变模型参数,让模型学习某种风格、格式或任务能力。对于频繁更新的知识、企业内部文档、论文知识库,更适合 RAG,因为文档可以随时更新;对于固定任务格式、特定输出风格或领域表达习惯,可以考虑微调。很多实际系统也会组合使用 RAG 和微调。


20.9 面试官问:你的项目有什么不足?

回答:

当前版本主要实现了基础 RAG 链路,使用固定长度切块和向量检索,适合小规模 PDF 问答。不足是对复杂 PDF 的表格、公式和图片支持有限;检索上主要依赖语义向量,对精确术语和公式编号的召回可能不够好。后续可以加入章节感知切块、Hybrid Search、rerank、表格解析和检索评估指标,进一步提升效果。


21. 简历写法

21.1 基础版本

基于 RAG 的论文知识库问答系统
- 基于 LangChain / Chroma 构建多 PDF 知识库问答系统,支持 PDF 解析、文本切块、向量化入库和语义检索。
- 设计 chunk size + overlap 的文档切块策略,并为每个 chunk 维护 source、page、chunk_id 等 metadata,实现答案来源追踪。
- 用户提问时先通过向量检索召回 top-k 相关片段,再将检索结果注入 Prompt,引导大模型基于上下文生成回答,降低幻觉风险。
- 支持返回答案引用来源,包括文件名、页码和原文片段,提升系统可解释性与结果可信度。

21.2 强化工程版本

基于 RAG 的论文知识库智能问答系统
- 使用 FastAPI + LangChain + Chroma 搭建端到端 PDF RAG 系统,支持多 PDF 上传、自动解析、切块、向量化入库和知识库问答。
- 设计文档处理流水线,将 PDF 页面内容切分为带有 source、page、chunk_id 的文本块,支持基于 metadata 的检索过滤和答案来源追踪。
- 实现 Retriever + Prompt + LLM 的问答链路,用户提问后先召回 top-k 相关片段,再基于上下文生成答案,并返回引用片段以降低幻觉。
- 提供上传、检索、问答等 RESTful API,支持检索结果展示,便于分析召回质量和定位问答错误原因。

21.3 可扩展优化版本

- 针对检索效果进行 chunk size、overlap、top-k 等参数调优,并设计 Hybrid Search / Rerank 作为后续优化方向,提高复杂问题下的召回准确率。

22.3 最核心的一句话

RAG 的本质是:

先检索外部知识,再让大模型基于检索到的资料生成答案。

PDF RAG 项目的核心链路是:

PDF → 解析 → 清洗 → 切块 → Embedding → Vector Store → 检索 → Context → LLM → 答案 + 来源

面试中你最需要体现的是:

我不是只会调库,而是理解 RAG 系统每个环节的作用、问题和优化方向。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐