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

Advanced-RAG生产落地:基于LlamaIndex的检索增强生成优化实战

Advanced-RAG生产落地:基于LlamaIndex的检索增强生成优化实战 ★ FEATURED ARTICLE
我见过太多团队在演示环境里把 RAG 跑得漂漂亮亮一上真实业务就原形毕露召回的东西沾边但不精准答案读起来通顺却答非所问用户追问两句就开始编。这类问题基本不是模型不够强而是还在用“切块加向量检索加拼提示词”这套最朴素的标准流程在扛复杂场景。Advanced-RAG高级检索增强生成要解决的就是这件事——把一条单薄的“检索到生成”直线改造成带查询理解、多路召回、重排压缩、自我校验的流水线。下面这套东西全部围绕 LlamaIndex 落地从索引期怎么切、查询期怎么改、后处理怎么压到评估怎么做都是可以直接抄去改的工程细节不管你是刚接触 RAG 的新手还是已经踩过一轮坑想系统性升级的老手应该都能拎走点东西。1. 标准 RAG 为什么在生产环境里频繁翻车1.1 先复盘一次真实的问答事故我接手过一个内部知识库问答的场景文档量不大两万多份制度、流程、操作手册切完大概十几万个块。第一版就是教科书式的做法按固定字数切块通用嵌入模型建向量索引用户提问向量化后取 Top5 拼进提示词。离线抽了五十条问题测试看着还行七成能答对团队就上线了。上线第一周投诉集中爆发。典型的一条是用户问“子公司报销审批权限是怎么规定的”系统答了一大段总部差旅标准。这条问题命中率掉下去的原因很具体文档里“总部”“分公司”“子公司”三套权限体系混在同一份制度里切块时正好把子公司的表格切成了两半向量检索抓到的是相邻那段总部内容因为那段文字里“报销”“审批”“权限”三个词密度更高。这件事让我彻底改变了对 RAG 的认知标准 RAG 的失败很少是模型不行通常是检索阶段就把正确的证据弄丢了后面生成模型再强也只是在错误材料上做漂亮的二次创作。而这个丢失动作在切分、召回、拼接、生成四个环节里都可能发生且每一环的失败看上去都像“模型胡说”极难定位。1.2 四个断点切分、召回、拼接、生成把标准 RAG 拉直看它其实只有四个动作每个动作都埋着断点。切分断点固定字数切块是最省事也最粗暴的做法。它会把一个完整定义从中间劈开把表格的行列关系打散把“以上情况除外”这种关键否定句甩到另一个块里。更麻烦的是切块大小本身是个矛盾体——块小则语义聚焦、召回精准但上下文不够模型拿不到完整条件块大则上下文完整但向量被稀释一个 2000 字的块里塞了五个主题它的向量方向必然是模糊的召回的准确性直线下降。召回断点单一向量召回天然不擅长处理三类查询。第一类是精确匹配类比如查某个编号、某个专有名词、某串代码向量相似度对这种“字面必须一致”的需求几乎没有分辨力。第二类是多跳推理类“A 部门的上级单位负责的 B 业务对应的 C 流程是什么”需要串三条信息单次召回只能拿到其中一条。第三类是模糊口语类用户说“那个报销的东西怎么弄”向量化之后得到的是一个噪声很大的方向召回结果全靠运气。拼接断点TopK 拼上下文是另一个隐形杀手。取 5 块看着不多但如果每块 800 token加上系统提示、对话历史、输出预留很容易顶到上下文窗口边缘。而长上下文并不等于有效上下文中间位置的信息被模型忽略是普遍现象业内常说的“中间遗忘”就是这个意思。拼得越多噪声越大模型越容易抓错重点。生成断点模型拿到材料后没有任何校验机制。它不会告诉你“这几块材料互相矛盾”也不会说“材料里没写这个我不敢答”。它倾向于把材料里所有沾边的信息揉成一段看起来很专业的回答而这正是幻觉最典型的来源——不是凭空编是把不相关的真实材料误当成答案。1.3 Advanced-RAG 的“高级”具体高在哪三层市面上讲 Advanced-RAG 的文章喜欢罗列十几个技术名词看完还是不知道怎么下手。我的理解是它的“高级”实际上落在三个层次上每个层次解决一类断点。第一层是索引侧的结构化。不再是“切块加向量”一条路走到底而是建立多种粒度的节点层级小块用于精准召回大块用于提供上下文、补充元数据用于过滤、必要时叠加关键词索引和摘要索引。这一层解决的是切分断点和部分召回断点。第二层是查询侧的主动加工。用户的原始问题不直接拿去检索而是先经过改写、扩展、拆解、路由。多跳问题拆成子问题分别检索模糊问题用假设性答案去检索跟知识库无关的问题直接走闲聊通道不查库。这一层解决的是召回断点。第三层是后处理与生成侧的质量控制。召回结果先重排再截断长块压缩成相关片段生成后再做一次忠实度校验不通过就重试或者拒答。这一层解决的是拼接断点和生成断点。三层叠起来整条链路的工程复杂度大概是从“两百行代码”变成“两千行代码加上一堆参数”但换来的是从“演示能用”到“生产可用”的质变。这个投入产出比我在三个项目上验证过值。2. 整体架构把 RAG 拆成一条能观测的流水线2.1 索引期与查询期的职责划分我很反感把所有逻辑都塞进一个“查询引擎”对象里的写法出问题根本没法定位。我习惯把整条链路明确切成两段索引期和查询期各自独立可测。索引期负责的事相对静态文档解析把 PDF、Word、网页统一成纯文本加结构信息、清洗去页眉页脚、去重复段落、修表格、分块多粒度、补充元数据、写入向量库和文档存储。这一段跑一次可以用很久所以值得做得慢、做得细。我的习惯是把索引期做成一个可重跑的离线流水线每份文档入库时带上来源、版本、生效日期、所属业务线这些元数据后续更新时按来源覆盖不做增量追加避免同一份文档出现多个版本互相打架。查询期是热路径每来一个问题都要跑一遍所以要快要稳。它由六个可选阶段串成查询理解改写、扩展、路由、召回向量、关键词、多路、融合RRF 或加权、重排交叉编码器、压缩窗口扩展或摘要、生成与校验。每个阶段都可以单独关掉做对照实验这一点非常关键——没有开关的流水线你永远不知道提升来自哪一步。2.2 三个必须一开始就定死的参数有三个参数如果不在项目初期想清楚后面会反复返工。第一是分块粒度。我的经验值是这样以中文为例正文类文档用 256 到 512 字符作为基础块注意是字符不是 token中文一个字大约 0.6 到 0.7 个 token制度、合同这类条款型文档可以小到 200 字技术手册、教程这类需要连贯上下文的可以到 600 字。同时配一个 3 到 5 倍的父块做层级小块负责召回父块负责提供上下文。这个数字不是拍脑袋是权衡出来的块太小150 字单块信息量不足模型拿到手也不知道在讲什么块太大800 字嵌入向量被主题稀释检索精度会明显下滑。第二是召回数量 TopK。这里有个常见误区觉得 TopK 越大召回率越高越好。实际上 TopK 的合理值取决于后面有没有重排。没有重排时TopK 直接进上下文我一般取 3 到 5有重排时粗召回可以放宽到 20 到 50重排后取 4 到 8。这么设计的原因是粗召回阶段追求召回率别漏重排阶段追求准确率别错两个阶段的优化目标本来就不一样用同一个 K 值就是让它们互相拖累。第三是上下文预算。先算清楚你用的模型上下文窗口多大然后按这个公式分配可用上下文 窗口 - 系统提示 - 对话历史 - 输出预留 - 安全余量。举例窗口 32K系统提示 800 token历史保留 3 轮约 1500 token输出预留 2000 token安全余量留 1000 token剩下约 26K 给检索内容。听起来很宽裕但我实测下来超过 8K 的检索上下文模型对后半部分的利用率就明显下降了。所以我实际会把检索内容控制在 3K 到 6K token宁可取少而精的块也不取多而杂的块。参数保守取值激进取值调整信号基础块大小256 字符512 字符答案缺条件就调大召回不准就调小粗召回 TopK1550重排后仍缺证据就调大精排后返回48答案拼接不全就调大检索上下文3K token6K token出现中间遗忘就调小2.3 为什么这套骨架我选 LlamaIndex 来搭框架选型这事没什么绝对对错关键看你的需求重心。我选 LlamaIndex 的原因很实在它的抽象层次刚好卡在“不用自己写但也没被锁死”的位置。一方面它把节点解析、索引结构、检索器、后处理器、响应合成器拆成了独立可组合的组件。你可以只用一个 SentenceSplitter也可以整套层级解析配自动合并检索可以只用一个向量检索器也可以把向量检索器和关键词检索器塞进融合检索器里重排环节换模型只需要替换一个后处理器对象不用动主流程。这种粒度对于要做大量对照实验的场景非常友好。另一方面它的评估模块是很多同类框架没有的。检索评估器可以直接算 MRR、Hit Rate、Recall生成评估器可以算忠实度和相关性而且能跟前面的检索器对象直接对接。这意味着你可以把“改一个参数、跑一遍评测、看指标变化”做成一个闭环而不是靠人肉看几条样例拍板。它的短板也要说清楚版本迭代快API 时有变动跨版本升级经常要改代码抽象层多出问题时调用栈很深日志不够细的话排查很痛苦。我的应对办法是给每个关键阶段都套一层自己的包装函数进出都打结构化日志问题文本、改写结果、召回 ID 列表、重排分数、最终上下文长度、耗时这样出问题一眼能看出是哪一层。3. 索引层实操分块、元数据与索引结构3.1 分块的分界线应该是语义而不是字数固定字数切块的唯一优点是简单缺点是它完全不理解文档结构。我的做法是分层处理先按文档的天然结构切标题层级、段落、表格结构切完再看长度太长的再按句子边界二次切分。LlamaIndex 的 SentenceSplitter 已经能做到按句子边界切、保留段落重叠比裸切字符好很多。但真正解决前面那个“子公司权限”问题的是层级解析加自动合并。思路是这样先用层级解析器把文档切成大中小三层最小层比如 256 字进向量索引用于精准召回中大层存在文档存储里检索时先命中最小层然后检查它所在的中层有没有足够多的兄弟节点也被命中如果有就把整个中层替换上来。这个机制的价值在于它是按需扩展上下文大部分问题命中的单块就够了不需要扩展只有当多个相邻小块同时被命中说明这一整段都相关时才合并成父块。这样既保证了召回精度又避免了小块上下文不足的问题还顺带减少了上下文总长度。from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes from llama_index.core.retrievers import AutoMergingRetriever from llama_index.core.storage.docstore import SimpleDocumentStore # 三层粒度2048 / 512 / 128 字符 parser HierarchicalNodeParser.from_defaults(chunk_sizes[2048, 512, 128]) nodes parser.get_nodes_from_documents(documents) leaf_nodes get_leaf_nodes(nodes) docstore SimpleDocumentStore() docstore.add_documents(nodes) base_retriever index.as_retriever(similarity_top_k12) retriever AutoMergingRetriever( base_retriever, docstore, simple_ratio_thresh0.4, # 兄弟节点命中比例超过 40% 就合并 )simple_ratio_thresh这个参数值得多试几个值。设太低0.2会导致动不动就合并成大块上下文迅速膨胀设太高0.8等于几乎不合并层级结构白建。我一般在 0.3 到 0.5 之间扫一遍看评测集上答案完整度和上下文长度两条曲线的交叉点。还有一个细节经常被忽略重叠窗口。相邻块之间留 10% 到 15% 的重叠可以缓解“关键句正好被切断”的问题。但重叠不是越多越好重叠 50% 等于索引体积翻倍、召回结果大量重复反而拉低多样性。我固定用 12% 左右实测比较平衡。表格类内容我建议单独处理——先把表格转成“行加列头”的结构化文本再切否则跨页表格切完基本报废。3.2 元数据不是装饰是过滤和重排的依据很多人建索引时只往节点里塞文本元数据随手带个文件名就完事。这在文档量小的时候看不出问题一旦上百个来源混在一起检索质量会莫名其妙地差。原因是向量检索是在全部节点里做近邻搜索来源混杂意味着语义空间被多个领域的表达习惯污染。元数据的第一个用途是硬过滤。比如用户明确问“今年的政策”那就把生效日期早于今年的节点直接排除在检索之外这一步能把候选集缩小一个数量级同时消除大量语义相近但时效错误的干扰。再比如多租户场景租户 ID 必须作为过滤条件这个不是优化项而是安全项。元数据的第二个用途是软加权。检索出来的候选可以按来源权威度、更新时间新鲜度做二次打分比如官方文档权重 1.0、历史归档 0.6、外部转载 0.3。这个加权不需要多复杂一个线性组合就能显著改善排序。我的元数据字段清单基本固定为这几项文档 ID、文档标题、上级标题路径这个特别有用因为标题路径本身就是极好的上下文补充、来源类型、业务线、生效日期、版本号、段落序号。标题路径我还会拼到块文本前面一起做嵌入比如把“第三章 财务管理 3.2 报销审批 3.2.1 子公司权限”作为前缀这样切碎的小块也自带层级语义召回准确率提升很明显。for node in nodes: node.metadata { doc_id: doc_id, title_path: 财务管理 报销审批 子公司权限, source_type: official, effective_date: 2025-01-01, version: v3, seq: idx, } # 标题路径注入正文增强小块语义 node.text f{node.metadata[title_path]}\n{node.text}注意元数据字段不要太多太散超过 10 个字段后维护成本会快速上升而且过滤条件写错导致召回为空的排查成本极高。我一般控制在 6 到 8 个高频字段其余信息放进文档存储按需查。3.3 四种索引结构各自的适用边界不是所有场景都需要向量索引也不是所有场景只需要向量索引。我梳理过四种常用结构各有明确边界。向量索引是默认选项适合语义模糊、表达多样的问题比如“怎么申请”“有什么要求”。它的软肋是精确匹配编号、人名、专有代码这类查询它经常抓瞎。**关键词索引BM25 一类**正好补上这块。中文要先分词分词质量直接决定效果业务术语多的场景必须加载自定义词典否则“审批流”会被切成“审批”加“流”召回全乱。关键词索引的优势在于字面精确和可解释劣势是不知道同义表达。摘要索引适合“整体性”问题比如“这份报告讲了什么”“这个项目的目标是什么”。做法是给每篇文档或每个章节生成一段摘要单独建索引检索时先命中摘要再顺着摘要找到原文。这类问题用普通块级检索效果极差因为答案需要跨全篇概括任何单块都不包含答案。属性图索引适合多跳关系查询比如“A 产品用的是哪个供应商的哪个型号这个型号还供货给哪些产品”。这类问题本质是图遍历向量检索只能碰到其中一段。建图的方式是用模型从文本里抽取实体和关系落到图结构上。代价是建索引成本高要跑一遍全量抽取、更新麻烦所以我只在明确有多跳需求的场景才上。索引类型擅长不擅长建索引成本向量索引语义相近、口语化提问编号、专名精确匹配低关键词索引字面精确、术语检索同义表达、跨语言低摘要索引全局概括类问题具体细节查询中属性图索引多跳关系推理单点事实查询高实际项目里我通常至少上向量加关键词双路这已经能解决八成问题摘要索引按需加图索引只在关系密集的领域才考虑。4. 查询侧改造让用户那句烂问题变得可检索4.1 查询改写与扩展HyDE 和多查询的取舍用户的提问和文档的表述之间存在天然的语体鸿沟。用户问“报销多久到账”文档里写的是“财务审核通过后 T3 个工作日内完成付款”。这两句话字面上几乎没有重叠词向量相似度自然不高。解法之一是指令改写。让一个模型把口语问题改写成更接近文档语体的查询比如把“报销多久到账”改写成“报销款项到账时间 财务审核 付款周期 T3”这个改写动作会显著提升召回率。改写的提示词要写得具体保留原问题的所有约束条件时间、部门、类型补充可能出现在文档里的同义术语不要回答问题本身只做表述转换。这条约束很关键不加的话模型会直接开始作答输出一段答案当查询用反而跑偏。解法之二是 HyDE假设性文档嵌入思路反着来先让模型凭空写一段“假想的答案”再用这段答案去检索。因为假答案的语体、用词、句式都更接近真实文档检索效果往往比原问题好。它的风险也很明确——假答案可能把方向带偏尤其是模型对领域不熟的时候假答案里塞了一堆错误概念检索出来的全是相关但错误的材料。我的用法是只在问题很短、很口语、且领域模型比较有把握的场景开 HyDE并且把假答案和原问题一起做检索取并集再重排而不是完全替换。解法之三是多查询扩展。让模型把一个问题改写成三到五个不同角度的查询分别检索后合并。这个办法对付“表达方式不确定”的场景特别有效代价是检索次数翻倍延迟上升。我的经验是把多查询限制在 3 个并且用并行检索延迟增加可以控制在 30% 以内。from llama_index.core.indices.query.query_transform import HyDEQueryTransform from llama_index.core.query_engine import TransformQueryEngine hyde HyDEQueryTransform(include_originalTrue, llmfast_llm) hyde_engine TransformQueryEngine(base_query_engine, query_transformhyde)include_originalTrue这个开关我基本必开就是上面说的“别让假答案独占方向”。4.2 查询路由先判断该不该查库我见过不少系统把所有输入都往检索链路里灌用户说“你好”也要跑一遍向量检索白白增加延迟还可能因为检索到无关内容而让模型答出莫名其妙的东西。查询路由就是在链路最前面加一个轻量判断。判断维度大致三类。第一类是是否需要检索闲聊、寒暄、对上一轮答案的确认“好的”“明白了”这类直接走生成不查库。第二类是该查哪个库多业务线场景下制度库、产品库、工单库分开建索引用一个分类器或者小模型判断问题属于哪个域只查对应库能大幅降低干扰。第三类是该用哪种检索策略事实型问题走向量加关键词概括型问题走摘要索引多跳问题走子问题拆解。路由的实现有轻有重。轻的做法是用规则加关键词匹配快但覆盖窄重的做法是用一个小模型做分类准但多一次调用。我一般用二次判断先用规则过一遍明显的长度小于 6 个字且无实词、命中寒暄词表剩下的交给模型分类。分类的选项不要太多控制在 5 个以内选项越多准确率掉得越快。LlamaIndex 的 RouterQueryEngine 提供了现成的封装from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector from llama_index.core.tools import QueryEngineTool, ToolMetadata tools [ QueryEngineTool(query_enginepolicy_engine, metadataToolMetadata(namepolicy, description适用于制度、流程、审批权限、报销规则类问题)), QueryEngineTool(query_engineproduct_engine, metadataToolMetadata(nameproduct, description适用于产品参数、功能说明、版本差异类问题)), ] router RouterQueryEngine( selectorLLMSingleSelector.from_defaults(llmfast_llm), query_engine_toolstools, )这里有个坑要提醒工具的description写得好不好直接决定路由准确率。别写“制度文档库”这种空泛描述要写成“适用于什么问题”的形式并且把容易混淆的边界说清楚比如“不适用于产品功能咨询”。我第一版就是因为两个工具的 description 都太笼统路由准确率只有六成多改成分场景描述之后上到九成。4.3 多路召回与 RRF 融合的实操多路召回的核心问题不是“怎么召回”而是“多路结果怎么合并”。直接拼接会让某一路的结果挤占全部名额加权求和又需要调权重且量纲不一致向量相似度是 0 到 1 的余弦值BM25 是无上界的实数。RRF倒数排名融合是我最常用的方案因为它只用排名不用分数天然规避了量纲问题。公式很朴素每个文档的最终得分等于它在各路结果中排名的倒数之和通常写成 1/(k rank)k 取 60 左右。这个 k 的作用是压平头部差异——k 越大第一名和第十名的分数差越小融合越“平权”k 越小头部结果的话语权越大。from llama_index.core.retrievers import QueryFusionRetriever from llama_index.core.retrievers import VectorIndexRetriever, BM25Retriever from llama_index.core.retrievers.query_fusion_retriever import FUSION_MODES fusion_retriever QueryFusionRetriever( [ VectorIndexRetriever(indexindex, similarity_top_k20), BM25Retriever.from_defaults(docstoredocstore, similarity_top_k20), ], similarity_top_k15, num_queries3, # 原问题 2 个改写变体 modeFUSION_MODES.RECIPROCAL_RANK, use_asyncTrue, )参数怎么定我的实测结论是这样num_queries取 1 到 3 收益最明显超过 3 之后边际收益快速衰减延迟却线性上涨融合后的similarity_top_k要留够给重排一般 12 到 20mode除了 RRF 还有相对分数融合但需要两路分数做过归一化工程上麻烦除非你发现 RRF 明显不够用否则没必要换。还有一个容易被忽略的细节关键词检索这一路在中文场景必须处理分词。默认分词器对业务术语的切分经常出错我一般会维护一份业务词典产品名、系统名、内部黑话加载进分词器。这一步投入大概半天收益能抵得上后面调一周参数。5. 后处理与生成重排、压缩、自反思三板斧5.1 重排层的成本账与阈值粗召回的目的是别漏精排的目的是别错。这两件事由不同模型干最合适向量检索用的是双塔结构问题和文档分别编码再算相似度速度快但精度有限重排用的是交叉编码器问题和文档拼在一起过一个模型精度高但必须逐对计算没法预计算。成本账要算清楚。假设粗召回 30 个候选每对输入平均 400 token重排模型跑一次前向大约需要处理 12000 token 的输入。用一个参数量在亿级的小重排模型单次重排在我的测试环境里大约 100 到 300 毫秒取决于硬件和批处理这个开销在对话场景是可接受的但如果你的 QPS 要求很高就得考虑批处理或者把候选数压到 15 以内。候选数和重排收益的关系大致是这样从 5 个候选提升到 20 个重排带来的准确率提升最明显从 20 到 50提升幅度收窄超过 50 基本是浪费算力。所以我固定用 20 到 30 的粗召回重排后取 4 到 8 进上下文。阈值策略也值得说。重排分数是可以拿来做拒答判断的如果重排后最高分低于某个阈值说明知识库里根本没有相关内容这时候与其硬答不如直说“我在现有资料里没有找到依据”。这个阈值不能拍脑袋定做法是拿一批已知无答案的问题跑一遍看它们的最高分分布取一个能覆盖 90% 无答案问题的分位点。我做过这个实验加阈值拒答之后用户对“编造”的投诉下降了大概六成。from llama_index.core.postprocessor import SimilarityPostprocessor postprocessors [ reranker, # 交叉编码器重排取 top_n6 SimilarityPostprocessor(similarity_cutoff0.35), # 低于阈值直接丢弃 ]注意similarity_cutoff的取值强依赖于你用的重排模型和它的分数分布换模型必须重新标定。我见过直接把别人博客里的 0.7 抄过来的结果所有查询都被过滤掉了因为那个模型的分数分布根本不在这个区间。5.2 上下文压缩句子窗口与自动合并上下文压缩解决的是“块里只有一句话相关但整块都得塞进去”的浪费。两种主流做法我都在用。句子窗口的思路是索引用单句或很小的块建检索命中后不返回这一句而是返回它前后各若干句组成的一个窗口。这样索引端保持了极高的精准度单句的向量很聚焦生成端又能拿到完整上下文。窗口大小一般取前后各 3 到 5 句具体看文档的句子密度。这个方案特别适合问答型内容密集的文档比如 FAQ、条款、参数表。元数据替换是它的实现方式LlamaIndex 里有现成的节点解析器和后处理器from llama_index.core.node_parser import SentenceWindowNodeParser from llama_index.core.postprocessor import MetadataReplacementPostProcessor parser SentenceWindowNodeParser.from_defaults( window_size4, window_metadata_keywindow, original_text_metadata_keyoriginal_text, ) pp MetadataReplacementPostProcessor(target_metadata_keywindow)摘要压缩是另一种思路对召回的每个块让模型只抽取与问题相关的句子把无关部分删掉。优点是压缩比高缺点是多一次模型调用增加延迟而且抽取过程本身可能丢信息。我一般在上下文预算特别紧张比如窗口只有 8K时才用并且用很小的模型来做提示词写成“只保留与问题直接相关的原文句子不做改写”。这两种方案有个共同的前提重排必须在前。先重排把候选压到 6 个以内再做压缩成本才可控。顺序反过来的话你要对 30 个块做压缩延迟直接爆炸。5.3 自反思与 Agent 化什么时候值得多跑一轮前面所有优化都是“一次检索一次生成”框架内的。有些问题一次检索确实拿不到答案比如多跳问题、需要对比的问题、需要计算的场景。这时候就要引入迭代。最轻量的迭代是答案自检生成完答案后让模型做一次判断——答案里的每个论断是否都能在给定材料里找到支撑如果有论断找不到支撑就标记为可能幻觉触发重试或者降级为“部分回答加说明”。这个检查用一个小模型就能做成本很低我基本所有项目都会开。稍重一点的是检索自检先让模型判断“当前召回的材料是否足以回答问题”不足则触发第二轮检索第二轮用改写后的查询。这个机制对多跳问题有效但要注意设置最大轮数我一般设 2 轮否则会陷入“总觉得材料不够”的死循环。最重的是Agent 化把检索、计算、查表、调接口都封装成工具让模型自己规划调用顺序。这个方案的上限很高能处理很复杂的任务但代价是延迟不可控可能调五次工具、成本不可控、行为不可预测。我的态度很明确只有当单轮加最多两轮迭代的准确率明显不够比如低于 70%且业务能接受秒级以上的延迟才值得上 Agent 化。否则就是拿稳定性和成本换一个用不上的能力。子问题拆解是个折中方案介于两者之间把复杂问题一次性拆成几个子问题每个子问题独立走检索加生成最后汇总。它比 Agent 可控拆解一次就定了不会动态反复又比单轮强能覆盖多跳。from llama_index.core.query_engine import SubQuestionQueryEngine sub_engine SubQuestionQueryEngine.from_defaults( query_engine_toolstools, llmstrong_llm, use_asyncTrue, )use_asyncTrue一定要开子问题是天然并行的串行跑延迟会翻好几倍。6. 评估与调优把玄学变成可复现的实验6.1 三层指标检索层、生成层、端到端调 RAG 最怕的就是“感觉好像好了一点”。必须分层量化因为不同层的指标指向不同的修改动作。检索层指标回答“正确的材料有没有被找到”。核心三个Hit Ratek前 k 个结果里有没有包含正确答案所在块、MRRk正确块的排名倒数的平均值衡量排得靠不靠前、Recallk所有相关块被召回的比例。这三个指标只看检索器跟生成模型无关所以最适合做参数扫描——改分块大小、改 TopK、改融合策略都看这三个。生成层指标回答“给的答案对不对、有没有编”。核心两个忠实度答案的每个论断能否在上下文中找到依据和相关性答案是否切题。这两个指标要用模型来做裁判成本不低所以只在检索层指标稳定之后再做。端到端指标回答“用户满不满意”。这个只能靠人工抽样加线上行为数据追问率、点赞率、转人工率。我一般每周抽 50 条真实问题人工标注跟模型指标对照如果模型指标涨了但人工标注没涨说明评测集和真实分布脱节了。LlamaIndex 的检索评估器用起来很直接from llama_index.core.evaluation import ( RetrieverEvaluator, generate_question_context_pairs ) # 用模型从语料反向生成问题-答案块对 qa_dataset generate_question_context_pairs( nodes, llmjudge_llm, num_questions_per_chunk2 ) retriever index.as_retriever(similarity_top_k6) evaluator RetrieverEvaluator.from_metric_names( [mrr, hit_rate, recall], retrieverretriever ) results await evaluator.aevaluate_dataset(qa_dataset)这个自动生成的评测集有个明显缺陷它是从块反推问题问题天然会带块里的措辞比真实用户提问“干净”得多所以指标会虚高。我的用法是把它当基线只用来横向对比不同配置的相对优劣不看绝对值。真实效果还是得靠人工标注的那批“脏”问题。6.2 评测集要“脏”才管用我见过太多团队的评测集是让模型从文档里生成问题然后拿这批问题测自己的系统测出来九十多的准确率上线依然被骂。原因很简单生成的问题和文档表述高度一致天然容易召回而用户从来不这么提问。我的评测集构建原则是“三脏”。第一脏是表述脏。真实问题充满口语、省略、错别字、指代不明。比如“上回说的那个流程走哪”“报销要多久急”“这个能不能通融一下”。这类问题必须占评测集的三成以上否则测不出查询改写和路由的价值。第二脏是答案脏。真实问题的答案有时分布在多个文档、多个章节需要拼接有时文档里根本没有明确答案需要系统拒答。我的评测集里会刻意放 10% 到 15% 的“无答案”问题专门测拒答能力。不加这类问题系统会越来越倾向于硬答。第三脏是边界脏。放出容易混淆的近似问题比如“子公司的报销权限”和“分公司的报销权限”看系统能不能分清。这类问题最能暴露元数据过滤和重排的短板。评测集规模不用很大我一般维护 200 到 300 条分成开发集150 条随便用来调参和保留集100 条只在版本发布前跑防止过拟合。这个规模下每次改动的效果差异能看出来又不会标注成本过高。6.3 线上可观测性和迭代节奏离线评测过了不代表线上没问题因为线上问题的分布会漂移。所以我要求每个 RAG 系统都必须记录这几样东西原始问题、改写后的查询、路由决策、召回的所有文档 ID 和分数、重排后的分数、最终进上下文的块 ID、生成答案、耗时拆分各阶段分别多少毫秒。这些日志的价值在出问题时立刻体现。用户投诉“答错了”你打开日志一看是改写把方向带偏了还是召回根本没拿到对的块还是拿到了但重排把它排到了第 7 位被截断还是都对了但模型没用好。四种情况的修复动作完全不同没有日志就只能靠猜。迭代节奏上我的习惯是两周一个小版本。每次只改一到两个变量比如这周只调分块大小下周只调重排候选数改完跑开发集指标有提升再跑保留集都过了才上线上线后盯一周的追问率。这个节奏慢但能保证每次改动都是可归因的。我踩过最大的坑就是一次性改了分块、召回、重排三处指标涨了 8 个点但完全不知道是哪一处带来的后面想回退优化都没法做。7. 常见故障排查实录7.1 十类高频症状的定位路径下面这张表是我这几年攒下来的排查速查表症状和病因的对应关系基本能覆盖日常遇到的大部分情况。症状最可能的病因优先验证动作答得很流畅但答非所问查询改写跑偏或路由选错库看改写后的查询文本、看路由决策日志答案只答一半、缺条件分块过小或父块未合并检查命中的块边界看是否切断了条件句精确编号/术语查不到缺关键词召回通道临时关掉向量路只走 BM25 对比相似问题答案混淆元数据未做硬过滤检查是否有可用的业务线/日期过滤字段上下文很长但答案很浅中间遗忘或噪声块过多把上下文从 8 块压到 4 块再看无答案问题强行编造缺拒答阈值看重排最高分分布标定 cutoff多跳问题只答一环未做子问题拆解加 SubQuestion 拆解做对照首字延迟高多查询串行或重排候选过多看耗时拆分把串行改并行相同问题两次结果不同生成温度过高或检索非确定温度设 0检查索引是否有多版本新文档入库后旧问题变差索引重复追加未覆盖检查同 doc_id 的节点数量用这张表的时候有个诀窍先看检索层日志再怀疑模型。我统计过自己排查过的案例八成以上的“模型答错”实际是检索阶段就没拿到正确材料剩下两成里还有一半是材料拿到了但被截断或排序靠后。真正属于模型能力不足的不足一成。7.2 几个只有踩过才知道的坑坑一重叠窗口造成的重复污染。我把重叠从 10% 提到 25% 试图解决切断问题结果发现召回结果里出现大量内容几乎相同的块重排后前 5 个里有 3 个是同义重复实际有效信息只有一个块。上下文被浪费了大半。后来改成重叠 12% 加去重按内容相似度做一次过滤相似度超过 0.9 的只留一个效果才好起来。坑二元数据过滤把召回干到零。有次上线后发现某些业务线的问题全部答“没找到”排查半天发现是新加的租户过滤条件在这些历史文档上字段为空等于永远不匹配。教训是任何硬过滤字段都必须有“空值放行”的兜底逻辑或者上线前跑一遍字段覆盖率统计。坑三自动合并把上下文撑爆。层级检索的合并比例设成了 0.3结果某个主题的文档块特别密集每次检索都触发大范围合并单次上下文冲到 20K延迟和成本一起飙升答案质量还下降了。后来把比例调到 0.45并且加了一个“单次合并后总长度不超过 6K”的硬上限才稳定下来。坑四重排模型的长度限制被忽略。用的重排模型最大输入 512 token但我的块有 800 字符实际超出后被静默截断导致后半段的关键信息在打分时根本没被看到。这个 bug 极其隐蔽因为不报错。后来在重排前加了长度检查超长的块先按句子边界截到上限以内或者改用支持更长输入的模型。坑五评测集泄漏进开发流程。有段时间我调参太频繁一天跑十几次保留集结果参数实际上是在保留集上过拟合的上线效果跟评测差距很大。后来强制规定保留集每月最多跑两次并且用一个脚本锁住跑一次记一次避免自己骗自己。坑六中文分词的词典更新没同步。业务方新增了一条产品线术语是全新的BM25 那一路把所有相关查询都切错了词但因为向量那一路还能兜底整体指标只掉了两三个点谁都没注意。直到有个只靠精确术语检索的场景完全失效才被发现。现在的做法是把业务词典纳入版本管理跟产品线的上线流程绑定。一句实在话RAG 系统的绝大部分质量问题都可以用“打印出中间结果看一眼”解决。改写后的查询、召回的块 ID 和分数、重排后的排序、最终拼进上下文的完整文本——这四样东西里随便哪一样打印出来看几眼都能比你盯着最终答案瞎猜快十倍。我个人在这几个项目里最大的体会是Advanced-RAG 的收益不是来自某一个“神级技巧”而是来自把每一层的失败模式都堵住一点累积起来才是质变。分块加层级省下的召回损失可能只有 5 个点查询改写加 8 个点多路融合加 6 个点重排加 10 个点拒答阈值再加几个点——单看每一项都不惊艳但叠在一起就是从“能演示”跨到“敢交给业务用”的那道坎。真正要花时间的不是把技术堆上去而是把评估和日志做扎实让自己每一次改动都知道为什么有效、为什么无效。没有这层基础设施堆再多高级技术也只是在黑暗里调参。
阅读完成 · 觉得有帮助?
咨询建站