先抛一个我最近常被问到的场景:企业内部积累了上千份制度文档、产品手册和运维记录,业务同事每天在群里问“报销流程到底怎么走”“这个接口报错以前怎么处理的”,大模型直接回答吧,要么一本正经地编,要么说“我知识截止到某年某月”。这时候聊起RAG,几乎所有人都会点头——但真到动手做,很多人又卡在“这到底是一个概念还是一个系统”上。
RAG的全称是Retrieval-Augmented Generation,检索增强生成。它的核心思路很简单:不在生成答案前让模型硬背知识,而是先从一个外部知识库里把相关资料“找”出来,再带着这些资料让大模型组织答案。这篇内容不堆术语,尽量把RAG从“为什么需要”到“怎么落地”讲透,适合刚接触RAG、准备做知识库问答、文档问答,或者正在调研技术方案的开发者参考。我会用自己的项目经验,把索引、检索、生成这几块一件件拆开,讲清楚每个环节的取舍和常见坑。
1. RAG到底是什么,为什么大家都在聊
1.1 一句话定义与核心思路
先给一个我的理解:RAG = 检索(Retrieval)+ 增强(Augmentation)+ 生成(Generation)。整个过程用一个开卷考试来类比最贴切。
普通的大模型调用,像是闭卷考试。模型脑子里装了什么,就只能答什么,知识截止时间一到,新信息一概不知道;遇到训练语料里没见过的问题,它就靠“发挥”硬编一个答案,这就是大家常说的幻觉。RAG相当于给了模型一份“参考资料”再让它答题:用户提问之后,系统先从资料库里检索出最相关的几个片段,把片段拼进提示词,模型根据这些片段组织答案。这个过程里模型不需要记住所有东西,它只需要会“查资料”和“读资料回答问题”。
这个思路解决的核心痛点有三个:
- 知识过时:模型训练有截止时间,但业务文档一直在更新,RAG让知识库成为独立于模型的可更新层,换文档不用重新训练模型。
- 私有领域知识缺失:企业内部流程、行业规则、专有名词,模型在训练时根本没见过,RAG通过检索把这类“非公开知识”喂给模型。
- 幻觉不可控:完全依靠模型记忆作答,无法约束它从哪里获取事实。RAG把答案限定在检索到的片段上,至少让模型“有据可依”。
这也是为什么RAG在近一年多几乎成了企业知识库问答的标准方案。它不改变模型本身,而是改变模型作答时的输入,实现成本低、见效快,适合绝大多数“基于已有文档回答问题”的场景。
1.2 RAG和微调、长上下文是什么关系
聊RAG的时候,不可避免要对比另外两条路:微调(Fine-tuning)和超长上下文(Long Context)。
微调是直接调整模型权重,让模型“记住”某些知识或行为风格。但实际做过的都知道,微调在“注入事实性知识”这件事上效率和成本都不算理想:你要准备大量高质量问答对,训练一轮要花不少算力,知识更新了还得重新训练。RAG侧重点不一样,它不改变模型,而是改变模型能看到的输入材料。如果说微调是给员工做培训,让他把业务记在脑子里;RAG就是给员工配一台能随时查资料的电脑,员工不需要背下所有细则,只要知道怎么检索和读资料就行。
超长上下文则是一个争议比较大的方向。以某些长窗口模型为例,窗口确实能塞下几百万token,但你真把一本几百页的手册全部塞进提示词,可能会遇到两个问题:一是成本随token数量线性增长,二是模型在长文本里“大海捞针”,对埋在深处的信息关注度会下降。我在实际测试里发现,同样一个问题,把相关段落精准抽出来拼成几千字上下文,比无脑塞几万字全文的效果更稳定,也更容易控制成本。
| 方案 | 知识更新 | 幻觉风险 | 私有知识 | 成本 | 适用场景 |
|---|---|---|---|---|---|
| RAG | 改知识库即可 | 较低 | 支持 | 低 | 文档问答、私域知识、开放问答 |
| 微调 | 需重新训练 | 中等 | 支持 | 高 | 固定格式输出、领域风格、特定任务 |
| 超长上下文 | 改提示词即可 | 中高 | 支持 | 高 | 单次大文件精读,如合同、论文 |
真实项目中这三者不是二选一,通常是组合使用。我见过的典型做法是:RAG负责事实性知识来源,微调负责输出格式和语气风格,长上下文只用在少量大文件单次分析场景。基础入门阶段,优先把RAG跑通,收益最直观。
2. 把RAG拆开看:索引、检索、增强与生成
2.1 索引构建:从原始文档到可检索的向量
RAG的第一步,是把散落的文档变成计算机能快速查询的结构化数据。这个过程通常叫“建索引”,包含三个关键动作:切分、向量化和存储。
先说切分。原始文档可能是一个几百页的PDF,也可能是一堆Markdown笔记,不可能整篇丢给模型,所以要把文档切成一块块小片段。这个“块”叫Chunk,切多大多小直接决定检索精度。块太大,一段里塞了太多主题,检索时容易把不相关的信息一并带回,生成时上下文冗余;块太小,语义不完整,检索可能只命中半句话,模型理解困难。我在实际项目里的经验是:通用文本可以按固定字符数切,比如500到800个字符;技术文档、制度文件这种章节结构强的,要优先按标题切,再用字符数兜底。同时相邻块之间保留10%到15%的重叠,防止关键信息正好被切在两块中间。
切好之后,需要对每个chunk做向量化,也就是用嵌入模型把文本转换成一串数字向量。这个环节最容易忽略的一点是:嵌入模型和生成模型是两回事,它们可能来自不同厂商,甚至使用不同语言生态。选择嵌入模型时优先看它对中文的支持度、向量维度、检索精度指标,以及是否能本地部署。
向量化完成后,所有向量要存入向量数据库。这里给出一个常见工具的选型参考:
| 工具 | 部署方式 | 特点 | 适合场景 |
|---|---|---|---|
| Chroma | 轻量、可嵌入应用 | 上手快,开发调试方便 | 原型、Demo、小规模知识库 |
| FAISS | 库形式,需自行封装 | 检索性能高,灵活 | 对性能敏感的自研系统 |
| Milvus | 独立服务 | 分布式、支持超大规模 | 企业级生产环境 |
| Qdrant | 独立服务 | 过滤能力强、接口友好 | 需要复杂条件过滤的场景 |
2.2 检索:向量相似度、Top-K与混合检索
索引建完,就到了RAG最核心的环节:检索。用户输入一个问题后,系统把问题也向量化,然后去向量库里找出与它“语义最接近”的若干片段。
这里说的“语义最接近”,在数学上通常用余弦相似度来衡量,公式可以简化理解为两个向量在方向上的一致性。夹角越小,余弦值越接近1,表示越相关。也有用内积或欧氏距离的,但结果排序逻辑类似。维度不是越高越好,高维度虽然表达能力强,但计算成本和存储开销都会上去,常见的中文嵌入模型维度在768到1024之间,这个量级在检索精度和性能上比较平衡。
检索返回的片段数量K也很有讲究。Top-K太小,可能漏掉正确答案;太大,生成的上下文就塞满噪音。我通常从5开始调,观察检索结果和答案质量再增减。如果知识库里有大量相似文本,又想提高精度,可以在向量检索之外叠加BM25关键词检索,再把两路结果合并打分,这就是常说的“混合检索”。之所以需要混合检索,是因为向量检索擅长语义匹配,但在精确术语、编号、代码报错这类场景下,关键词往往更可靠。比如用户问“预算编号BGT-2025-013怎么填”,如果知识库里恰好有完全相同编号的文档,纯向量检索反而可能因为“语义相近”把其他预算流程排到前面。
如果对召回质量要求更高,可以再加一层重排序(Rerank):先让向量检索捞回几十个候选,再用一个专门的重排序模型逐条精排,最后取最相关的3到5条进入生成。重排增益明显,但会多一次模型调用,延迟会上升。在入门阶段,先不加重排也能跑通,等发现“答案找不到”时再叠加更划算。
2.3 增强与生成:如何把资料变成好答案
检索到相关片段之后,下一步是把这些片段组装进提示词,让生成模型基于它们作答。这个过程虽然看起来只是“拼字符串”,但其中的细节对回答质量影响很大。
首先要设计一个清晰的系统提示词。我常用的基础模板大概是这样的:
你是一个知识库问答助手。请严格根据下面提供的参考内容回答用户问题。 如果参考内容中没有足够信息,请直接回复“根据现有资料无法回答”,不要自行编造。 回答时请尽量使用参考内容中的原文表述,并在句末标注信息来源编号 [1]、[2]。 参考内容: [1] (第1个片段) [2] (第2个片段) [3] (第3个片段) 用户问题: (用户输入的问题)这个模板里有几个关键设计:约束模型只从参考内容作答,降低幻觉;引导模型明确说“不知道”,而不是硬答;要求标注来源编号,便于溯源。不要小看这几句提示词的作用,我在实际对比测试中发现,加了“无法回答就明说”之后,编造率显著下降。
生成阶段还有一个容易被忽视的问题:上下文的组织顺序。理论上模型会均匀注意到上下文的所有位置,但实际测试中,紧挨在问题前面的片段往往对答案影响最大。所以我在拼接时,会把重排后得分最高的片段放在最前面,而不是按原文顺序拼。
另外要提一句,生成模型的选择同样影响效果。如果知识库内容偏中文专业领域,找一个中文指令遵循能力强的模型往往比通用英文模型更合适,尤其当文档里存在大量专有名词时,模型的“读懂并复述资料”能力比“临时发挥创作”能力更重要。
3. 从零跑通一个最小RAG:以某份内部技术手册为例
3.1 准备物料与整体流程
理论知识说了一堆,接下来我拿一份虚构的“某团队内部技术手册”当示例,走一遍完整流程。假设这份手册是一份Markdown文件,内容包括部署命令、常见报错排查、参数配置说明三部分。目标场景是:让同事问“服务启动失败怎么排查”时,系统能从手册里找到对应内容并给出带出处的回答。
一个最小可用的RAG流程,可以拆成离线与在线两段。离线阶段做索引构建:加载文档、切分、向量化、写入向量库。在线阶段做问答:用户输入问题、问题向量化、检索Top-K、拼装提示词、调用大模型生成答案。两段流程的代码框架可以复用同一套嵌入模型和配置,但运行时机完全不同。
在动手之前,还有两个物料需要确定:嵌入模型和向量库。入门阶段,我建议先用轻量方案:嵌入模型选一个本地可跑的中文模型,向量库直接在一个脚本进程里用Chroma或者FAISS起步,先把流程跑通,再考虑迁移到分布式部署。生成模型可以调用云端的商用大模型API,也可以本地部署一个小参数模型,根据环境和预算自己定。
3.2 文档加载与切分:参数选择背后的原因
文档加载本身没什么难度,用现成的解析库读入文本就可以。关键在于切分。我用代码展示一下常见做法,并以注释说明其中的参数逻辑。
from langchain.text_splitter import RecursiveCharacterTextSplitter # 按章节标题优先,字符数兜底 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个块目标长度,单位字符 chunk_overlap=50, # 相邻块重叠长度,约为块长的10% separators=["\n## ", "\n### ", "\n", "。", "!", "?", " ", ""], keep_separator="end", # 保留分隔符,让标题跟着正文走 ) chunks = text_splitter.split_text(document_text)这里为什么选500字符、重叠50?500字符对中文来说大约覆盖一个三级标题下的一段完整内容,既不会因为太长混入多个主题,也不会因为太短导致语义碎片化。50字符的重叠相当于给块与块之间留了一条“缓冲带”,如果某句话的核心主语正好落在前一块末尾,后一块开头还能把它接住。为什么分隔符里要放“\n## ”和“\n### ”?因为手册的二级、三级标题往往是下一个主题的起点,在标题前断开,能最大限度保证每个块的语义独立。
切分完成后,把chunks逐个喂给嵌入模型,得到向量列表,再连同原文文本和元数据一起写入向量库:
from chromadb import Client from chromadb.config import Settings client = Client(Settings(anonymized_telemetry=False)) collection = client.create_collection("team_manual") for idx, chunk in enumerate(chunks): embedding = embedding_model.encode(chunk) collection.add( ids=[f"chunk_{idx}"], embeddings=[embedding], documents=[chunk], metadatas=[{"source": "team_manual.md", "chunk_index": idx}] )这里把原文和元数据同步存进去,是个很容易踩漏的细节。后面做引用溯源时,需要靠documents字段拼接上下文,靠元数据里的source字段告诉用户“这段话出自哪个文档的哪一块”。
3.3 在线问答:从问题到带引用的答案
离线索引构建好了,在线问答的代码结构就清晰了。分四步:问题向量化、检索、拼提示词、调生成模型。
# 1. 问题向量化 query_embedding = embedding_model.encode(user_question) # 2. 检索Top-5 results = collection.query( query_embeddings=[query_embedding], n_results=5, include=["documents", "metadatas", "distances"] ) # 3. 按距离排序,拼装上下文 context_blocks = [] for idx, (doc, meta, dist) in enumerate(zip( results["documents"][0], results["metadatas"][0], results["distances"][0] )): context_blocks.append(f"[{idx + 1}] {doc}") context_text = "\n".join(context_blocks) # 4. 构造提示词并调用生成模型 prompt = f""" 你是一个运维知识库问答助手。请严格根据下面提供的参考内容回答问题。 如果参考内容中没有足够信息,请直接回复“根据现有资料无法回答”。 每句话末尾请标注信息来源编号,如 [1]。 参考内容: {context_text} 用户问题: {user_question} """ answer = chat_model.generate(prompt)这一步看起来简单,但有三个细节我建议认真对待。
第一,检索时不要只返回向量和文档文本,把distances也带出来。它帮你判断“这次检索到底匹配不匹配”。如果最相似片段的距离已经很高,那说明知识库里大概率没有答案,这时宁可让它说“无法回答”,也不要硬答。第二,Top-K在这个例子里设了5,我一个常用的调法是:先看检索结果中前两条是否已经覆盖答案,如果前两条都不相关,那多半是切分或Embedding模型的问题,而不是把K调到10就能解决的。第三,生成模型返回的答案要和原始chunk做关联,最简单的办法就是上面代码里给chunk编号的方式,编号保留在回答中,用户就能顺着编号反查原始文档。
跑通这个最小流程之后,一个能用的RAG问答系统就已经立起来了。后面要增强的话,通常是在检索这一层做文章,比如加混合检索、加重排、加知识库的增量更新。但基础版本的价值在于:它让你清楚看到整个链路的每一个环节,之后做任何优化都知道该动哪里。
4. RAG落地最常见的坑与排查手册
4.1 检索不到:答案永远是“我不知道”
这是RAG项目里最让人头大的问题。你明确知道知识库文档里有答案,但系统答不上来。排查顺序一般是这样的:
先看切分。如果文档被切成均匀的500字小块,一个完整流程说明可能被拆到了三个不同的块里,而每块内容都只有一半信息,模型自然拼不出完整答案。解决方法是改用按标题切分,或者把chunk_size加大到1000试试,同时把重叠增加到10%。再看嵌入模型。我遇到过用通用英文模型处理中文技术手册的情况,检索效果非常差,换一个在中文语料上训练过的嵌入模型之后,准确率肉眼可见地提升。最后看Top-K。K设太小会漏召回,但如果前几个结果的相似度分数本身就不高,问题通常出在前面两步,K值不是根本原因。
4.2 检索到了但回答胡说:幻觉没有彻底消失
有时检索结果是对的,大模型还是答非所问,或者自己脑补细节。这种情况多半在提示词约束和生成模型两个环节出了问题。
提示词上,最常见的错误是“参考内容”和“用户问题”在提示词里的位置不够清晰,模型没有明确意识到“只能依据参考内容作答”。我在模板里专门加了“严格根据参考内容回答”,并对“无法回答”的情况做了明确指示,这是两个非常有效的约束。生成模型方面,如果用的是偏对话风格的通用模型,它在面对“知识库问答”这一任务时可能更倾向于“闲聊式发挥”而不是“引用式作答”,有条件的话可以换一个指令遵循能力更强,或者专门做过RAG评估优化的模型对比一下。
还有一个隐蔽原因:检索返回的chunk本身质量不高,比如同一块里混了两段冲突信息。这时候要回去看切分和清洗,而不是只调prompt。
4.3 上下文太长、成本暴涨、响应变慢
有些人一做RAG就把Top-K设成10甚至更多,chunk_size还很大,结果一次问答就消耗大量token,响应也慢。这里我有几条降本提速的经验。
第一,控制进入上下文的片段数量。先检索Top-20,但只把重排后得分最高的Top-3到Top-5拼进上下文,噪音少,成本也低。第二,对chunk做截断或压缩,比如把每个chunk限制在300到500字符,超出部分丢弃。这个方法对“答案只会在文档某一小段里出现”的场景很有效。第三,考虑给知识库做精简,把重复内容、失效内容、纯图片型PDF里的无用文本清洗掉,检索精度和成本都会改善。
4.4 知识更新了,旧答案还在
知识库不是建一次就一劳永逸的。最常见的场景是运维手册更新了部署命令,但系统还按旧命令回答。处理方式分两步:一是向量库要支持按文档ID删除旧chunk并写入新chunk,也就是upsert;二是在检索和生成时把文档版本信息作为元数据一起返回,让用户能感知到“这条来源来自哪个版本”。
我在之前的一个项目里,用“文档名+更新时间”作为元数据,每次更新文档时把旧版本的chunk先删除再重新向量化。这样不会因为新旧版本同时存在而导致答案混乱。这个习惯一旦养成,知识库的长期维护成本会低很多。
4.5 一个速查表:RAG排查关键点
| 症状 | 可能原因 | 优先排查项 |
|---|---|---|
| 总是“不知道” | 切分不合理 / 嵌入模型不匹配 / K太小 | 先调切分,再换嵌入模型 |
| 答案编造 | 提示词约束不足 / chunk质量差 | 加“无法回答就明说”约束,检查chunk语义 |
| 检索结果跑偏 | 向量检索对术语不敏感 | 叠加BM25关键词检索 |
| 上下文超长 | K太大 / chunk太长 | 降低K到3-5,压缩chunk |
| 更新后旧答案残留 | 旧chunk未删除 | 用元数据管理版本,upsert更新 |
| 答案没有出处 | 拼接时未保留元数据 | 把文档名、chunk编号写入提示词和引用 |
做RAG项目到现在,我最大的体会是:这个技术方向看起来入门门槛不高,但真正要把效果调到可用,功夫全在细节里。切分、嵌入、检索、提示词、生成,每个环节都可能成为瓶颈,而大多数问题并不需要上复杂框架,先把基础流程跑通,再按这套排查思路逐步优化,基本上就能解决七八成问题。如果你正准备做自己的第一个RAG系统,我建议先用一个中等体量、内容质量较高的文档集,把上面这套最小流程完整走一遍,亲眼看看每个环节的输出长什么样,再去考虑混合检索、重排这类进阶组件。