1. 为什么需要RAG:从“背课本”到“翻资料”的思维转变
先说个很多人都会遇到的场景:你兴致勃勃地部署了一个大模型,让它回答公司内部的制度问题,结果它一本正经地给出了完全错误的答案,甚至编造了一个不存在的流程。这不是模型不够聪明,而是它“记忆”里根本没有你们公司的资料。大模型的训练数据是某个时间点之前公开的互联网语料,它不可能知道你们内部系统里新上传的那份一百多页的运维手册。这就是RAG系统框架要解决的核心问题——让模型在生成回答之前,先从外部知识库里检索相关的资料,再把资料作为上下文交给模型,让它“看着材料说话”。
我从一开始接触RAG就很喜欢这个思路:它是典型的“检索 + 生成”两头配合。检索负责从海量文档里找出跟当前问题最相关的几个片段,生成负责把这些片段组织成通顺、准确的答案。你可以把它理解成一个开卷考试:模型不再闭卷背诵,而是给它一本可以随手翻阅的参考书,它只需要学会怎么快速翻到正确的那一页,再结合问题写答案。
这套框架特别适合这几类朋友:
- 做企业内部知识库问答系统的开发者,不想把公司机密文档直接拿去微调模型;
- 做智能客服、法律文书辅助、医疗资料查询等垂直领域应用的同学,需要模型回答基于最新、最准确的资料;
- 研究大模型应用架构,想搞清楚从文档处理到向量检索再到Prompt拼接这条链路的人。
这篇文章我会按实际搭建的顺序,把RAG完整链路拆开揉碎讲一遍,从索引构建、检索、重排序、Prompt组装到生成,每一段都会给出我在实际项目里验证过的参数、操作步骤和踩坑记录。不管你是刚入门的初学者,还是已经搭过一版但检索效果不理想的老手,都能在里面找到可以直接用的东西。
2. RAG系统整体架构拆解:一条完整链路要经过哪些环节
2.1 链路全景图:四个核心阶段一个都不能少
很多人以为RAG就是“把文档存进向量库,然后查一下再给模型”。真做起来就会发现事情远没那么简单。一个完整的RAG系统框架,至少包含四个核心阶段:索引构建(Indexing)、查询处理(Query Processing)、检索召回(Retrieval)和生成回答(Generation),除此之外还应该有评估与反馈环节,否则你根本不知道自己的系统改完是变好了还是变差了。
我习惯把整条链路画成一条流水线:
- 先是离线阶段,把原始文档(PDF、Word、Markdown、HTML都算)读出来,做清洗、分块、向量化,最后写入向量数据库,这一整段我们叫“索引构建”;
- 然后是在线阶段,用户输入一个query,系统先对这个query做理解、改写、扩展,然后去向量库里做相似度检索,拿到候选片段后再用重排序模型精排,这段叫“检索”;
- 检索完会把命中的片段和原始问题一起组装成一个精心设计的Prompt,交给大模型去生成最终答案,这段叫“生成”;
- 最后是“评估优化”,用一组标注好的测试问题去跑整个系统,算召回率、命中率、答案正确率等指标,根据问题来调整前面的参数。
在这里我必须强调:索引阶段的质量决定了检索的上限,检索阶段的质量决定了生成的下限。很多RAG效果不好,不是大模型不行,而是前面几步就没做好。
2.2 链路中的两个关键视角:离线和在线
区分离线和在线非常重要,因为它们对性能、成本、可更新性的要求完全不同。
离线阶段(索引构建)是“慢工出细活”的环节。它不要求实时,可以花比较多的时间去清洗文档、精细分块、用比较大的Embedding模型做向量化。这个阶段的核心目标是把非结构化的文档转换成结构化的、可检索的向量索引。你需要考虑的不仅是“把文本切成块”,还包括:文档格式解析是否丢失了表格结构?图片里的文字要不要OCR?PDF里的页眉页脚是否污染了正文?这些细节直接决定了后面检索到的内容干不干净。
在线阶段(查询和检索)是“分秒必争”的环节。用户发出一个问题后,系统必须在几百毫秒内完成从问题向量化到召回、重排序的整个过程。因此在线阶段通常会选择轻量级的Embedding模型,或者借助向量数据库的索引加速能力。如果检索耗时太长,整个对话体验就会很差,哪怕生成得再好也白搭。
离线建索引、在线做检索,这两部分可以独立部署。离线任务可以用批处理框架定时跑;在线服务则要保证高可用、低延迟。我个人建议在一开始就把这两条链路分开设计,否则后面想优化某一侧时就容易牵一发动全身。
2.3 数据流在链路中是如何流动的
我用一个实际查询“公司年假制度是怎么规定的”来走一遍完整的数据流,这样你会有更直观的理解。
离线阶段,系统已经把《员工手册》等文档拆成了几十个片段,每个片段都通过Embedding模型转成了一个高维向量,连同原文和元数据(页码、来源文档名等)一起存进了向量数据库。
在线阶段开始后,用户输入进入系统,查询处理器会对它进行改写,例如扩展成“公司年假 规定 天数 申请流程”这种更利于检索的形式。接着这个查询被同一个Embedding模型转成向量,拿去向量数据库里做最近邻搜索,返回Top 20个候选片段。随后重排序模型在这20个候选中挑出最相关的5个。最后,这5个片段被拼接到Prompt的系统指令区,连同用户原始问题一起发送给大模型。大模型基于这些片段生成一段带有明确引用来源的回答,返回给用户。
整个过程看起来简单,但每个环节都有不少可以深度优化的细节,下面我分段展开讲。
3. 索引构建:决定整条链路上限的“地基工程”
3.1 文档解析与清洗:这一步做不好,后面全白搭
很多人拿到文档的第一反应就是直接分块,这是新手最容易犯的错误。实际上,文档解析质量对RAG效果的影响甚至超过了Embedding模型的选择。
我举个真实的例子:某次做一个企业知识库项目,文档里有大量PDF是从Excel导出的表格,直接读出来的文本全部挤成一团,分块之后每个块里都是“字段名:值 字段名:值”的碎片,检索根本没法用。后来花了一天时间针对性写了解析脚本,把表格按行重构,效果立刻提升了一大截。
在处理非结构化文本时,你要注意这么几个问题:
- PDF的文本提取顺序:有些扫描件需要OCR,而OCR出来的文本顺序可能是乱的,尤其是双栏排版。遇到双栏文档,建议先做版面分析再拼接文本。
- 表格与图片:如果文档里大量信息是表格,纯文本提取会丢失行列关系。可以考虑用表格识别工具把表格转成Markdown格式,再把Markdown文本送去分块。
- 噪声清理:页眉、页脚、页码、水印、目录、参考文献这些内容在分块之前应该去掉,否则检索时会被无效信息干扰。
我通常的做法是:先转成统一格式(比如Markdown或纯文本),再做一轮正则清洗,最后人工抽看10到20个片段确认质量。这一步属于投入不高但回报极大的环节。
3.2 分块策略:大小、重叠和边界是三个核心参数
分块是RAG里最容易被低估的环节。分块太小,单个块包含的语义信息不完整,检索时容易丢失关键上下文;分块太大,块内噪声增多,向量表示的语义被稀释,而且可能超出大模型上下文窗口,导致生成阶段不得不截断。这里没有一个万能参数,但我的经验可以供你参考:
- 分块大小:对于通用知识库问答,我一般默认用300到500个字符(中文约150到250个汉字),这个范围既能保证语义完整,又不会让向量太“糊”。如果是代码文档、法律条文这种结构化强的文本,可以适当调小到200到300字符。
- 重叠长度:相邻块之间留10%到20%的重叠,可以避免关键句子恰好被切分到两个块的边界。比如块大小400、重叠80,是个不错的起点。
- 分块边界:优先按照标题、段落、句子这类自然边界切分,而不是硬按字符数切。像Markdown的标题层级、PDF的章节结构,都是很好的分割信号。
另外要注意,分块时最好保留元数据。比如每个块能追溯到它来自哪个文档、哪个章节、第几页。这些元数据在生成阶段可以用来做引用溯源,也能在后端做权限过滤。
3.3 Embedding模型选型:不要盲目追大模型
Embedding模型的任务是把文本映射成一个稠密向量,让语义相近的文本在向量空间中距离更近。选择Embedding模型有几个关键考量:
- 领域适配性:通用Embedding模型在通用领域效果不错,但在法律、医疗等专业领域可能会表现一般。有条件的话,用你自己领域的数据微调一个Embedding模型,效果提升会非常明显。
- 向量维度与距离计算:常见模型的向量维度从384到3072不等。维度越高,表达能力越强,但存储和计算成本也越高。查询时通常用余弦相似度或内积来衡量相关性,所以向量必须做归一化。
- 语言支持:如果你的文档是中文为主,一定要选对中文语料充分训练过的模型,否则经常会出现“字面很像但语义不对”的情况。
说句实在话,在Embedding这一步,通用模型已经能覆盖80%的场景,不要一开始就追求微调。先把流程打通,再根据评估结果决定是否需要升级模型。检索效果不理想时,优先去查分块和查询改写,而不是急着换Embedding。
3.4 向量数据库与索引:存得下还得查得快
向量数据库负责存储Embedding向量并支持近似最近邻(ANN)检索。目前市面上可选的方案很多,从轻量级的FAISS这种库,到功能完整的专用向量数据库都有。它们各有特点:
| 方案类型 | 代表 | 适用场景 | 注意事项 |
|---|---|---|---|
| 轻量级库 | FAISS | 单机、原型验证、数据量小 | 不支持持久化、需要自己管理索引 |
| 开源数据库 | 某开源向量数据库 | 生产级部署、要求高可用 | 需要运维,默认参数不一定适合业务 |
| 云服务 | 某云厂商向量检索服务 | 不想自己维护基础设施 | 注意数据隐私,成本随用量上升 |
不管用哪种,核心要关注三个参数:索引类型(如HNSW、IVF)、相似度度量(通常选余弦距离)、查询返回数量TopK。索引类型直接决定了检索速度和召回效果的平衡。HNSW这类基于图的索引召回质量高但建索引慢、内存占用大;IVF类索引速度快但需要训练聚类,参数调起来稍微复杂。我建议数据量在百万级以内直接用HNSW,配合合理的ef_search和ef_construction参数,效果和性能都能接受。
注意:向量数据库的返回结果不一定都是“语义相关”的。它本质上是向量空间里的几何最近邻,如果文档里出现同义词较多,或者某个query本身有歧义,TopK结果里混入无关片段很正常。所以后面必须有重排序环节来兜底。
4. 检索与召回:怎么把“最相关”的资料捞上来
4.1 查询理解与改写:别让用户的“口语”难为向量检索
用户提问往往很随意,比如“年假咋请啊”,如果直接把这句话丢给向量检索,效果通常不理想。因为向量检索更偏好“信息密度较高”的查询表达,而口语化问题里关键词少,泛词多。所以查询处理是容易被忽略但非常实用的环节。
常见的查询改写策略有这几种:
- 关键词提取:从用户问题中提取核心实体和关键词,再组成检索query。例如“年假咋请啊”可以提取为“年假 申请 流程”。
- 同义词扩展:把“请假”扩展成“休假、请休假、假期申请”,再通过OR逻辑检索,提高召回。
- HyDE(假设性文档嵌入):先让大模型根据问题生成一个假设的答案文档,再用这个假设文档去做向量检索。这个方法的思路是:假设文档的向量比问题本身的向量更接近目标片段,因为它是“完整句子”而不仅仅是一个疑问句。实测在某些领域效果不错,但会增加一次大模型调用延迟,需要按业务场景取舍。
- 多路召回:不只走向量检索,同时对关键词走一次ES/BMI25文本检索,然后把两路结果做融合(RRF或加权)。
我会建议至少做两个动作:一是对用户query做简单清洗(去停用词、标点),二是用LLM做一次查询改写(顺便让用户问题转成更适合检索的表达)。延迟能接受的话,HyDE也可以试试。
4.2 向量检索:选TopK的设置与常见坑
向量检索时,TopK的值直接影响后面重排序和生成的效果。TopK太小容易漏掉正确答案,TopK太大又会让噪声进入Prompt,增加大模型混淆的风险。我一般分两步:召回阶段取20到50个候选,重排序后再取3到5个最终片段进Prompt。这样既保证召回率,又不会污染上下文。
在检索过程中最容易踩的坑是只按向量相似度排,忽略了文档本身的时效性和权威性。比如新旧两版制度文档同时存在,旧版内容可能和问题相似度更高,但答案已经过时。解决方法是在检索结果上叠加元数据过滤,比如按更新时间倒排,或者给不同来源设置权重。
4.3 重排序:从“几何相似”到“语义相关”的关键一跃
很多RAG系统做到向量检索就直接拼接Prompt了,这其实是效果提升空间最大的一步。向量检索得到的TopK结果里,常常会有那么几个“看起来像但答非所问”的片段。重排序模型的任务是对这些候选进行更精细的语义匹配打分,把真正相关的、与问题意图一致的片段排到最前面。
重排序模型和Embedding模型不同,它通常是一个交叉编码器,会同时把query和document拼接成一对输入,然后输出一个相关性分数。这种方式计算成本高,但由于它能看到query和document之间的深层交互,排序准确性远高于双塔式的向量检索。
我建议的流程是:向量检索召回Top 20到50个候选,然后交给重排序模型,把分数最高的3到5个送去生成。重排序模型选型上,优先选参数适中、速度可接受的开源模型。实测下来,重排序对最终答案正确率的提升往往非常明显,属于“花小钱办大事”的环节。
5. 生成链路:把检索结果变成高质量答案的临门一脚
5.1 上下文组装:不是把所有检索结果都塞给大模型
检索完成后,你手上有一批候选片段。直接把它们一股脑拼起来塞进Prompt,是我见过最多的错误。原因有两个:一是片段数量太多会超过大模型的上下文窗口,二是无关片段会引入噪声,让模型胡说八道。
合理的上下文组装包含三个部分:
- 系统指令:告诉模型它的角色、任务和回答约束。例如“你是企业知识助手,请严格根据给定的参考资料回答问题,不要编造内容,如果参考资料中没有相关信息,请明确说明不知道。”
- 参考片段:把重排序后的片段按相关性降序排列,用清晰的标记分隔开,并给每个片段标注来源编号。
- 用户问题:放在最后,让模型在看过参考资料之后针对问题作答。
组装完毕之后,Prompt大概长这样:
系统:你是企业知识助手,请严格根据给定的参考资料回答问题。 如果参考资料中没有相关信息,请回答“资料库中未找到相关内容”。 不要编造答案,并在回答末尾标注引用的片段编号。 参考资料: [1] 文档《员工手册》第5章:员工每年享有5天带薪年假,需提前3个工作日提交申请。 [2] 文档《休假管理制度》:年假不可跨年累计,特殊情况需HR审批。 用户问题:公司年假怎么申请?你可以看到,每个片段都给了编号,并要求模型在回答中引用编号。这样用户可以对答案进行追溯,方便检验系统是否忠实于资料。
5.2 Prompt设计细节:约束、格式与温度参数
生成阶段的大模型参数也很重要。我个人常用的配置是:
- temperature:设为0,尤其在知识问答场景,我们要的是稳定、忠实于资料的答案,不需要模型自由发挥。
- top_p:设为0.9左右,但配合temperature=0时影响不大。
- max_tokens:根据答案长度预期设置,避免超长废话。
- stop:可设结束符,防止模型自己加戏。
Prompt里还应该明确要求“如果检索到的资料与问题无关,就拒绝回答”。这句约束能显著降低幻觉率。另外,如果业务需要多轮对话,还要把历史对话摘要也放进Prompt。历史对话存在两个作用:一是让模型理解指代关系,比如用户问“它的申请流程呢”,你要知道“它”指代上一轮的年假;二是把上一轮检索到的关键信息继续作为上下文,保持连续性。但历史对话不宜太长,建议做摘要或者只保留最近两三轮。
5.3 生成结果的后处理:引用标注与答案校验
生成完答案之后,还应该有一个轻量级后处理环节。至少做两件事:
- 引用校验:检查模型生成的引用编号是否真实存在于参考资料中。有时候模型会自己编一个[3]出来,而实际只提供了两个片段,这时候要去掉虚假引用,或者重新生成一次。
- 答案与参考资料一致性校验:可以单独用一次大模型调用,把生成答案和参考资料放一起,让模型判断“答案是否完全基于参考资料”。如果没有,则标记为低置信度,甚至可以触发重新检索。
很多生产环境里,这步校验能拦截掉大量幻觉输出。成本是一次额外大模型调用,但对答案质量要求高的场景非常值得。
6. 打通完整链路:一个可参考的企业知识库问答实现过程
6.1 需求场景与整体设计
假设我们要给某公司做一个内部制度问答助手,知识库里有员工手册、差旅制度、报销流程等若干文档。用户希望员工能通过自然语言提问,系统返回准确的制度条款并且给出出处。
整体设计如下:
- 离线索引部分:读取文档,清洗,分块,调用Embedding模型生成向量,写入向量数据库。
- 线上检索部分:用户提问,先做查询改写,再向量检索,然后重排序。
- 生成回答部分:组装Prompt,调用大模型,后处理校验引用,返回给用户。
- 评估部分:准备一批业务问题,定期评估准确率和召回率,用来指导调优。
6.2 关键代码骨架
为了让思路更具体,我给出一个简化的Python代码示例,框架无关,你可以根据自己项目技术栈调整。
# 1. 索引构建 from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = PyPDFLoader("employee_handbook.pdf") documents = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=["\n\n", "\n", "。", ";", ",", " ", ""] ) chunks = text_splitter.split_documents(documents) # 2. 向量化并写入向量数据库 from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS embedding_model = HuggingFaceEmbeddings(model_name="your-embedding-model") vectorstore = FAISS.from_documents(chunks, embedding_model) vectorstore.save_local("faiss_index") # 3. 在线检索与重排序 query = "年假怎么申请" expanded_query = expand_query_with_llm(query) # 改写 retriever = vectorstore.as_retriever(search_kwargs={"k": 20}) candidates = retriever.invoke(expanded_query) reranked = rerank_model.rerank(query, candidates, top_n=5) # 4. 组装Prompt并调用大模型 from openai import OpenAI client = OpenAI(base_url="/your-llm-endpoint", api_key="your-key") prompt = build_rag_prompt(query, reranked) response = client.chat.completions.create( model="your-llm", messages=[ {"role": "user", "content": prompt} ], temperature=0 ) print(response.choices[0].message.content)上面代码只是串起流程的最小骨架。实际生产里你还需要考虑索引版本管理、增量更新、缓存查询结果、异步处理、日志追踪等工程问题。
6.3 一次完整的运行效果演示
我拿一个具体问题做示例效果展示,帮你理解整个系统的输入输出行为。
用户输入:“差旅住宿标准是多少?”
系统内部执行:
- 查询改写:把query扩展成“差旅 住宿 标准 报销 限额”;
- 向量检索Top 20;
- 重排序Top 5,其中命中了《差旅管理制度》中相关的两段;
- 组装Prompt,包含系统约束和这两段资料;
- 大模型输出:
“根据《差旅管理制度》第4.2条,国内出差住宿标准为一线城市每天500元,其他城市每天350元。以上为参考信息[1]。”
如果知识库里确实没有相关内容,模型会输出:“资料库中未找到相关内容,建议联系财务部门确认。”——这是Prompt约束起的作用,有效避免了凭空编造。
7. 常见问题与调优实战:从“勉强能用”到“生产可用”的跳跃
7.1 问题一:检索结果明明屁用没有,但向量相似度还挺高
这种情况我遇到过太多次。原因往往不是向量检索本身,而是分块碎了:一个完整的制度条文被硬切成几块,每一块都只是片段,单独看根本回答不了问题。解决思路是调整分块策略,比如加大重叠、改用按章节切分,同时考虑把分块粒度控制在语义完整的最小单元。此外,如果问题带有明显的限定词(比如“2024年”),最好在query改写时把它提取出来作为过滤条件,而不只是塞进向量里。
7.2 问题二:TopK里有正确答案,但生成时模型还是不用
检查一下Prompt里参考资料的排列顺序。大模型会受到位置影响,过于靠后的片段容易被忽略。把重排序后分数最高的片段放在最前面,并且用清晰的分隔符把每个片段包裹起来。另一个常见问题是系统指令里没有强调“必须严格根据参考片段”,模型觉得自己想象一下也行。要加明确约束,并且把temperature设为0。
7.3 问题三:重排序太慢,用户等不了
重排序模型是交叉编码器,计算量比双塔大不少。如果召回50个片段,每个片段都要和query做一次拼接推理,延迟确实容易爆。我的建议是:先用一个比较宽的向量检索设定召回到50,然后重排序模型只对前20个做精排,最后保留5个进Prompt。这样可以平衡效果和延迟。如果重排序模型本身推理很慢,可以考虑用并行批处理,或者将模型量化加速。
7.4 问题四:回答出现了自相矛盾或者事实错误
这种情况最要命,通常意味着检索到的片段之间存在冲突,或者模型在多个片段之间“自由发挥”了。处理办法:一是增加“如果多个参考资料矛盾,以最新发布/最高优先级文档为准”的规则;二是在后处理阶段增加一致性校验,发现低置信度就触发重新检索或者返回“资料冲突,请人工确认”;三是丰富元数据过滤,在检索阶段就限制时间范围和文档版本。
7.5 评估体系:没有量化指标,所有调优都是玄学
RAG系统调优最忌讳“凭感觉”。你必须建立一套评估集,包含几十到几百条用户真实问题,每条问题标注了期望命中的文档片段或标准答案。离线评估时,把这些问题重新跑一遍完整链路,算两个核心指标:
- 召回率(Recall@K):TopK检索结果中包含了多少比例的期望片段;
- 答案正确率(Answer Accuracy):大模型生成的答案与标准答案一致的比率。
有了这两项,你改动分块、换Embedding、加重排序之后就有的放矢了。我的经验是:先保证召回率在85%以上,再去优化重排序和Prompt;如果召回率不够,问题大概率出在分块和查询改写上,而不是生成侧。
8. 一些掏心窝的实操心得
最后分享几点我在多个RAG项目里反复验证过的心得,权当给你提个醒。
第一个心得是关于技术选型的。不要一开始就把很多高级组件全堆上去,什么混合检索、图数据库、多层RAG,听着很酷,实际调试起来痛苦加倍。我建议第一版只做“向量检索 + 重排序 + Prompt约束”这条最朴素的链路,就能覆盖大部分业务场景。等评估指标告诉你短板在哪,再针对性地加模块。很多团队花了几周搭了一个超级复杂的系统,结果效果还赶不上一个调好参数的朴素系统。
第二个心得是前后端都要重视日志追踪。每一条线上问答都应该记录下:用户query、改写后的query、召回Top20片段ID、重排序后Top5片段ID、最终生成的答案、引用编号、耗时。这样当用户反馈答案错误时,你可以快速定位是检索环节丢了正确资料,还是重排序把正确资料挤掉了,还是大模型没按资料回答。没有日志,排查问题就像大海捞针。
第三个心得是文档质量是RAG的天花板。如果你的源文档本身就是过时的、互相矛盾的、排版混乱的,那不管算法怎么优化都没用。我见过一个项目花了很多精力换模型、调参数,最终发现是文档版本没更新。所以说,RAG系统上线前,最好拉上业务方一起对知识库做一轮“内容体检”,把重复文档、过期文档清掉,这个投入的回报比任何模型调优都高。
第四个心得是保持迭代的心态。RAG不是一个“做完就完”的系统,因为你把新文档加进去,或者用户提问方式变了,效果都会漂移。我习惯每两周用固定的评估集跑一次回归测试,顺便用线上日志找一些答错的bad case,把它们补充进评估集里。这样系统才会越用越顺手,而不是越用越烂。
这篇文章从RAG系统框架的整体设计讲到了索引、检索、重生成以及最后的评估调优,每一步都值得我们琢磨。但我更希望你把它当作一份“踩坑地图”,遇到问题知道先去查哪里,而不是照着某个配置抄一遍就完事。RAG真正难的部分一直都在细节里。