首页 / 资讯中心 / 文章详情

RAG技术实战:从原理架构到代码实现的完整指南

RAG技术实战:从原理架构到代码实现的完整指南 ★ FEATURED ARTICLE
聊到“大模型 RAG 技术”这两年凡是做AI应用开发的几乎没人绕得开这个词。我第一次听到它时脑子里只有“给大模型配个外挂知识库”这种抽象印象等自己动手搭过一整套RAG问答系统把离线索引、在线检索、生成融合整条链路完整跑通之后才真正体会到它为什么能成为大模型应用开发中最实用、最容易落地的方案。这篇不是官方文档也不是项目宣传而是我从零搭建过程中的完整记录包含原理架构、通俗解读、可复现的实战代码以及一堆踩过坑之后总结出的排错经验。适合正在学大模型应用开发、想把通用模型变成能回答自己业务问题的“定制助手”的开发者不需要你有多深的算法基础只要有Python基础就能跟下来。1. RAG是什么先从“开卷考试”说起1.1 三个核心环节一眼看懂RAG全称是Retrieval-Augmented Generation直译成中文就是“检索增强生成”。我第一次给别人讲这个概念的时候喜欢用结构拆解它由两个动作组成一个是检索Retrieval一个是生成Generation而“增强”两个字点明了这两个动作的关系——用检索的结果去增强生成的质量。拆开来看就是三个环节首先是“建库”把业务文档切分成小片段每一段做向量化建立可检索的索引然后是“召回”用户提问后系统把问题也向量化在库里找出最相近的几个片段最后是“生成”把这些片段作为上下文拼进提示词交给大模型组织成自然语言答案。整个闭环里面大模型本身的参数没有动过一分一毫知识却“长在”了系统里。我习惯用“开卷考试”给非技术朋友打比方。大模型相当于一个知识面很广、记忆力极强的考生但它有固定的知识截止时间最新资料和内部文档没学过就是没学过。RAG相当于给这位考生配了一个随身的资料员考试时你提出问题资料员快速翻出一叠相关材料考生照着材料答题。考生不需要把整本资料背进脑子只需要在给定材料里做归纳、总结、引用答案自然准确很多也不容易胡编。1.2 RAG到底解决了大模型的哪些硬伤先聊最核心的一点知识时效性。大模型内部的知识全部来自训练数据训练结束的那一刻知识就固化在参数里了。你问它几个月前刚发布的新政策、新产品的参数它要么答错要么说不知道。RAG把知识更新从“重新训练模型”变成了“更新知识库”今天改了文档明天重建索引模型立刻就能用上新知识成本可以低到忽略不计。第二点是幻觉问题。生成式模型本质是在推理“下一段最可能的文字”遇到它拿不准的问题它会顺着语义惯性往下编而且编得像模像样。RAG把正确答案所在的上下文塞到模型眼前相当于给了它一个“标准材料”模型在材料范围内组织答案才敢答得有理有据。实际体验下来幻觉率能压下去一大截。第三点是领域知识匮乏。通用大模型对电商、法律、医疗器械、企业内部制度这些垂直领域的内容往往一知半解。RAG可以把这些行业的专业资料直接作为上下文注入完全不需要微调模型。这三个问题恰恰是大模型落地到真实业务时最痛的地方所以RAG在应用开发里才会如此吃香。1.3 RAG和微调到底该怎么选很多初学者会在RAG和微调之间纠结我自己的选择标准很简单如果目标是“让模型能回答某类问题”并且答案分散在大量持续更新的文档中优先做RAG如果目标是“改变模型的行为习惯”比如让它总是输出JSON格式、对话风格必须固定才真正需要微调。微调的问题是成本高、周期长、更新难每次知识变化都得重新训练而且小数据量微调很容易过拟合效果反而不稳定。RAG则天然适合知识密集型的问答场景可解释性也更强——我甚至可以把每个答案链接到具体是哪一篇文档片段方便用户追溯来源这在企业场景里是刚需。实际项目里两者也不是水火不容微调负责定行为规范RAG负责供业务知识组合使用是很多成熟应用的常见做法。对我来说一个还没跑通RAG的项目我不会轻易去碰微调。2. 架构拆解从“入库”到“生成”的完整链路2.1 离线索引文档清洗、分块与向量化的细节RAG系统运行起来以后大体分成两个阶段离线索引和在线推理。离线索引做的事情是把一堆原始文档变成机器能快速检索的向量索引它发生在用户提问之前决定了整个系统的知识基础。第一步是文档清洗。很多人会跳过这一步直接切文档结果后面检索时频繁踩坑。原始数据里常有多余的页眉页脚、超链接、表格标记、乱码这些噪声不清理向量化之后会让语义变得浑浊检索得分也会被干扰。我的习惯是先统一转成纯文本去掉图片说明、脚注等无关内容再进入分块环节。第二步是分块。这一步是RAG质量的第一道关卡也是我最初踩坑最深的环节。分块的基本要求是“块内语义完整”一块内容尽量围绕一个主题别把一句话切得七零八落。我常用递归切分策略优先按章节标题和段落边界切段落太长再按句号拆实在不行才按字符长度硬切。初始参数通常设为每块300到500字符重叠50字符。为什么要设置重叠因为如果一块恰好从两句话中间截断后半句的意思就丢了。重叠窗口让相邻的两块共享一部分内容即使边界卡在句子上信息也还能在上一块或下一块里找到。重叠比例一般控制在总长的10%到20%太大则重复内容过多浪费存储和检索资源。第三步是向量化。向量化就是把文本映射成高维空间里的一个向量语义相近的文本在空间里距离更近。嵌入模型的选择会影响检索效果中文业务场景优先选对中文支持好的开源嵌入模型不必一味求大小模型同样能给出不错的相似度排序。向量维度决定了存储成本和检索速度维度过高大数据量下索引构建和查询都会明显变慢。第四步是把向量写入索引。轻量本地索引适合学习和测试好处是零配置、加载快生产环境则建议用分布式向量数据库支持亿级数据、过滤条件和更高并发。选型时主要看数据规模、查询量和对元数据过滤的需求。2.2 在线推理查询向量化、召回与重排序用户提问后在线链路正式启动。第一个动作是把用户问题用同一个嵌入模型转换成向量注意这里有一个很多人忽略的约束查询向量化和入库向量化必须使用完全相同的嵌入模型否则两边的向量不在同一语义空间检索结果会彻底失真。我早期在做版本升级时遇到过这样的问题换了一个新嵌入模型后忘了重建索引检索结果完全乱套排查花了大半天。接下来是从索引中召回Top-K个最相似的文本片段。K越大能覆盖的信息越多但噪声也越多K越小返回结果越聚焦但可能漏掉正确答案。在上下文窗口有限的条件下K取3到5是比较平衡的选择。如果数据规模大、检索结果良莠不齐我强烈建议加一个重排序环节。初次召回时我们为了效率会优先选计算快的向量相似度算法但它的排序质量往往不够精细重排序模型则会对候选片段做更细致的比对把最相关的片段排到最前面。实测下来加一层重排序答案质量提升非常明显可以说是性价比最高的一步优化。另外当文档库术语性强、专有名词多时单靠向量检索容易漏掉“字面上不完全匹配但语义相同”的内容。这时我会加入关键词检索做混合召回向量检索负责语义相关内容关键词检索负责精确命中型号、编号这类信息两者结果合并后再重排覆盖范围更完整。2.3 生成融合怎样把检索结果“喂”给大模型检索完成之后还差最后的生成环节。很多人以为把检索片段随便拼接进提示词就行实际上这一步做不好前面检索得再好也会白费。核心原则是明确告诉模型“哪些是外部材料以及应该如何使用这些材料”。我常用的提示词模板是这样组织起来的第一段是系统指令说明“下面是从知识库中检索到的参考资料请严格根据参考资料回答用户的问题参考资料中没有的信息不要臆测”第二段是检索到的文本片段按来源编号列出第三段是用户问题最后追加一句“回答时请尽量引用片段中的原话或关键信息”。这样写表面上只是多了几句约束话术实际上作用巨大。它把模型的自由度限制在材料范围内极大压低了幻觉空间。还有一个小技巧要求模型在回答里标注引用来源例如“根据【材料2】”这样既方便用户核验也让答案显得更可信。生成阶段还要注意token预算不能把召回的所有片段一股脑塞进去。超过上下文窗口时宁可保留得分最高的3个片段把每段的长度控制好也不要硬塞进大量平庸内容把关键信息挤掉。反正核心信息只有一段平台再大没用到关键材料也没用。3. 实战代码手写一个轻量RAG问答系统3.1 环境准备与依赖选型这个实战项目我刻意没有用集成开发框架而是直接用最核心的库手写整条链路。好处是每一步都在自己眼皮底下运行出了问题能立刻定位到具体环节非常适合理解RAG的运作机制。用到的核心依赖有三个一个用于句向量编码的嵌入模型库一个本地轻量向量索引库再加一个Python的HTTP请求库作为模型调用通道。嵌入模型选开源中文语义模型可以离线在本地跑索引库用CPU版本即可几千条文档毫无压力大模型接口部分留成独立的函数方便替换成你实际使用的服务。安装依赖用一条命令能完成pip install sentence-transformers faiss-cpu requests如果网络环境受限也可以先把依赖下载到本地再安装过程不复杂。准备的测试文档我建议用纯文本格式内容可以是你手头任意一份业务说明文档方便对照效果。3.2 分块与索引构建的实现先实现分块函数。我写了一个按中文标点优先的切分逻辑避免在句子中间截断def split_text(text, chunk_size300, overlap50): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) # 优先在句号处截断 if end len(text): cut text.rfind(。, start, end) if cut start chunk_size * 0.5: end cut 1 chunks.append(text[start:end]) start max(end - overlap, start 1) return chunks这个函数会尽量把块的边界控制在“。”后面即使找不到句号也不会陷入死循环。实际用下来中文文档切分效果比较稳定。接下来加载嵌入模型并对切好的所有文本块做批量向量化然后写入索引from sentence_transformers import SentenceTransformer # 加载开源中文嵌入模型 embedder SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) documents [...] # 你的业务文档列表纯文本 chunks [] for doc in documents: chunks.extend(split_text(doc)) # 批量编码并归一化 vectors embedder.encode(chunks, normalize_embeddingsTrue) dimension vectors.shape[1] print(f文本块数量: {len(chunks)}向量维度: {dimension}) import faiss index faiss.IndexFlatIP(dimension) # 归一化后可用内积表示余弦相似度 index.add(vectors)我特意把向量做了归一化并用内积索引代替余弦距离这样得到的得分就是余弦相似度排序语义更直观。索引构建完成后整个离线阶段就结束了。3.3 检索链路与答案生成函数在线检索阶段先对用户问题做同样的向量化再调用索引的search方法取回最相似的K个片段def retrieve(query, top_k3): query_vec embedder.encode([query], normalize_embeddingsTrue) scores, indices index.search(query_vec, top_k) results [] for score, idx in zip(scores[0], indices[0]): if idx 0: results.append({text: chunks[idx], score: float(score)}) return results这里返回了片段文本和相似度得分方便调试时打印查看。接下来是把检索结果拼进提示词并交给大模型生成答案import requests def call_llm(prompt): # 这里替换为你实际使用的大模型服务接口 # 无论是本地部署的推理服务还是云端API统一在这里配置即可 # 示例 # url http://你的模型服务地址 # resp requests.post(url, json{prompt: prompt, max_tokens: 500}) # return resp.json()[text] raise NotImplementedError(请先配置大模型接口) def generate_answer(question, retrieved): context \n.join( [f[材料{i1}] {r[text]} for i, r in enumerate(retrieved)] ) prompt f请根据以下参考资料回答问题。 参考资料 {context} 用户问题{question} 要求只使用参考资料中的信息回答若资料中没有相关内容请明确回答“资料中未提到”。 return call_llm(prompt)这个函数的提示词结构就是前面讲到的“指令 材料 问题 约束”实际用下来模型基本能在给定材料范围内作答不再天马行空。3.4 完整可运行的代码骨架把所有环节串起来就是一整套可直接跑的RAG问答主链路# rag_demo.py import faiss import requests from sentence_transformers import SentenceTransformer # 1. 准备嵌入模型 embedder SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) # 2. 准备业务文档替换成你的真实数据 documents [ 智能客服系统支持多渠道接入包括网页、应用程序和社交媒体 系统会自动同步历史会话记录。, 智能客服系统内置意图识别引擎可以自动判断用户意图 并分配给对应的技能组进行处理。, 系统支持知识库管理运营人员可以随时新增、修改问答对 上线后立即生效。, 对于无法自动解答的问题系统会自动转接人工客服 并保留完整会话上下文。, ] def split_text(text, chunk_size300, overlap50): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) if end len(text): cut text.rfind(。, start, end) if cut start chunk_size * 0.5: end cut 1 chunks.append(text[start:end]) start max(end - overlap, start 1) return chunks chunks [] for doc in documents: chunks.extend(split_text(doc)) vectors embedder.encode(chunks, normalize_embeddingsTrue) index faiss.IndexFlatIP(vectors.shape[1]) index.add(vectors) def retrieve(query, top_k3): query_vec embedder.encode([query], normalize_embeddingsTrue) scores, indices index.search(query_vec, top_k) return [ {text: chunks[idx], score: float(scores[0][i])} for i, idx in enumerate(indices[0]) if idx 0 ] def call_llm(prompt): # 接入你的模型服务接口 raise NotImplementedError(请配置模型接口) def generate_answer(question, retrieved): context \n.join( [f[材料{i1}] {r[text]} for i, r in enumerate(retrieved)] ) prompt f请根据以下参考资料回答问题。 参考资料 {context} 用户问题{question} 要求只使用参考资料中的信息回答若资料中没有相关内容请明确回答“资料中未提到”。 return call_llm(prompt) if __name__ __main__: q 智能客服系统支持哪些接入渠道 results retrieve(q, top_k3) print(检索结果如下) for i, r in enumerate(results): print(f材料{i1} | 得分{r[score]:.4f} | {r[text]}) print(生成答案) print(generate_answer(q, results))把call_llm函数接上你实际使用的模型服务这个脚本就已经是一个完整的问答系统了。我用这个骨架改造过几个内部文档问答场景从切文档到拿到可用答案整个过程不到半天。4. 常见问题排查RAG落地最容易踩的坑4.1 检索召回了相似文本但答案完全不对这是最典型的问题检索结果的文字看起来和问题主题相关但里面根本没有用户想要的关键信息。原因通常是问题中的关键词和文档里的表达方式不相同比如用户问“怎么退款”文档里写的却是“退货流程和资金返还规则”语义虽然接近但向量化后并没有落到同一块上。排查时先把检索结果打印出来人工看一遍片段内容。如果发现Top-K里始终没有关键信息先试降低分块大小让更小的块更容易命中细节再把K从3调大到5或10观察候选片段能否覆盖答案。如果这样还不行就该上查询改写或混合检索了。查询改写的思路是先用大模型把用户问题改写成多个变体或拆成若干子问题分别检索后合并结果覆盖“同一件事、不同说法”的情况。4.2 分块边界破坏语义答案时好时坏答案质量忽高忽低十有八九和分块有关。块太大检索到的片段含大量无关内容块太小可能连一个完整的事实都没包含。我建议用“块内只有一个主题”作为标准来调参。中文场景尤其要注意标点边界尽量在句号、问号后面切不要在逗号或名词短语中间硬切。重叠值设置为块长的10%到20%既能照顾边界信息又不会造成太多冗余。我把常用分块参数整理成了一个对照参考分块策略适用场景常见问题按字符固定长度内容结构零散、无明显段落边界容易截断句子、破坏语义按段落句子切分大多数业务文档、制度手册段落过长时仍需二次切分按标题层级切分操作手册、产品说明、合同文书需要额外解析文档结构重叠窗口与上述策略配合使用重叠过大则索引冗余严重4.3 上下文变长后token开销失控把检索到的5个片段全部塞进提示词一轮问答的token消耗很容易翻倍接口费用和响应延迟都会明显上升。这里我采用的策略是先粗过滤再做精排初始检索召回部分候选后先按相似度得分保留Top-K再根据片段与问题的关键词覆盖率做一次截断只把最相关的片段送进模型。更进一步的方案是给每个片段做一个摘要进入上下文的是摘要而非全文不过这会增加一次模型调用适合对成本不敏感但对精度要求高的场景。日常使用中把K控制在3每段300字符以内预算通常就在可控范围内了。4.4 有正确答案但模型还是在编造这种情况说明提示词没有给出足够强的约束。我早期也遇到过明明检索结果里写得清清楚楚模型还是按照自己的“常识”作了发挥。给提示词加约束是最快的解法明确写“只根据参考资料回答”“资料中没有的内容必须如实说明”“回答中引用材料原文时标注材料编号”。这些指令的效果在大模型上基本是立竿见影的。还有一个高级技巧是做答案合规性校验生成完成之后检查答案里是否出现了检索片段之外的关键实体如果比例过高说明模型越界了此时可以让模型重新生成一次或者直接返回“资料覆盖不足请补充文档”。这个校验虽然简单却能在生产环境里拦截住大量幻觉内容。4.5 向量检索变慢怎么办数据量从几千条涨到百万条时暴力检索的耗时就会变得不可接受。解决思路主要从索引结构和过滤条件入手一是换用支持倒排文件IVF的索引类型先聚类再检索减少扫描范围二是给每条记录打上元数据标签检索时先用标签做过滤把搜索压缩到小范围内再做相似度比较三是向量量化降低每个向量的内存占用检索速度也会跟着上来。我实际经验是索引类型从暴力检索换成倒排索引后千万级数据量下查询可以做到几十毫秒级别。但如果只是几千条文档的学习Demo完全不需要考虑这一步别过早优化。5. 进阶项目方向与个人心得5.1 从Demo到生产环境的差距在哪跑通上面的代码骨架只是第一步真实投产还有不少工程问题要处理。首先是数据更新机制业务文档每天都在变需要有定时任务做增量索引而不是每次都全量重建其次是可观测性系统上线后要能记录“每个问题检索了哪些片段、每个答案引用了哪些材料”否则问题只能靠用户口头反馈来发现。然后是权限控制不同角色能看到的知识库范围不同。比如普通员工查询制度文档管理层查询绩效数据检索前就要按角色过滤索引内容这一步必须在召回前完成避免敏感信息进到结果里。最后是效果评测。给一套标注好标准答案的测试问题集每次修改分块、替换嵌入模型或调整提示词后用同一套测试集跑一遍对比命中率和答案质量而不是凭感觉调参。我把这个方法坚持了几个月之后系统效果一直处于可控状态。5.2 我对RAG落地最想说的一句话自己从最早跑通Demo到现在把它用在真实项目里最深的体会是RAG系统的效果上限取决于检索质量而检索质量又由分块、嵌入模型、重排序这些基础细节共同决定。它没有那么多“奇技淫巧”只要把工程链路做扎实答案质量就会踏踏实实地往上涨。最后再分享一个小技巧给知识库里的每篇文档加一个“更新时间”字段检索结果中带上时间信息提示模型“资料更新时间”能有效应对动态变化的业务场景。模型看到新日期的资料会自然优先采纳。这一点在制度政策类文档的场景下尤其好用。好了这篇就写到这里。如果你正准备上手 RAG建议先不要急着套框架把上面的代码跑通亲手打印一次检索结果看看返回了什么再逐步加入重排序、查询改写这些优化。这样你对整个系统的理解会扎实很多。
阅读完成 · 觉得有帮助?
咨询建站