RAGRetrieval-Augmented Generation检索增强生成最近几乎成了私有知识库问答的代名词。我见过不少人第一天照着教程搭好 demo第二天就发现模型开始用通用知识“硬答”内部问题表面上流畅实际错得离谱。真正让我意识到 RAG 价值的是一次内部文档问答需求资料里明明写了明确的制度条款模型却答得模棱两可甚至把几条不同政策混在一起编造答案。原因倒不复杂——它没有真正“读”资料只是被告知要参考资料。RAG 要做的事是先把资料清洗、切块、索引再用用户问题去检索相关片段最后把这些片段作为证据一起交给生成模型让它回答之前确实有东西可查。这篇文章不打算按框架教程的套路写而是记录我自己从零构建第一个 RAG 系统时真正决定成败的环节和踩过的坑。1. 你以为的 RAG 和实际要解决的差距很多人在概念上把 RAG 简化成“向量数据库 大语言模型”觉得只要会调用接口就能搭出知识库问答。这种理解不是全错但它掩盖了真正的问题一个可用的 RAG 系统成败不在于单点技术而在于整条链路上误差的累积。文档清洗差一点分块不合理一点检索噪声大一点最后生成模型都会把它们放大成一次明显的错误回答。1.1 完整链路的最小划分先抛开复杂的生产架构一个最小 RAG 系统也要覆盖四个环节导入把原始文档读进来去掉标记、页眉、目录等干扰信息。索引将清洗后的文本按一定规则切块做向量化连同元数据写入存储。查询用户问题向量化后在存储中召回候选片段必要时做重排。生成把重排后的片段拼进提示词让模型依据这些片段作答。这四个环节里前两个经常被忽略却决定了后两个的上限。很多 demo 的问题就出在这里文档里有答案但答案所在的片段根本没有被正确索引或者检索时被更高相似度的噪声片段挤掉了位置最终模型拿到的是“相关内容”却不是“关键内容”。1.2 “先跑通”和“能用”的分界线我建议第一次构建时不要追求完整系统先做一个最小闭环选定一批内部文档做基础清洗和切块用一个轻量向量存储接一个大模型接口跑通十几个固定问题。这里的验收标准不是“能回答”而是三点答案是否能在对应文档片段里找到依据检索返回的前几条是否语义相关当资料里确实没有答案时模型能不能老实说不知道。这个“最小闭环”的价值在于定位问题。如果你的系统在十几个问题上表现很好但换个问题就崩往往不是生成模型的问题而是索引和检索链路的问题。相反如果检索出来的片段明明正确答案仍然胡说那才需要去调整提示词或换更强的生成模型。先跑通不是为了交差是为了获得一个可以逐段排查的基线。2. 第一版数据工程清洗、分块与元数据设计我见过不少团队直接把一堆 PDF、Word 丢进解析工具然后把输出的纯文本全部切块入库。等到线上查询时检索结果里出现大量“目录”“页眉”“参考文献”答案自然被带偏。数据工程这一关省不掉尤其当文档来源复杂时脏数据会直接污染向量索引。2.1 清洗规则先决定“哪些内容根本不配进向量库”清洗不是把所有文本都变干净而是主动丢弃那些对回答问题没有帮助的内容。比较常见的需要扔掉的包括重复的页眉页脚、目录条目、纯装饰性文字、乱码段落、扫描件自带的 OCR 噪声。判断标准很简单如果一个片段被检索出来后人类无法从它身上找到任何可用于回答的信息那它就只是在浪费检索候选位。以一份制度文档为例我常用的清洗策略是保留正文段落、条目化条款、表格中可读的说明文字但去掉封面、目录、无编号的页眉。表格要先判断是否为“问答可依赖内容”如果表格是核心数据最好转换成“字段名值”的文本行而不是保留换行和竖线格式。这个转换要保留语义例如“报销时限30个工作日”而不是把表格矩阵原样塞进文本否则向量匹配时很难被问题召回。2.2 分块粒度一个我在 512 与 1024 之间反复调整的真实过程切块粒度没有标准答案但有规律。我第一次做分块时习惯按固定字数硬切比如每 1024 字一段结果发现很多句子被从中间切断语义残缺。后来改成按语义边界切优先在段落、句号、问号、分号处断句再结合字数上限做聚合效果好很多。对于中文制度类文档我比较常用的参数是单块 600 到 800 字重叠 100 到 150 字。数字本身不是真理关键在于重叠。重叠的作用是让跨块被切断的完整语义有机会在多个块中出现检索时不会因为关键句恰好在切缝处而漏掉。你可以做一个小实验找一个必须看后半段才能完整理解的段落分别用“无重叠”和“有重叠”的分块方式入库然后问一个问题的答案出现在第二段里的查询观察召回结果差异。大多数情况下加入重叠后召回稳定很多。也要考虑块类型。代码片段、表格、长篇引用说明需要单独切块并在块上打上类型标签。比如chunk_type可以标记为paragraph、table、code。这样做的原因是同一种切块策略很难同时适配所有内容把类型单独记录后后续可以做更有针对性的检索过滤。2.3 元数据让检索结果自带“上下文说明牌”很多第一个 RAG 系统只存了文本向量完全忽略元数据这是一个很隐蔽的坑。元数据的作用不只是展示来源它还能参与过滤和在提示词中帮助模型理解上下文。我建议每一条块至少记录这几个字段来源文件名、页码或章节编号、块标题、更新时间、权限级别。还是拿制度文档举例假设用户问的是差旅报销流程而你有一堆不同部门的制度文件没有“部门/适用范围”这个元数据检索结果可能把销售制度和研发制度混在一起。有了元数据就可以在检索时按权限或业务域过滤也可以在生成时把来源信息展示给调用方。这些字段写起来不难难的是数据准备阶段就要设计好。等系统上线后想再补元数据通常得重新清洗和索引成本比一开始就加上高得多。3. 选型不迷信“全家桶”向量库、Embedding 与混合检索第一次做选型时人很容易被各种宣传带偏觉得分布式集群、多种检索方式、管道化编排是标配。但一个小型内部系统真正需要的是易上手、可替换、能解释问题。3.1 向量库选型从文件型存储起步的可行路径如果你的文档量在百万级以内第一个版本完全可以用轻量级文件型向量存储而不是一上来就上分布式集群。原因很直接你的主要目标是验证检索效果而不是验证架构上限。轻量方案可以本地启动、直接调试能很快暴露检索策略的问题。我列过一个简单的选型对比方案类型适合数据量维护复杂度适合阶段文件型/嵌入式存储小规模千级块低进程内即可第一个 RAG 原型单机向量数据库中等规模百万级向量以下中需要备份和升级有一定真实用户后分布式集群超大规模、高并发高需要专职运维企业级生产环境这不是说轻量方案没有缺点它的过滤能力和并发写能力都比较弱。但第一阶段你缺的不是性能而是对检索效果的判断能力。等检索策略稳定了再迁移到更重的存储迁移成本其实可控。3.2 嵌入模型选择中文场景比你想象的更敏感嵌入模型决定了文本被映射到向量空间后的语义亲疏关系选错模型后面所有检索优化都像在沙子上盖楼。中文场景下我吃过一个亏用的模型对口语化查询理解可以但对正式制度条文里的术语匹配很弱用户问“报销时限”检索结果却召回一堆“预算执行”相关片段。选嵌入模型时不要只看榜单分数直接拿自己的文档跑一组自测准备 10 个真实查询每个查询从文档里挑 5 个正确答案片段和 10 个干扰片段然后看模型召回的 Top 3 里有多少个正确答案。这个测试比抽象指标有用得多。另外要注意模型的文本长度上限如果你的分块经常超过上限文本会被静默截断对应关系也不知道。尽量选支持较长输入的模型否则你的分块策略会被迫让步。还有一点容易被忽略不同嵌入模型产出的向量维度、相似度分布不在同一尺度不要混用。同一个系统里文档向量和查询向量必须用同一个模型生成否则相似度没有意义。3.3 混合检索关键词与向量不是二选一向量检索擅长“语义相近”但遇到精确术语、编号、特殊写法时它的召回不一定比关键词更好。比如用户问“Q3-2024 报告”如果相关文档里写成“2024年第三季度报告”向量模型可能认为是一回事但如果写成“3Q24”这种缩写向量模型就不一定了。所以我建议即便是第一个 RAG也要做最轻量的混合检索一路是向量召回一路是关键词召回BM25 这类经典算法就够用。两路结果合并去重后再统一打分。最简单的融合方法是 RRFReciprocal Rank Fusion对每个文档统计它在两路排序中的名次得分是名次倒数的累加值。score(doc) sum(1 / (k rank_i(doc)))其中k通常取 60 左右。这个方案没有调参负担也不会因为某一路分数分布偏移导致另一路失效。混合检索不一定让每个问题都变好但能明显减少“明明有精确匹配却召回不到”的情况。4. 检索链路组装召回、重排、上下文窗口与提示词检索链路是从“用户输入”到“模型生成”的中间地带也是第一个 RAG 系统优化空间最大的地方。很多人把精力放在换更强的生成模型上却忽略了检索结果只要更准一点生成质量就会有明显提升。4.1 召回阶段的 Top-K 根本不该拍脑袋定我最早实现时直接把 Top-K 设为 3结果很多情况下前三片语义相关却都不包含真正的答案。后来意识到召回阶段的任务是“尽可能不遗漏”而不是“只给最像的几条”。Top-K 太小跨章节问题时容易召回不到关键片段Top-K 太大生成阶段噪声太多。推荐的做法是召回阶段取一个较大的候选池比如 20 条然后通过重排压缩到 3 到 5 条。这样既保留了“能找到答案”的可能性又避免了把一堆相似片段全塞给模型。当然这个数字要跟着你的数据分布走文档块比较碎、答案分布更零散时候选池可以再大一些。4.2 重排序先用一个轻量级模型筛掉无效片段向量召回本质上是快速粗筛它对语义相关性的判断是“近似”的。现在很多系统会在召回后接一个重排序模型它的做法是把查询和每个候选片段拼接成一段输入逐字计算相关性比单纯计算两个向量相似度更精细。重排序最直接的效果是纠正“语义相关但不相关”的情况。举个例子用户问“申请流程”早一轮召回里可能全是“申请条件”“申请材料”文本长度也长向量相似度不低但真正描述步骤的段落反而排在了后面。用重排序模型重新打分后准确率会明显提升。不过重排序不是免费的逐条计算耗时会增加。我的经验是先用一个小候选池比如召回 20 条只对池内做精排把延迟控制在可接受范围。注意不要把重排结果当作绝对的“真值”它同样有误差但它相对原始向量排序更稳定尤其适合判断“某个片段有没有直接回答这个问题”。4.3 上下文组装避免把“相关但不充分”的片段凑给模型生成阶段能交给模型的片段数量有限而且不是越多越好。相关内容塞得过多模型反而不知道哪些是核心。这里有一条经验宁可给 3 个高度相关的片段也不要给 10 个泛泛相关的片段。组装上下文时我会保留每个片段的来源编号比如资料 [1]《差旅报销规定》第三章部门财务部 资料 [2]《费用管理制度》第五章报销时限要求这些来源信息放在提示词里既帮模型理解片段边界也方便人工检查答案是否把出处搞混。实际生产时如果同一文档的相邻两块都出现在候选里可以考虑在组装时适当去重或合并避免重复内容占用宝贵的上下文窗口。4.4 提示词怎么写给模型一个“引用原文再作答”的任务很多人第一个 RAG 直接把检索片段拼在“请回答以下问题”后面剩下的交给模型自由发挥结果模型依然会借题发挥。提示词应该是任务的约束而不是背景介绍。我目前比较常用的模板是请依据下面的资料片段回答问题。 资料 [1] ... [2] ... 规则 1. 优先使用资料片段作为依据回答时标注片段编号 2. 资料不足以回答时直接说明“资料中未找到”不要推测 3. 不要使用资料之外的内部信息或背景知识。这个模板看着简单但它做了三件事限制模型的信息来源强迫答案与证据绑定给模型一条体面的“不知道”退路。你可以在同样检索结果下对比“自由发挥版”和“引用约束版”后者在可追溯性上会好很多。5. 把第一个 RAG 变得可度量测试集与指标不做评估的 RAG 系统基本上是在盲调。我见过有些团队上线后靠感觉判断“效果还行”一旦换一批问题就崩。第一个系统就应该建立一套低成本、可重复的评估方式。5.1 两类评估端到端回答质量与检索质量评估可以拆成两个层面不要混在一起。检索侧评估关心“该找的内容找没找到”常用指标有 RecallK前 K 条包含相关文档的比例和 MRR第一个相关文档排名的倒数。生成侧评估关心“回答是否准确、忠实”常用指标是答案相关性和忠实度。忠实度尤其重要它衡量答案中的关键论断能否在上游检索片段中找到出处也就是“是否在瞎编”。评估对象常用指标关键问题检索链路RecallK、MRR正确答案片段有没有进入候选池生成链路答案相关性、忠实度答案是否准确且每个关键句有依据一个常见误区是只做端到端人工评价发现问题却不知道根因在哪。正确的做法是两条链路分别评估先看检索有没有召回正确片段再看模型是否在正确片段上生成了正确结论。这样每次 bad case 都能量化到具体环节。5.2 自己构造测试集的方法以及“坏问题”的排除规则没有现成测试集时不要直接拿网上语料最好从真实用户场景里收集候选问题。选材料时有个原则问题不要抄文档原文。如果你把某个标题改成问句当测试题系统很容易靠关键词匹配作答这会高估检索质量。尽量做改写让测试问题更像真实提问。我一般会构造三类问题有明确答案的事实型问题例如“申请报销需要提前几天提交”跨章节综合型问题答案是多个片段拼出来的例如“新员工入职后社保和差旅报销分别找哪个部门”资料中没有答案的反问题例如“离职后还能报销在职期间的费用吗” 这时候系统应当给出“未找到”而不是强行回答。测试集规模不用很大20 到 50 条足够发现第一版的大问题。但要注意把测试集分成开发集和验证集避免你边调边看同一批问题而“过拟合”。我的做法是留 30 条做日常调试另外 10 条只在最终验证时跑防止自我感觉良好。5.3 发现“检索失败但答案正确”的常见盲区最迷惑人的 bad case 是检索结果完全错误但最终答案看起来正确。原因是模型用自身知识把答案“圆”了回来。这种情况下端到端评估会给高分但系统实际上没有在做 RAG它只是在做一个带抽取干扰的生成任务。要堵住这个盲区方法很直接要求答案必须带引用编号或者在评估脚本里检查答案文本与给定资料片段是否存在可验证的对齐关系。比如在测试集上强制要求“如果资料没有相关内容回答必须是资料未找到”。如果一个查询检索失败模型却给出很具体的答案直接标记为高风险样本回到检索侧去修而不是自欺欺人地认为生成模型够聪明。6. 从 Demo 到内部可用缓存、日志与迭代维护RAG 系统从“能跑”到“能长期用”中间还隔着一层工程化。很多原型死在没人维护、问题无法定位、数据一变就崩。我在第一个系统中被迫解决过这些问题整理几个非常实际的抓手。6.1 缓存不是照搬 Web 缓存就行简单缓存相同问题确实能省不少调用费但 RAG 的缓存容易踩两个坑一是答案会过期文档更新后旧回答需要作废二是错误答案不能因为缓存就长期存续。我建议至少做两层缓存。第一层相同的问题文本加索引版本号后在短时间内直接返回历史答案适合高频重复问题。第二层缓存检索候选列表不缓存最终答案这样可以在文档没有变化的前提下让新的生成模型快速重新生成。要注意的是缓存键里最好携带用户权限维度避免不同权限的用户共享到本不该看到的片段。文档更新后我会对更新过的文件重新切块和向量化并把缓存版本号加一。这个机制比定期全量重建简单得多起步阶段完全够用。6.2 日志里要能看到哪些信号生产环境的日志是看不见的测试集。我要求每个查询至少记录用户问题、召回的候选列表及排序分、重排后的最终片段、答案文本、答案引用的片段编号、耗时。不一定要全量持久化但至少保留最近一段时间内的记录供 bad case 复盘。有一条特别实用把“模型未引用任何片段”的回答单独标记出来。我在早期从日志中筛出了一批这类案例发现它们几乎都是检索质量差而不是提示词问题。没有这层日志你很难确定优化方向。另外日志里要对“文档版本”做关联。同一个问题在不同版本下的答案差异最能暴露索引同步问题。6.3 一个小技巧把“用户不点反馈”的答案回流成测试用例很多系统做了用户点赞/点踩按钮但数据躺在数据库里没人看。其实最有效的闭环是把“不认可的答案”直接加入测试集回归跑一遍。这一步成本很低收益却很大每一个 bad case 都是未来改动后的“锚点”保证优化不会倒退。我会为每个回流样本记录三类信息原始问题、实际答案、可能原因检索遗漏、排序错误、生成跑偏。下次改完分块策略或重排模型后把全部历史 bad case 重跑一遍看修复比例是多少。这比任何抽象指标都更能说明问题。迭代本身也是这样先修数据再调检索最后才动生成。顺序反了经常白忙一场。第一个 RAG 系统真正难的不是跑通而是让它每次回答都能拿出站得住的依据。我个人的体会是先把数据清洗和评估测试集做扎实再去追新工具并不迟。你第一次构建时踩过的那些坑几乎都是宝贵的判断依据把它们变成可回归的测试用例系统才能越用越稳。