RAG 这个词从 2023 年火到现在,我身边做后端、做算法、做产品的朋友几乎都动过手。但真正让我印象深刻的,不是谁把 Demo 跑通了,而是谁把它推到了生产环境还能稳住。我见过太多团队卡在同一个地方:本地用几十页 PDF 试的时候效果惊艳,一上真实知识库就开始胡言乱语,检索回来的东西驴唇不对马嘴,答非所问,最后项目不了了之。这篇就把我从零搭一套 RAG 知识库的完整路径摊开讲,包括每一步为什么这么做、参数怎么定、以及那些文档里不会写、只有踩过才知道的坑。不管你是刚听说 RAG 想上手,还是已经跑通 Demo 正发愁怎么落地,下面这些内容应该都能对上你的场景。
1. 先把 RAG 到底在解决什么想清楚
1.1 大模型的两个硬伤,决定了 RAG 的存在
很多人一上来就急着装环境、拉模型,结果做到一半发现方向都不对。我觉得动手之前,得先弄明白 RAG 到底补的是哪块短板。大模型有两个绕不过去的问题:一是知识截止,它的训练数据有明确的时间点,之后发生的事它一概不知;二是幻觉,遇到不知道的东西它不会说"我不知道",而是编一个听起来特别合理的答案给你。这两个问题在通用闲聊里无所谓,但一旦落到企业知识库、客服问答、内部文档检索这些场景,就是致命的。
RAG 的全称是 Retrieval-Augmented Generation,检索增强生成。它的思路特别朴素:既然模型不知道,那我就在它回答之前,先把相关资料从我的知识库里捞出来,塞进它的上下文,让它"看着材料答题"。这就像开卷考试,模型是那个脑子好使但没背过这本书的考生,检索系统负责在考试时把对的那几页翻给它看。理解了这一点,后面所有的工程决策都有了判断标准——任何一步都是为了让"翻到的那几页"尽可能准、尽可能全、尽可能不干扰模型。
1.2 一个完整的 RAG 链路拆成哪几段
我把整条链路拆成两大阶段,这个划分方式贯穿全文,后面每一节都对应其中一段。
离线索引阶段(数据进来的时候做一次或定期做):文档加载 → 文本切分 → 向量化 → 存入向量库。这一步的目标是把非结构化的文档,变成可以被快速检索的结构化数据。
在线检索阶段(用户每次提问时做):查询改写 → 向量检索 → 重排序 → 拼装上下文 → 交给大模型生成。这一步的目标是,在用户提问的瞬间,从海量片段里精准捞出最相关的那几条。
提示:新手最容易犯的错,是把全部精力砸在"换个更强的模型"上,而忽略了索引和检索。实测下来,RAG 效果差,八成问题出在检索环节,而不是生成环节。模型再强,你喂给它的材料是错的,它也答不对。
1.3 为什么"能跑通"和"能落地"是两回事
Demo 阶段你用的是自己精心挑选的几篇文档,问题也是你自己想的,当然效果好。但真实场景里,文档格式五花八门(PDF、Word、Excel、网页、扫描件),内容有大量重复和噪声,用户的问题千奇百怪,还有错别字和口语化表达。这时候你会发现,检索命中率(hit rate)断崖式下跌。所谓 hit rate,就是用户真正需要的那条知识,有没有被检索结果覆盖到。这个指标是 RAG 落地的生命线,后面我会专门讲怎么把它从 60% 拉到 90% 以上。
2. 文档加载与切分:决定上限的一步
2.1 文档加载远没有想象中简单
我一开始以为加载就是把文件读成文本,结果第一个坑就栽在这。PDF 分两种:一种是原生电子版,文字可以直接提取;另一种是扫描件或图片型 PDF,本质是一堆图片,直接提取出来是空的。后者必须走 OCR。我当时的做法是先判断 PDF 里有没有文本层,没有就走 OCR 流程,这个判断逻辑能省掉大量无效处理。
Word 和 Excel 相对好办,但 Excel 有个坑:表格里的数据如果直接按行转文本,会丢失表头信息,检索时根本对不上。我的处理是把每一行都拼上表头,变成"字段名: 值"的形式再入库。网页内容则要处理导航栏、广告、页脚这些噪声,不然检索出来的全是"版权所有"这种废话。
# 判断 PDF 是否需要 OCR 的简化逻辑 import fitz # PyMuPDF def needs_ocr(pdf_path, sample_pages=3): doc = fitz.open(pdf_path) text_len = 0 for i in range(min(sample_pages, len(doc))): text_len += len(doc[i].get_text().strip()) # 采样几页,如果平均每页文本极少,基本就是扫描件 return text_len / min(sample_pages, len(doc)) < 502.2 文本切分的粒度,直接决定检索质量
切分(chunking)是 RAG 里最被低估、也最影响效果的一步。切太大,一个片段里混了好几个主题,检索时噪声大;切太小,一句话被拦腰截断,语义不完整。我试过固定长度切、按段落切、按标题层级切,最后发现没有万能方案,得看文档结构。
对于结构清晰的文档(比如产品手册、规章制度),我优先按标题层级切,让每个 chunk 天然带一个语义边界。对于没有明显结构的连续文本,用递归字符切分,配合重叠(overlap)。重叠的作用是防止关键信息正好卡在切分点上被割裂,一般设成 chunk 大小的 10% 到 20%。
| 切分策略 | 适用场景 | chunk 大小建议 | 重叠建议 |
|---|---|---|---|
| 按标题层级 | 手册、规范、结构化文档 | 按语义自然分段 | 无需重叠 |
| 递归字符切分 | 通用连续文本 | 300-500 字 | 50-100 字 |
| 按句子切分 | 问答对、短文本 | 1-3 句 | 1 句 |
| 语义切分 | 高质量要求场景 | 动态 | 动态 |
2.3 中文切分的一个隐藏坑
英文按空格和标点切很自然,中文不行。我早期用默认的字符切分器处理中文,结果经常把"人工智能"切成"人工"和"智能"两半,检索时两个片段都匹配不上完整语义。解决办法是换用对中文友好的切分器,或者自己按中文标点(。!?;)做句子边界识别。另外中文的 token 密度和英文不同,同样 500 字,中文的信息量往往更大,所以中文 chunk 可以适当小一点,我一般控制在 300 到 400 字。
注意:切分完一定要抽样人工看一眼。我见过有人切出来的 chunk 全是半句话,检索效果差还找不到原因。花十分钟抽查,能省掉后面几天的排查。
3. 向量化与向量库选型:别被参数吓住
3.1 Embedding 模型怎么选
向量化的本质,是把一段文本映射成一个高维向量,语义相近的文本在向量空间里距离也近。选 embedding 模型,我主要看三点:中文效果、维度、以及能不能本地跑。维度不是越高越好,高维度检索慢、存储贵,1024 维对大多数中文场景已经够用。如果数据敏感不能出内网,就得选能本地部署的模型。
我实测下来,中文场景里,专门针对中文优化的模型比通用多语言模型效果好一截,尤其是在专业术语和长句上。选型时别只看榜单,拿你自己的真实数据跑一批 query,看 hit rate,这才是最靠谱的评估方式。
3.2 向量库:从轻量到生产
向量库的选择跨度很大,从几行代码就能跑的本地库,到需要集群运维的分布式方案都有。我的建议是按数据量级和并发需求选,别一上来就上重型方案。
- 数据量小(几万条以内)、单机、验证阶段:用本地文件型向量库,零运维,装完就能用,适合快速验证。
- 数据量中等、要持久化、要并发:用支持持久化和索引优化的单机数据库方案。
- 数据量大、高并发、要分布式:才考虑集群型向量数据库。
我踩过的一个坑是:早期为了"显得专业",直接上了分布式方案,结果数据才几千条,运维成本高得离谱,查询延迟还不如本地库。技术选型要匹配当前阶段,过度设计是另一种浪费。
3.3 相似度度量方式的选择
向量检索靠计算相似度,常见的有余弦相似度、点积、欧氏距离。大多数文本 embedding 模型训练时用的是余弦相似度,所以检索时也优先用余弦。这里有个细节:如果你的向量做了归一化,余弦相似度和点积是等价的,可以省一次计算。我一般入库前统一归一化,检索时用点积,速度更快。
4. 检索环节:hit rate 从这里开始爬坡
4.1 纯向量检索的天花板在哪
纯向量检索(也叫稠密检索)擅长语义匹配,用户问"怎么退款",文档里写"申请退货流程",它能匹配上,这是关键词检索做不到的。但它也有软肋:对精确的专有名词、编号、代码不敏感。用户问"错误码 E1024 怎么解决",向量检索可能给你返回一堆泛泛的错误处理文档,就是找不到 E1024 那条。
这就是为什么很多落地项目最后都上了混合检索:向量检索负责语义,关键词检索(比如 BM25)负责精确匹配,两路结果融合。融合方式我常用的是加权求和或者倒数排名融合(RRF),后者不需要调权重,更省心。
4.2 重排序:把最相关的顶上来
检索回来一堆片段,顺序对不对很关键,因为大模型的上下文窗口有限,排在前面的权重更高。重排序(rerank)就是用一个更精细的模型,对初步检索的结果重新打分排序。它的原理是:初步检索用的是"双塔"结构,query 和文档分别编码,快但精度有限;重排序用的是"交叉编码",query 和文档一起送进模型,精度高但慢。所以典型做法是:先用向量检索快速召回几十条,再用重排序精选出最相关的几条。
我实测的一个数据:加了重排序之后,top-3 的命中率能提升 15 到 25 个百分点。这个投入产出比非常高,强烈建议加上。
4.3 查询改写:用户问的和文档写的往往不是一回事
用户提问很随意,可能带口语、带错别字、或者问得很笼统。直接拿原始 query 去检索,效果经常打折。查询改写有几个常用手段:
- 同义扩展:把"咋退钱"改写成"如何申请退款",补上正式表达。
- 多查询生成:让模型基于原问题生成几个不同角度的子查询,分别检索再合并,覆盖更全。
- 指代消解:多轮对话里,"它多少钱"这种问题,得先结合上文把"它"还原成具体对象再检索。
提示:查询改写会引入额外的一次模型调用,增加延迟。我的做法是只在检索结果置信度低的时候才触发改写,平时走快速路径,兼顾速度和效果。
5. 上下文拼装与生成:最后一步别翻车
5.1 上下文不是塞得越多越好
新手常犯的错是把检索到的所有片段一股脑塞给模型,觉得给得越多答得越准。恰恰相反,无关片段是噪声,会干扰模型判断,甚至诱发幻觉。我的原则是:宁缺毋滥,只放真正相关的 top-k 条,一般 3 到 5 条。如果检索结果的相关性分数都很低,那说明知识库里根本没有这个知识,这时候应该让模型直接说"资料中没有相关内容",而不是硬编。
5.2 提示词模板的设计要点
拼装上下文时,提示词模板很关键。我一般会明确告诉模型三件事:一是你的回答必须基于下面提供的资料;二是资料里没有的,不要编,直接说不知道;三是如果资料之间有冲突,指出来。这三条能大幅降低幻觉率。另外,给每个片段标上来源,方便模型引用,也方便用户溯源。
PROMPT_TEMPLATE = """你是一个严谨的知识库助手。请严格根据下面提供的资料回答问题。 要求: 1. 只使用资料中的信息,不要依赖你自己的知识补充。 2. 如果资料中没有相关信息,直接回答"根据现有资料无法回答该问题"。 3. 如果不同资料存在矛盾,请指出矛盾之处。 资料: {context} 问题:{question} 回答:"""5.3 引用溯源:让答案可信
生产环境里,光给答案不够,用户会问"你凭什么这么说"。所以我会让模型在回答时标注引用了哪条资料,前端再把对应原文展示出来。这不仅提升可信度,也方便用户自己核对。实现上,给每个 chunk 一个唯一 ID,拼上下文时带上 ID,让模型在回答里引用。
6. 那些文档不会写的踩坑记录
6.1 坑一:知识库更新了,检索还是老答案
这是最隐蔽的坑。文档更新了,但向量库里还是旧数据,检索出来的自然是过时内容。根因是索引和源文档没有同步机制。我的解决方案是给每个 chunk 记录来源文档的 ID 和版本号,文档更新时,先删掉该文档对应的所有旧 chunk,再重新索引。别偷懒做增量追加,否则新旧数据混在一起,检索结果会非常混乱。
6.2 坑二:相似文档太多,检索结果高度重复
企业文档里经常有大量相似内容,比如同一份制度的不同版本、不同部门的类似流程。检索时这几条高度相似的片段会一起被召回,占满了 top-k 名额,反而把真正有用的那条挤掉了。解决办法是在入库时做去重,或者检索后做多样性筛选(比如 MMR 算法),保证召回结果的多样性。
6.3 坑三:表格和图片信息丢失
纯文本切分会把表格结构破坏掉,图片里的信息更是直接丢失。我处理表格时,会把表格转成 Markdown 或自然语言描述再入库。图片则用多模态模型生成文字描述,把描述文本一起索引。这样检索时,图片内容也能被匹配到。有朋友问 RAG 知识库能不能存图片,答案是能,但存的不是图片本身,而是图片的语义描述。
6.4 坑四:评估缺失,全靠感觉调参
我早期调 RAG 全靠"感觉好像好了一点",非常不靠谱。后来我建了一个小评估集:准备 50 到 100 个真实问题,每个问题标注好标准答案和应该命中的文档。每次改动后跑一遍,看 hit rate 和答案准确率的变化。有了这个基准,调参才有方向。这个评估集不用很大,但一定要有,而且要来自真实用户问题。
| 常见坑 | 根因 | 解决方案 |
|---|---|---|
| 更新后检索旧答案 | 索引未同步 | 按文档 ID 删除重建 |
| 结果高度重复 | 相似文档多 | 入库去重 + MMR 筛选 |
| 表格图片丢失 | 纯文本切分 | 转结构化描述再索引 |
| 调参无方向 | 缺评估集 | 建真实问题评估基准 |
7. 从 Demo 到生产还差哪些工程化动作
7.1 缓存:省下大量重复计算
真实场景里,很多问题是重复或高度相似的。我给检索结果和最终答案都加了缓存,相同或相似的 query 直接返回缓存结果,既降延迟又省钱。缓存 key 可以用 query 的向量做近似匹配,这样"怎么退款"和"如何退钱"能命中同一个缓存。
7.2 监控:上线只是开始
上线后要盯几个指标:检索延迟、生成延迟、hit rate、用户反馈(点赞点踩)、以及"无法回答"的比例。其中"无法回答"比例突然升高,往往意味着知识库有内容缺失或者检索出了问题,是最灵敏的报警信号。
7.3 渐进式优化路线
我的建议是分阶段推进:第一阶段先跑通基础链路,用最简单的方案上线,收集真实问题;第二阶段根据真实问题优化切分和检索,加混合检索和重排序;第三阶段再做查询改写、缓存、监控这些工程化增强。别想着一步到位,RAG 是个需要持续迭代的系统,先上线拿到反馈,比闭门造车强一百倍。
8. 进阶方向:本体 RAG 与智能体 RAG 值不值得上
8.1 本体 RAG 解决的是知识割裂
普通 RAG 把文档切成孤立片段,片段之间的关联关系丢了。比如"A 是 B 的上级"和"B 负责 C 项目"这两条信息,分开检索永远拼不出"A 通过 B 关联到 C"这个结论。本体 RAG(Ontology RAG)的思路是引入知识图谱,把实体和关系显式建模,检索时能沿着关系做多跳推理。它适合知识之间关联性强、需要推理的场景,比如医疗、法律、复杂设备故障排查。但它的构建成本高,需要先定义本体、抽取实体关系,不是所有场景都值得。
8.2 智能体 RAG 让检索更主动
传统 RAG 是"一问一检索一答"的固定流程。智能体 RAG(Agentic RAG)则把检索交给一个能自主决策的智能体:它可以判断这个问题需不需要检索、需要检索几次、检索结果够不够、要不要换个查询再试。这种模式在处理复杂多步问题时优势明显,但延迟和成本也更高。我的经验是,简单问答用传统 RAG 就够,复杂推理场景再考虑智能体 RAG,别为了追新概念而过度设计。
8.3 怎么判断该不该上进阶方案
判断标准很简单:如果你的评估集显示,失败案例主要是"知识缺失"或"检索不到",那先优化基础检索;如果失败案例主要是"信息都在但拼不出答案",那才考虑本体或智能体方案。先定位瓶颈,再选方案,别反过来。
9. 一套可复制的本地知识库搭建流程
9.1 环境准备与依赖
我把整套流程整理成可复制的步骤,零基础也能跟着走。核心依赖就几个:文档解析库、embedding 模型、向量库、以及一个大模型(本地或调用均可)。本地跑的话,模型量化版本对显存要求低,消费级显卡也能带动。
# 核心依赖(示意,按实际选型调整) pip install pymupdf # PDF 解析 pip install sentence-transformers # embedding pip install chromadb # 本地向量库 pip install langchain # 流程编排9.2 完整流程串起来
- 加载:遍历文档目录,按格式分别解析,扫描件走 OCR。
- 清洗:去掉页眉页脚、导航、重复段落。
- 切分:按文档结构选切分策略,中文控制 chunk 在 300-400 字,加 10%-20% 重叠。
- 向量化:用中文优化的 embedding 模型,向量归一化。
- 入库:连同来源 ID、版本号一起存入向量库。
- 检索:混合检索召回,重排序精选 top-3 到 top-5。
- 生成:套用提示词模板,要求基于资料回答并标注来源。
- 评估:用真实问题集跑 hit rate,持续迭代。
9.3 上线前必须做的几件事
上线前我必做三件事:一是抽样验证检索质量,随机抽 20 个问题看召回结果对不对;二是压测延迟,确认高峰期响应时间可接受;三是准备兜底话术,检索不到时给用户一个体面的回复,而不是让模型硬编。这三件事做完,心里才有底。
10. 我个人的几条经验之谈
做了这么多套 RAG,我最大的体会是:RAG 的功夫八成在数据,两成在模型。你把文档清洗干净、切分合理、检索精准,用个中等模型效果都不会差;反过来,数据一团糟,用再强的模型也是白搭。所以别急着追新模型、新框架,先把数据这条线理顺。
第二个体会是评估先行。没有评估集,所有的优化都是盲人摸象。我现在的习惯是,项目一开始就同步建评估集,边做边测,改动有据可依。
第三个是别过度设计。我见过太多项目,数据才几千条就上分布式向量库、上智能体框架,结果复杂度爆炸,效果还不如简单方案。技术选型永远匹配当前阶段,能跑通、能迭代,比看起来高级重要得多。
最后分享一个我常用的小技巧:给检索结果加一个相关性阈值。当最高相关性分数低于某个阈值时,直接判定为"知识库无相关内容",走兜底话术,不交给模型生成。这一招能挡掉相当一部分幻觉,实测非常有效。阈值怎么定?拿你的评估集跑一遍,看正确命中和错误命中的分数分布,取一个能分开两者的值就行。