阶段 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
Embedding OpenAI 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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐