
不需要特别声明“本文是RAG入门”“本文介绍了什么”之类的铺垫咱们直接从一个真实到让人头大的场景开始。你辛辛苦苦调了一下午提示词模型终于答应不再乱编可它转头就把合同金额从“一万二千”念成了“一万零二百”还一脸笃定。这种“一本正经的胡说八道”几乎是每一个做过大模型集成的人都会撞上的墙。RAGRetrieval-Augmented Generation检索增强生成就是专门用来拆这堵墙的不是逼模型“记住”你的业务规则而是每次回答问题前先让它去你指定的知识库里把相关材料翻出来照着材料再说人话。这篇文章不玩虚的。我会从大模型为什么会产生幻觉这个底层原因讲起把RAG全链路的选型、最小可复现的工程代码、以及我实际部署中踩过的坑全部摊开目的只有一个让新手和刚入门RAG的程序员看完之后能独立搭出一个让模型“知道就是知道、不知道就说不知道”的可靠系统。适合正在做大模型应用集成、私有知识库问答、或者被幻觉折磨得想砸电脑的各位。1. 大模型到底为什么会“一本正经地胡说八道”在动手做RAG之前先把模型的“病根”看清楚。很多人以为幻觉是模型太笨、记性不好实际上模型脑子里根本没有“记忆”这个概念。它只有参数一组经过海量文本训练后压出来的权重。每个回答都是根据当前输入在这组权重上做概率采样选出“最像样”的下一段文字。也就是说模型不是在回忆“合同金额是多少”它是在推算“这种语境下哪种数字组合最顺口”。1.1 幻觉的成因从概率生成到知识截断幻觉的本质是“知识没有边界”。预训练虽然让模型掌握了语言规律和部分常识但这个“知识”有两个致命问题一是截止时间固定训练数据之后发生的事它一概不知二是存储密度有限它只能记住高频出现的、被反复强调的信息你的私有文档、内部规则、小众参数对模型来说根本不存在。所以当你问它一个超出训练分布的问题比如“某公司内部新发布的报销标准是什么”模型并没有“我不知道”这个标准答案它只会按照语言习惯把“报销”“标准”“元”这些词拼成一个看起来合情合理的句子。这就是为什么它敢把金额说错、敢凭空捏造出处、敢在你不确定的时候编出个根本不存在的文件编号。1.2 RAG为什么能治这个病不是给模型“行医”而是给它“开卷考试”RAG的治本思路其实特别朴素考场里允许你翻书它就不再需要背诵。传统集成是“闭卷考试”你把问题扔给模型它只能靠参数里那点压缩过的记忆硬答遇到盲区必然编造。RAG改变了信息流向收到问题后先从一个你指定的外部知识库里检索出最相关的少量文档片段把片段和原始问题一起打包送进模型让模型“基于这些材料回答”。开卷之后模型要答的内容基本就在眼前捏造空间被大幅压缩。它不需要“知道”你的业务细节只需要“读懂并转述”你提供的材料。这也是RAG在私有知识问答、客服辅助、企业知识管理等场景里迅速成为标配的核心原因——它治的不是单个字眼的对错而是让模型从“无中生有”转向“有据可依”。2. RAG全链路的技术选型与架构搭这栋楼之前先认清建材RAG不是某个单一模型或库而是一条完整的数据流水线。我开始做第一个RAG项目时误以为“接个向量库就完事了”结果上线后召回几乎全是垃圾。所以这一章先把整条链路讲透让你明白每个环节是在干什么、为什么必须存在。2.1 一条完整的链路从用户提问到答案返回一个标准的RAG流程可以拆成四个连续动作先对知识文档做清洗与切块再把每个块转成高维向量存在向量数据库里用户提问时对问题做同样的向量化去库中做近似度检索取出最相关的Top-K块把问题和这些块拼成一段带上下文的提示词最后交给大模型并约束它“只根据提供内容作答”。前两步属于离线准备后两步属于在线推理很多项目把前两步做成定时任务数据和文档一更新就重跑一次。有个细节容易被新手忽略检索召回不是靠“关键词匹配”而是靠“语义相似”。文档块和问题都被映射到同一个向量空间里语义越近向量夹角越小。比如用户问“离职赔付是几个月”如果文档里写的是“离职补偿按N1执行”关键词完全对不上但语义是相关的好的向量模型依然能把它召回。这也是为什么选对embedding模型比选对向量数据库更影响效果。2.2 embedding模型怎么选决定检索效果的第一关embedding模型负责“把文字变成向量”它的质量直接决定召回的准确性。国内能直接调用的大模型服务一般会自带embedding接口省事但效果参差开源生态里常见的本地模型也各有侧重。这里我给出一个粗线条的选型参考场景特点推荐方向一句话理由通用中文问答、文档少、刚起步大模型配套接口零部署成本接入最快效果稳定涉及大量专业术语、垂直领域领域微调过的embedding模型通用模型常把术语的语义“压扁”容易漏召回需要本地离线、有隐私要求本地开源embedding模型数据不出内网但需要一块不错的显卡多语言混合、代码混排多语言向量模型避免中英切换时向量空间错位一个常见误解是“embedding模型越大越好”实际上embedding效果和参数量并不是严格正相关模型太大反而会让离线批处理变慢。我个人的习惯是先拿一个小型但口碑稳定的模型跑通链路再用自己业务里的真实问答对去做召回率评测评测不过再换更强的不要一头扎进模型海洋里出不来了。2.3 向量数据库怎么挑别嫌选择困难先搞懂这几类向量数据库的核心任务是“存向量 做近似检索”市面上选择很多但本质也就三大类。第一类是专业向量库比如常见的开源方案专为向量检索设计性能好、接口丰富适合作为RAG系统的核心组件第二类是传统数据库自带向量插件如果你的业务原本就用它可以直接在现有表上加向量字段少维护一个组件但高并发近似检索性能通常弱一些第三类是基于大模型框架自带的简易检索层它其实只是一层内存里的暴力比对适合Demo和原型。对新项目我的建议是别在数据库选型上花太多时间。数据量不超过几百万专业向量库和数据库插件的效果差距你用不出来。真正影响体验的是“怎么把文档切成合适的块”以及“检索后如何排序”这两块没做好换再强的向量库也白搭。等知识库规模上来再按并发和延迟指标去做压测选型。3. 用一段工程化代码跑通最小RAG Demo理论讲再多不如跑通一个最小闭环。这一章我用一个非常基础的知识库案例带大家走一遍目标是让模型基于本地几个文档回答问题并且当文档里没有答案时明确说“不知道”而不是硬编。3.1 前期准备本地用文本建一个最小知识库我们先准备几个纯文本文件模拟一个简单的公司制度知识库。别嫌这些文档简单实际项目里无论PDF、Word还是网页最终都要清洗成类似的纯文本块所以用txt做原型反而最没有干扰。我建三个文件分别叫rules/onboarding.txt入职流程、rules/leave.txt休假制度和rules/salary.txt薪资发放内容用五六句话描述核心规则即可。这一步的目的只是让知识库有可检索的内容等流程通了再去接真实PDF和爬虫。然后写一个最基础的工具函数负责把整个目录下的文档按固定长度切成块import os from pathlib import Path def load_and_chunk(source_dir: str, chunk_size: int 500, overlap: int 50): chunks, metadatas [], [] for path in Path(source_dir).glob(*.txt): text path.read_text(encodingutf-8).strip() text .join(text.split()) # 压缩多余空白 for i in range(0, len(text), chunk_size - overlap): chunk text[i:i chunk_size] if chunk.strip(): chunks.append(chunk) metadatas.append({source: path.name}) return chunks, metadatas切块时留一点重复区域overlap是为了避免一句话恰好被从中间截断导致检索时语义不完整。这个细节后面会专门讲。3.2 核心实现检索实现与再生成接下来我们复用框架能力把链路串起来。这里用现有的向量库和大模型SDK做整合。核心代码划分成三步先构造知识库索引再实现检索函数最后拼装提示词生成回答。from sentence_transformers import SentenceTransformer import chromadb # 1. 加载embedding模型准备向量库 model SentenceTransformer(BAAI/bge-small-zh-v1.5) client chromadb.PersistentClient(path./kb_store) collection client.get_or_create_collection( namerules, metadata{hnsw:space: cosine}, ) def embed_docs(): chunks, metadatas load_and_chunk(./rules) ids [fchunk_{i} for i in range(len(chunks))] embeddings model.encode(chunks, normalize_embeddingsTrue).tolist() collection.upsert(idsids, documentschunks, embeddingsembeddings, metadatasmetadatas)向量库建好之后检索就是一句查询的事def search(query: str, top_k: int 3): q_vec model.encode([query], normalize_embeddingsTrue).tolist() res collection.query(query_embeddingsq_vec, n_resultstop_k) return res[documents], res[metadatas] # 测试问一句语义相近、但关键词不一致的问题 docs, metas search(离职的时候赔偿怎么算) print(docs)当前这个最小Demo里回复生成可以先用最基础的方式拼接。但我在工程化时更倾向于使用专门的检索模板把上下文和问题明确分隔开并把约束条件写进系统提示词后面会在护城河章节详细展开。3.3 把Demo“升级”成真正可用的工程Demo跑通之后有四个点必须立刻动手改否则量一大就会崩。第一离线索引要做成可增量更新的任务不能每次全量重算第二检索结果要带来源元数据前端展示时可以附上出处用户也能自己核对第三要给查询加超时、重试和熔断第四所有文档块的来源必须能追溯到原始文件否则知识库更新时老的脏数据会一直污染在线结果。另外一些细节能显著改善体验提示词里让模型先“看”检索到的内容再回答如果检索结果为空或者与问题无关则强制要求模型回复“知识库中没有找到相关答案”避免模型强行利用自身常识作答。这个约束我在下一节会给出具体模板。4. 让RAG从“能跑”到“好用”的细节那些文档里不愿写的坎很多人跑通Demo后觉得效果不错可一放到真实数据和真实用户面前就露馅召回结果看着相关生成答案却张冠李戴用户换个说法检索就漏知识库数据一多延迟跟着爆炸。这些都不是玄学而是若干细节没有处理到位。这一节捡最关键的讲。4.1 chunk切分直接定死“512个字”最容易翻车切块是RAG里最容易被低估的环节。很多教程会告诉你“每块512个字”但真实项目里这个参数必须跟着内容结构走。如果文档有明确的章节标题、列表、表格那么最好的切法是以语义边界为准优先保持段落完整段落太长再按更细的规则切代码块、表格要单独处理不能硬切。我踩过一个真实案例某文档里一个Markdown表格被切成了两块上半块还在表头下半块直接是数据和单位。用户问某个指标时模型收到不完整的表格居然“脑补”出了错误数值。解决办法是给不同内容类型写不同的chunk策略尽量保证“一个语义单元放进一个块里”。另一个经验是块太小模型看不到足够上下文容易断章取义块太大检索出的内容包含过多无关信息又会稀释答案。一般从300-800字起步用你自己的真实问答集做回归对比不同块大小时的召回准确率和答案准确率而不是抄别人博客里的参数。4.2 元数据过滤召回“相关”不等于召回“正确”纯向量检索只关心语义相似但它不理解业务规则。比如知识库里既有旧版本的《报销制度》又有新版本的用户问“打车报销上限”向量检索可能把两个版本都召回模型就不知道该信谁。这时候就需要元数据过滤给每个文档块打上版本号、部门、发布时间、文档类型等标签检索时带上过滤条件把过期版本直接挡在外面。实现也不复杂。写入向量库时把这些标签放在metadata字段里查询时在检索请求中带上过滤表达式。例如只查版本号为v2的块collection.query( query_embeddingsq_vec, n_results3, where{version: {$eq: v2}} )再进阶一点可以把“权限”也做成元数据过滤。不同角色用户只能检索到各自权限范围内的文档块比答案生成后再做一次审核要靠谱得多很多政务和金融项目都是这么要求落地权限的。4.3 混合检索重排把召回质量再往上提一档纯向量检索不是万能的最大的问题在于对精确匹配不敏感。用户问“订单号20240501的状态”向量检索可能因为整体语义相近把“订单查询流程”这种泛泛的文档排在前面而真正包含具体订单号的记录反而排后面。关键词检索擅长精准匹配但它不懂语义。成熟做法是“混合检索”向量检索和全文检索各自召回一批结果然后合并去重再做重排。重排rerank可以用专门的跨编码器模型逐对计算“文档块和问题的相关度分数”把真正相关的排名往上挪。这一步对最终效果提升非常明显代价是多了一次模型推理延迟会高几十毫秒但对绝大多数知识库问答场景来说完全值得。我在生产里用的链路是向量检索取Top50关键词检索取Top50合并后交给rerank模型重排取Top5再进大模型生成。效果比单纯的Top5向量召回好很多尤其是那些包含精确编号、产品型号、人名地名的查询。5. 绕开护城河让模型“不知道就是不知道”前面所有工作都在提高“知道”的上限但真正决定一个RAG系统可不可信的是“不知道”时的表现。很多项目检索做得很漂亮一到知识库覆盖不到的问题模型就开始仗着自身知识硬答把内部规定和公共常识混在一起。解决这个问题需要在提示词层面设计一道“护城河”。5.1 设置拒答边界让“不知道”成为合法答案护城河的核心思想很简单如果不确定答案来自提供的知识块就明确说无法作答。实现方式主要是靠系统提示词约束。我在工程里经常用这样一段话你是一名严格遵守知识库回答的助手。回答只能基于提供的上下文如果你的知识库内容无法支撑答案请明确回答“当前知识库中未找到相关信息建议咨询负责人”。严禁使用常识或自身知识补充严禁编造文档中不存在的细节。把它和大模型一起送进生成接口就能把“不知道”变成合法选项。不过要注意提示词只是软约束不能完全挡住模型“手痒”所以还必须配合后面的策略。5.2 护城河模板如何实操落地我在实际项目中是把护城河约束固化成一个公共函数所有RAG查询统一走它。模板包含三个必要段落角色约定、检索上下文、拒答规则。一个可参考的简化版本大致长这样PROMPT_TEMPLATE 你是一个严谨的智能问答助手。请严格按照下列步骤操作 1. 仔细阅读用户问题 2. 仔细阅读已知的检索上下文 3. 判断检索上下文是否足以回答问题 4. 如果充足则基于上下文回答并给出引用片段 5. 如果不充分直接回复“当前知识库中未找到相关信息建议换个说法再试一次。” 6. 禁止编造禁止使用检索上下文之外的知识。 检索上下文 {context} 用户问题 {question} 回答这个模板有几个可调参数如果你希望答案更严谨就把“禁止编造”放在最前面如果你希望答案更口语可以加入“请用自然语言组织回答”。但无论如何拒答规则绝不能省它是系统可信度的底线。5.3 实测效果检验用对抗问题压测系统搭建完之后一定要做对抗性测试。别拿知识库里的标准问题测要找那些“知识库里没有、但模型可能知道”的刁钻问题来测。比如我给上面的休假制度知识库专门准备了这些测试用例测试问题期望结果实测常见问题年度调薪的流程是什么知识库无信息拒绝回答模型可能按常识给出调薪套路加班费怎么算若无具体文档则拒答模型可能根据劳动法编造内容病假和事假有什么区别引用文档相关条目回答可能混淆不同条款压测的标准很简单只要模型开始自由发挥你就该调提示词或增强检索过滤而不是急着换一个大模型。坦率说护城河不是一次性调完就一劳永逸的我几乎每隔一周就会根据用户实际提问把“容易让模型自由发挥”的问题补充进知识库或者加强拒答模板。这就是一个持续迭代的过程。6. 从Demo到上线还有哪些坑等着你把RAG系统部署上线和跑通Demo完全是两码事。这一节挑几个高频率踩坑点提醒大家很多我都是付过学费的。6.1 知识库更新向量索引不是“改改文件就行”知识库里的文档一旦变更向量库里对应的老块不会自动消失。如果你的更新逻辑做得不仔细就可能出现“旧文件已经删了旧向量还在库里用户一问还能查到”的怪事。我的做法是每个文档块都带文档ID与版本号更新时按文档ID把旧块先删除再写入新块如果是API接口触发的增量更新还要做好并发控制避免索引写坏。6.2 延迟和成本RAG不是“多一次查询”那么简单很多人以为RAG只是“多检索一次”但实际线上延迟要算上向量化用户问题、向量库检索、重排模型推理、大模型生成四个环节。如果知识库数据量大向量库检索可能从几十毫秒涨到几百毫秒如果加了一个重排模型又要额外几十毫秒。优化时先把不必要的环节从链路里拆出去比如小规模知识库根本不需要重排直接Top3进大模型就够了。6.3 评测体系没有评测就谈不上优化RAG链路环节多参数多如果没有一套自己的评测集你根本分不清是切块问题、检索问题还是提示词问题。我建议每个项目初始化时就整理50-100对“标准问题-标准答案数据”答案标注出期望回答来源。每次改动链路都用评测集跑一遍记录召回率、正确率、拒答准确率。评测驱动优化才真正“从零到精通”否则就只能一直靠直觉调参。我个人在做了几个RAG项目之后最大的体会是RAG真正难的从来不是某个模型选型而是整套系统的边界意识——明确哪里该检索、哪里该拒答、哪里该更新把这些边界管好了大模型幻觉问题基本能压到可接受范围。以上这些都是我实打实踩过来的路希望对此刻正跟幻觉作斗争的你有点帮助。