去年给客户做内部制度问答第一版上线当天就被打脸。用户问「试用期能不能休年假」系统回答试用期员工年假按 80% 折算需提前 3 个工作日申请。——语气笃定、格式工整而公司制度里根本没有这一条。复盘时我们才想明白模型不是记错了它是在没有任何证据的情况下用最像答案的语气补全了一个最合理的句子。这就是要上 RAG 的真正原因——不是为了让答案更聪明是为了让答案有据可查。关键词RAG、检索增强生成、向量检索、Embedding、Rerank、大模型、知识库问答一、RAG 原理一句话以及它为什么真的管用1.1 原理本身一句话就能说完RAGRetrieval-Augmented Generation检索增强生成答题前先翻资料照着翻到的内容答并注明出处。形式化一点普通生成$\hat{y} \arg\max P(y \mid x)$RAG 生成$\hat{y} \arg\max P(y \mid x, z)$其中 $z$ 是检索器找回来的证据片段差别只在多了一个 $z$但这个 $z$ 把模型觉得自己知道什么换成了资料里写着什么。1.2 它为什么管用把两类知识拆开这是 RAG 原始论文[1]最核心的贡献也是很多人忽略的一层理解知识类型存在哪里怎么更新适合装什么参数化知识模型权重里训练烧进去的重训 / 微调成本极高通用的语言能力、推理方式、世界常识非参数化知识外部索引里向量库 / 搜索引擎改文档即可秒级生效公司制度、产品手册、实时数据、私有文档RAG 的本质不是给模型外挂一个硬盘而是把会说话和知道什么这两件事解耦——让模型只负责它擅长的理解、组织、表达把事实性知识交给可随时修改的外部系统。想通这一点很多设计问题就自动有答案了为什么要做溯源因为 $z$ 是可查的外部事实答案的信任来自 $z$ 而不是模型为什么改文档就能改答案因为知识压根不在权重里为什么 RAG 不等于消灭幻觉因为它只管住了 $z$管不住模型在 $z$ 之外自由发挥。1.3 三个先天毛病决定了什么时候必须上 RAG知识截止训练数据有时间终点问它之后的、以及高频变动的事它只能猜私有数据看不见公司内部制度、工单、客户资料从来不在训练集里无证据时仍会补全这是最危险的——模型被训练成总要给出一个通顺的回答于是没有答案时它会编一个最像的答案而且语气和真答案毫无区别。第三条是开头那个事故的根因也是 RAG 最不可替代的价值它给了模型一条我不知道的合法出路。1.4 RAG vs 微调 vs 塞长上下文这三个方案经常被拿来一起比较但它们在解决不同的问题维度RAG微调长上下文改变的是模型看到什么模型怎么说话模型一次能看多少知识更新改文档秒级重新训练天级改文档秒级适合灌知识✅ 事实、制度、数据❌ 不适合✅ 但受长度和成本限制适合改风格/格式❌ 很难✅ 强项❌可溯源✅ 天然支持❌ 无法溯源✅ 但token 成本高成本中检索 长 prompt高训练 维护高每次全量付费典型误用拿它教模型语气要客气拿它灌产品手册把整个知识库每次都塞进去一句话选型口诀教它怎么做 → 微调告诉它是什么 → RAG一次性读完整份材料 → 长上下文。三者也经常组合使用RAG 召回 微调过的生成风格。二、完整链路总览两条链而不是一条新手最容易犯的认知错误是把 RAG 想成一次问答调用。实际上它是两条独立运行的链路═════════════ 离线链写文档进来索引建好跑一次或定时跑 ═════════════ ​ 原始文档 解析 切片 向量化 写入 PDF/Word → Loader → Chunking → Embedding → VectorDB HTML/DB ① ② ③ ④ │ │ │ │ │ └── 元数据 ─────┘ │ │ (标题/页码/时间/权限) │ └──────────────────────────────────────────┘ ↑ 90% 的线上问题出在这里 ​ ═════════════ 在线链读用户提问实时执行 ═════════════ ​ 用户问题 │ ├─→ ⑤ Query 改写指代消解 / 多查询扩展 / HyDE │ ├─→ ⑥ 检索关键词 BM25 向量语义 → 融合 → Top-N 粗排 │ ├─→ ⑦ 重排 Rerank交叉编码器精排 → Top-K │ ├─→ ⑧ 上下文组装去重 / 排序 / 截断 / 加引用编号 │ └─→ ⑨ 生成 溯源带拒答约束的 Prompt → LLM → 带 [1][2] 出处的答案这张图里最值钱的一句话绝大多数RAG 答不准的问题根因在离线链但团队的时间都花在调在线链的 prompt 上。我们项目踩过这个坑线上答案质量不达标两周内改了十几版生成 prompt收效甚微。后来把召回的 chunk 打印出来肉眼一看——该找的内容压根没被召回来。分片把一条完整规则切成了两半两半的相关性分数都不够高双双落榜。改了切片策略之后prompt 一字未动准确率直接涨了 20 多个点。这个教训后来变成了我们团队的第一条排查纪律先打印召回结果再谈生成质量。顺带一提RAG 的演进通常被划分为 Naive / Advanced / Modular 三个阶段[6]上面这条链路属于 Advanced 级别——它的每个环节都可独立替换与优化。三、九个模块逐个拆解干什么、不做会怎样① 文档解析Loader作用把 PDF / Word / HTML / Markdown / 数据库里的非结构化内容变成带元数据的纯文本。不做会怎样PDF 双栏排版被读成左右两栏文字交错页眉页脚混进正文扫描件全是图片一个字都提取不出来。这是最容易被低估的脏活——它不产生任何可见的技术含量但它决定了后面所有环节的输入质量上限。关键决策优先选结构化源数据库 Markdown Word PDF 扫描件 PDF表格是最大的坑。表格直接转文本会变成一堆对不上号的散字行级展开把每行拼成一句自包含的自然语言如产品A单价199元库存30件是性价比最高的处理法扫描件必须上 OCR并且要接受 OCR 有误这一事实在下游设计容错。② 切片Chunking—— RAG 的第一杀手作用把长文档切成适合检索和塞进 prompt 的小段。为什么它是第一杀手这个环节同时卡着两个互相矛盾的目标——切得太小切得太大语义完整但噪声多信息集中但语义被切断单块信息量不足答不全无关内容稀释相关性检索分数被拉低召回多但质量低prompt 塞不下超出 embedding 模型的最佳输入长度关键决策按结构切优先于按字数切先按标题层级、段落、句号切实在切不动再按字数硬切重叠overlap不是可选项每块开头回带上一块末尾的 1~2 个完整句子能显著缓解答案恰好落在切分边界的问题。注意要按整句回带不要按字数回带——按字数回带会切出天。如前所述这类语义碎片它们本身无法独立读懂反而污染检索第四节的代码里特意避开了这个坑每块要能独立读懂块里不能只有见上表如前所述——必要时把标题、表头回填进每个块。这一条的收益经常被低估块大小没有银弹我们的起点是中文 300~500 字然后按召回结果的实际情况调。③ 元数据Metadata作用给每个 chunk 挂上标题、章节、页码、生效时间、来源 URL、权限标签等结构化信息。为什么值得单独当一节它是成本最低、收益最高的优化手段——不需要换模型、不需要改算法只在入库时多存几个字段。它的两个核心用途检索前过滤只查2024 年之后的研发部可见的文档把检索空间砍掉一大半准确率和性能同时提升检索后展示答案末尾给出来源《员工手册》第 3 章2025 版用户能一键点进去核对。这里有个安全红线权限过滤必须发生在检索层查询条件里带权限标签不能靠在 prompt 里写不要泄露你没有权限的内容——那是许愿不是访问控制。④ 向量化Embedding作用把文本映射成高维向量让语义相近变成空间里距离近。关键决策中文场景优先选中文榜单靠前的模型BGE 系列等不要直接用只训过英文的通用模型。选型的经验标准是看 MTEB / C-MTEB 这类公开榜单的中文子集而不是看模型名气检索任务和语义相似度任务是两回事要选标注为 retrieval 的模型query 和 doc 的处理要区分BGE 这类模型官方建议给 query 加指令前缀如为这个句子生成表示以用于检索相关文章文档侧不加。忽略这个细节会白白损失几个点的召回率模型和索引绑定换 embedding 模型 全库重算重灌。所以这个选型要在项目早期定死中途换的代价是全量重建。⑤ Query 改写作用把用户的大白话变成更适合检索的查询。为什么需要用户问的和文档写的是两套语言。用户问能不能提前走文档写的是离职申请流程与审批权限。直接拿原句去检索语义匹配不上就召不回。三种主流手段按成本递增指代消解多轮对话里那病假呢必须还原成病假如何申请才能检索多查询扩展把一个问题改写成 3~5 个不同角度的查询分别检索后合并用召回率的提升换一点成本HyDE[2]让模型先编一个假答案再用这个假答案去检索——假答案虽然内容可能不准但它的用词和表述方式与真实文档更接近检索效果往往比原问题更好。这个思路很反直觉但确实有效。⑥ 检索Retrieval作用从海量 chunk 里粗筛出几十条候选。三条路线及其分工路线原理强项弱项关键词BM25词频 逆文档频率精确词、专有名词、型号、报错码同义词、口语化表达向量稠密检索语义相似度同义表达、模糊描述精确词、罕见专有名词混合检索两路都查再融合覆盖率最高实现复杂一点实践结论直接用混合检索。纯向量检索在真实业务里几乎总会漏掉一些看着就应该命中的精确词而这类失败用户最容易感知到。融合算法用RRFReciprocal Rank Fusion就够了它只依赖排名不依赖分数不需要归一化两路的可比性$$score(d) \sum_{i} \frac{1}{k rank_i(d)} \quad (k \approx 60)$$⑦ 重排Rerank作用对粗排结果做精排把 Top-50 收敛成 Top-3~5。为什么必须有这一步粗排用的是双塔模型query 和 doc 各自独立编码成向量再比距离速度快但精度有限重排用的是交叉编码器把 query 和 doc 拼在一起过一遍模型能真正读到两者的交互精度高得多代价是慢——所以只适合作用在小候选集上。交叉编码器之外ColBERT 的延迟交互是另一条折中路线精度接近交叉编码速度却快得多[8]。这也是目前投入产出比最高的一个模块加一个开源 rerank 模型如 bge-reranker通常能带来肉眼可见的准确率提升而改造成本只是多一次模型调用。什么时候可以不上召回量本身很小知识库只有几十条或者对延迟极度敏感的场景。⑧ 上下文组装作用把选中的 chunk 编排成喂给模型的 prompt。容易被忽略的三个细节去重多路检索和 overlap 都会带来重复内容重复的 chunk 会挤占宝贵的上下文窗口位置研究发现长上下文中模型对开头和结尾的内容利用得最好中间部分容易丢失[3]。所以不要把最相关的 chunk 放在中间——最相关的放首尾次相关的放中间加引用编号给每个 chunk 编号[1][2][3]并在 prompt 里要求模型在每个结论后标注出处。这既让答案可核也是一个隐性约束——模型知道每句话都要挂到某个编号上时会更少地自由发挥。⑨ 生成与溯源作用产出最终答案并让它可追溯。Prompt 里必须写死的三件事缺一件就会回到编得很好听的老路只允许依据给定资料回答每句结论标注引用编号资料不足时明确拒答——给出根据现有资料无法回答这个合法选项模型才不会硬凑。第三条最关键。我们上线前后对比过加了显式拒答约束后用户投诉答案看着对但查无此项的比例下降了一个数量级。给模型一条体面的退路比反复强调不要编造有效得多。四、跑一遍一份能直接运行的最小 RAG下面这份代码不依赖任何向量数据库用 numpy 手算余弦相似度装两个包就能跑通完整链路。真实项目里把第 4 步换成向量库、第 6 步换成你的大模型客户端即可。# pip install sentence-transformers numpy import re import numpy as np from sentence_transformers import SentenceTransformer ​ # ══════════════ 离线链 ══════════════ ​ RAW_DOC 第五条 年假员工累计工作已满1年不满10年的年休假5天 已满10年不满20年的年休假10天已满20年的年休假15天。 第六条 试用期试用期为3个月试用期员工不享受年休假。 第七条 病假员工请病假需提供二级及以上医院出具的诊断证明 病假期间按当地最低工资标准的80%发放病假工资。 第八条 加班调休工作日加班按1.5倍时薪计算休息日加班优先安排调休 调休须在加班发生后三个月内使用完毕逾期作废。 第九条 离职正式员工离职需提前30日书面通知试用期员工提前3日通知即可 离职前应完成工作交接与资产归还未完成的暂缓办理离职手续。 ​ def chunk(text: str, size: int 120, overlap_sents: int 1) - list[str]: 先按标点断句再合并成块——比粗暴按字数切更保语义完整 sents [s.strip() for s in re.split(r(?[。\n]), text) if s.strip()] # 兜底单句本身就超过 size 时按字数硬切否则这个块会无限长 units: list[str] [] for s in sents: while len(s) size: units.append(s[:size]) s s[size:] if s: units.append(s) chunks: list[str] [] cur: list[str] [] # 当前块由若干整句组成 for u in units: if sum(len(x) for x in cur) len(u) size: cur.append(u) else: if cur: chunks.append(.join(cur)) # 回带上一块末尾的整句按字数回带会切出天。如前所述这类碎片 cur cur[-overlap_sents:] if overlap_sents else [] cur.append(u) if cur: chunks.append(.join(cur)) return chunks ​ chunks chunk(RAW_DOC) print(f[离线] 切成 {len(chunks)} 块) for i, c in enumerate(chunks, 1): print(f [{i}] len{len(c):3} {c[:40]}...) ​ # Embedding中文检索场景用 BGE 系列 model SentenceTransformer(BAAI/bge-small-zh-v1.5) # BGE 官方建议检索场景下 query 侧加指令前缀doc 侧不加 QUERY_PREFIX 为这个句子生成表示以用于检索相关文章 ​ docs_vec model.encode(chunks, normalize_embeddingsTrue) # shape: (N, d) # 归一化之后向量内积 余弦相似度 ​ # ══════════════ 在线链 ══════════════ ​ REJECT_THRESHOLD 0.45 # 拒答阈值必须用自己的数据实测标定别抄这个值 ​ def retrieve(query: str, top_k: int 3): qv model.encode([QUERY_PREFIX query], normalize_embeddingsTrue) scores docs_vec qv.T # (N, 1) idx np.argsort(-scores.ravel())[:top_k] return [(chunks[i], float(scores[i, 0])) for i in idx] ​ def build_prompt(query: str, hits) - str: ctx \n.join(f[{i1}] {c} for i, (c, _) in enumerate(hits)) return f请只依据下面的资料回答问题并在每句结论后用 [编号] 标注出处。 如果资料中没有足够信息请直接回答根据现有资料无法回答不要推测、不要编造。 ​ 资料 {ctx} ​ 问题{query} ​ def answer(query: str) - str: hits retrieve(query) ​ # —— 排查纪律第一条先看召回再看生成 —— print(\n[在线] 召回结果) for i, (c, s) in enumerate(hits, 1): print(f [{i}] score{s:.3f} {c[:40]}...) ​ # 最高分都够不着阈值说明库里大概率没有直接拒答 if not hits or hits[0][1] REJECT_THRESHOLD: return 根据现有资料无法回答。 ​ prompt build_prompt(query, hits) # 换成你自己的大模型客户端即可 # resp client.chat.completions.create( # modelqwen-max, # messages[{role: user, content: prompt}], # ) # return resp.choices[0].message.content return prompt # 没有 API Key 时打印 prompt方便调试 ​ print(answer(试用期能不能休年假))这份代码里有两个刻意保留的实战痕迹REJECT_THRESHOLD带着一句警告相似度的绝对值没有跨数据集的通用阈值必须在你自己的文档上采样标定。这个值抄来的结果不是更准是该拒的没拒、该答的拒了召回结果强制打印这是我们团队那条排查纪律的代码化——出问题时第一件事是看这里打印了什么。五、RAG 的优势不只是减少幻觉大部分文章讲 RAG 优势只讲减少幻觉这只说了最小的那一层。真正让它在企业场景不可替代的是下面这五条1. 知识可以热更新且更新成本近乎为零改一份文档答案立刻跟着变不需要重新训练、不需要发版。对于制度、价格、库存这类高频变动的知识这个特性的价值远超准确率高几个点。2. 答案可溯源——这是企业场景的准入门槛每个答案能给出出自哪份文件第几节用户可以一键核对。在金融、医疗、法务这类领域不能溯源的答案等于不可用的答案无论它多准确。这是微调方案永远给不了的能力。3. 私有数据不必离开自己的域知识索引可以完全部署在内网只有问题和召回的片段会经过模型。相比把文档塞进外部大模型的做法RAG 让用上大模型和数据不出域不再互斥。4. 成本可控且有明确的优化杠杆不需要训练算力主要的成本是检索和 prompt。而且优化方向非常清晰——检索不准就改切片和检索生成不好就改 prompt 和模型两者解耦可以分别优化、分别衡量。5. 权限控制有真实的落点访问控制可以做在检索层查询条件带权限标签这是在执行层面真正生效的隔离而不是寄希望于模型懂事。需要泼的一盆冷水是上面每一条都有前提条件。知识热更新的前提是索引流水线可靠可溯源的前提是生成端真的老老实实标注且不作弊权限控制的前提是你真的把它写进了查询条件。RAG 给的是可能性不是保证。六、RAG 的不足这些坑踩过才知道这是全文最该认真读的一节。RAG 不是银弹它有明确的能力边界而且边界比大多数人以为的要窄。① 检索是硬天花板且它是乘法不是加法$$最终质量 \approx 检索质量 \times 生成质量$$检索召不回生成再强也是零——模型不会因为你 prompt 写得好就凭空知道库里的内容。更麻烦的是错误不会自己消失只会被包装得更好看召回了错误片段时模型会非常自信地基于错误证据给出一个逻辑严密的错答案比直接说不知道危险得多。一句话总结RAG 的质量上限由检索决定下限由生成决定。这也解释了为什么近年的改进大多集中在检索侧——比如 CRAG 引入一个轻量级检索评估器一旦发现召回质量不合格就触发纠正动作重写查询、补充网页检索等[7]。② 切片天然会割裂语义只要切了片就一定存在答案恰好横跨两个块的问题。overlap 能缓解但无法根除。表现是问一个需要跨段归纳的问题系统只答出了一半而且答得很笃定。③ 多跳和全局性问题天生不擅长「去年四个季度里哪个季度的客诉最多」——这个问题需要扫一遍全部相关数据再比较而检索只给你 Top-K 个片段。Top-K 检索假设答案藏在少数几个片段里这个假设对全局统计类问题根本不成立。这正是 GraphRAG[4]、RAPTOR 这类预先构建层级摘要方案出现的动机——用离线建索引时的额外成本换取全局问题的回答能力。④ 表格、图片、公式先天弱势向量化是为自然语言设计的。表格被展平成文本后行列关系大量丢失图片里的流程图、架构图基本无解数学公式的语义相似度几乎无意义。多模态 RAG 目前仍是个未解决的开放问题不要在方案里假装它已经成熟。⑤ 长上下文的中间丢失效应模型对长上下文的利用并不均匀——开头和结尾记得最牢中间部分容易忽略[3]。这意味着你辛辛苦苦召回并重排好的 10 个片段如果最相关的那个排在第 5 位它的实际影响力可能还不如排在第 10 位的。这也是为什么第 ⑧ 个模块里强调最相关的放首尾。⑥ 拒答是最难训的能力让模型承认我不知道极其困难——它被训练的目标就是给出下一个最可能的 token而根据现有资料无法回答在多数语境下并不是那个最可能的输出。即便 prompt 里写了模型仍经常绕着弯子给出推测性内容。这需要专门的约束设计、专门的测试用例以及接受一定的误拒率。⑦ 延迟和成本是叠加的一次问答的模型调用可能是改写 1 次 向量化 1 次 重排 1 次 生成 1 次再加检索本身。四个环节各自达标串起来仍然可能超时。每一个优化项都在和延迟做交易多查询扩展提升召回但 ×3 成本重排提升精度但增加一次模型调用。⑧ 实时是假的索引更新是有延迟的——文档改了要重新解析、切片、向量化、入库才能生效。RAG 的实时取决于你的索引流水线有多快不是天然实时的。对秒级一致性有要求的场景比如实时库存RAG 不是正确答案应该走接口查询。⑨ 评估困难且错因会互相掩盖答错了是检索没召回召回了但重排把它排下去了还是召回了正确的模型没照着说三种故障的表现完全一样但修法完全不同。这就是为什么必须把检索和生成分开评估RAGAS[5] 这类框架的 context_recall / faithfulness 分指标设计就是为此context_recall低说明检索有问题faithfulness低说明生成有问题。笼统地看准确率你连该改哪里都不知道。七、什么时候不该上 RAG基于上面的不足这份决策表比RAG 有什么优势更实用场景该不该上 RAG理由企业制度 / 产品手册问答✅ 非常适合知识稳定、需要溯源、更新频繁客服知识库✅ 适合同上且答案需要可核对实时库存 / 订单状态查询❌ 不该需要强一致走接口而不是检索数学计算 / 精确统计❌ 不该应走代码执行或 SQL检索解决不了精确性教模型改语气、改输出格式❌ 不该这是微调的活全局性统计汇总全年趋势⚠️ 谨慎Top-K 检索假设不成立需额外设计重度依赖表格 / 图片的文档⚠️ 谨慎需先解决多模态解析否则效果很有限知识库只有几十条⚠️ 未必可能直接全量塞进上下文更简单八、八条工程经验踩坑换来的先打印召回结果再调生成 prompt——离线链的问题占大多数调 prompt 是最低效的排查路径切片策略是第一个该调的旋钮不是最后一个。块大小、overlap、标题回填这三件事的收益通常大于换模型元数据过滤 检索算法优化。能用只查 2024 年后的研发部文档缩小检索空间就别指望语义模型从全库里捞对直接上混合检索BM25 向量 RRF纯向量在真实业务里几乎总会漏掉精确词重排是性价比最高的单点优化一个开源 rerank 模型的收益往往超过换更大的生成模型最相关的 chunk 放首尾别放中间prompt 里必须给我不知道这个选项否则模型一定会编检索和生成分开评估——准确率是一个平均数它会掩盖你真正该修的那个环节。九、参考文献只列真正查阅过、对本文观点有直接影响的资料Lewis P. et al.Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 2020. arXiv:2005.11401 —— RAG 范式奠基之作参数化/非参数化知识的切分即出自此文Gao L. et al.Precise Zero-Shot Dense Retrieval without Relevance LabelsHyDE. 2022. arXiv:2212.10496 —— 用生成假答案再检索提升召回的反直觉方法Liu N.F. et al.Lost in the Middle: How Language Models Use Long Contexts. 2023. arXiv:2307.03172 —— 上下文位置效应的实证依据本文第 ⑧ 模块与不足 ⑤Edge D. et al.From Local to Global: A Graph RAG Approach to Query-Focused Summarization. 2024. arXiv:2404.16130 —— 面向全局性/多跳问题的代表性方案本文不足 ③Es S. et al.RAGAS: Automated Evaluation of Retrieval Augmented Generation. 2023. Ragas —— 检索与生成分开评估的指标体系本文不足 ⑨Gao Y. et al.Retrieval-Augmented Generation for Large Language Models: A Survey. 2023. arXiv:2312.10997 —— Naive / Advanced / Modular 三阶段划分理解 RAG 演进路径的权威综述Yan S.-Q. et al.Corrective Retrieval Augmented GenerationCRAG. 2024. arXiv:2401.15884 —— 对检索结果做质量评估并触发纠正应对检索是硬天花板的思路Khattab O. Zaharia M.ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. SIGIR 2020 —— 重排环节延迟交互路线的代表工作十、写在最后回到开头那个试用期年假 80% 折算的事故。修好它之后客户反馈里最让我们意外的一条不是答案变准了而是现在它说根据现有资料无法回答的时候我反而更相信它的其他答案。这句话说出了 RAG 真正的价值——它给模型装上的不是更强的记忆力而是一个可核对、可追溯、也敢于承认无知的机制。所以如果你只带走一句话我希望是这句RAG 的上限由检索决定下限由生成决定而它的可信度由敢不敢说不知道决定。如果觉得本文有帮助欢迎点赞、收藏、评论三连。你的 RAG 系统踩过什么坑评论区见。本文首发于 CSDN作者原创。转载请注明出处。