1. 为什么RAG值得你花时间搞明白
RAG这个词,这两年在大模型圈子里出现的频率高得离谱。不管你是做AI应用的开发者,还是刚入门大模型的小白,只要涉及到“让模型回答得更准”“让模型知道它原本不知道的东西”“让模型别瞎编”,绕来绕去最终都会落到RAG上。RAG的全称是Retrieval-Augmented Generation,翻译过来叫检索增强生成。名字听着挺学术,但核心逻辑其实特别朴素——模型自己记不住或者不知道的东西,你帮它去外面查资料,查到了再让它组织语言回答。
我刚开始接触RAG的时候,也觉得这东西应该不难:不就是把文档切一切、向量化、存进数据库、查询的时候做相似度匹配、把匹配到的内容塞进提示词里让模型生成答案吗?但真正动手做起来才发现,坑远比想象的多。切分粒度怎么定?向量模型选哪个?检索回来一堆不相关的内容怎么办?检索评估怎么做?这些问题不解决,搭出来的RAG系统就是个花架子,看着能用,实际一问就露馅。
这篇内容适合谁看?如果你是零基础想入门RAG,我会从最基础的概念讲起,把每个环节的原理和实操都拆开揉碎;如果你已经搭过简单的RAG流程但效果不理想,我会重点讲检索评估和优化策略,这些是区分“能用”和“好用”的关键。整篇内容会围绕RAG的功能原理、系统构建、检索评估三条主线展开,每一步都配上可复现的操作思路和参数选择逻辑。
注意:RAG不是万能药。它解决的是“知识时效性”和“领域知识注入”的问题,但解决不了模型本身推理能力不足的问题。搞清楚它的能力边界,比盲目上手更重要。
2. RAG到底在解决什么问题
2.1 大模型的三道硬伤
要理解RAG的价值,得先搞清楚大模型本身有哪些绕不过去的局限。我总结下来主要是三道硬伤。
第一道是知识截止。任何大模型的训练数据都有时间边界,比如某个模型训练数据截止到2024年初,那你问它2025年发生的事,它要么说不知道,要么就开始编。这不是模型笨,是它确实没见过。
第二道是私有知识盲区。你公司内部的文档、产品手册、客户资料,这些东西不可能出现在公开训练数据里。你直接问模型“我们公司产品的退货政策是什么”,它只能瞎猜。
第三道是幻觉问题。大模型本质上是概率模型,它在生成每一个token的时候,是在预测“下一个最可能出现的词”。当它没有足够信息支撑的时候,它会用看起来合理但实际错误的内容来填充答案。而且它说得特别自信,你如果不了解情况,根本分辨不出来。
这三道硬伤,靠重新训练模型或者微调,成本高、周期长、效果还不一定好。RAG提供了一条更轻量的路径:不改模型参数,通过外挂知识库的方式,让模型在生成回答之前先“查资料”。
2.2 RAG的核心工作流拆解
RAG的完整流程可以拆成两个阶段:索引阶段和查询阶段。
索引阶段是离线的,你先把所有知识文档处理好,存进向量数据库。具体步骤包括:文档加载、文本切分、向量化、存入向量数据库。这个阶段相当于你给模型建了一个“图书馆”,把所有的书都编好目、上好书架。
查询阶段是在线的,用户提问之后,系统先把问题向量化,然后去向量数据库里找最相似的文本片段,把这些片段和原始问题一起塞进提示词,最后交给大模型生成答案。这个阶段相当于用户来图书馆问问题,你先去书架上找到相关的几本书,翻到相关页码,然后基于这些内容来回答。
听起来很简单对吧?但每个环节都有大量细节需要做决策。比如文本切分,你切得太碎,语义不完整;切得太粗,检索精度下降。比如向量模型,你选中文效果差的模型,检索出来的东西驴唇不对马嘴。比如检索策略,你只用向量相似度,可能漏掉关键词精确匹配的情况。
2.3 RAG和微调怎么选
经常有人问:RAG和微调到底用哪个?我的经验是,它们解决的不是同一类问题。
微调适合改变模型的行为模式,比如让模型的输出风格更正式、更简洁,或者让模型学会某种特定的输出格式。微调是在教模型“怎么说话”。
RAG适合给模型补充知识内容,比如让模型知道最新的政策法规、公司内部流程、特定领域的专业知识。RAG是在告诉模型“说什么”。
实际项目中,两者经常配合使用。先用RAG解决知识注入的问题,如果发现模型在特定任务上的输出格式或推理方式不理想,再考虑用微调来调整行为。但绝大多数场景下,RAG的投入产出比远高于微调,因为微调需要标注数据、需要GPU资源、需要反复实验,而RAG的迭代周期短得多。
实操心得:如果你刚开始做RAG,不要一上来就追求完美。先用最简单的方案跑通全流程,哪怕切分策略很粗糙、向量模型用的是默认的,先看到效果,再逐步优化每个环节。我见过太多人卡在“选哪个向量模型”这一步纠结好几天,结果全流程都没跑通。
3. 动手搭建RAG系统:从文档到向量库
3.1 文档加载与预处理
搭建RAG的第一步是把你的知识文档加载进来。文档格式可能五花八门:PDF、Word、Markdown、HTML、Excel、数据库导出等等。不同格式需要不同的加载器。
PDF是最麻烦的格式之一。有些PDF是扫描件,需要OCR;有些PDF有复杂的表格和图表,直接提取文本会丢失结构信息;有些PDF是多栏排版,提取出来的文本顺序是乱的。我的建议是,如果PDF质量太差,宁可手动整理成Markdown或纯文本,也不要硬用自动提取,因为垃圾进垃圾出,后面检索效果一定好不了。
Word文档相对好处理,用python-docx之类的库可以提取段落和表格。Markdown和纯文本最简单,直接读取就行。HTML需要去掉标签,保留正文内容。
预处理阶段还需要做几件事:去掉页眉页脚、去掉重复内容、统一编码格式、处理特殊字符。这些看起来是小事,但如果不做,后面切分出来的文本块会包含大量噪音,影响检索质量。
# 以PDF加载为例的伪代码思路 from langchain.document_loaders import PyPDFLoader loader = PyPDFLoader("knowledge_base.pdf") pages = loader.load() # 检查每页内容质量 for i, page in enumerate(pages): text = page.page_content.strip() if len(text) < 50: print(f"第{i}页内容过短,可能是扫描件或空白页,需要单独处理")3.2 文本切分:粒度决定成败
文本切分是RAG系统中最容易被低估的环节。很多人随便设个chunk_size=1000、chunk_overlap=200就完事了,结果检索效果一塌糊涂。
切分的核心矛盾在于:块太小,语义不完整;块太大,噪音太多。你想想,如果一个文本块只有一句话“该政策自2024年1月1日起施行”,检索出来之后模型根本不知道这是什么政策。但如果一个文本块有3000字,里面涵盖了五个不同的主题,你检索“退货政策”的时候,可能匹配到的是这个块里关于“换货政策”的那部分,但模型看到的是整个3000字,容易被无关信息干扰。
我的经验是,按语义边界切分比按固定字数切分效果好得多。具体来说:
- 对于结构化文档(如Markdown、HTML),按标题层级切分,每个小节作为一个块,如果小节太长再按段落细分。
- 对于非结构化文档(如纯文本、对话记录),按段落切分,如果段落太长再按句子切分。
- 对于代码文档,按函数或类切分。
- 对于表格,把表头和每一行组合成一个完整的描述块。
chunk_size的设置没有标准答案,但有一个经验范围:中文文本建议在300-800字之间,英文文本建议在200-500词之间。chunk_overlap建议设为chunk_size的10%-20%,目的是让相邻块之间有上下文衔接,避免在边界处丢失信息。
# 按标题层级切分的思路 from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on = [ ("#", "一级标题"), ("##", "二级标题"), ("###", "三级标题"), ] splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on) chunks = splitter.split_text(markdown_content) # 如果某个块超过800字,再用递归切分器细分 from langchain.text_splitter import RecursiveCharacterTextSplitter recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", ",", ""] )注意事项:切分的时候一定要保留元数据。比如每个块属于哪个文档、哪个章节、哪个页码。这些元数据在检索阶段可以用来做过滤,在生成阶段可以用来做引用标注。我见过有人切完只保留文本内容,后面想加过滤功能的时候发现元数据全丢了,只能重新处理一遍。
3.3 向量化:选对模型比选贵的更重要
向量化就是把文本转成一串数字(向量),这串数字代表了文本的语义信息。语义相近的文本,向量距离就近;语义无关的文本,向量距离就远。
向量模型的选择直接决定了检索质量的上限。如果向量模型本身对中文语义理解不好,后面再怎么优化检索策略都是白搭。
选向量模型的时候,我主要看几个维度:
- 语言支持:是否支持中文,中文效果如何。有些模型英文很强但中文拉胯。
- 维度:向量维度越高,表达能力越强,但存储和计算成本也越高。常见的有384维、768维、1024维、1536维。
- 最大输入长度:模型能处理多长的文本。如果你的chunk_size是800字,模型最大输入只有512个token,那超出的部分会被截断。
- 推理速度:如果你有大量文档需要向量化,推理速度直接影响索引构建时间。
- 是否开源:开源模型可以本地部署,数据不出内网;闭源API模型通常效果更好但需要联网调用。
目前中文场景下,常用的向量模型有几类:一类是专门针对中文优化的开源模型,一类是多语言通用模型,还有一类是商业API。我的建议是,如果你的数据敏感度不高,可以先用商业API快速验证效果;如果数据不能出内网,就选开源模型本地部署。
# 向量化示例(以开源模型为例) from sentence_transformers import SentenceTransformer model = SentenceTransformer("your-chinese-embedding-model") texts = ["文本块1的内容", "文本块2的内容"] embeddings = model.encode(texts, normalize_embeddings=True) # normalize_embeddings=True 很重要 # 归一化之后,余弦相似度计算可以用点积代替,速度更快实操心得:不要盲目追求高维度。我实测下来,768维和1536维在大多数中文检索场景下的效果差异并不明显,但存储成本差了一倍。先用768维跑起来,如果发现检索效果确实不够,再考虑升级。
3.4 向量数据库选型与入库
向量数据库是专门用来存储和检索向量的。选型的时候主要考虑几个因素:数据量级、查询延迟要求、是否需要持久化、是否需要分布式、运维成本。
小规模场景(几万到几十万条向量),用FAISS就够了。FAISS是Facebook开源的向量检索库,轻量、快、不需要额外部署服务,直接嵌在Python代码里就能用。缺点是它本质上是个索引文件,不支持增删改查的实时操作,每次更新都需要重建索引。
中等规模场景(百万到千万级),可以考虑Milvus、Qdrant、Weaviate这类专门的向量数据库。它们支持实时增删改查、支持分布式部署、有完善的API和监控。缺点是部署和运维复杂度上来了。
如果已经有PostgreSQL,可以用pgvector扩展,直接在关系型数据库里存向量。好处是不用额外维护一套数据库,坏处是性能和功能不如专门的向量数据库。
大规模场景(亿级以上),那就需要考虑分布式向量数据库了,比如Milvus集群版。这个量级一般公司也碰不到,这里不展开。
入库的时候,除了向量本身,还要存原始文本和元数据。原始文本用于后续塞进提示词,元数据用于过滤和引用。
# 以FAISS为例的入库思路 import faiss import numpy as np dimension = 768 # 向量维度 index = faiss.IndexFlatIP(dimension) # IP = Inner Product,内积 # 假设embeddings是numpy数组,shape为(n, 768) embeddings = np.array(embeddings).astype('float32') index.add(embeddings) # 保存索引 faiss.write_index(index, "vector_index.faiss") # 同时保存文本和元数据 import json with open("chunks.json", "w", encoding="utf-8") as f: json.dump(chunks, f, ensure_ascii=False)4. 检索策略与效果评估
4.1 向量检索的局限与补充方案
向量检索的核心是语义相似度,它擅长处理“意思相近但用词不同”的情况。比如用户问“怎么退钱”,文档里写的是“退款流程”,向量检索能匹配上。
但向量检索也有明显的短板。第一,它对精确匹配不敏感。比如用户问“产品型号X200的保修期”,如果文档里写的是“X200型号保修期为两年”,向量检索可能匹配到其他型号的保修信息,因为语义上都是“保修期”。第二,它对否定语义处理不好。比如用户问“哪些情况不支持退货”,向量检索可能匹配到“支持退货的情况”,因为语义相似度很高。第三,它对数字和专有名词不敏感。比如“2024年政策”和“2023年政策”,向量可能很接近,但实际内容完全不同。
解决这些问题的常见方案是混合检索:向量检索 + 关键词检索(如BM25),然后把两路结果融合。融合策略有几种:加权求和、RRF(Reciprocal Rank Fusion)、先向量后关键词过滤等。
# RRF融合策略的伪代码 def rrf_fusion(vector_results, keyword_results, k=60): scores = {} for rank, doc_id in enumerate(vector_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) for rank, doc_id in enumerate(keyword_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) # 按分数降序排列 sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [doc_id for doc_id, _ in sorted_docs]RRF的好处是不需要调权重,对两路检索的分数尺度不敏感。我实测下来,RRF在大多数场景下比加权求和更稳定。
4.2 重排序:让最相关的结果排到最前面
检索回来Top-K个结果之后,还有一个优化空间:重排序。向量检索用的是双塔模型,查询和文档分别编码,然后算相似度。这种方式速度快,但精度有限。重排序用的是交叉编码器,把查询和文档拼在一起输入模型,模型直接输出相关性分数。这种方式精度高,但速度慢,所以只适合对少量候选结果做精排。
典型流程是:向量检索召回Top-50,然后用重排序模型对50个结果精排,取Top-5塞进提示词。这样既保证了召回率,又保证了精度。
重排序模型的选择和向量模型类似,也要看中文支持、推理速度、是否开源。有些向量模型本身就提供了配套的重排序模型,搭配使用效果更好。
注意事项:重排序不是必须的。如果你的检索结果本身已经很好,Top-5都是相关的,加不加重排序差别不大。但如果你的检索结果噪音比较多,重排序能显著提升效果。建议先做检索评估,看看当前的问题出在召回阶段还是排序阶段,再决定要不要加重排序。
4.3 检索评估:怎么知道你的RAG好不好
这是很多人忽略的环节。搭完RAG系统,随便问几个问题觉得“好像还行”,就上线了。结果用户一问稍微偏一点的问题,就露馅了。
检索评估需要一套系统的指标和方法。核心指标包括:
- 召回率:相关文档有多少被检索出来了。召回率低,说明你的检索策略漏掉了重要信息。
- 精确率:检索出来的文档有多少是相关的。精确率低,说明噪音太多,会干扰模型生成。
- MRR:第一个相关文档排在第几位。MRR高,说明最相关的内容排在最前面。
- NDCG:考虑排序位置的相关性指标,越相关的内容排得越靠前,分数越高。
做评估需要标注数据。你需要准备一批查询,每个查询标注哪些文档是相关的。标注数据不用很多,50-100个查询就能看出问题。关键是覆盖不同类型的查询:事实型、对比型、否定型、多跳推理型等。
# 简单的检索评估示例 def evaluate_retrieval(queries, ground_truth, retriever, k=5): recall_scores = [] precision_scores = [] mrr_scores = [] for query, relevant_ids in zip(queries, ground_truth): retrieved = retriever.search(query, top_k=k) retrieved_ids = [doc.id for doc in retrieved] # 召回率 hit = len(set(retrieved_ids) & set(relevant_ids)) recall = hit / len(relevant_ids) if relevant_ids else 0 recall_scores.append(recall) # 精确率 precision = hit / k precision_scores.append(precision) # MRR for rank, doc_id in enumerate(retrieved_ids): if doc_id in relevant_ids: mrr_scores.append(1 / (rank + 1)) break else: mrr_scores.append(0) return { "recall@k": sum(recall_scores) / len(recall_scores), "precision@k": sum(precision_scores) / len(precision_scores), "mrr": sum(mrr_scores) / len(mrr_scores) }评估结果怎么用?如果召回率低,说明你的切分粒度、向量模型、检索策略有问题,需要调整。如果精确率低但召回率高,说明检索回来的东西太多太杂,需要加重排序或者调整Top-K。如果MRR低,说明排序有问题,最相关的内容没有排到前面。
4.4 生成阶段的优化技巧
检索做完了,最后一步是把检索结果和用户问题一起塞给大模型生成答案。这一步也有不少优化空间。
提示词设计是关键。你需要明确告诉模型:只根据提供的参考资料回答,如果参考资料里没有相关信息,就说不知道,不要自己编。这个约束能大幅降低幻觉。
上下文长度控制也很重要。检索回来的内容不是越多越好。塞太多内容进去,一是可能超出模型的上下文窗口,二是无关信息会干扰模型。我的经验是,Top-3到Top-5通常就够了,具体看chunk_size和模型上下文窗口。
引用标注能提升可信度。在提示词里要求模型在回答中标注信息来源,比如“根据文档A第3节的内容...”。这样用户能验证答案的可靠性,也方便排查问题。
# 提示词模板示例 PROMPT_TEMPLATE = """你是一个知识助手。请根据以下参考资料回答用户问题。 参考资料: {context} 用户问题:{question} 回答要求: 1. 只根据参考资料回答,不要使用你自己的知识 2. 如果参考资料中没有相关信息,直接说"根据现有资料无法回答该问题" 3. 回答时标注信息来源,格式为[来源:文档名] 4. 回答要简洁准确,不要展开无关内容 回答:"""5. 常见问题与排查技巧实录
5.1 检索效果差的排查思路
检索效果差是最常见的问题。排查的时候,我一般按这个顺序来:
第一步,检查切分质量。随便抽几个chunk出来看看,是不是语义完整?有没有被切断的句子?有没有包含大量噪音?如果切分质量差,后面怎么优化都是白搭。
第二步,检查向量模型。拿几个查询和对应的相关文档,手动算一下向量相似度。如果相关文档的相似度还不如无关文档高,说明向量模型不适合你的场景。
第三步,检查检索策略。是不是只用了向量检索?有没有考虑混合检索?Top-K设的是多少?K太小可能漏掉相关结果,K太大噪音太多。
第四步,检查评估方法。你的评估指标合理吗?标注数据准确吗?有时候不是系统效果差,是评估方法有问题。
5.2 模型回答不准确的原因分析
检索回来的内容是对的,但模型回答还是不对,这种情况通常有几个原因:
- 提示词约束不够:模型没有严格遵循“只根据参考资料回答”的指令,掺杂了自己的知识。
- 上下文太长:塞了太多内容,模型注意力被分散,关键信息被淹没。
- 参考资料格式混乱:检索回来的文本没有清晰的边界,模型分不清哪些是参考资料、哪些是问题。
- 模型本身能力不足:有些小模型在长上下文场景下表现确实差,换更大的模型可能就解决了。
5.3 性能优化的几个方向
RAG系统的性能优化主要从两个维度考虑:延迟和成本。
延迟优化:向量检索本身很快,瓶颈通常在向量化和重排序。如果向量模型推理慢,可以考虑用更小的模型或者做量化。如果重排序慢,可以减少候选数量或者用更快的重排序模型。
成本优化:如果用的是商业API,向量化和生成都是按量计费的。优化方向包括:减少不必要的向量化(比如增量更新而不是全量重建)、压缩上下文长度、缓存常见查询的结果。
实操心得:我踩过最大的坑是忽略了增量更新。一开始每次文档有更新就全量重建索引,几万条数据要跑好几个小时。后来改成只对新增和修改的文档做向量化,删除的文档从索引里移除,更新时间缩短到几分钟。如果你的知识库更新频繁,一定要设计好增量更新机制。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 检索结果完全不相关 | 向量模型不匹配 | 检查向量模型的中文效果 | 换用中文优化的向量模型 |
| 检索结果漏掉关键信息 | 切分粒度过粗或过细 | 检查chunk_size和切分边界 | 调整切分策略,按语义边界切分 |
| 相似问题检索结果不稳定 | 向量模型对语义变化敏感 | 测试不同表述的相似度 | 增加查询改写或混合检索 |
| 模型回答包含幻觉 | 提示词约束不够 | 检查提示词模板 | 强化“只根据参考资料回答”的约束 |
| 系统响应慢 | 向量化或重排序耗时 | 分阶段计时 | 优化模型选择或减少候选数量 |
| 新增文档检索不到 | 索引未更新 | 检查索引更新机制 | 实现增量更新流程 |
6. 从能用走向好用:持续迭代的思路
RAG系统不是搭完就完事了,它需要持续迭代。迭代的依据来自两个方面:用户反馈和评估数据。
用户反馈是最直接的信号。用户点了“答案不准”的按钮,或者追问“你确定吗”,这些都是优化线索。把这些问题收集起来,分析是检索问题还是生成问题,然后针对性优化。
评估数据是更系统的依据。定期跑一遍评估集,看各项指标的变化趋势。如果召回率在下降,可能是知识库内容老化了;如果精确率在下降,可能是文档噪音增加了。
迭代的优先级建议是:先解决检索问题,再解决生成问题。因为检索是根基,检索不准,生成再优化也没用。检索问题里,先解决切分和向量模型的问题,再考虑混合检索和重排序。
还有一个容易被忽略的点:知识库的维护。文档会过期、会更新、会删除。你需要一套机制来保证知识库和实际业务保持一致。我见过一些RAG系统,刚上线效果很好,过了半年就越来越差,因为知识库没人维护,里面全是过时信息。
最后分享一个我在实际项目中总结的小技巧:给每个chunk打上时间戳和版本号。检索的时候,可以优先返回最新版本的内容。如果同一个问题有多个版本的答案,模型可以基于时间戳来判断哪个是最新的。这个小小的元数据字段,在知识频繁更新的场景下能省很多事。