在实际大模型应用项目里,RAG(Retrieval-Augmented Generation,检索增强生成)系统从“能跑通”到“能上线”,中间隔着的往往就是准确率。同样的企业知识库,有的团队做出来准确率长期停留在 60% 左右,用户问三句错一句;而在补齐文档解析、切分策略、混合召回、精排、上下文组装和生成约束之后,同一套底座完全可以把准确率稳定提升到 80% 以上。这条优化路径不依赖更换更大的模型,也不依赖堆更多算力,核心是把 RAG 链路上每个环节的损失逐一找出来补上。
这篇文章面向正在做大模型应用落地、RAG 知识库、企业问答系统的开发者和算法工程师,也适合刚开始接触 RAG 实战的初学者。文章会先讲清楚准确率到底丢在哪里,再按“切分 -> 向量化 -> 召回 -> 精排 -> 上下文组装 -> 生成约束”的顺序给出可执行的优化方法,最后给出评测脚本、排查链路和上线前检查清单。学完可以直接用在自己项目的知识库问答上,不需要更换整套架构。
1. 先定位准确率损失:RAG 的六个环节都可能是短板
1.1 RAG 不是“检索 + 生成”两步,而是六到七个环节
很多人对 RAG 的理解是“先从知识库检索,再把检索结果交给大模型生成”。这个理解没有错,但不完整。真正的 RAG 链路至少要包含下面这些环节:
- 文档解析:把 PDF、Word、Excel、网页等原始资料转成干净文本。
- 文本切分:把长文档拆成适合向量化的文本块。
- 向量化与索引:用 Embedding 模型把文本块转成向量,写入向量数据库。
- 召回:根据用户问题取回 Top-K 候选片段。
- 精排:用重排模型对候选片段重新打分排序。
- 上下文组装:把最终选中的片段整理成模型可读的上下文。
- 生成与校验:大模型基于上下文生成答案,必要时做引用、格式和事实校验。
任何一个环节出错,最终答案都会偏离知识库事实。这也是为什么很多团队只调提示词时准确率怎么都上不去——问题根本不在提示词,而在前面几道检索工序。
实际调优时,可以把第 4 步到第 6 步看作“检索质量带”,第 7 步看作“生成质量带”。先调检索,再调生成。检索侧拿不到正确内容,生成侧再怎么写提示词也不可能凭空答对。
1.2 60% 的准确率通常丢在哪里
下面这张表列出六个环节的典型问题、外在表现和优化方向。它基本覆盖了大多数 RAG 项目准确率上不去的场景。
| 环节 | 典型问题 | 外在表现 | 主要优化方向 |
|---|---|---|---|
| 文档解析 | PDF 表格错位、扫描件没有 OCR | 答案缺数字、缺字段 | 先 OCR,再做版面分析 |
| 文本切分 | 固定字符数硬切,语义被切断 | 一个结论被拆到两个块 | 语义切分、父子分块 |
| 向量召回 | 专有名词、编号、型号召不回 | 正确答案不在 Top-K 里 | 混合检索、查询改写 |
| 精排 | 只按向量相似度排序 | 相似但错误的文档排在前面 | 引入 Cross-Encoder 重排 |
| 上下文组装 | Top-K 全部塞给模型 | 模型被无关内容带偏 | 限制片段数量、保留元数据、去除重复 |
| 生成 | 提示词没有拒答约束 | 模型强行编造答案 | 加拒绝回答规则、引用溯源、后置校验 |
这些损失是叠加的。单看每个环节可能只损失几个百分点,连起来就会整体落到 60% 区间。反过来,每个环节都修补一点,最终提升空间往往能到 20 个百分点左右,这正是“从 60% 拉到 80%”的来源。
1.3 先建立评测集,否则所有优化都是心理安慰
动手调参之前,先做一件看起来最不重要、实际上最重要的事:建立评测集。
评测集至少要包含三部分内容:
- 问题:从真实用户提问里抽样,或让业务人员编写,不要只做“文档里有现成答案”的问题。
- 标准答案:每题给出权威答案,或者至少标注“正确答案出现在哪几个文档片段”的文档 ID。
- 难度标记:区分事实题、流程题、判断题、对比题,方便单独看每一类题型的准确率。
数量不需要一次性做几千条,先做 100 到 200 条覆盖主要业务场景的题目。太少,参数调优会被偶然性淹没;太多,前期标注成本高,迭代也慢。100 到 200 条是常见项目的起步区间。
有了评测集,才能回答一个关键问题:这次改动到底让准确率上升了还是下降了。第五节会给出具体的评测脚本和迭代流程。
注意:评测集一旦建立,就不要在优化过程中随意改题目,否则前后两次实验无法对比。每次做重大改动之前,先跑一遍基线,记录基线准确率。
2. 检索质量决定上限:切分、Embedding 与混合召回
2.1 文档切分要按语义边界,而不是固定字符数
评测集就绪后,第一个动手点是文本切分。切分的核心目标只有一个:让每个文本块尽量是一个完整语义单元。
固定字符数切分最常见的问题是语义被切断。比如一段制度文档里写着“员工连续旷工 3 天,公司可以解除劳动合同”,如果按照 300 字硬切,恰好把“员工连续旷工 3 天”和“公司可以解除劳动合同”切到两个块里,那么这个问题几乎无法被某个单独块完整回答。
推荐先用递归分隔符切分,让文本优先在段落、句号、分号等位置断开:
from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=150, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], ) chunks = text_splitter.split_text(document)这段代码的关键在两点:
separators的顺序决定了切分优先级:先按段落切,再按换行切,然后按句号切,最后才按字符硬切。这样能最大限度保住语义完整性。chunk_overlap设置相邻块之间的重叠内容。它解决的是“一个结论正好落在两个块边界”的问题,让前后块都保留部分重叠信息。
chunk_size 和 chunk_overlap 没有万能值。常见项目里 chunk_size 取 300 到 800 字符,chunk_overlap 取 50 到 150 字符。块越小,召回越精准,但上下文越碎片化;块越大,上下文越完整,但容易混入无关内容。需要拿评测集实际跑一遍才能确定。
对于长文档,更推荐用“父子分块”策略:
# 子块用于向量召回,父块用于生成时喂给模型 parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200) child_splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50) parent_chunks = parent_splitter.split_text(document) child_chunks = child_splitter.split_text(document)子块小,向量召回更精准;命中了子块之后,再把包含该子块的父块整体交给大模型,保证生成时有完整上下文。这种方式在制度文档、合同、说明书这类内容密度高的场景下效果很明显。
| 切分方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定字符切分 | 前期跑通链路 | 实现简单 | 语义被切断,准确率不稳定 |
| 递归分隔符切分 | 大多数文档 | 在语义边界处断开,通用性好 | 需要调整 chunk_size 和 overlap |
| 父子分块 | 长文档、制度、合同 | 召回精准,生成上下文完整 | 索引和存储成本更高 |
| 语义切分 | 内容主题跳跃明显的文档 | 每个块主题相对统一 | 需要额外模型或算法,耗时较长 |
2.2 Embedding 模型的选择和向量化细节
切分之后的文本要转成向量,这一步的模型选择直接决定向量召回的语义理解能力。不同 Embedding 模型在中文支持、行业术语、长文本能力上的差异很大,不是随便换一个就行的。
实际选择时重点关注以下几点:
- 中文业务优先使用中文预训练或中英双语模型,纯英文模型在中文语义匹配上会明显偏弱。
- 如果知识库领域性很强(医疗、法律、金融、政务),优先找领域微调过的 Embedding 模型,或者用自有业务数据做增量微调。
- 向量计算前要做归一化,这样相似度计算可以用余弦相似度,分数范围也更稳定。
- 向量维度越高,表达能力越强,但检索和存储成本越高。需要根据数据量和查询延迟做权衡。
下面是一个最小示例,使用开源中文 Embedding 模型做向量化:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-large-zh-v1.5") embeddings = model.encode( ["员工连续旷工几天可以解除劳动合同"], normalize_embeddings=True, )这里把查询问题和文档块用同一个模型编码,得到的向量才能放在同一个空间里计算相似度。如果查询和文档用不同模型编码,相似度对比会失去意义。
注意:落地时要先确认模型许可证是否满足商用要求,并检查模型版本与向量数据库的兼容性。更换 Embedding 模型后,所有已入库的向量都必须重新生成,否则新旧向量不在同一语义空间。
2.3 混合检索:向量召回加关键词召回,再做融合
纯向量召回有一个明显短板:对精确术语、编号、型号、政策条款号这类内容不敏感。用户问“考勤制度第 7 条怎么规定的”,向量检索可能把“第 7 条”当成普通文本,召回来的却是另一条相关内容。
正确做法是混合检索,让向量召回负责语义相似,让关键词召回负责精确匹配,然后把两路结果融合成一个排序列表。最常用的关键词召回算法是 BM25。
from rank_bm25 import BM25Okapi corpus = ["员工连续旷工 3 天可以解除劳动合同", "员工年休假 5 天", ...] tokenized_corpus = [doc.split() for doc in corpus] bm25 = BM25Okapi(tokenized_corpus) query = "员工旷工几天可以解除合同" bm25_scores = bm25.get_scores(query.split())得到 BM25 分数后,和向量相似度做加权融合:
final_score = 0.5 * normalized_bm25_score + 0.5 * vector_similarity加权系数需要根据业务特点调整。业务里精确术语多,可以提高 BM25 权重;业务里口语化表达多,可以提高向量权重。
另一种更稳定的融合方式是 RRF(Reciprocal Rank Fusion)。它不依赖分数绝对值,而是基于排名位置融合:
def rrf_score(rank, k=60): return 1.0 / (k + rank)把两路召回结果按排名计算 RRF 分数并求和,再按总分排序。RRF 的优点是不同检索算法返回的分数量纲不一致时,排名仍然可比。
2.4 查询改写:短问题、指代和多轮问题要先规范化
用户提问往往很简洁,或者带有指代。比如“它要多久才能审批完”,如果直接拿去检索,向量模型很难定位到正确文档。这里需要查询改写,把用户问题转成一个适合检索的独立问题。
rewrite_prompt = """ 请把下面的用户问题改写成适合检索知识库的独立问题。 要求: 1. 保留原文中的业务术语、编号、日期。 2. 不要扩展事实,不要补充资料里没有的信息。 3. 如果是多轮对话,把指代替换成具体实体。 用户问题:{query} 改写结果: """改写不只是把问题变长,更关键的是把“隐含实体”显式化。例如“它”替换成“固定资产申请审批”,把用户上一轮提到的业务对象补进来。这一步对准确率的提升在真实用户场景里非常明显,因为生产环境里的用户提问远比测试集里的规范问句更口语化。
3. 精排与上下文组装:检索之后还有 20% 的空间
3.1 重排器解决“相似但不相关”的问题
向量召回和混合检索的目标是把候选集从全库缩小到 Top 50 到 Top 100。这个阶段求的是“别漏掉”,允许混入一些相似但无关的内容。真正决定答案质量的是最后一公里:精排。
精排阶段推荐使用 Cross-Encoder 重排模型。它把查询和文档片段拼接在一起输入模型,能够捕捉两者之间更细的交互关系,排序能力比双塔向量模型强,但速度慢。所以重排器的位置是:只对向量召回结果的前几十条重新打分,而不是对全库打分。
from FlagEmbedding import FlagReranker reranker = FlagReranker("BAAI/bge-reranker-base") scores = reranker.compute_score( [[query, doc] for doc in top_candidates] )得到重排分数后,按分数从高到低取前 3 到 5 条作为最终上下文。重排器把精确的关联信息排在前面,能显著减少“看着相似但答非所问”的情况。
实际使用时要注意:重排器的分数不能直接和向量相似度混用。先用向量或混合检索圈出候选,再用重排器在候选内重新排序,两阶段各司其职。
3.2 上下文组装:不是把 Top-K 全部塞给模型
检索链路跑完之后,很多人会把 Top-K 的文档片段全部拼接进提示词,这往往好心办坏事。相关度不够的片段会引入干扰信息,让模型在多个候选答案之间摇摆。
推荐的组装策略是:
- 只取重排后的前 3 到 5 条片段,具体数量根据 LLM 的上下文窗口和片段长度调整。
- 按相关度从高到低排列,最重要的片段放在最前面。
- 每条片段保留元数据,包括来源文档名、页码、章节号、日期。
- 去除重复或高度重叠的片段,避免同一内容占据多个位置。
context_blocks = [] for doc in top_docs: source = doc.metadata.get("source", "未知来源") page = doc.metadata.get("page", "N/A") block = f"[来源:{source} 第{page}页]\n{doc.page_content}" context_blocks.append(block) prompt = f"""请基于下面的参考资料回答用户问题。 参考资料: {chr(10).join(context_blocks)} 用户问题:{question} 要求: 1. 只能使用参考资料中的信息。 2. 如果参考资料中没有相关信息,直接回答“无法从现有资料中确认”。 3. 答案末尾列出用到的来源编号。 """带来源信息的上下文有两个好处:一是让大模型在需要时能引用出处,减少凭空编造;二是后续做答案核验时,可以拿着“答案 - 来源”对照原文档检查。
3.3 元数据过滤:知识库结构对准确率的影响被严重低估
很多 RAG 项目把文档全部切块后一股脑丢进向量库,不做任何结构化处理。这在数据量小的 demo 里没问题,数据量一大,准确率就会明显下降。原因是范围太大,噪声太多。
推荐的思路是给每个文本块打上结构化的元数据:
- 文档类型:制度、流程、产品说明、FAQ、公告。
- 业务线或部门:人事、财务、采购、生产。
- 生效版本、发布年份、作废状态。
- 针对的目标用户或适用范围。
检索时不是只给向量库一个纯文本查询,而是同时带过滤条件。例如用户问“辞退补偿怎么算”,可以先限定文档类型为“制度”,部门为“人事”,版本为“现行有效”,再在过滤结果里做相似度检索。这样命中率和准确率都会提升。
更进一步,还可以引入本体或知识图谱辅助检索,也就是热词里常说的 Ontology RAG。它把知识库里的实体、关系和属性组织成结构化框架,检索时先定位实体再找关联片段。相比纯向量检索,这种方式对“复杂关系类问题”有明显优势,但构建成本也高,适合后续作为进阶方向。
4. 生成侧约束:提示词、引用与拒答策略
4.1 提示词决定模型“敢不敢说不知道”
检索质量再高,生成侧如果不加约束,模型依然可能凭训练记忆编答案。RAG 的提示词核心目标不是“让模型回答得更好”,而是“让模型只基于上下文回答,并且敢于说不知道”。
下面是一份经过实践检验的提示词模板,核心是给出了明确的行为边界:
你是一个企业知识库问答助手。 回答规则: 1. 只能使用“参考资料”里出现的事实进行回答。 2. 如果参考资料不足以回答问题,直接回答“无法从现有资料中确认”,并列出缺失的关键信息。 3. 不要使用模型自身的知识补充参考资料外的事实。 4. 回答中引用事实时,标注对应资料编号,格式为 [1] [2]。 参考资料: [1] [来源:员工手册 第12页] 员工连续旷工 3 天,公司可以解除劳动合同。 [2] [来源:考勤管理制度 第4条] 旷工天数以自然日计算,法定节假日不计入旷工。 问题:员工连续旷工几天公司可以解除合同?这个模板的关键是“如果资料不足,就承认不足”。没有这条约束的模型,即使检索结果里没有答案,也会强行凑一个看起来合理的回答,这是 RAG 幻觉的主要来源之一。
4.2 引用溯源和结构化输出:把幻觉变成可核查
纯文本答案很难核验,结构化输出则让自动检查变得容易。推荐让模型按 JSON 格式输出答案、相关来源和置信度判断。
{ "answer": "员工连续旷工 3 天,公司可以解除劳动合同。", "sources": ["员工手册第12页", "考勤管理制度第4条"], "supportiveness": "supported", "missing_info": [] }supportiveness字段很关键,它表示答案是被资料完全支持、部分支持还是完全不支持。后置校验程序可以据此把低置信度的回答拦截下来,交给人工处理。把幻觉从“模型悄悄编”变成“系统可核查”,这是生产环境 RAG 的必备能力。
4.3 后置校验:关键词、数字和一致性检查
生成完成之后,不要直接返回给用户,先做一层轻量校验。常见校验手段包括:
- 数字一致性检查:如果问题和答案都出现数字,核对是否与来源片段一致。
- 关键词覆盖检查:确认答案中的核心实体确实出现在引用来源里。
- 拒答符合性检查:如果模型返回“无法确认”,确认回答中确实没有额外编造的断言。
- 来源有效检查:引用条目标注的文档 ID 是否真实存在于知识库。
def check_numeric_consistency(answer, source_chunks): import re answer_numbers = set(re.findall(r"\d+", answer)) source_text = " ".join(source_chunks) source_numbers = set(re.findall(r"\d+", source_text)) # 答案里的数字应当都能在来源里找到 return answer_numbers.issubset(source_numbers) or not answer_numbers这类校验无法保证答案百分之百正确,但能拦截掉很大一部分明显编造的内容。生产环境中,把校验不通过的答案降级为“抱歉暂无法准确回答”,准确率统计上反而更干净。
5. 用评测集驱动迭代:从 60% 提升到 80% 的实操流程
5.1 评测集怎么建:三类问题都要覆盖
评测集不是随便写几十个问题就行。建议按问题类型分层构建,每类问题各占一部分:
| 问题类型 | 示例 | 考察点 |
|---|---|---|
| 事实型 | 年假天数怎么计算 | 能否召回准确条款并原样回答 |
| 流程型 | 报销流程具体分几步 | 能否把多个步骤完整串联 |
| 判断型 | 连续旷工 3 天是否会被辞退 | 能否基于制度条文做正确判断 |
| 对比型 | 事假和病假的工资算法差异 | 能否同时召回两条制度并对比 |
每题还需要标注“golden_chunk_ids”,也就是正确答案所在的文档块 ID。这个标注决定了一件事:当答案错误时,你能立刻判断错误发生在检索侧还是生成侧。
5.2 指标怎么算:召回指标和答案指标分开看
准确率是一个笼统的说法,实际评测建议拆成两个维度。
检索侧指标用 Recall@K:
def recall_at_k(retrieved_ids, golden_ids, k=5): if not golden_ids: return 0 hits = set(retrieved_ids[:k]) & set(golden_ids) return len(hits) / len(golden_ids)Recall@K 回答的问题是:正确答案有没有被召回到候选里。只要正确答案被召回,生成侧才有机会答对。如果 Recall@K 低,问题出在检索链路,要去调切分、Embedding、混合检索和精排。
答案侧指标回答的问题是:最终生成答案是否正确。常见做法有三种:
- 精确匹配:答案与参考答案完全一致。适合答案是数字、名称、状态等确定性内容的题。
- 包含匹配:参考答案中的关键实体或关键句是否出现在答案里。适合长文本答案。
- LLM-as-Judge:用一个大模型当裁判,按标准答案对生成答案打分。适合主观判断类问题。
精度要求高的场景建议三者结合:关键实体包含匹配为底,LLM 裁判做整体评分,人工抽样复核。
5.3 一个最小评测脚本
下面是一个可直接改造的最小评测脚本,假设已经有评测集、检索函数和生成函数:
import json def run_evaluation(dataset_path, retriever, generate_func, k=5): dataset = json.load(open(dataset_path, encoding="utf-8")) total = len(dataset) retrieval_hits = 0 answer_hits = 0 for item in dataset: query = item["query"] golden_ids = set(item["golden_chunk_ids"]) retrieved = retriever.retrieve(query, top_k=k) retrieved_ids = [doc["chunk_id"] for doc in retrieved] # 检索侧评估 if set(retrieved_ids) & golden_ids: retrieval_hits += 1 # 生成侧评估(简化版:参考答案是否出现在答案中) answer = generate_func(query, retrieved) if item["answer_keywords"] and all(kw in answer for kw in item["answer_keywords"]): answer_hits += 1 return { "retrieval_recall@{}".format(k): retrieval_hits / total, "answer_accuracy": answer_hits / total, }这个脚本故意把评估逻辑简化,便于说明思路。实际使用时,关键词匹配不够稳定,建议用实体级匹配或 LLM-as-Judge 替代。
5.4 迭代顺序和实验记录:一次只改一个变量
从 60% 提到 80% 不是靠一次大改,而是靠多轮小步快跑叠加出来的。推荐按下面的顺序迭代,一次只改一个变量:
- 先修文档解析,确保文本干净。
- 再调切分策略,观察 Recall@K 变化。
- 换或微调 Embedding 模型,观察 Recall@K 变化。
- 加入混合检索,观察 Recall@K 变化。
- 引入重排器,观察最终答案准确率变化。
- 最后调提示词和生成约束,观察答案准确率变化。
每轮实验都要记录参数、指标和样本案例。建议实验记录用固定表格:
| 实验编号 | 改动项 | Recall@3 | Recall@5 | 答案准确率 | 错误类型分析 |
|---|---|---|---|---|---|
| baseline | 固定 500 字切分 + 纯向量 | 0.62 | 0.70 | 0.60 | 召回丢失多 |
| exp01 | 递归切分 600/150 | 0.72 | 0.79 | 0.66 | 边界切断减少 |
| exp02 | 加 BM25 混合检索 | 0.80 | 0.85 | 0.71 | 条款编号命中提升 |
| exp03 | 加重排器 | 0.80 | 0.85 | 0.77 | 干扰片段减少 |
| exp04 | 加拒答约束和引用 | 0.80 | 0.85 | 0.82 | 编造内容减少 |
这是一条典型的提升曲线:召回率先上去,答案准确率随后跟上;到了最后阶段,答案准确率反而可能略高于召回率,因为生成侧的拒答约束把答不准的问题变成了“无法确认”,而不是硬答错。
注意:表格中的数据只用于说明迭代形态。真实项目的提升幅度取决于知识库质量、文档复杂度、模型选型和评测集难度,不能照抄数值。
6. 高频问题排查:准确率上不去时按什么顺序查
6.1 先定位“召回丢失”还是“生成错误”
准确率低时,第一个动作不是继续调参,而是把错误案例分成两类。
拿一条错误答案,人工查看检索结果:正确答案的文档块在不在 Top-K 候选里?
- 如果答案块不在候选里,说明是召回丢失,问题出在解析、切分、向量化、混合检索或查询改写。
- 如果答案块在候选里但最终答案还是错的,说明是生成错误,问题出在上下文组装、提示词、引用约束或后置校验。
这个二分法可以省掉大量无效调参。很多人一上来就换大模型,结果正确答案根本没被检索出来,换什么模型都答不对。
6.2 高频问题速查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 正确答案不再 Top-K | 切分切断了语义 | 打印命中的 chunk 文本 | 改成句号级切分,增加 overlap |
| 条款编号命不中 | 向量模型对精确编号不敏感 | 用条款号直接搜向量库 | 加入 BM25 关键词召回 |
| 召回了但答案错 | 上下文被无关片段干扰 | 查看传给模型的 Top-K 原文 | 引入重排器,只保留前 3 到 5 条 |
| 模型总是编造 | 提示词没有拒答约束 | 查看无资料时返回什么 | 加“无法确认”规则和引用要求 |
| 改了切分没有效果 | 评测集和参数没对齐 | 确认评测集质量 | 增加真实问题,减少简单题 |
| 换 Embedding 后变差 | 新旧向量混用 | 检查索引是否重建 | 全量重建向量索引 |
| 答案缺数字 | PDF 表格解析错位 | 检查文档解析结果 | 先 OCR + 版面分析再切分 |
| 线上准确率低于离线 | 用户提问和测试集风格不一致 | 收集线上 query 对比 | 定期把线上问题补充进评测集 |
6.3 一条完整排查链路
准确率异常时,按下面的顺序排查,不要跳步:
- 查文档解析结果:文本里有没有乱码、表格错位、扫描页空白。
- 查切分结果:语义完整句是否被切断,chunk 是否过大或过小。
- 查索引状态:向量库里的 chunk 是否对应最新文档版本,Embedding 模型是否统一。
- 查召回结果:正确文档 ID 是否进入 Top-K,没有进就是检索侧问题。
- 查精排结果:正确文档是否被重排器排到了后面,排除分数融合错误。
- 查上下文组装:传给模型的内容是否包含正确片段,是否夹带大量干扰片段。
- 查生成输出:模型是否忠实引用上下文,是否出现了来源之外的事实。
7. 生产环境差异、Agentic RAG 扩展与检查清单
7.1 学习环境与生产环境的差异
本地 demo 跑通只是第一步,生产环境和学习环境在工程要求上有本质差异。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据量 | 几百个文档块 | 几十万到几千万个文档块 |
| 文档更新 | 手动重建索引 | 增量更新、定时同步、版本管理 |
| 评测 | 手工验证几个问题 | 自动化评测流水线,回归测试 |
| 日志 | 打印到控制台 | 全链路 trace,记录 query、召回、重排、生成 |
| 安全 | 不敏感数据 | 权限隔离、数据脱敏、访问审计 |
| 性能 | 单用户 | 并发控制、缓存、限流、超时处理 |
| 回滚 | 不涉及 | 模型和配置版本化,支持快速回滚 |
| 监控 | 不涉及 | 监控准确率、召回率、延迟、错误率 |
其中最容易忽视的是文档版本管理。知识库内容会定期更新,旧版本文本块如果还留在向量库里,检索时可能把已作废的制度当成现行制度返回。生产环境的文档入库流程必须包含生效日期、作废标记和定期清理任务。
7.2 Agentic RAG 是下一步扩展方向
当传统单跳 RAG 接近上限时,可以考虑往 Agentic RAG 方向演进。它的核心区别是:检索不再是“一次查询取回 Top-K”,而是让大模型根据问题自主决定检索计划。
常见形态包括:
- 子问题分解:把复合问题拆成多个子问题,分别检索后再汇总。
- 多轮检索:第一轮结果不足时,根据缺少的信息发起第二轮检索。
- 工具调用:当问题涉及“查天气”“查库存”等实时数据时,调用外部工具获取事实。
- 自我反思:模型根据检索结果判断是否满足问题要求,不满足就换一种方式重新检索。
这些能力能进一步提高复杂问题的准确率,但代价是推理链路变长、延迟变高、工程复杂度显著上升。建议先把经典 RAG 链路的准确率做到 80% 区间,再评估是否引入 Agentic RAG。
7.3 上线前检查清单:可复用的 10 项确认
项目上线前,建议逐项确认下面这份清单:
- 评测集是否覆盖真实线上提问,数量是否不少于 100 条。
- 是否记录了基线指标,后续每次改动是否有对比数据。
- 文档解析结果是否经过人工抽检,表格、扫描件是否正确。
- 切分参数是否用评测集调过,而不是直接用默认值。
- Embedding 模型的许可证和商用限制是否确认。
- 向量索引是否与当前文档版本完全一致,是否配置了定时重建或增量更新。
- 检索链路是否包含关键词召回,条款编号、型号类查询是否验证过。
- 重排器是否只加载到候选集上,线上延迟是否可接受。
- 提示词是否包含拒答约束和引用要求,模型在无资料时是否会乱答。
- 答案是否做了结构化和后置校验,低置信度回答是否有降级策略。
把这 10 项逐条做掉,RAG 系统的准确率提升才不是碰运气。最后再强调一句:评测集是一切优化的前提。没有客观评测,任何“感觉变好了”都不可信;有了评测集,每一次参数调整、模型替换、提示词改动都能变成可积累的经验。对刚接触 RAG 的人来说,最快的学习方式就是拿一份真实业务文档,从最粗糙的切分加向量检索开始跑通,然后照着本文的顺序一步步把准确率从 60% 推到 80%,你会真正理解 RAG 每个环节为什么存在。