简介:面向AI应用开发者的RAG实践手册,系统讲解构建知识库与问答系统的完整流程,可帮助解决大模型私有知识整合、问答准确率优化等问题。资源包为单个PDF文件,大小4.11MB,目前已有466人学习使用。内容结构清晰,涵盖RAG原理与核心概念,从工作流程、参考架构到分类与优势均有说明;技术栈选择与架构设计部分重点介绍了Cloudflare Vectorize索引的创建与元数据管理,并剖析了选型考量。随后梳理RAG流水线的模块职责、关键参数、语言策略与常见演进路线,再进入环境准备,包括接口申请、Cloudflare服务配置和项目初始化步骤。整体兼具原理图解与实战指引,适合正在选型向量数据库、搭建知识库问答系统或优化RAG性能的初中级AI工程师阅读,也可作为技术决策者评估方案的参考。
1. RAG 知识库问答的落地链路:这份手册到底在解决什么问题
RAG 实践手册这类资源,我读完最大的感受是:它不教概念,教你流程。知识库问答系统听起来玄乎,拆开了无非是六步——文档加载、切块、向量化、检索、生成、评估。你如果正想用公司内部文档搭问答系统,或者打算做个开源知识库项目,这本手册能帮你把整条链路的每一步都落到代码和参数上。它不是微调方案,不是图谱方案,而是纯检索增强生成:让大模型在回答问题之前先搜一遍知识库。适合的人群我给三个:想把散落 PDF 变成可回答系统、想用 RAG 框架做 MVP、以及已经在跑 Dify 流水线但不知道怎么调参的人。
2. RAG 组件拆解:加载、切块到向量化的选型与实现
2.1 为什么是 RAG 而不是微调:先分清边界再动手
很多人一上来就纠结:要不要微调?手册里给了一个很实际的判断标准:看知识更新的频率,以及回答需不需要引用。知识如果月月改,微调成本很高,RAG 只需要把新文档重新切块、重建索引,问答系统立刻就能用上新知识。回答如果必须可追溯,比如企业规章制度、产品说明书、维修手册,RAG 能在 prompt 里直接带上原文片段,模型答错了你也能顺着引用回去查。这类场景天然适合 RAG。
这里还要说清楚 RAG 的边界。「RAG 知识库和结构知识库的区分」是很多人会混淆的问题。结构知识库指的是知识图谱:实体、关系、属性,适合「A 公司的关联方有哪些」这种多跳关系查询。RAG 向量库适合「和这句语义最像的段落」,本质是模糊匹配。手册的结论很务实:文档类知识优先做 RAG,只有当你明显需要稳定的关系推理时,才去叠加图谱层,别一上来就上图谱,维护成本会吞掉收益。
| 需求特征 | 推荐方案 | 理由 |
|---|---|---|
| 知识经常更新 | RAG | 改文档后重建索引即可 |
| 回答必须带出处 | RAG | 原文片段可溯源 |
| 多跳关系推理 | 结构知识库 / 图谱 | 需要实体关系查询 |
| 固定话术与语气 | 微调 | 改变生成风格而非注入知识 |
2.2 文档加载与解析:PDF、网页和 Markdown 的常规处理
先把文档变成文本,这是 RAG 的入口。我一般按来源分三类:PDF 走 PyPDFLoader,网页走 RecursiveUrlLoader,Markdown 走 TextLoader。注意扫描版 PDF 必须提前 OCR,否则切出来的全是空白页。
from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("./docs/manual.pdf") pages = loader.load() print(f"共加载 {len(pages)} 页") for page in pages[:2]: print(page.page_content[:200])这段代码没什么花样,关键在后续处理。PyPDFLoader 按页切文档,每页是一个 Document 对象,metadata 里带着页码。手册里反复强调:不要把 pages 直接当最终片段,因为一页 PDF 可能包含多个主题段落,也可能一个表格被拆成上下两页。正确的顺序是先把整份 PDF 读进来,清洗页眉页脚,再按语义切块。
图片处理在这里容易翻车。如果你的知识库要存产品截图、表格图片,向量库本身是存不了图片的。常规做法是:图片先用 OCR 或多模态模型转成文本描述,文字走 RAG 检索;原图存到对象存储(MinIO、OSS 都行),把 URL 放进切块的 metadata,回答时一起返回。很多人问「RAG 知识库能存储图片吗」,准确答案是:能,但存的是图片的文本描述和路径,不是图片本身。
2.3 切块参数:chunk_size、overlap 与分隔符怎么定
切块是整个 RAG 里最玄学的环节,但手册把这个玄学变成了可测的参数。先看标准写法:
from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ",", " ", ""], length_function=len, ) chunks = splitter.split_documents(pages)chunk_size 是每个片段的目标字符数,chunk_overlap 是相邻片段重叠的部分。为什么需要 overlap?因为一个句子的后半句可能落在上一块的尾部,检索时被整块截断。separators 列表的顺序有讲究:RecursiveCharacterTextSplitter 会按顺序尝试分隔符,\n\n优先级最高,保证段落尽量不被随意切开;中文场景要把句号、分号、逗号加进去,否则长句会被硬切。
| 场景 | chunk_size | chunk_overlap | 备注 |
|---|---|---|---|
| 制度手册 / 长段落 | 300-500 | 50-100 | 保句子完整 |
| 技术文档 / 操作步骤 | 200-300 | 30-50 | 答案要精确 |
| 代码片段 | 500-800 | 100 | 避免拆坏函数 |
另一个容易被忽略的点:把标题拼进 chunk 内容。比如切块前把「## 保修政策」作为前缀拼到正文前面,检索时模型能带着章节上下文理解片段,效果比裸句子好不少。我一般在构造 Document 时就把标题写进 page_content,而不是只放 metadata,因为多数 embedding 模型不会把 metadata 算进向量里。
「RAG 的瓶颈」这个词,很多人到检索阶段才遇到,其实索引阶段就埋下了。chunk 太大,一个碎片包含太多主题,向量会被平均语义拉偏,检索精度下降;chunk 太小,上下文不足,生成阶段没有足够信息。Dify 知识库流水线里也有这个滑杆,但默认值不是万能解,要拿真实问题去试。
2.4 向量化与存储选型:Embedding 模型和向量库怎么配合
Embedding 模型决定检索质量的上限。开源社区里 bge-m3 是很多知识库项目的默认选择,多语言、支持长文本,本地部署不花 API 费用。如果团队已经接入大模型 API,直接用配套的 embedding 服务更省事,少一个要维护的组件。向量库选型看数据量和部署条件:
| 向量库 | 适用场景 | 特点 |
|---|---|---|
| FAISS | 单机原型,数据量百万以内 | 内存索引,重启要重建 |
| Qdrant | 小团队生产 | 轻量,Docker 一键起 |
| Milvus | 大规模生产 | 分布式,支持标量过滤 |
| PGVector | 已有 PostgreSQL | 复用数据库,支持 SQL 过滤 |
向量库存的是「向量 + 文本 + metadata」。metadata 里我通常放来源文件、页码、标题、更新时间。检索时按 metadata 过滤非常实用,例如只搜 2024 年以后的文档,在 Milvus 里一个 filter 就解决,不需要单独建一个库。索引阶段多留几个字段,后面排查时能省很多事。
提示:向量库不是数据仓库,不要把原始文档和向量混在一个系统里管。原始文件留在对象存储或数据库,向量库只负责检索。
3. 检索层实战:混合检索、重排和阈值三个参数的配合
3.1 向量检索和关键词检索为什么不能二选一
纯向量检索有硬伤:对专有名词、英文缩写、长尾词不敏感。把「Dify 的更新日志」改写成「Dify 的 changelog」,语义向量可能匹配不到,但 BM25 这种传统关键词检索反而稳定。所以工程上常用混合检索:BM25 负责精确命中,向量负责语义召回,最后再合并排序。
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import FAISS bm25_retriever = BM25Retriever.from_documents(chunks, k=10) vector_retriever = FAISS.from_documents(chunks, embedding).as_retriever( search_kwargs={"k": 10} ) ensemble = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.3, 0.7], ) docs = ensemble.invoke("设备保修期是多久?")这里 weights 是两种检索结果的加权比例。0.3/0.7 表示向量为主、关键词为辅。如果你的文档里型号、编号特别多,把 BM25 权重拉到 0.5 甚至 0.6 更合适。这个参数的确定方法很朴素:拿 20 条真实问题跑一遍,看哪种比例下 top 10 的命中率最高,不用一开始就调得很精细。混合检索的合并逻辑各家框架细节不同,但核心都是先各取 top k,再做去重和融合打分。
3.2 top_k 与相似度阈值:先确定你要拿多少片段进 Prompt
一个常犯的错误是把 top_k 拉到 10 以上,把所有检索结果都塞进 prompt。上下文数量一多,模型反而被无关信息干扰。手册里的常规做法是分两层:先召回 20-30 个候选片段,经过重排后只把 top 3-5 个交给生成层。
阈值这个问题要单独说。向量相似度分数不是概率,不同 embedding 模型的分数分布完全不同。bge-m3 的 0.4 可能是很好的匹配,另一个模型的 0.7 可能还靠不住。正确做法是先观察分布:
results = vector_store.similarity_search_with_score(query, k=20) for doc, score in results: print(f"{score:.3f}\t{doc.page_content[:30]}")打印出来的分数能大致看出「相关片段」和「不相关片段」的分界。我一般取分界点做阈值,比如 0.45。低于这个值的片段直接丢弃,不进入重排,也能省下重排的算力。注意这个分界点必须按当前 embedding 模型重新统计,换模型之后沿用老阈值是典型的翻车姿势。
3.3 重排:把「可能相关」变成「一定相关」
重排是我最看重的模块。向量检索算的是 query 和片段的整体语义相似度,但「整体相似」不等于「含有所需要的那个答案」。重排模型是 cross-encoder,把 query 和候选片段逐个拼在一起打分,精度比双塔 embedding 高一个档次,代价是慢,所以只对 top 20-30 的候选做。
from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch model = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-v2-m3") tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-v2-m3") pairs = [[query, doc.page_content] for doc in candidates] inputs = tokenizer(pairs, padding=True, truncation=True, max_length=512, return_tensors="pt") with torch.no_grad(): scores = model(**inputs).logits.squeeze(-1).tolist()这段代码的核心是构造 query 和片段对。max_length=512 是 token 上限,超出部分被截断。注意重排模型一般只接受很短输入,如果 chunk_size 超过 500 token,重排时就要考虑截断带来的信息损失,这也是为什么手册建议 chunk_size 控制在 300-500 之间。重排后的分数不能直接当相似度用,但它的排序能力比 embedding 距离可靠,我只看排序不看绝对值。
4. 生成与编排:从检索结果到结构化答案的完整设计
4.1 系统提示词:给模型划定知识边界
RAG 生成层的成功一半来自检索,另一半来自提示词。一个稳定的提示词至少包含四段:角色设定、任务说明、知识片段的输入位置、以及「不知道就直说」的兜底规则。
SYSTEM_PROMPT = """你是企业知识库问答助手。 请仅根据下面提供的【知识片段】回答问题。 要求: 1. 如果知识片段包含答案,请给出直接回答,并在句末标注来源编号,例如 [1]; 2. 如果知识片段不包含答案,请直接回答“该问题在知识库中未找到答案”; 3. 不要使用知识片段以外的信息,不要编造。 【知识片段】 {context} 【用户问题】 {question} """这里第 2 条最重要。没有这句兜底,模型几乎必然会用自己预训练的知识硬答,RAG 就失去了意义。我在实际项目里见过最典型的现象:知识库里明明没有的内容,模型还能像模像样答出一大段。加了这条约束之后,准确率反而提升了,因为模型知道「承认不知道」是被允许的。另外,知识片段里如果混入了上一轮对话的检索结果,要清理干净,否则模型会被过期片段带偏。
4.2 引用溯源:回答不裸奔,每条信息都要有出处
企业场景里,回答要能回去翻原文。做法是在切块时给每个 chunk 一个唯一索引,检索时保留原片段,生成时要求模型引用编号。
# 构造片段时给编号 for idx, doc in enumerate(chunks): doc.metadata["source_id"] = idx doc.metadata["source_file"] = doc.metadata.get("source", "unknown") # 生成时把编号一起交给模型 context = "\n\n".join( f"[{doc.metadata['source_id']}] {doc.page_content}" for doc in retrieved_docs )之后在答案的后处理里把 [1]、[2] 映射回文件名和页码。这个映射表可以放在内存,也可以直接存进向量库的 metadata。注意:引用溯源不是给用户看的安慰剂,它是排查工具。答案引用错了,说明检索结果和生成结果之间存在断裂,顺着引用能直接定位到是哪一段出问题,而不是对着答案猜。
4.3 工作流编排:用现成框架还是自己拼管道
现在可选的 RAG 框架很多,LangChain、LlamaIndex、Dify,以及各种开源知识库项目。手册的结论很务实:验证期用低代码平台,上线期用代码自己控制链路。
Dify 的知识库流水线已经比较完整:上传文档、自动分段、调用 embedding、建索引,可视化界面里能直接看到每步的输入输出,适合拿来先跑通业务。但它也有黑匣子成分,分段参数的细节暴露得有限。LangChain 和 LlamaIndex 自由度大,代价是自己维护流水线——加载、切块、embedding、建库、检索、重排、prompt,每一步的日志都要自己打。
我把生产环境的流水线固定成五个阶段:文档入库触发重建索引,索引完成后更新检索配置,检索阶段记录召回片段列表,重排阶段记录分数,生成阶段记录最终引用。每个阶段都留日志,后面排查时才不是盲人摸象。框架选型这件事,别迷信某一家,关键在于你能否在关键节点插入自己的逻辑。
5. 排查手册:RAG 问答翻车的五个高频坑
5.1 检索不到内容:向量库里明明有,却搜不出来
现象:用「设备的保修期限」能搜到,改成「保修期几个月」就返回空。
原因:embedding 对同义表达敏感度不够,或者切块把关键句和主题词切散了。手册里给的排查路径是:先看召回片段的 score 分布,如果分数低到离谱,多半是 chunk 的问题,而不是 embedding 模型的问题。比如 chunk_size=200 时一段话被截成两半,向量各管一半,整体语义就丢了。
解决:把 overlap 从 50 提到 100,确认每段至少包含一个完整句子。同时把标题拼到 chunk 内容前,例如「## 保修政策\n设备保修期为 36 个月」,检索效果比纯句子好不少。这个改动通常五分钟就能验证,不需要动 embedding 模型。
5.2 检索结果看起来相关,但模型答非所问
现象:top 5 里确实有正确答案,但生成回答时模型用了另一个片段的信息。
原因:这是最典型的 RAG 瓶颈——正确答案的排序不在最前面,模型优先读了排在第一的片段。不要急着改 prompt,先检查候选片段里正确答案排第几。如果排第 3 以下,说明检索层有偏差。
解决:先跑 rerank,再考虑调 embedding。我之前一个案例,加了 bge-reranker 之后,正确答案从第 5 位直接跳到第 1 位,生成结果立刻对了。检查排序这一步要写进调试流程,每次检索日志里都带上候选片段的顺序和分数,而不是只记最终答案。
5.3 相似度阈值设太低,垃圾片段全进 Prompt
现象:任何问题都能召回结果,回答反而胡言乱语。
原因:没有设阈值,或者阈值是拍脑袋定的。不同 embedding 模型的分数分布差异很大,固定值不可靠。比如某模型对完全不相关的内容也能打出 0.5 以上的分数,用 0.4 当阈值等于没有过滤。
解决:对每一批 embedding 模型,离线跑一次测试集,统计相关片段和不相关片段的分数分布,取分界点作为阈值。每换一次 embedding 模型或切块参数,重做一次。这个动作要制度化,我一般把它写进 CI 脚本里,自动出分布图。
5.4 PDF 表格被切碎,表格数字答不对
现象:知识库里的 PDF 带产品参数表,问「C 型号的额定功率是多少」,答案经常是错的。
原因:PDF 解析按页切,表格跨页时单元格被劈成两半;哪怕不跨页,表格也会被当作普通文本流切开,单元格和表头之间的关系就丢了。
解决:表格单独处理。先识别含表格的页,用表格解析工具转成 Markdown 表格,再放进切块流程;如果是扫描件,先 OCR。转成 Markdown 后,表格作为一个整体不参与递归切块,一个 chunk 就是一张表。注意转换后检查一次渲染效果,有些工具会把多行表头拆散。
5.5 知识库上传后一直排队中
现象:用 Dify 这类低代码平台上传大文档后,界面长时间显示「排队中」,问答系统无响应。
原因:常见原因是文档量大、分段数多,索引任务全挤在一个队列里;如果 embedding 走的是远端 API,一旦网络超时,任务会反复重试,排队任务越来越多。
解决:先把文档拆小,比如每次只上传 100 页以内的文档;确认网络到 embedding 服务稳定后再批量传。如果队列已经积压,最有效的办法是清掉队列里的旧任务,而不是反复重新上传,重复上传只会让队列更拥堵。索引任务加个失败重试上限,避免死循环。
6. 验证与回归:用三个评测指标拦住知识库劣化
6.1 先攒 50 条真实问答题
评测集不用大,但必须是真实用户问过的。整理 50 条问题,人工从知识库里找到正确答案和所在段落,存成一个 JSON 文件。这个文件就是知识库项目的回归基线。没有基线,后面每次改动都只能靠手感,改完感觉好了,上线才发现坏了。
6.2 三个指标跑分
| 指标 | 含义 | 怎么测 |
|---|---|---|
| 上下文相关性 | 检索回来的片段是否覆盖答案 | 看 top 5 是否包含标注段落 |
| 忠实度 | 答案是否有依据,有没有幻觉 | 逐句核对是否来自片段 |
| 答案相关性 | 用户问的和答的是否对齐 | 人工或大模型打分 1-5 |
我一般让大模型按这三项给分,设定好评分标准后和人工评分做对比。实践里最灵敏的是忠实度,它直接反映 RAG 的底线有没有保住。每次改完分块参数、换 embedding、调重排,都在评测集上跑一遍全流程,输出三张分数表,才不会出现「改完感觉更好,上线发现更差」的翻车事故。
从那以后我每次调参都强制走一遍这三项指标,快速跑完 50 条问题也就几分钟,却能省下一整周的手动回归。这个习惯帮我避开了很多次「看着合理、上线出问题」的改动,希望帮到你。
本文还有配套的精品资源,点击获取