1. RAG 到底在解决什么问题
1.1 从一次尴尬的问答说起
去年年底我帮一个做工业设备维保的团队做技术咨询,他们想用大模型做一个内部知识助手。第一版做出来特别简单,就是把设备手册、故障处理记录、历史工单全部塞进提示词里,然后让模型回答工程师的问题。Demo 阶段效果惊艳,所有人都觉得这事成了。结果上线第三天就翻车了:一位老师傅问“3号产线液压站压力波动超过0.5MPa时,按照去年11月那份技改方案应该先查哪个阀”,模型一本正经地回答说“请先检查电磁换向阀”,但那份技改方案里明确写的是“先确认蓄能器氮气压力,再排查比例阀”。答案错得离谱,而且语气极其自信。
这个场景几乎每天都在各种团队里重演。大模型本身是一个“参数化知识容器”,它记住的是训练语料里的统计规律,而不是你企业内部的、私有的、时效性极强的知识。你问它通用问题它很在行,你问它“我们公司上周刚定的报销新规”,它只能靠猜。更麻烦的是,它猜错的时候不会告诉你“我不确定”,而是用同样流畅的语气编一个听起来很合理的答案。这就是所谓的幻觉。
RAG(Retrieval-Augmented Generation,检索增强生成)就是冲着这个问题来的。它的核心思路非常朴素:别让模型凭记忆答题,先帮它把相关资料找出来,摆在它面前,让它照着资料回答。就像开卷考试,学生不需要背下整本书,但需要知道去哪一页找答案。RAG 要解决的就是“去哪一页找”和“怎么照着答”这两件事。
1.2 RAG、微调、长上下文,三条路怎么选
很多人一上来就问“我该用 RAG 还是微调”。我的经验是,先问自己三个问题:知识更新频率多高?知识量多大?对答案可追溯性的要求多强?
| 方案 | 适合场景 | 知识更新成本 | 可追溯性 | 典型坑 |
|---|---|---|---|---|
| RAG | 知识频繁更新、量大、需要引用来源 | 低,改库即可 | 强,能给出原文出处 | 检索不准导致答非所问 |
| 微调 | 固定领域风格、固定格式输出 | 高,要重新训练 | 弱,说不清依据 | 知识更新要重训,成本高 |
| 长上下文 | 单次任务、资料量小 | 无 | 中 | 贵、慢、超长后注意力衰减 |
我一般建议:只要你的知识是“文档形态”且会变,优先 RAG。微调更适合教模型“怎么说话”,而不是“记住什么”。长上下文适合一次性分析,比如“把这份合同总结一下”,但不适合做长期知识库,因为每次都要把全部资料塞进去,成本和延迟都受不了。
1.3 一条完整的 RAG 链路长什么样
把 RAG 拆开看,其实就是一条流水线,我习惯把它分成两大阶段、六个环节:
离线建库阶段:文档加载 → 文本切块 → 向量化 → 存入向量数据库。
在线检索阶段:用户提问 → 问题向量化 → 向量检索召回 → 重排 → 拼装提示词 → 大模型生成答案。
这条链路里,任何一个环节出问题,最终答案都会崩。我见过太多团队把 90% 的精力花在“换个更强的模型”上,结果检索环节召回的全是无关内容,模型再强也只能对着垃圾资料编。RAG 的瓶颈几乎永远在检索,不在生成。这句话你先记住,后面每一节我都会反复印证它。
2. 建库:把文档变成模型能用的知识
2.1 文档加载:别小看格式清洗这一步
建库的第一步是把各种格式的文档读进来。PDF、Word、Markdown、HTML、Excel、甚至扫描件,LangChain 里对应一堆 Loader,比如PyPDFLoader、UnstructuredWordDocumentLoader、TextLoader。看起来很简单,但这里埋的坑最多。
我踩过最狠的一次是 PDF 解析。一份 200 页的设备手册,用默认的 PDF Loader 读出来,表格全部错位,页眉页脚混进正文,双栏排版被读成一行一行的乱码。结果切块之后,每个块都是语义破碎的碎片,检索出来的内容驴唇不对马嘴。后来换成unstructured库配合strategy="hi_res",表格识别明显改善,但速度慢了好几倍。
我的实操建议是:
- PDF 优先用
unstructured或pdfplumber,尤其是含表格的文档,别用最基础的解析器。 - 扫描件必须先做 OCR,否则读出来是空白。OCR 质量直接决定后续一切。
- 页眉页脚、页码、水印要清洗掉,这些噪声会污染向量,让检索跑偏。
- 保留文档的层级结构,比如标题、章节号,后面切块时能派上大用场。
提示:文档加载阶段一定要抽样人工检查。随机抽 10 个块,读一遍,看看语义是否完整。这一步花 20 分钟,能省你后面几天的调试。
2.2 文本切块:RAG 里最被低估的环节
切块(Chunking)是 RAG 里最不起眼、却最影响效果的一步。为什么?因为向量检索的最小单位就是块。块切得不好,检索再准也没用。
先说块大小。太小,比如 100 字,一个完整的意思被切碎,检索出来缺上下文;太大,比如 2000 字,一个块里混了好几个主题,向量被“平均”了,检索精度下降。业界常见的经验值是256 到 512 个 token,但这只是起点,不是标准答案。
我一般用这个思路定块大小:看你的知识颗粒度。如果用户的问题通常针对某个具体操作步骤,块就切小一点,300 token 左右;如果问题需要一整段论述才能回答,块就切大一点,800 token 左右。没有万能值,必须拿真实问题去测。
再说切块策略。最粗暴的是固定长度切,按字符数硬切,简单但容易切断句子。好一点的是递归切分(RecursiveCharacterTextSplitter),按段落、句子、词的优先级依次尝试,尽量在语义边界断开。再进一步是按文档结构切,比如按 Markdown 标题、按章节切,这样每个块天然是一个完整主题。
from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = splitter.split_text(document_text)这里的chunk_overlap是块之间的重叠部分,我一般设成块大小的 10% 到 20%。为什么要重叠?因为切块难免在边界处丢信息,重叠能让相邻块共享一部分上下文,检索时不容易漏。比如一个关键结论正好卡在两个块的交界处,没有重叠就可能两边都只拿到半句。
注意:中文文档的 separators 一定要加上中文标点。默认的英文分隔符对中文很不友好,会把一整段中文当成一个“词”处理,切出来的块质量很差。
2.3 向量化:Embedding 模型怎么选
切完块,下一步是把每个块转成向量。这一步用的是 Embedding 模型,它把一段文本映射成一个高维浮点数组,语义相近的文本在向量空间里距离更近。
选 Embedding 模型,我主要看四个维度:语言支持、维度、性能、成本。
| 模型 | 语言 | 维度 | 特点 | 适用场景 |
|---|---|---|---|---|
| text-embedding-3-small | 多语言 | 1536 | 便宜、快、效果均衡 | 通用场景首选 |
| text-embedding-3-large | 多语言 | 3072 | 效果更好、更贵 | 对精度要求高 |
| bge-large-zh | 中文 | 1024 | 中文效果好、可本地部署 | 中文知识库、数据敏感 |
| m3e-base | 中文 | 768 | 轻量、本地 | 资源受限的本地部署 |
我的经验是:中文知识库优先考虑 bge 系列或 m3e,尤其是数据不能出内网的场景,本地部署是刚需。如果追求省事且能接受调用外部服务,text-embedding-3-small 是性价比很高的选择。
这里有个关键点很多人忽略:建库用的 Embedding 模型和检索时用的必须是同一个。你建库用 A 模型,检索用 B 模型,两个向量空间根本对不上,检索结果就是随机的。我见过有人建库时用了一个模型,后来觉得另一个更好,直接换了检索端,结果整个库废掉,只能重建。
还有一个细节是归一化。有些模型输出的向量没有归一化,做余弦相似度之前要手动归一化,否则距离计算会偏。LangChain 的很多封装会自动处理,但自己写代码时要注意。
2.4 向量数据库选型:Milvus、Chroma、Qdrant 怎么挑
向量数据库是 RAG 的存储层。市面上的选择很多,我重点说三个最常被问到的:Chroma、Qdrant、Milvus。
Chroma是最容易上手的,几行代码就能跑起来,支持内存模式和本地持久化。适合原型验证、小规模知识库、个人项目。缺点是生产环境的扩展性一般,数据量大了之后性能会吃紧。
Qdrant是我个人最推荐的中小规模生产选择。它是 Rust 写的,性能好,支持过滤、混合检索,部署也简单,Docker 一条命令就能起来。API 设计清晰,Python 客户端用起来很顺手。
Milvus是重量级选手,适合大规模、高并发的场景。它支持多种索引类型(IVF、HNSW、DiskANN 等),能水平扩展,但部署和运维复杂度明显更高,需要依赖 etcd、MinIO 等组件。如果你的数据量在百万级以下,用 Milvus 有点杀鸡用牛刀。
| 数据库 | 上手难度 | 扩展性 | 适合规模 | 部署方式 |
|---|---|---|---|---|
| Chroma | 极低 | 一般 | 万级以下 | 内存/本地 |
| Qdrant | 低 | 好 | 十万到百万级 | Docker/云 |
| Milvus | 中高 | 极好 | 百万级以上 | 集群 |
选型的核心原则是:别过度设计。我见过一个团队,知识库总共就 3000 个块,非要上 Milvus 集群,结果运维成本比开发成本还高。先用 Chroma 或 Qdrant 跑通,等数据量真的上来了再迁移,迁移成本远低于你想象。
from qdrant_client import QdrantClient from langchain.vectorstores import Qdrant client = QdrantClient(path="./qdrant_data") # 本地模式 vectorstore = Qdrant( client=client, collection_name="knowledge_base", embeddings=embedding_model ) vectorstore.add_documents(chunks)3. 检索:决定 RAG 成败的关键一环
3.1 相似度检索的基本原理
检索的本质是:把用户的问题也转成向量,然后在向量库里找距离最近的几个块。距离度量常用余弦相似度,值越接近 1 越相似。
听起来简单,但“最近”不等于“最相关”。向量检索是语义近似,不是逻辑精确。比如用户问“设备过热怎么处理”,向量检索可能召回“设备温度监测方案”,语义上很近,但用户要的是处理步骤,不是监测方案。这就是为什么单纯靠向量检索不够,后面要加各种优化。
检索时有个参数叫top_k,就是召回多少个块。太小,可能漏掉关键信息;太大,噪声变多,还会挤占提示词空间。我一般从 4 到 6 开始调,配合重排使用。
3.2 混合检索:向量 + 关键词,效果立竿见影
纯向量检索有个软肋:对专有名词、型号、编号不敏感。比如用户问“ERR-2047 报错怎么解决”,向量模型可能把“ERR-2047”当成普通文本,召回一堆泛泛的报错处理,反而漏掉真正提到这个编号的文档。
解决办法是混合检索:向量检索负责语义匹配,关键词检索(BM25)负责精确匹配,两路结果融合。这样既能理解“过热”和“温度过高”是同一回事,又能精准命中“ERR-2047”这种硬编码。
Qdrant 和 Milvus 都支持混合检索,LangChain 里也有EnsembleRetriever可以把多个检索器组合起来:
from langchain.retrievers import EnsembleRetriever, BM25Retriever bm25 = BM25Retriever.from_documents(chunks) bm25.k = 4 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) ensemble = EnsembleRetriever( retrievers=[bm25, vector_retriever], weights=[0.4, 0.6] )实测下来,混合检索在专有名词密集的工业、医疗、法律场景里,召回率提升非常明显。权重怎么设?我一般让向量占 0.6,关键词占 0.4,具体还得拿测试集调。
3.3 重排:把最相关的顶到最前面
混合检索召回了 8 个块,但它们的相关性参差不齐。这时候需要一个重排模型(Reranker)来精排。重排模型通常是交叉编码器(Cross-Encoder),它把问题和每个块拼在一起打分,精度比向量相似度高得多,但速度慢,所以只用在召回后的少量候选上。
流程是:向量检索召回 20 个 → 重排模型打分 → 取前 5 个送给大模型。这样既保证了召回广度,又保证了精度。
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compressor = CrossEncoderReranker(model=model, top_n=5) retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble )重排是我认为性价比最高的优化手段之一。加一个重排模型,往往比换一个更贵的生成模型效果提升更明显。因为生成模型再强,喂给它的资料不对,它也答不对。
3.4 查询改写:让用户的问题更好被检索
用户提问往往很口语、很模糊。比如“那个东西坏了咋整”,向量检索根本不知道“那个东西”是什么。查询改写就是在大模型检索之前,先把问题改写成更适合检索的形式。
常见做法有几种:一是问题扩展,把一个问题改写成多个相关查询,分别检索再合并;二是指代消解,结合对话历史把“那个东西”替换成具体名词;三是HyDE,让模型先“假装”写一个答案,再用这个答案去检索,因为答案和文档的语义更接近。
rewrite_prompt = """根据对话历史,把用户的问题改写成独立、完整、适合检索的查询。 对话历史:{history} 用户问题:{question} 改写后的查询:"""查询改写对多轮对话场景尤其重要。单轮问答可能感觉不出来,一旦用户开始追问“那它呢”“还有别的吗”,没有改写就会检索得一塌糊涂。
4. 生成:把检索结果变成靠谱答案
4.1 提示词拼装:给模型立规矩
检索到相关块之后,要把它们和用户问题一起拼成提示词。这一步看似简单,其实决定了模型会不会“跑偏”。
我的提示词模板一般包含四部分:角色设定、参考资料、用户问题、回答约束。关键是约束部分,必须明确告诉模型:只根据参考资料回答,资料里没有就说不知道,不要编。
prompt_template = """你是一个严谨的知识助手。请严格根据下面的参考资料回答用户问题。 参考资料: {context} 用户问题:{question} 回答要求: 1. 只使用参考资料中的信息,不要编造。 2. 如果参考资料中没有相关信息,直接回答“根据现有资料无法回答”。 3. 回答时标注信息来源的文档名称。 """这个“不知道就说不知道”的约束极其重要。我做过对比测试,不加这条约束,模型在资料缺失时的幻觉率能到 40% 以上;加上之后,降到个位数。宁可它说“不知道”,也不要它编一个错误答案,尤其在工业、医疗这种场景,错误答案的代价太高。
4.2 引用溯源:让答案可验证
RAG 相比纯生成的一大优势是可追溯。答案里的每句话,最好都能对应到具体的文档块。实现方式是在拼装提示词时给每个块编号,要求模型在回答时引用编号,前端再把编号映射回原文。
这样做有两个好处:一是用户能自己验证答案对不对,二是出问题时你能快速定位是检索错了还是生成错了。我在项目里会强制要求模型输出引用,哪怕牺牲一点流畅度也值得。
4.3 生成模型的选择与参数调优
生成模型的选择要看场景。如果对成本敏感,中小参数模型配合好的检索往往够用;如果对推理能力要求高,比如需要综合多个块做推理,就得上更强的模型。
参数上,temperature建议设低,0 到 0.3 之间,因为 RAG 要的是忠实于资料,不是发挥创意。max_tokens要留够,别让答案被截断。还有一个容易忽略的是上下文长度,检索回来的块加上提示词不能超过模型的上下文窗口,超了会被截断,所以top_k不能无限大。
5. 常见问题与排查实录
5.1 检索不准的排查思路
检索不准是最高频的问题。我的排查顺序是:
- 先看召回内容。把检索到的块打印出来,人工判断相关性。如果召回的就是垃圾,问题在检索层,不在生成层。
- 检查切块质量。块是不是语义破碎?是不是太大混了多个主题?
- 检查 Embedding 模型。建库和检索是不是同一个模型?模型是否适合你的语言和领域?
- 加混合检索和重排。这是提升召回最直接的手段。
- 做查询改写。用户问题本身是否适合检索?
5.2 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 答非所问 | 召回内容不相关 | 检查切块、加混合检索、加重排 |
| 答案编造 | 提示词约束不足 | 加强“不知道就说不知道”约束 |
| 漏掉关键信息 | top_k 太小或切块切断 | 增大 top_k、加块重叠 |
| 专有名词检索不到 | 纯向量检索不敏感 | 加 BM25 混合检索 |
| 多轮对话检索乱 | 指代未消解 | 加查询改写 |
| 响应慢 | 重排或模型太大 | 减少候选数、换轻量模型 |
| 答案被截断 | max_tokens 或上下文超限 | 调大参数、减少 top_k |
5.3 几个我踩过的坑
坑一:块重叠设太大。有次我把 overlap 设成块大小的一半,结果检索出来一堆重复内容,提示词被撑爆,模型反而抓不住重点。重叠 10% 到 20% 就够了。
坑二:忽略元数据过滤。知识库里有多个版本的手册,检索时没按版本过滤,召回了旧版本的内容,答案自然错。后来给每个块加了版本、部门、时间等元数据,检索时先过滤再检索,准确率大幅提升。
坑三:盲目追求大模型。有段时间我总觉得答案不好是模型不够强,换了个更大的模型,效果提升有限,成本翻倍。后来发现瓶颈在检索,把检索优化好,小模型也能答得很准。
坑四:没有评测集。早期我全靠感觉调参,改来改去不知道有没有变好。后来建了一个几十条真实问题的评测集,每次改动都跑一遍,才知道哪些优化真的有效。没有评测的调优都是玄学。
6. 从 RAG 到 Agentic RAG 的演进
6.1 传统 RAG 的天花板
传统 RAG 是一条固定流水线:检索一次,生成一次。但真实问题往往需要多步推理。比如“对比 A 方案和 B 方案的成本差异”,一次检索可能只召回 A 方案,模型就答不全。再比如“根据故障现象推断原因”,需要先检索现象,再检索原因,再关联。
这就是传统 RAG 的天花板:它不会“想”,只会“查一次然后答”。
6.2 Agentic RAG:让模型自己决定怎么查
Agentic RAG 的思路是,把检索变成模型可以调用的工具,让模型自己决定:要不要检索、检索什么、检索几次、结果够不够、要不要再查。这就用到了 Agent 框架,比如 LangChain 的 Agent、LangGraph 的状态机。
LangChain 和 LangGraph 的区别这里顺带说一句:LangChain 更偏向链式编排,适合线性流程;LangGraph 是图结构,支持循环、分支、状态管理,适合需要多步推理和条件跳转的 Agent 场景。做 Agentic RAG,LangGraph 更合适。
一个典型的 Agentic RAG 流程是:模型先判断问题类型 → 决定检索策略 → 检索 → 评估结果是否充分 → 不充分就改写查询再检索 → 充分了再生成。这个循环让 RAG 从“一次性”变成“迭代式”,能处理复杂得多的任务。
6.3 什么时候该上 Agentic RAG
我的建议是:先用传统 RAG 跑通,遇到多步推理的瓶颈再上 Agentic。Agentic RAG 更灵活,但也更慢、更贵、更难调试。如果你的问题大多是“查一个事实”,传统 RAG 完全够用。只有当问题需要“查多个事实再综合”时,Agentic 的价值才体现出来。
7. 一套可复用的落地清单
最后把我这些年做 RAG 的经验浓缩成一份清单,你照着走能少踩很多坑:
- 建库阶段:文档清洗要彻底,切块要按语义,块大小拿真实问题测,Embedding 模型建库检索必须一致。
- 检索阶段:混合检索是标配,重排是性价比之王,查询改写解决多轮和模糊问题。
- 生成阶段:提示词约束要硬,引用溯源要强制,temperature 要低。
- 评测阶段:一定要建评测集,没有评测的调优都是玄学。
- 演进阶段:传统 RAG 够用就别上 Agentic,需要多步推理再考虑。
我个人在实际操作中的体会是,RAG 这件事,80% 的功夫在数据准备和检索优化上,只有 20% 在模型和提示词上。很多人本末倒置,天天研究换哪个模型,却不肯花时间把文档切好、把检索调准。你把检索做扎实了,哪怕用中等模型,答案也能又准又稳。反过来,检索一塌糊涂,用再贵的模型也是白搭。这个道理,我是在踩了无数次坑之后才真正明白的。