基于Notebook的RAG实战指南:从零搭建检索增强生成知识库
有不少读者问我:想看一个能把 RAG 从零跑通的 Notebook 项目,而不是只看概念图。RAG 刚火的时候,大家关注的是“它是什么”,现在更多人关注的是“我的文档放进去,怎么才能真的搜得准、答得对”。
这篇文章就围绕一个RAG Refresher Notebook来展开。我会先讲清楚 RAG 的核心链路,再带你用一个 Jupyter Notebook 把整套流程完整跑一遍,包括文档加载、文本切分、向量化、检索、重排、生成回答。最后还会补充知识库指标的理解方式、常见报错排查,以及让 RAG 更精准的工程经验。
不管你是刚接触 RAG 的新手,还是已经搭过 demo 但效果不理想的开发者,这份笔记都值得收藏对照实践。
1. 为什么要用 Notebook 复现 RAG
1.1 RAG 是什么,解决什么问题
RAG 全称是 Retrieval-Augmented Generation,也就是“检索增强生成”。
通俗地说,大语言模型本身的知识有截止时间,也不知道你私有的业务文档。RAG 的思路是:在模型回答之前,先从你的知识库里把相关片段检索出来,再把这些片段和用户问题一起交给大模型,让模型基于检索到的内容组织答案。
典型流程如下:
用户问题 -> 文本向量化 -> 向量库检索 -> 召回相关片段 -> 拼接提示词 -> 大模型生成回答RAG 解决的核心问题是:
- 模型不知道你公司的制度、产品手册、私有 PDF 内容。
- 模型知识有截止日期,无法回答最新信息。
- 模型容易“一本正经地胡说八道”,RAG 用检索结果约束答案来源。
- 相比重新训练模型,RAG 更新知识成本低得多,换一个文档就能更新知识。
1.2 为什么 Notebook 是复现 RAG 的好工具
Jupyter Notebook 最大的特点是“按单元格运行、结果留在页面里”。这对 RAG 学习非常有帮助。
举例来说:
- 加载完文档后,你可以立刻打印前 500 个字,确认文档读对了。
- 切分完文本后,可以打印几个 chunk,看切得是否合理。
- 检索之后,可以把命中的文本打出来,人工判断“召回结果到底准不准”。
- 最后再调用大模型生成回答,整个过程是透明的,每一步都能看到中间结果。
这种“逐步可视化”的调试方式,比直接写一个.py脚本跑完要好理解得多。所以我一直建议,做 RAG 原型验证阶段,优先用 Notebook,跑通了再工程化封装成服务。
1.3 这份 Notebook 会带你完成什么
这份RAG Refresher Notebook的目标不是做一个生产级系统,而是帮你把 RAG 的完整链路亲手实现一遍。
完成之后,你将掌握:
- 文档加载与解析的常见方式。
- 文本切分的原理和参数含义。
- 使用开源 Embedding 模型做向量化。
- 使用 Chroma 作为本地向量库。
- 实现向量检索与简单的重排逻辑。
- 调用大模型生成最终回答。
- 理解 RAG 知识库评估指标。
2. 环境准备与工具链
2.1 Anaconda、Jupyter Notebook 和 Lab 怎么选
做 Python 数据类开发,Anaconda 是目前最省心的发行版。安装 Anaconda 后,自带的conda可以方便地创建虚拟环境。
很多新手会问:Jupyter Notebook 和 JupyterLab 到底有什么区别?
简单来说:
- Jupyter Notebook 是经典的单文档交互界面,适合逐格运行代码。
- JupyterLab 是新一代 IDE 风格界面,支持多标签、拖拽文件、终端、文件管理,功能更全面。
- 两者底层内核一样,代码、
.ipynb文件互相兼容。
如果你只是打开一个.ipynb快速运行,Notebook 够了。如果你要同时看文档、调代码、开终端,更推荐 JupyterLab。
在 Anaconda Prompt 中启动命令如下:
# 启动 Jupyter Notebook jupyter notebook # 启动 JupyterLab jupyter lab2.2 创建虚拟环境并安装依赖
建议为 RAG 项目单独创建一个虚拟环境,避免依赖冲突。
conda create -n rag_notebook python=3.10 -y conda activate rag_notebook然后安装依赖。这里需要注意版本,本示例以常见稳定版本为例,不同版本 API 可能有差异,请以你安装后的实际版本为准。
pip install jupyter pip install langchain langchain-community langchain-text-splitters langchain-huggingface pip install chromadb sentence-transformers pip install python-docx pypdf unstructured pip install openai如果你的网络较慢,建议使用国内镜像源安装,例如:
pip install langchain chromadb sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 Notebook 侧边栏标题总览
长 Notebook 写完后,右侧的“目录 / 大纲”视图非常有用。JupyterLab 默认有“Table of Contents”插件,可以点击左侧目录图标查看所有 Markdown 标题。
如果你使用的是 Jupyter Notebook 7,打开 Notebook 后,在视图菜单中开启“Show Table of Contents”即可。这样你可以像看文章目录一样,快速跳转到不同章节。
3. RAG 核心链路拆解
在写代码之前,先把 RAG 的每个环节拆开讲清楚。理解了每个环节的作用,后面看代码才不会晕。
3.1 文档加载与解析
RAG 的第一步是读取文档。文档可能是 PDF、Word、Markdown、HTML 或纯文本,不同格式需要不同的解析器。
常见文档加载器如下:
| 文档类型 | 常用加载器 | 说明 |
|---|---|---|
| TXT / Markdown | TextLoader | 最简单,读成纯文本 |
PyPDFLoader | 按页读取 PDF 文本 | |
| Word | Docx2txtLoader | 读取 .docx |
| HTML | BSHTMLLoader | 解析 HTML 标签 |
这里最常见的坑是:PDF 解析出来的文本可能丢失结构。比如一份带标题层级、表格、多栏排版的 PDF,用 PyPDF 直接抽取,结果可能是混乱的。
如果你的文档是合同、技术规范等强结构文档,建议优先使用保留了标题结构的信息源。例如先把 PDF 转成 Markdown,或者使用 Unstructured 这类保留更多结构的工具。
3.2 文本切分
切分是为了让模型只关注相关片段。如果整篇文档都塞给模型,会超出上下文限制,也会引入大量噪声。
最常用的是递归字符切分器RecursiveCharacterTextSplitter。它的策略是:先用一个大的分隔符切,如果切出来的块还是太长,就换更小的分隔符继续切。
核心参数:
chunk_size:每个块的最大字符数。chunk_overlap:相邻块之间的重叠字符数,避免关键信息正好被切在边界上。
举个例子:
from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", " ", ""] )需要注意的是:中文的切分和英文不太一样。英文可以按空格切,中文更适合按句号、感叹号、问号等句子边界切。上面把中文标点加进了 separators,就是为了减少句子被硬切的风险。
3.3 向量化与向量库
向量化的作用是让文本变成计算机能计算相似度的数字数组。
你可以把 Embedding 模型理解为“翻译官”:它把一段文本映射成一个高维向量,语义相近的文本,向量距离也近。
常用的本地 Embedding 模型:
BAAI/bge-small-zh-v1.5BAAI/bge-m3shibing624/text2vec-base-chinese
使用开源模型的好处是数据不出内网,适合企业私有化场景。缺点是中文效果需要测试,不同模型差距很大。
在 Notebook 中加载 HuggingFace Embedding 模型:
from langchain_huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", encode_kwargs={"normalize_embeddings": True} )normalize_embeddings=True表示对向量做归一化。归一化之后,用内积计算相似度等价于余弦相似度,这在用 Chroma 检索时更稳定。
向量库的作用是存储向量并提供相似度检索。Chroma 是一个非常适合做原型验证的轻量级向量库,支持本地持久化,不需要额外启动服务。
3.4 检索与重排
检索阶段,系统把用户问题也转成向量,然后在向量库中找到最相似的 Top K 个片段。
但向量检索不一定完美。可能出现两种问题:
- 召回结果语义相关,但关键信息缺失。
- 召回结果顺序不合理,真正有用的片段排在了后面。
这时候就需要重排(Rerank)。重排模型会结合“用户问题 + 候选文档”做更精细的匹配打分,把最相关的片段提到前面。
常见的重排模型有BAAI/bge-reranker-base、bge-reranker-v2-m3等。
3.5 生成回答
最后一步是把检索到的文档片段和用户问题拼接成 Prompt,交给大模型。
一个简洁的 Prompt 模板如下:
你是一个知识库问答助手。请仅根据以下资料回答问题。 如果资料里没有相关内容,请直接回答“知识库中未找到相关信息”。 资料: {context} 问题:{question}这里要注意:不要给模型太多自由发挥的空间。RAG 的核心价值是“有依据地回答”,如果 Prompt 中没有强调“仅根据资料回答”,模型很可能又用训练知识自由发挥,导致回答“看起来对但没依据”。
4. 完整实战:用 Notebook 搭建一个最小 RAG
下面我们会走进一个完整的 Notebook。建议你在本地按顺序创建新单元格运行,而不是一次性粘贴全部代码。
4.1 项目结构与准备数据
建议的项目目录如下:
rag_refresher_notebook/ ├── data/ │ └── rag_intro.txt ├── rag_refresher.ipynb └── requirements.txt先手动创建一个data/rag_intro.txt,内容可以用你自己关心的文档,也可以先用下面的示例文本。我这里用一段非常短的 RAG 介绍举例,实际使用时换成你自己的业务文档即可。
RAG 全称是 Retrieval-Augmented Generation,即检索增强生成。 它的核心思想是在大语言模型生成回答之前,先从外部知识库中检索相关文本片段。 检索到的片段会和用户问题一起拼接到提示词中,用于约束模型回答的内容和范围。 RAG 适合解决私有知识问答、企业制度问答、产品手册问答等场景。 与重新训练模型相比,RAG 的优势是更新成本低、可解释性强、支持快速接入新知识。 传统 RAG 流程包括文档加载、文本切分、向量化、检索、重排和生成回答。 近年来,RAG 还发展出了 Agentic RAG、Graph RAG、多模态 RAG 等进阶形态。4.2 初始化依赖与全局参数
在 Notebook 的第一个单元格中导入依赖,并设置全局参数。
import os from pathlib import Path from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 路径配置 BASE_DIR = Path(".") DATA_PATH = BASE_DIR / "data" / "rag_intro.txt" CHROMA_PATH = BASE_DIR / "chroma_db" # 切分参数 CHUNK_SIZE = 200 CHUNK_OVERLAP = 30 # 检索参数 TOP_K = 3 # Embedding 模型 EMBED_MODEL = "BAAI/bge-small-zh-v1.5" print("初始化完成")需要注意的是,langchain_community在较新的langchain版本中已经被拆分出去,所以这里单独安装。如果你使用的是旧版 LangChain,导入路径可能是langchain.document_loaders,请根据你的版本调整。
4.3 文档加载与切分
# 1. 加载文档 loader = TextLoader(str(DATA_PATH), encoding="utf-8") documents = loader.load() print(f"加载到 {len(documents)} 个文档对象") print(documents[0].page_content[:200])预期输出是文档前 200 个字。
接着做切分:
# 2. 切分文本 text_splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分出 {len(chunks)} 个文本块") print("示例块:") print(chunks[0].page_content) print("---") print(chunks[1].page_content)这里每个 chunk 都是一个Document对象,里面除了page_content,还有metadata。我们可以给每个 chunk 加上序号,方便后面追踪来源。
4.4 构建向量库
# 3. 初始化 Embedding 模型 embeddings = HuggingFaceEmbeddings( model_name=EMBED_MODEL, encode_kwargs={"normalize_embeddings": True} ) # 4. 生成向量库并持久化 vector_store = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=str(CHROMA_PATH) ) print(f"向量库已创建,位置:{CHROMA_PATH}")第一次运行需要下载 Embedding 模型,时间取决于网络状况。下载完成后,模型会缓存到本地,后续运行不需要重复下载。
如果你要重新构建向量库,建议先删除已有的chroma_db目录,避免旧数据和新增数据混在一起。
4.5 检索召回与答案生成
先做向量检索,看看检索结果是否合理。
query = "什么叫检索增强生成?" retriever = vector_store.as_retriever(search_kwargs={"k": TOP_K}) retrieved_docs = retriever.invoke(query) print(f"为问题检索到 {len(retrieved_docs)} 个相关块:\n") for i, doc in enumerate(retrieved_docs): print(f"--- 第 {i + 1} 个结果 ---") print(doc.page_content) print()运行后,你应该能看到和问题语义相关的几个片段。
然后接入生成模型。这里以 OpenAI 兼容接口为例。如果你使用 OpenAI,直接配置官方 Key;如果你使用国内大模型服务,通常也提供 OpenAI 兼容的 HTTP 接口。
from langchain_openai import ChatOpenAI # 这里配置你的 API Key 和 Base URL # base_url 根据你的服务商调整 llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, openai_api_key=os.getenv("OPENAI_API_KEY") ) def build_prompt(question: str, docs) -> str: context = "\n\n".join([doc.page_content for doc in docs]) prompt = f"""你是一个知识库问答助手。请仅根据以下资料回答问题。 如果资料中没有相关内容,请直接回答“知识库中未找到相关信息”。 资料: {context} 问题:{question} """ return prompt prompt = build_prompt(query, retrieved_docs) answer = llm.invoke(prompt) print("模型回答:") print(answer.content)如果你的机器上没有大模型 API,也可以先跳过生成环节,只保留检索结果做人工判断。这一步不影响理解 RAG 前半段的工作。
4.6 运行效果与结果说明
整个 Notebook 运行完成后,你的输出流程大致如下:
加载到 1 个文档对象 切分出 7 个文本块 向量库已创建,位置:chroma_db 为问题检索到 3 个相关块: --- 第 1 个结果 --- RAG 全称是 Retrieval-Augmented Generation,即检索增强生成。 它的核心思想是在大语言模型生成回答之前,先从外部知识库中检索相关文本片段。 ... 模型回答: 检索增强生成(RAG)是一种在生成回答前先从外部知识库检索相关文本片段的技术。到这里,一个最小的 RAG 链路就完全跑通了。
5. RAG 知识库的指标与评估
很多读者搭建完 RAG 后,觉得“效果时好时坏”,但说不清楚哪里差。这通常是因为缺少一套评估指标。
5.1 检索层指标
检索层关注的是“该搜出来的有没有搜出来”。
常用指标如下:
| 指标 | 解释 | 直观理解 |
|---|---|---|
| Recall@K | 前 K 条结果中命中的相关文档数 / 总相关文档数 | 该有的结果召回了几成 |
| Precision@K | 前 K 条结果中相关文档占比 | 召回的 K 条里有几条靠谱 |
| Hit Rate@K | 前 K 条结果中是否至少有一个相关文档 | 有没有命中一个关键答案 |
| MRR | 第一个相关结果排在第几位,取倒数 | 第一条有用结果出现得够不够早 |
| NDCG@K | 考虑排序位置的折损累计增益 | 排序顺序是否合理 |
这里最关键的是 Recall 和 MRR。Recall 低说明知识库内容被切碎或索引错了,MRR 低说明排序有问题,需要通过重排改善。
5.2 生成层指标
检索准不等于答案好。生成层主要看三个指标:
- 忠实度(Faithfulness):生成内容是否严格依据检索到的资料,没有编造。
- 答案相关性(Answer Relevance):回答是否针对用户提出的问题。
- 上下文相关性(Context Relevance):检索到的上下文是否足够支撑回答。
这组指标可以从“主观感受”变成可量化的打分,通常做法是让一个更强的模型作为裁判,给生成结果逐项打分。
5.3 如何理解这些指标
在实际项目中,不要只看一个指标。我建议按以下顺序排查:
- 先看 Hit Rate 和 Recall:如果召回结果里压根没有正确答案,生成再强也没用。
- 再看 MRR 或 NDCG:如果答案在召回结果里,但排得很靠后,会导致上下文过长、噪声过多。
- 最后看忠实度:如果检索没问题但答案还是编造,问题出在 Prompt 或者说生成模型。
你可以用这种思路给自己的知识库做一次“体检”。
6. 如何让 RAG 更精准
很多人问“如何创建精准的 RAG”。答案不是某一个技巧,而是下面这几个环节的综合优化。
6.1 从源头控制文档质量
输入垃圾,输出垃圾。RAG 也一样。
- 去掉页眉页脚、水印、导航栏等无关文本。
- 保留标题层级,尽量使用 Markdown 或结构化数据。
- 对 PDF 要人工抽样检查解析结果,尤其是表格。
- 对协议类文档,比如 3GPP 规范或合同条款,要按条款编号保留结构,不要按固定字符数硬切。
如果你发现某个文档检索效果总是差,先检查原始解析结果,大概率是解析阶段就丢了信息。
6.2 混合检索与查询改写
纯向量检索对关键词不敏感。比如用户搜“怎么请假”,文档里写的是“休假流程”,向量相似度可能不够高。
改进思路有两种:
- 混合检索:向量检索 + 关键词 BM25 检索,再把结果合并排序。
- 查询改写:先让大模型把用户问题改写成更利于检索的形式,例如补全同义词、扩展专业术语。
例如用户问“工资什么时候发”,可以改写成“公司工资发放日期、工资发放规则、发薪日”。
6.3 Rerank 重排
重排是提升 RAG 效果性价比最高的手段之一。
先用向量库快速召回 20 条候选,再用重排模型精排,取前 3 条给大模型。这样可以兼顾召回率与精度。
Rerank 的伪代码如下:
# 假设候选文档已经在 candidates 中 # reranker 是重排模型 scores = reranker.score(question, candidates) top_results = sort_by_score(candidates, scores)[:3]在 Notebook 中,你可以先打印重排前后的对比,直观感受重排模型的作用。
6.4 Agentic RAG 与 Ontology RAG
如果问题链比较复杂,比如“对比这两个产品的售后政策”,一次检索往往不够。
这时可以引入 Agentic RAG:让大模型像 agent 一样,根据问题拆解检索计划,多次检索,直到信息足够。常见实现是 LangGraph 中的多步检索节点。
另外还有 Ontology RAG,它提前定义好概念之间的关系,检索时不只找字面片段,还会顺着知识图谱关系找到相关内容。适合做企业知识体系比较复杂、实体关系密集的场景。
6.5 多模态 RAG 拓展
如果你的知识库里有大量图片、表格、流程图,纯文本 RAG 会丢失信息。
多模态 RAG 的思路是:
- 使用多模态 Embedding 模型,将图片和文本统一向量化。
- 或者先用视觉模型把图片内容转录为文字,再进入普通 RAG 流程。
第二种方案实现成本更低,目前也是很多项目的首选。
7. 常见问题与排查思路
在实际跑 RAG Notebook 时,有几个高频问题经常被问到。
7.1 Notebook 环境类问题
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Jupyter 无法启动 | conda 环境未激活 | 先运行conda activate rag_notebook |
| 找不到已安装的包 | Kernel 使用的 Python 不是当前环境 | 在 Notebook 中运行import sys; sys.executable检查 |
代码能运行但报ModuleNotFoundError | 依赖装错了环境 | 确认 pip install 和 Jupyter 在同一个环境 |
| 云 Notebook 长时间无操作断开 | 空闲会话被回收 | 定期运行简单代码保活,或分阶段保存结果 |
检查 Kernel 环境是最关键的一步。很多人明明pip list能看到包,但 Notebook 里导入失败,几乎都是 Kernel 环境没切换对。
7.2 中文切分与编码问题
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 读取 TXT 出现乱码 | 文件编码不是 UTF-8 | 使用encoding="utf-8",必要时改为gbk |
| 切分后句子被拦腰切断 | 分隔符未包含中文标点 | separators 中加入 “。”、“!”、“?” |
| 一个 chunk 全是空行 | 原始文档有大量连续换行 | 加载后先做文本清洗 |
中文切分目前没有一个万能方案。我的建议是:先按分隔符递归切,再人工打印前 20 个 chunk 检查。如果发现大量句子被截断,就应该调整chunk_overlap或分隔符。
7.3 检索效果差
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 检索结果和问题完全无关 | Embedding 模型不适合领域 | 换成效果更好的中文 Embedding 模型 |
| 相关结果排得太靠后 | 向量检索不擅长精确关键词 | 增加 BM25 混合检索,或加 Rerank |
| 检索结果都是同一段文字 | 切分太小导致重复片段 | 调整 chunk_size,增加多样性 |
| 换成新文档后效果没变 | 向量库没有重新构建 | 删除 chroma_db 目录后重新生成 |
这里要特别提醒:每次修改切分参数或文档后,记得清空向量库目录再重建。Chroma 持久化通常只会新增,不会自动删除旧的分块,容易造成“老的脏数据还在影响结果”的问题。
8. 最佳实践与工程建议
8.1 用模块化方式组织 Notebook
一个 RAG Notebook 不要把所有代码堆在一个单元格里。建议按以下结构组织:
01 环境检查与依赖安装 02 文档加载与预览 03 文本清洗与切分 04 向量库构建 05 检索测试 06 重排测试 07 生成问答 08 效果评估这样当你调试时,只需要重跑某个阶段的单元格,不用从头再来。
8.2 给 chunk 打上完整元数据
在构建向量库时,强烈建议保留每一条 chunk 的来源信息。
示例代码:
for i, chunk in enumerate(chunks): chunk.metadata["chunk_id"] = i chunk.metadata["source"] = str(DATA_PATH)因为生产环境中,用户最终需要知道“这个答案出自哪份文档的哪个部分”。如果元数据里只有文本内容,后续审计和纠错都会很困难。
8.3 从原型到生产环境的注意事项
Notebook 跑通只是第一步。生产环境需要考虑更多内容:
- 权限控制:不同角色只允许检索自己有权限的文档,通常通过元数据过滤实现。银行、医疗等敏感行业尤其重要。
- 文档更新机制:新文档上线时,只删除并重建对应文档的向量,不要全库重建。
- 监控与日志:记录每个问题的命中文档、Rerank 分数、模型回答,方便回溯分析。
- 内容安全:对检索内容和生成结果做合规过滤,涉及敏感信息时要有脱敏和审计能力。
- 评估闭环:建立一套评估集,每次修改切分策略、模型参数后,都跑一遍评估,用指标验证效果,而不是凭感觉。
8.4 合理选择 Embedding 模型与底座
不要迷信“最大最贵的模型”。对中文知识库场景,建议先在你的测试集上对比几个开源模型,选效果好、推理速度快的。
我见过不少项目,问题不在模型,而在文档解析。原始文档质量差,换再大的模型也救不回来。
9. 总结与下一步学习方向
到这里,这份RAG Refresher Notebook已经把 RAG 从文档加载到最终生成的完整链路走了一遍。你不仅看到了每个环节的代码,还理解了为什么要切分、为什么要向量化、为什么要加重排、如何用指标评估效果。
如果你想把 RAG 从“能跑”提升到“好用”,下一步可以重点研究这几块:
- 混合检索与查询改写,解决长尾问题。
- Rerank 模型引入,提升排序质量。
- LangGraph 实现 Agentic RAG,处理多步复杂问题。
- Graph RAG 或 Ontology RAG,处理强关系型知识。
- 评估数据集建设,让每一项优化都可以量化。
希望这份笔记本能在你搭建 RAG 知识库时少走一些弯路。如果中途遇到报错,优先对照“常见问题与排查思路”一节,大部分环境类问题都出在依赖版本和 Kernel 环境上。
