1. 为什么是RAG大模型天生的两个短板最近后台私信里出现频率最高的词大概就是RAG。可能你已经听说过这个概念也看过一些零散的资料但真正要动手做一套的时候又会发现很多细节说不清楚为什么要RAG它到底解决了什么问题完整流程有哪些坑这篇文章我会结合自己实际做过项目的经验把这些东西一次性讲透。文章会偏实操一些代码示例也不复杂稍后你可以直接照着跑一遍。先聊一个最核心的问题大模型最大的短板其实就是它的“知识”是静态的。所谓大模型本质是一个在海量文本上训练出来的概率模型它把所有学到的知识压缩进了神经网络参数里。一旦训练完成这些参数就固定了。如果你问它训练截止日期之后的事情它没有见过就只能靠概率“编”——这就是大家常说的幻觉。哪怕你把最新文档喂给它看它也只会对着无关的旧知识生成一个听起来很合理的答案。另一个短板是私域知识。一个行业公司内部的制度文档、一个开发者的个人笔记、一份刚签的合同这些数据不在公开语料里通用大模型根本不会知道。即便你强行走微调训练成本高、周期长不说新增数据还得重新训练一遍完全不现实。所以业界的解法慢慢收敛到了两条路上一条是把新知识微调进模型参数里费用高、收益慢另一条就是RAG中文叫检索增强生成。通俗地说RAG不强行改变模型的“记忆”而是给模型装上一个外挂知识库。每次用户提问系统先到知识库里把相关内容检索出来再连同问题一起交给大模型让模型基于这些材料来回答。这个思路相当于给一个专家配了台能随时查档案的电脑。模型的参数不用动知识却能实时更新。这也是为什么RAG被很多人称为“让大模型突破静态知识边界的黑科技”——它的本质不是改写模型而是改变模型获取信息的方式。从现在开始往下我会从零开始拆解RAG的完整链路。无论你是刚入门的技术爱好者还是已经在项目里爬过几次坑的开发者这篇文章都会值得你收藏。2. 一套RAG系统的完整骨架网上关于RAG的资料很多但能把流程讲清楚的很少。其实拆开来看核心链路只有五步文档加载、文本切分、向量化存储、检索匹配、生成回答。前面三步通常属于“离线索引阶段”后面两步发生在每次用户提问时叫“在线问答阶段”。为了方便理解我拿一个真实的业务场景举例某公司要把员工手册、产品说明、运维文档汇总成一个内部问答机器人。员工问“年假可以累积吗”系统需要从几十份文档里找到对应的段落再由大模型用自然语言回答“可以累积几天的具体规定”。2.1 离线索引阶段做什么离线索引阶段的核心任务是把这些非结构化的文档处理成计算机能快速查找的形式。第一步是文档加载。不同格式的文档需要不同的解析器PDF要处理扫描件和文字版Word要考虑表格Markdown相对简单。文档加载完后通常还有一道清洗工序把页眉页脚、目录、水印、无意义的空白去掉。这些噪声如果带进后续流程会严重影响检索质量。第二步是文本切分。这一步很多人容易忽略但它直接决定了检索效果。文档内容必须以固定策略切成小块每一块都会成为一个独立的检索单元。如果切得过粗一块包含了很多主题检索命中后模型会被无关信息干扰切得过细又会导致上下文信息不完整检索到大段代码或复杂表格时效果尤其差。第三步是向量化。每一块文本输入Embedding模型转换成一个几百维甚至上千维的浮点向量。这个向量是文本语义的数学表示语义相近的两段文字它们的向量在多维空间里的距离也接近。然后把这些向量连同原始文本一起存入向量库。查询的时候同样把问题转成向量在库里做近似最近邻搜索就能快速找到语义最相关的那几段文本。2.2 在线问答阶段怎么运作在线问答阶段做的事情其实很直观。系统收到用户问题后会做三件事把问题文本向量化用同一个Embedding模型。在向量库中检索出最相似的Top-K个文本块。把这K个文本块按顺序拼接进Prompt模板让大模型“基于以下资料回答问题”。这里最关键的一点是大模型不直接看整本手册而是只看检索回来的几个片段。检索质量决定了回答质量的底线。如果你检索回来的内容压根不相关后面的生成过程就是无米之炊大模型只能被迫编造。所以业内常说一句话RAG的难点不在生成而在检索。生成能力由大模型本身决定RAG开发者要做的是把最相关的资料精确地“递”到模型面前。3. 检索质量是命门核心部件逐个拆解如果你看完上一节的流程现在想动手搭建你马上会遇到一系列选型和调参问题。文档怎么切分Embedding用什么模型向量库怎么选检索应该召回多少条最合适我做过的几个项目里这些环节每一个都踩过坑下面逐条讲清楚。3.1 文档切分决定了检索颗粒度文本切分是RAG里最“手工作坊”的一步也是最值得投入精力的优化点。主流策略有两种固定长度切分和结构感知切分。固定长度切分最简单按字符数或Token数硬切。比如每块500个字符相邻块之间重叠100个字符。重叠的目的是防止某个完整语义刚好被一刀切断——比如一句话前半段在第10块、后半段在第11块检索时只命中前半段模型就会因为信息不完整而答错。我个人的经验是不要对所有文档用同一个切片长度。Markdown文档可以按标题层级切保证每个章节内部语义连贯代码文档按函数或类切法律合同可以按条款编号切。如果库里绝大多数是普通文本用400到600个字符的窗口、100到200个字符重叠是一个相对安全的起点。另一个容易忽略的问题是切分时最好保留原文的上下文元数据。比如这一块文本来自哪个文件名原章节标题是什么。后续做引用溯源时这些信息会被拼进Prompt让模型在回答末尾自动附上“以上内容源自《员工手册》第3章第2节”。没有元数据你很难做出带引证的可靠回答。3.2 Embedding模型怎么选Embedding模型是把文本映射成向量的关键组件选型直接关系到检索效果。目前主流的选择方向是中文场景优先选择在中文语料上训练过的开源Embedding模型英文场景则有不少通用模型可供选择。不同模型输出的向量维度不同有768维的也有1024维甚至更高的。并不是维度越高越好——高维度意味着向量库占用空间更大、检索计算更慢但语义表达能力未必等比提升。真正决定效果的是模型训练语料的分布是否贴近你的文档领域。专业领域的自定义语料往往是影响最大的变量。比如医疗术语、金融术语、代码符号通用Embedding模型可能没学好。这时候有两个选择一是换一个垂直领域向量模型二是先跑一个几百条真实问题的小规模验证集把不同模型的检索命中率跑出来对比用数据说话而不是只看指标。补充一点容易被忽视的同一个系统里索引文档和用户问题必须使用同一个Embedding模型。有人会在离线阶段用A模型向量化文档上线后却调用了B模型的接口导致两个向量不在同一语义空间里检索结果乱成一团。这种问题极难排查建议把模型名称直接写进配置在启动时做一次强校验。3.3 向量库和混合检索向量库负责高效存储向量并支持快速检索。大规模场景下主流向量库都会采用基于HNSW这类索引结构的算法把检索范围剪枝到很小的候选集。配置索引时的核心参数是M参数和efConstruction简单理解就是建索引时每个节点连接的邻居数。M越大召回精度越高但内存占用也越高efConstruction控制在构建质量和构建速度之间取舍。在小规模验证阶段直接使用默认参数即可等到数据量到了百万级再回头调优。但纯向量检索有一个固有缺陷它只看语义相似度对关键词和精确ID不敏感。比如你检索产品型号“XJK-2000”语义相近的文本向量可能完全对不上而那些包含该字符串的文档由于整体语义分布相近反而可能排得不够靠前。解决思路是混合检索同时跑向量检索和传统关键词检索BM25算法就是经典选择然后把两部分结果合并去重再做一次排序。我个人在实际项目中混合检索带来的提升往往比调Embedding模型更大因为它互补了两种检索范式的盲区。尤其在企业内部文档这类带有大量专有名词、编号、缩写的数据集上效果非常明显。3.4 重排序经常被忽略的关键环节混合检索合并出的候选集通常有几十条甚至上百条。如果直接把Top-10拼进Prompt前几条可能并不相关模型会被噪声带偏。行业里的通行做法是加一层重排序模型把候选集逐条和用户问题拼接由重排序模型重新打分再输出最相关的少数几条。为什么要单独加这一层因为Embedding模型计算的是整体语义相似度重排序模型则更关注“这段文本是否真的回答了这个问题”两者的优化目标不同。实测中重排序后Top-5的命中率常常能比纯向量检索的Top-5高出一大截。这里有一个实效建议重排序很消耗计算资源因为它需要对每条候选文本都做一次模型推理。所以线上一般先靠Embedding快速筛出50到100条再交给重排序模型精选出3到5条。窗口这么设计兼顾了速度和质量。3.5 Prompt构造与生成策略检索只是上半场下半场的Prompt也大有讲究。我推荐的Prompt模板由四部分构成系统指令说明模型的角色和回答边界。参考材料按顺序列出检索到的文本块并标注来源。用户问题原文呈现。输出约束如果参考材料中没有答案必须直接回答“资料中未找到相关内容”不得编造。我会写一个典型模板给你参考你是企业内部知识库助手。 请严格依据下面提供的【参考材料】回答问题。 如果参考材料中没有相关信息请直接回复“在知识库中未找到相关内容” 不要尝试编造不要使用参考材料之外的知识。 【参考材料】 材料1来源员工手册_第3章_第2节 年假每个自然年度的5个工作日... 材料2来源休假制度_v2_第1章 ... 【用户问题】 年假可以累积吗 【回答要求】 1. 用中文简洁回答不超过200字 2. 如果引用了材料在末尾列出对应来源。注意一个细节我特意在“回答要求”里写“不要使用参考材料之外的知识”。原因是大模型天生倾向于“表现渊博”你如果不约束它就会把脑海中训练过的旧知识与检索材料混在一起输出这正是RAG幻觉的一个主要来源。4. 实操示例从零搭一个最小可用的RAG理论讲再多不如跑通一个小Demo来得直接。下面我写一个最小可用的RAG示例逻辑完整但不引入重模型和复杂度适合本地验证流程。出于通用性考虑代码中直接使用向量库的通用API风格你可以替换成自己熟悉的产品。4.1 文档准备与切分假设有三份Markdown文档放在docs目录下我用递归遍历的方式把它们全部加载进来。切分时利用Markdown的标题结构识别出二级和三级标题同一标题下的内容合并为一个语义块。import os from markdown_it import MarkdownIt def load_markdown_docs(folder_path): docs [] for root, _, files in os.walk(folder_path): for file in files: if file.endswith(.md): file_path os.path.join(root, file) with open(file_path, r, encodingutf-8) as f: text f.read() docs.append({path: file_path, text: text}) return docs def split_by_heading(text, min_chunk_len200): md MarkdownIt() tokens md.parse(text) chunks [] current_chunk current_heading for token in tokens: if token.type heading_open: if current_chunk.strip(): chunks.append({ content: current_chunk.strip(), heading: current_heading }) current_chunk current_heading token.tag elif token.type inline: if current_heading and token.content: current_chunk token.content \n else: if token.content: current_chunk token.content \n if current_chunk.strip(): chunks.append({ content: current_chunk.strip(), heading: current_heading }) return chunks实际工程里你还需要处理代码块、表格、列表等特殊格式上面的代码只是演示基础思路。重点项目是在切分之后你还应该做一次长度检查如果某一块远小于切片目标可以考虑把相邻小块合并防止出现大量无实际内容的碎片。4.2 向量化与入库向量化步骤最核心的一点是统一Embedding模型。我先从配置里读取模型名初始化一个embedding函数然后逐块生成向量并入库。from sentence_transformers import SentenceTransformer EMBEDDING_MODEL your-chosen-embedding-model def init_embedder(): model SentenceTransformer(EMBEDDING_MODEL) return model def index_documents(docs, embedder, vector_store): for doc in docs: chunks split_by_heading(doc[text]) for chunk in chunks: vector embedder.encode(chunk[content]) vector_store.add( vectorvector, payload{ content: chunk[content], source: doc[path], heading: chunk[heading], }, )这里的vector_store是我用一个字典结构模拟的简易向量存储真实项目中可以替换成任何OpenSource的向量数据库。入库的payload里存了content、source、heading是为了后续重排序和溯源时使用。4.3 检索与生成在线侧我的流程是问题向量化、检索Top-20、重排序取Top-3、拼Prompt、调用大模型接口。def retrieve(query, embedder, vector_store, top_k20): query_vector embedder.encode(query) results vector_store.search(query_vector, top_ktop_k) return results def rerank(query, candidates, reranker): scored [] for item in candidates: score reranker.score(query, item[content]) scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) return [item for _, item in scored[:3]] def build_prompt(query, selected_chunks): materials \n.join( [f材料{i1}来源:{chunk[source]}\n{chunk[content]} for i, chunk in enumerate(selected_chunks)] ) prompt f你是企业知识库助手。请严格依据参考材料回答问题。 如果参考材料没有相关信息请直接回复“知识库中未找到相关内容”不要编造。 【参考材料】 {materials} 【用户问题】 {query} 【回答要求】 1. 简洁回答不超过200字 2. 引用材料时末尾列出对应来源。 return prompt def answer(query, embedder, vector_store, reranker, llm_client): candidates retrieve(query, embedder, vector_store, top_k20) selected rerank(query, candidates, reranker) prompt build_prompt(query, selected) response llm_client.chat(prompt) return response这份伪代码可以直接平移成真实实现。我特意把检索数量设为20而不是5原因就是给重排序留出足够的候选空间让排名靠后的相关片段也有机会被捞回来。如果你直接拿Top-5去做Prompt很多潜在相关片段会漏掉。4.4 参数选择的经验值给一套我实测相对稳妥的起步参数你可以在此基础上调整参数推荐起始值说明切片长度400-600字符中文场景常用值英文可适当放宽到800字符切片重叠100-200字符防止语义被切断初检Top-K50-100在重排序前保留足够候选重排序Top-N3-5进入Prompt的最终块数Embedding维度模型默认无需手动修改向量索引M参数默认百万级数据量再调优这套参数在大多数通用文档问答场景都能跑出一个可用的基线之后再用更贴近业务的验证集做针对性调整。5. 那些年踩过的坑和优化经验RAG项目最磨人的地方往往不是搭建链路本身而是上线后回答质量的反复波动。这一节我把自己踩过的坑按问题类型整理出来每条都附上排查思路和解决方法。5.1 什么也没检索到答案靠编如果你发现参考答案经常在知识库根本没有内容的情况下胡编第一件事就是去查检索引擎返回的相似度分数。如果返回的分数普遍很低说明检索根本没命中。常见原因有三类。第一Embedding模型选错了。有些模型对中文支持极差中英文混合文档完全没法用。第二切分方式不对。有时候整份文档被切成了几千个碎片每条碎片只有一句话语义信息太少检索自然匹配不上。第三查询问法和文档表述风格差异太大。比如新人提问“我想休假几天”文档里写的是“员工依法享有年休假”纯向量检索很难把这两句关联起来——此时混合检索、加同义词扩展、或者让大模型先把用户问题改写成一个更规范的知识库查询表述都能缓解。5.2 检索到了但答案还是乱的这种情况最让人困惑因为你明明看到参考资料里有正确答案但大模型就是答非所问。我在排查这类问题时通常先检查Prompt结构。有一次项目里Prompt里参考材料过多模型读到中后段的时候注意力已经分散了。后来把重排序后的Top-N从5改成3回答准确率反而上升了。另一次是参考材料顺序问题相关的那段被排在最后模型输出时干脆忽略了它。你可以尝试在Prompt中明确强调“优先参考靠前材料同时注意所有材料”或者用更可靠的做法把最相关的那条材料放在最前列并让模型在回答前先复述材料要点。5.3 幻觉还是存在怎么压很多刚接触RAG的人以为有了RAG就不会再有幻觉这个想法是错的。检索增强能大幅降低幻觉概率但不能消除。当模型遇到参考材料时如果对某个细节不确定它仍可能调用训练时的旧知识来“脑补”。我的做法是三管齐下。第一Prompt里反复强调“严格依据材料资料没有就直说”。第二生成时调低温度参数压制模型发挥空间。第三也是最重要的做引证溯源——让模型在每个答案末尾标出它引用了哪份材料、哪个章节。这招算是一种软性约束它逼着模型走到推理“我到底看了哪些资料”这一步编造的概率会大幅下降。如果严谨度要求极高还可以再加一个验证环节把模型生成的答案重新切分再检索一遍知识库计算答案与资料的相似度低分的就判定为疑似幻觉触发人工复核流程。5.4 多轮对话总是“失忆”RAG交互往往不止一轮问答。用户第一轮问“年假怎么算”第二轮紧接着问“那可以累积吗”系统如果只拿第二轮的问题去检索就丢失了上下文效果很差。解决办法是引入对话重写模块。在把问题送进检索引擎前先让大模型根据历史对话生成一个独立的检索式表述。例如把上面两轮对话改写成“年假是否可以累积到下一自然年”此时检索的目标就清晰多了。同时被选入Prompt的历史轮次也要控制数量全部塞进去会导致上下文过长、注意力稀释保留最近一到两轮的关键信息通常就够用。5.5 系统性能瓶颈最后聊一个上线后的问题RAG链路多了向量检索和重排序用户对首字延迟通常比纯大模型接口要高出不少。性能优化的空间主要在两方面。一是向量检索层面把全量扫描改成基于索引结构的分区查询同时引入缓存对高频问题直接缓存最终答案。二是重排序做异步化不必每次都同步跑完可以考虑把明显相似度高的候选直接送生成其余候选再排队评分。这种“先粗后精、分批回流”的策略对在线系统的体验改善非常明显。6. 哪些场景真正适合RAGRAG不是万能的它有自己的能力边界。从项目选型角度看下面三类场景是它真正的主场第一类是知识库问答。企业内部制度、产品手册、运维文档、客服话术这些资料内容相对稳定又持续更新传统搜索只能返回链接RAG能给出直接答案这是体验上的跨代提升。第二类是私有数据与实时数据的结合。实时新闻摘要、股票研报分析、剧集更新信息都可以通过RAG让大模型接触最新数据源。模型参数不需要重新训练只要每天把新资料重新索引进去即可。第三类是跨文档综合理解。旧办法要人工打开几十份文档找答案RAG能把散落在不同文件里的相关段落同时检索出来再由模型整合输出。我见过一个项目把全年两百多份项目周报做成了总结系统效果远好于文档汇总后让模型直接读一长串内容。但有些场景不适合RAG。比如大篇幅的创意性写作、需要模型自由发挥的头脑风暴RAG会把它限制在已有资料里反而压制生成多样性。再比如包含大量图表、图片的文档如果OCR和多模态模型能力不足RAG检索到的往往是残缺的文本效果不如直接用多模态大模型读图。选型时还有一个容易犯的错误不问场景直接上复杂架构。一个几十条固定问答的客服Bot完全不需要RAG微调甚至改Prompt就能解决。RAG的价值起点是“知识库在持续变大、问题不可穷举”的场景。项目初期先用手动挑选的少量资料把链路跑通再进行工程化改造往往比一开始就铺开复杂体系要稳得多。我在实际项目中最大的体会是RAG的效果是“检索端决定上限生成端决定体验”。整个链路里真正值得反复迭代的是检索模块——文档清洗、切分策略、Embedding选型、混合检索、重排序每一步都能扎扎实实提升精度。而生成端的Prompt编写和约束技巧则是确保系统稳定输出的最后一道保险。如果你正准备上手建议不要一上来就追求完整生产环境。先用一套文档、一个开源Embedding模型、一个简单的向量库、一个大模型接口把最小闭环跑通。再逐步加入混合检索、重排序、对话重写这些进阶模块。一个能持续更新的外挂知识库远比频繁重训模型来得实用——这大概就是RAG现阶段最值得投入的原因。
阅读完成 · 觉得有帮助?