最近总有朋友问我:RAG到底是什么?为什么大模型明明很能聊,一问到自家文档里的事实就胡编乱造?实际上,RAG(Retrieval-Augmented Generation,检索增强生成)是目前把大模型接进私有知识库最实用的一条路。它不要求你重新训练模型,也不需要你具备多深的算法背景,核心思路三句话就能讲清:先根据用户问题去文档库里“查资料”,再把查到的内容连同问题一起交给大模型,让模型“看着资料作答”。你不需要成为AI研究员也能上手,我需要你先记住一件事:RAG的难点不在原理,而在工程细节。
这篇文章我打算把整套流程完整展开,从“为什么要用RAG”讲到“自己动手搭一个最小系统”,再讲我实际踩过的坑。全文尽量少堆术语,能类比的地方绝不放公式,适合刚接触大模型应用开发、想快速搭建文档问答场景的开发者。内容有点长,建议收藏后分段看。
1. RAG到底在解决什么问题?先搞懂需求再学技术
1.1 大模型的“知识过期”和“幻觉”到底出在哪
大模型本质上是一个“概率语言模型”,它学的是海量文本里的统计规律,而不是一本随时更新的百科全书。即便很多商用大模型已经训练了几万亿个Token,训练完成的那一刻,它的知识截止时间就固定了。遇到训练数据里没有的、或者截止日期之后才出现的内容,它只能根据已有知识“猜”。这个“猜”的过程非常丝滑,以至于看起来完全像在陈述事实——这就是大家常说的“模型一本正经地胡说八道”。
另一个问题更现实:企业内部的规章制度、产品说明书、技术文档、合同文本,几乎不可能出现在任何公开数据集里。你直接把这类文档粘贴给模型,问它“报销流程是什么?”,它大概率会给你编一套流程。这不是模型不聪明,而是它压根没见过这些内容。想让模型“知道”这些私有内容,传统思路有两类:一是继续训练或微调,让私有知识固化进模型参数;二是做检索增强,让模型在回答时临时“翻书”。
1.2 为什么微调不是唯一选择,RAG凭什么成为主流
微调(Fine-tuning)当然有效果,但你得想清楚代价:先要清洗和标注一批高质量问答对、需要足够的训练算力、每次文档更新都要重新训练。对一个内容频繁变动的知识库来说,微调的成本极高,而且改了模型参数还有可能“灾难性遗忘”——模型学会了新内容,反而忘掉了旧能力。
RAG走的是另一条路:把知识放在模型之外,需要回答时先检索相关片段,再让大模型基于这些片段生成答案。这样有几个非常实际的好处:文档更新即时生效,不用动模型权重;回答可以附上原文引用,方便追溯;不同知识领域只需要换检索库,模型本身保持不变。你可以把它类比成开卷考试——微调是你把整本书背进脑子,RAG是考试时直接翻书找答案。对绝大多数应用场景来说,开卷显然更快、更稳、更容易验证。
1.3 RAG的核心工作流程一句话拆解
整套流程可以压缩成“索引—检索—生成”三段式。
- 索引阶段:把原始文档切分成小块,每一块喂给嵌入模型(Embedding Model),得到一串表示语义的向量,存入向量数据库。
- 检索阶段:收到用户问题后,把问题也转换成向量,在向量数据库里做相似度搜索,找出最相关的几个文档片段。
- 生成阶段:把检索到的片段组装成一个带指令的提示词(Prompt),连同原始问题一起交给大模型,模型用这些片段作为上下文来生成答案。
后面所有技术细节,都是在给这三段做优化。先把骨架记住,再往下看才不会迷路。
2. 小白最容易绕晕的三个环节:索引、检索、生成
2.1 文档加载与切分:Chunk尺寸怎么选,为什么不是越小越好
很多人第一次做RAG,以为把所有文档变成向量存进去就行了,结果查询效果一塌糊涂。问题往往出在文档切分上。切分的基本单位叫“Chunk”,也就是一个文档片段。这个片段既要保留完整的语义,又要足够小以便精准检索。切分太大,一次检索返回的内容混杂了多个主题,大模型分不清重点;切分太小,一个完整的知识点被切得七零八落,检索时经常漏掉关键内容。
如果你用的是某个开源的嵌入模型,先看一眼它的最大输入长度。有的模型支持512 Token,有的支持8000 Token。实际经验是,大多数通用知识库场景,单个Chunk控制在300到800个Token比较稳妥。我个人的起点通常是512 Token,并让相邻Chunk之间重叠100到150个Token。重叠的目的是避免一个句子在两段边界被拦腰截断,导致语义不完整。
另外,纯按固定长度切分只是兜底方案。只要文档本身有标题结构,就尽量先按段落、章节、列表结构做一次粗切,再对过长的章节做二次细切。结构化切分能显著提升检索质量,因为模型检索到的是一个有完整上下文的段落,而不是半句话。以前有一个文本问答项目,最初纯按长度切分,用户问“消假流程是什么”,检索到的片段永远缺前半段,改成按文档里的步骤清单切分后,效果立刻改观。
2.2 向量化与相似度检索:向量是什么,余弦距离为什么常见
嵌入模型做的事情,是把一句话变成一个高维向量。向量里的每个数字都代表该文本在一个维度上的“特征强度”。语义相似的句子,向量在高维空间里会靠得更近。检索的过程,本质上就是在向量空间里找“离问题向量最近”的文档向量。
衡量“近不近”有好几种方式:余弦相似度、内积、欧氏距离。用得最多的还是余弦相似度。原因是它对文本长度不敏感,只比较两个向量的方向趋势。比如一句话的长短版本语义一样,内积可能差了很多,但余弦相似度基本能保持一致。计算也简单:两个向量夹角的余弦值,范围在-1到1之间,越接近1表示越相似。实际系统中,有的向量数据库默认用内积加速计算,但会在存储前做归一化处理,让内积等价于余弦相似度,速度与效果兼顾。
| 相似度方法 | 计算方式 | 特点 | 常见用途 |
|---|---|---|---|
| 余弦相似度 | 方向夹角余弦 | 对向量长度不敏感,语义检索首选 | 文本/文档检索 |
| 内积 | 各维度乘积求和 | 计算快,要求向量已归一化 | 大规模向量检索加速 |
| 欧氏距离 | 直线距离 | 对数值幅度敏感,通常不直接用 | 少量场景下的异常检测 |
2.3 生成增强:把检索结果拼进Prompt的几种姿势
检索完成之后,工作还没结束。把检索结果“塞”进Prompt的方式,直接影响回答质量。最基础也最稳妥的模板是这样:
请根据以下资料回答问题。资料如下: <context> 片段1... 片段2... </context> 问题:{user_question} 要求: 1. 只能使用上述资料中的信息作答。 2. 如果资料中没有足够信息,请明确回答“资料中未找到相关信息”。 3. 引用来源时请标注片段编号。这个模板里有几个细节值得说。第一,必须给模型明确的“封闭域约束”,告诉它不要用内部知识脑补。第二,检索到的多个片段要带着编号送进去,这样模型回答时可以引用“根据片段2”,方便你做溯源。第三,如果检索结果与问题无关,宁愿让模型说“不知道”,也不要硬答。没有约束的Prompt,模型很容易被用户问题带偏,自己编出一个自洽但错误的答案。
更进一步,你可以把历史对话里提到的“上一轮答案”也放进上下文,做多轮对话RAG。但注意上下文越长,模型越容易被中间夹着的无关信息干扰。所以多轮场景下,通常只追溯最近一两轮,不要无限堆积历史。
3. 自己动手搭一套最小RAG系统:技术选型与完整步骤
3.1 工具选型:本地优先还是API优先
开始写代码之前,先把选型想清楚。RAG系统里至少有三个核心组件:嵌入模型、向量存储、大模型生成。这三个组件你既可以选择本地部署的开源方案,也可以选择托管API。我整理了一张对比表,方便你做选择。
| 维度 | 本地部署 | 托管API |
|---|---|---|
| 数据隐私 | 数据不出内网,私密性好 | 需评估数据是否能出域 |
| 成本模型 | GPU固定成本,用得越多越划算 | 按量付费,初期成本低 |
| 运维复杂度 | 需要自己管GPU、版本、并发 | 运维省心,但受制于平台状态 |
| 效果上限 | 取决于开源模型选择 | 通常有较大模型可选,效果上限高 |
| 更新频率 | 自己掌控 | 依赖平台发布节奏 |
我的建议是:如果是个人学习、快速验证Demo,优先用托管API,理由是你不需要一开始就背上GPU运维的包袱。如果做企业内部系统且数据敏感,那就必须本地化部署,哪怕是先用一个较小的开源嵌入模型加上一个参数量适中的大模型,也要守住数据边界。另外,嵌入模型、向量库、大模型这三个组件不一定要来自同一个生态,完全可以混搭。比如用某个开源的嵌入模型负责向量化、用某个自建的向量数据库做存储、再调用另一个开源模型做生成,这种组合在工程上很常见,也更容易找到性价比最优解。
3.2 六步实现RAG Demo:从文档到回答的完整链路
接下来我们走一遍完整的最小实现。下面的代码用伪代码示意,重点在于理解流程。不同库的调用名称会有差异,但逻辑完全一致。
第一步:加载文档。假设你已经有一份TXT或PDF格式的技术手册,先按段落内容读取。
documents = load_documents("manual.txt") # 返回一个文档列表,每个元素记录正文文本第二步:结构化切分。先把文档按标题拆分,再对超过512 Token的块做二次切分,相邻块保留100 Token重叠。
from your_chunker import RecursiveChunker chunker = RecursiveChunker(chunk_size=512, chunk_overlap=100) chunks = chunker.split_documents(documents) # 每个 chunk 包含 text 和 source(来源页码或标题)第三步:向量化。调用嵌入模型接口,把每个Chunk变成向量。这一步往往是最耗时的,可以批量处理。
from your_embedding import get_embedding chunk_vectors = [] for chunk in chunks: vector = get_embedding(chunk.text) chunk_vectors.append({ "id": chunk.id, "text": chunk.text, "vector": vector })第四步:写入向量存储。向量数据准备好以后,连同原文一起写入向量数据库。务必把原文存在数据库里,避免后面还得回头翻原始文件。
from your_vector_store import VectorStore store = VectorStore(collection="manual") store.add(chunk_vectors) # 向量和原文一并写入第五步:查询阶段。收到用户问题,做同样的向量化操作,然后执行相似度搜索,召回Top-K个Chunk。
query_vector = get_embedding(user_question) results = store.search(query_vector, top_k=5) # results 中包含匹配片段、相似度分数、来源信息第六步:组装Prompt并交给大模型。
from your_llm import generate context = "\n".join( f"[{idx}] {item.text}" for idx, item in enumerate(results) ) prompt = f""" 请根据以下资料回答问题。资料如下: <context> {context} </context> 问题:{user_question} 要求: 1. 只能使用资料中的信息回答。 2. 资料中没有的信息,请直接说明“未找到”。 3. 引用时标注片段编号。 """ answer = generate(prompt) print(answer)整套流程跑通后,你就拥有了一个最简RAG系统。后续所有优化,不过是在这六步里加细节。
3.3 关键参数调优:Top-K、分数阈值、长文本裁剪
代码能跑通只是第一步,真正决定效果的是几个参数。
首先是Top-K,也就是检索召回几个片段。K太小,可能漏掉关键信息;K太大,无关信息会混进上下文,大模型容易被干扰。经验上,从3开始试;知识库片段较短时可以用5;如果发现模型总是“答非所问”,先检查是不是K值太大,把不相关的片段也带进来了。我在一个技术文档场景里测试过,K从5降到3后,回答准确率反而上升,因为被检索出来的第4、第5个片段通常是弱相关甚至无关信息。
其次是相似度分数阈值(Score Threshold)。向量检索总会返回“最接近”的结果,哪怕全部不相关,它也会硬返回Top-K。所以一定要设一道门槛。比如某向量库用余弦相似度,范围在-1到1之间,我会把阈值设在0.6到0.75。低于这个分数,说明检索结果大概率不相关,这时候宁可让系统回复“未找到相关信息”,也不要误导用户。阈值没有通用值,它跟你用的嵌入模型、文档领域强相关,需要拿一批真实问题去试。
最后是长文本裁剪。如果检索到的Chunk很多,拼接后的上下文可能超过大模型的输入窗口。这时候不要无脑截断,而是按相关度排序后,优先保留最相关的前几个Chunk,超过窗口的部分直接丢弃。不要把从中间硬切出来的半句话塞进上下文,那样只会干扰模型。
4. 实测踩坑复盘:效果不好时先从哪查起
4.1 检索质量差:召回不对,问题很可能出在切分
我见过太多人一上来就怀疑嵌入模型不给力,实际上八成问题出在切分和索引策略。最常见的现象是:问题明明很具体,检索出来的却是整篇文档里无关的段落。这时候你先别急着换模型,先做两件事:第一,把落入Top-K的片段原样打出来看一眼,人工判断这些片段和问题是否语义相关;第二,检查切分是否破坏了句子结构。段落边界上有半句话、列表被拆成两半,这些都是检索失败的重灾区。
如果切分没问题,再考虑加“混合检索”。纯向量检索擅长找语义相近的内容,但对付精确的术语和编号就不太灵。比如用户搜“设备型号XL-2000”,向量检索可能觉得“XL-2100”和它语义相近,实际上用户要的是严格匹配。这时候需要在向量检索之外,叠加一层稀疏向量或关键词检索,再把两组结果合并去重。这种混合策略不复杂,但对专业文档的效果提升立竿见影。
4.2 生成质量差:模型没错,是Prompt和上下文的问题
有时候检索结果明明很准,模型回答仍然瞎编。这种问题的源头一般在生成侧。你可以做一个实验:把检索片段直接贴给模型,不附带任何指令,看看模型会不会照着原文说。如果模型自由发挥,那多半是Prompt缺少约束。
我的建议是,生成侧的Prompt一定要包含这两类信息:身份指令和边界指令。身份指令是“你是知识库助手”;边界指令是“只能根据给定资料回答,资料中没有的内容请直接承认不知道”。大多数开源模型对这类指令的遵循程度不算完美,因此你还可以在提示词里显式说明“回答时优先使用片段中出现的术语和表述,不要自行扩展”。这一步相当于把模型“按在资料上”回答问题。
另一个常见坑是把检索结果按字母序或原文顺序直接拼进上下文,而没有做重排序(Rerank)。向量检索的Top-K只是初筛,最相关的片段可能排在第二位。你可以用一个更精小的重排模型对Top-K再做一次打分,然后把得分最高的片段放在上下文最前面。大模型对上下文开头的内容注意力更强,把最相关的片段放在开头,回答质量的提升会很明显。
4.3 成本与性能优化:缓存、批量、混合检索
RAG系统跑通以后,你会开始关注成本和延迟。这里分享三个最实用的优化手段。
查询缓存:如果同一个问题被反复问到,把每次的检索结果和大模型回答都缓存起来。下次遇到完全相同或语义相似的问题,直接返回缓存结果,不再调用嵌入和大模型接口。能缓存命中,成本就是零。我做过一个内部问答工具,接入缓存后重复问题占比接近三分之一,成本直接降了一截。
批量向量化:文档入库阶段,不要一条一条地调嵌入接口。把切分好的Chunk攒成一个批次一起提交,接口支持的话,一次性传入几十个文本,吞吐量能提升好几倍。模型推理阶段也同样适用,能合并的请求尽量合并。
混合检索兜底:有些问题本身没有明确的语义,比如全是数字编号。纯向量检索召回质量差时,用关键词检索先兜一层,往往能拿到更精确的候选集。混合检索的代价是多一次检索,但换来的是召回稳定性。
5. 踩过不少坑之后的个人体会
RAG的学习路径其实很适合“先跑通再优化”:第一步用默认参数搭一个能回答问题的Demo;第二步逐环节做诊断,打日志、看中间结果;第三步再有针对性地调整切分和Prompt。不要一上来就追求完美系统,因为很多问题等你真正跑通以后才会暴露出来。我个人体会最深的一点是,RAG最值钱的环节永远是“检索”而不是“生成”——上下文里根本没有正确答案,模型再强也答不对。反过来,只要检索到的片段准确,哪怕用一个参数量不大的开源模型,也能给出让人满意的答案。
最后再分享一个我建议你养成的习惯:每次改完切分策略、Prompt模板或者阈值,都保留一组固定的测试问题,跑完一轮人工看一遍结果,把“答对的/答错的/答偏的”记下来。这个测试集就是你的效果基准,没有它,你会永远凭感觉调参,今天觉得好了,明天改个参数又退了,却说不清到底哪里变了。带着一套自己的评估集去迭代,RAG系统的效果曲线才会看得清楚。