做 RAG 系统的同学基本都遇到过同一个困境:数据铺了一堆,方案看起来也很完整,向量库、大模型、Prompt 全都有,但用户问几个业务问题,答案不是答非所问,就是漏掉关键信息,再一测准确率只有 60% 上下。这个数字放在 demo 里勉强能看,放到生产环境根本不敢用。
这次我们直接讨论一个问题:一个准确率只有 60% 的 RAG 系统,怎么通过系统化优化把它拉到 80% 以上。先说结论,RAG 准确率不是靠某一个“神奇模块”解决的,而是靠切块策略、Embedding 模型、混合检索、Rerank 重排、Prompt 约束、知识库治理这六件事一起抬起来的。每一步单独看都不复杂,但串在一条链路上效果会非常明显。
这篇文章会先讲清楚“准确率”到底怎么定义、怎么测,然后给出一套可落地的优化顺序和代码示例,最后补充常见问题排查和工程化建议。适合正在做 RAG、RAG 知识库、大模型应用开发,或者准备做 RAG 测评的读者。
1. RAG 准确率优化全景速览
在展开细节之前,先把完整的优化链路放在一张表里。后续所有操作都是围绕这些模块展开的。
| 优化模块 | 核心手段 | 对准确率的影响方向 |
|---|---|---|
| 评测集构建 | 建立问题-标准答案-文档范围三元组 | 让准确率可量化,先有基准线 |
| 文本切块 | 从固定长度切分改为语义段落切分 | 减少上下文碎片,提升召回质量 |
| Embedding 选型 | 选择匹配领域的中文/多模态向量模型 | 提升语义召回的上限 |
| 混合检索 | 向量检索 + BM25 关键词检索 | 同时覆盖语义匹配和关键词精确匹配 |
| Rerank 重排 | 引入交叉编码器对候选文档二次打分 | 准确率提升最明显的一步 |
| Prompt 约束 | 限定回答格式、引用来源、拒答策略 | 减少幻觉,提升答案可用性 |
| 知识库治理 | 去重、清洗、版本管理、元数据补充 | 从源头减少错误召回和冲突答案 |
如果你现在的 RAG 系统还停留在“Embedding + 向量库 + LLM”三件套,那准确率卡在 60% 左右很正常。这三件套只是最基础的原型,离可用状态还有一条完整的调优链路要走。
2. 先把“准确率”定义清楚
很多团队说“准确率 80%”,但问他们怎么算的,往往说不清楚。RAG 系统的准确率不是一个单一数字,至少要拆成四个维度看。
| 评测维度 | 说明 | 常见计算方式 |
|---|---|---|
| 文档召回率 | 正确答案对应的文档是否被检索到 | 召回文档中包含标准答案片段的比例 |
| 答案准确率 | 生成答案是否与标准答案核心语义一致 | 人工打分或 LLM 打分 |
| 引用准确率 | 生成答案引用的来源是否真实存在 | 引用的文档片段是否与回答内容匹配 |
| 拒答准确率 | 知识库没有答案时,系统是否正确拒答 | 应拒答但错误回答的比例越低越好 |
建议先做一套内部评测集,不需要很大,50 到 100 条高质量问题就够起步。每条问题包含三个字段:
{ "question": "公司的请假审批流程是什么?", "reference_answer": "员工发起请假申请后,由直属主管审批,超过 3 天需由部门负责人审批,审批结果会通过 OA 系统通知员工。", "reference_docs": ["hr_leave_policy_v2.pdf"], "should_refuse": false }这套评测集会贯穿整个优化过程。每次调整切块策略、换 Embedding 模型、加 Rerank,都在同一套评测集上重新跑一遍,用数字判断改动是正向还是负向。没有评测集的 RAG 优化,本质上是在靠感觉调参。
3. 为什么 RAG 系统一开始只有 60%
先诊断,再动手。以下五个问题是最常见的准确率杀手。
3.1 固定长度切块破坏了语义完整性
很多快速原型直接用固定长度切块,比如 500 个字符一段,不管这段文本是不是一个完整的语义单元。结果就是一个完整的产品说明被拦腰切断,向量召回时检索到的只是其中半句话,大模型拿到的上下文残缺,答案自然不准。
3.2 Embedding 模型和领域不匹配
Embedding 模型决定了文本被映射到向量空间后,语义相近的文本是否真的会靠在一起。通用 Embedding 模型在通用语料上表现不错,但在医疗、法律、金融、内部工单这类强领域语料上,效果会明显下降。
3.3 纯向量检索漏掉关键词精确匹配
向量检索擅长语义相似,但不擅长精确关键词匹配。比如用户问“AP-200 型设备报错 E403”,如果知识库里只有“AP200”的写法,没有“AP-200”,向量检索可能召回不到那条关键文档,但 BM25 这种关键词检索可以。
3.4 没有 Rerank,召回结果太粗糙
向量检索通常取 top 20 或 top 50 候选,再交给大模型生成答案。但向量检索的排序质量有限,真正有用的文档可能排在中间位置,而前面全是语义相似但不直接相关的文档。Rerank 的作用就是把这批粗糙候选重新精排一次。
3.5 Prompt 没有任何约束,模型自由发挥
如果 Prompt 里只写了“根据以下内容回答问题”,大模型在找不到答案时容易自行脑补。正确做法是在 Prompt 里明确要求“如果给定内容里没有答案,请直接回答不知道,不要编造”。
4. 目标基线:先搭一套可评测的最小 RAG
优化之前,先确保你有一个能跑的基线系统。这里给出一套最小可运行的结构,不绑定具体框架,方便你替换成自己的技术栈。
from openai import OpenAI from chromadb import PersistentClient client = OpenAI(api_key="your-api-key", base_url="your-endpoint") vector_db = PersistentClient(path="./chroma_data") collection = vector_db.get_or_create_collection("knowledge_base") def chat_with_rag(question: str, top_k: int = 5) -> str: query_embedding = client.embeddings.create( model="your-embedding-model", input=[question] ).data[0].embedding candidates = collection.query( query_embeddings=[query_embedding], n_results=top_k ) context = "\n\n".join( doc + "\n来源:" + meta.get("source", "未知") for doc, meta in zip(candidates["documents"][0], candidates["metadatas"][0]) ) response = client.chat.completions.create( model="your-llm-model", messages=[ {"role": "system", "content": "你是企业知识库助手,请严格依据提供的内容回答问题。"}, {"role": "user", "content": f"参考内容:\n{context}\n\n问题:{question}"} ] ) return response.choices[0].message.content这个基线实现包含三个部分:召回、拼装上下文、生成答案。后面所有优化都是在这个流程上做增量修改。
5. 提升准确率的第一个关键:重新设计文本切块
切块是 RAG 优化里最容易被低估的一步。好的切块不是把文本平均切成 n 段,而是让每个切块尽量成为一个完整的语义单元。
5.1 优先按文档结构切分
如果原始文档是 Markdown、HTML 或带有标题结构的文本,优先按标题层级切分。比如一个政策文档有“适用范围”“审批流程”“违规处理”三个章节,就把每个章节作为一个独立切片,而不是把“适用范围”的结尾和“审批流程”的开头拼在一起。
import re def split_by_headings(text: str): lines = text.splitlines() sections = [] current_title = "前言" current_content = [] for line in lines: # 匹配 Markdown 标题 match = re.match(r"^(#{1,3})\s+(.*)", line) if match: if current_content: sections.append({ "title": current_title, "content": "\n".join(current_content).strip() }) current_title = match.group(2) current_content = [] else: current_content.append(line) if current_content: sections.append({ "title": current_title, "content": "\n".join(current_content).strip() }) return sections5.2 切片后叠加元数据
切块不是切完就结束了。每个切片需要保留尽可能多的元数据:来源文件名、章节标题、页码、版本号、更新时间。这些元数据有两个作用:一是 Rerank 阶段可以作为过滤条件,二是回答时可以直接给出精确引用来源。
chunk = { "text": section["content"], "metadata": { "source": "HR_Leave_Policy_v2.pdf", "section": section["title"], "updated_at": "2025-06-01", "category": "hr" } }5.3 小块检索,大块生成
这是一个在生产环境很有效的策略。检索阶段用较小、语义更聚焦的切块提高召回精度,生成阶段再把命中的多个小块向上合并到完整的章节上下文,避免上下文信息量不够。
6. 检索召回优化:从纯向量到混合检索
向量检索擅长语义扩展,BM25 擅长精确匹配。混合检索的核心思想是把两者结果合并,再用分数归一化统一排序,最后把 top N 候选交给 Rerank。
6.1 混合检索实现
如果用 Python 实现,可以将向量检索结果和 BM25 结果做加权合并。BM25 这部分可以直接用rank_bm25库:
pip install rank_bm25from rank_bm25 import BM25Okapi import jieba def bm25_search(query: str, documents: list[str], top_k: int = 20): # 中文场景建议先用分词器处理 tokenized_docs = [list(jieba.cut(doc)) for doc in documents] bm25 = BM25Okapi(tokenized_docs) tokenized_query = list(jieba.cut(query)) scores = bm25.get_scores(tokenized_query) ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True) return ranked[:top_k]6.2 分数融合策略
向量检索和 BM25 的分数不在同一个尺度上。常见做法是做归一化或者取排名序号进行融合:
def fusion_rank(vector_hits, bm25_hits, alpha=0.5): # 用排名倒数作为融合分数 fused = {} for rank, doc_id in enumerate(vector_hits): fused[doc_id] = fused.get(doc_id, 0) + alpha / (rank + 1) for rank, doc_id in enumerate(bm25_hits): fused[doc_id] = fused.get(doc_id, 0) + (1 - alpha) / (rank + 1) return sorted(fused.items(), key=lambda x: x[1], reverse=True)混合检索可以解决一类非常实际的问题:知识库里存在大量产品型号、工单编号、合同号码,这类内容用户经常原样输入,关键词精确匹配比语义匹配更可靠。加了混合检索之后,召回率往往是第一波明显提升点。
7. Rerank 重排:准确率提升最明显的一步
如果整个优化链路里只能选一个优先做,我建议先做 Rerank。它带来的准确率提升通常是最明显的。
7.1 Rerank 的原理
Rerank 模型是交叉编码器结构,把所有候选文档和用户问题一起送入模型,输出一个更精准的相关性分数。相比双塔结构的向量检索,Rerank 能更精细地判断“这段内容是否真的回答了这个问题”,代价是推理速度更慢,所以它只能对召回后的少量候选重排,不能直接对全库跑。
7.2 典型调用流程
先向量检索+关键词召回 top 20 到 50,再用 Rerank 精排取 top 3 到 5 送进大模型。
from sentence_transformers import CrossEncoder rerank_model = CrossEncoder("your-rerank-model") def rerank(question: str, candidates: list[dict], top_k: int = 5): pairs = [[question, item["text"]] for item in candidates] scores = rerank_model.predict(pairs) ranked = sorted( zip(candidates, scores), key=lambda x: x[1], reverse=True ) return [item for item, _ in ranked[:top_k]]Rerank 模型选择建议直接在本地测试几个开源模型的效果差异,以自己评测集上的数字为准。注意,越大的 Rerank 模型效果一般更好,但推理时间也越长。如果是实时问答服务,需要权衡延迟。
8. Prompt 工程与答案生成约束
检索和重排解决的是“有没有找到对的内容”,Prompt 解决的是“找到之后答案生成得对不对”。两者同样重要。
8.1 一个高约束的系统 Prompt 模板
system_prompt = """ 你是企业知识库问答助手。回答问题时必须遵守以下规则: 1. 只依据"参考资料"中的内容回答,禁止使用参考资料之外的内部知识。 2. 如果参考资料中没有足够的信息,请直接回答"我无法从知识库中找到准确的答案",不要猜测。 3. 回答时尽量使用原文中的表述,减少自行概括。 4. 在回答末尾标注引用来源,格式为:[来源:文档名-章节名]。 5. 如果问题涉及操作步骤,按步骤序号输出,不要合并步骤。 6. 如果知识库中存在矛盾信息,请同时列出两种观点,并说明信息不一致。 """这套 Prompt 的核心价值有两点:一是强制模型“不知道就说不知道”,二是要求标注引用来源,方便人工核对。生产环境里,引用准确率和答案准确率几乎同等重要,引用来源作为审计依据,也能反向推动知识库治理。
8.2 温度参数与生成策略
答案生成阶段,建议把大模型的 temperature 调到 0.1 到 0.3 之间,太低容易机械重复,太高容易自由发挥。同时在请求层面打开流式输出,方便前端逐字展示,优化用户等待体验。
9. 知识库治理与更新策略
RAG 准确率的天花板不取决于模型,而取决于知识库本身。如果知识库里全是过期文档、重复内容、互相冲突的版本,任何检索和重排都救不回来。
9.1 入库前清洗清单
| 检查项 | 处理方式 |
|---|---|
| 重复文档 | 按文件哈希和文本相似度去重 |
| 过期版本 | 保留最新版本,旧版本打上 deprecate 标签 |
| 扫描件 / 图片 PDF | 先过 OCR 再用文本切块,不能直接入库乱码 |
| 加密 / 损坏文件 | 入库前解析,解析失败则记录并跳过 |
| 内部涉密字段 | 数据脱敏后再入库,身份证、手机号要打码 |
| 格式严重错误 | 统一转换为 Markdown 或纯文本后入库 |
9.2 知识库版本管理
不要直接在原集合上覆盖更新。推荐做法是给知识库加版本字段,每次全量更新生成一个新版本批次。检索时优先召回最新版本,旧版本只在指定场景下回退。如果切块元数据里有updated_at和source_version,排查答案冲突时能很快定位到具体文档版本。
9.3 定期评估知识库健康度
统计每个文档被命中后,生成答案是否被用户点踩或人工复核驳回。持续没有命中的文档要么是检索不到,要么是内容没有价值。长期零命中的文档可以直接标记为低质量,从库里移除,减少检索干扰。
10. 评测闭环与回归测试
RAG 系统优化是典型的“没有评测就没有进步”的工程。每一轮改动都要在固定评测集上重跑,用数据看方向对不对。
10.1 评测脚本核心逻辑
def evaluate_rag(questions, rag_function): metrics = { "answer_accuracy": 0, "recall_rate": 0, "refusal_accuracy": 0 } total = len(questions) for item in questions: answer = rag_function(item["question"]) # 如果不该拒答,但模型拒答了,扣分 if not item["should_refuse"]: if "无法从知识库中找到" in answer: continue # 用标准答案关键词做简单召回判断 # 更严谨的做法是调用 LLM 打分或人工审核 hit = any(keyword in answer for keyword in item["reference_answer"][:10]) if item["should_refuse"]: if hit and "无法从知识库中找到" in answer: metrics["refusal_accuracy"] += 1 else: if hit: metrics["answer_accuracy"] += 1 metrics["recall_rate"] += 1 return {k: round(v / total * 100, 2) for k, v in metrics.items()}10.2 评测迭代流程
- 第一轮:跑基线,记录切块方式、Embedding 模型、检索策略下的准确率。
- 第二轮:只改切块策略,其他不变,对比准确率变化。
- 第三轮:加混合检索,对比变化。
- 第四轮:加 Rerank,对比变化。
- 第五轮:改 Prompt,对比变化。
每一次只动一个变量,才能判断哪个优化真正有效。如果同时改了三四个东西,准确率提升了也说不清是哪一个起的作用。
10.3 评测集需要持续扩充
固定评测集跑一个月之后,需要从真实用户问题里挑选新的难例扩充进去。比如用户反复问但系统总是答错的问题,必须收录到评测集里,确保后续优化不会再次在同一个地方翻车。
11. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索不到对应文档 | 切块粒度太大,语义被稀释 | 输出检索命中的文档 ID 对比 | 改用语义切块或混合检索 |
| 检索能命中但答案错误 | 大模型没有正确利用上下文 | 查看送入 Prompt 的上下文内容 | 优化 Prompt,加强“严格依据原文回答”约束 |
| 答案答非所问 | Rerank 排序结果不理想 | 打印 Rerank 分数和候选排名 | 更换 Rerank 模型,扩大召回候选数 |
| 知识库更新后答案还是旧的 | 向量库没有删除旧版本切片 | 检查元数据和版本字段 | 先按版本删除旧向量,再写入新向量 |
| 用户问简称答不对 | 知识库只有全称,没有建立指称映射 | 检查切块是否包含别名信息 | 入库时加入扩展词、同义词元数据 |
| 答案包含幻觉内容 | 模型自行补充了知识库外信息 | 检查模型输出的引用来源 | 提高模型拒答约束,加强引用追溯 |
| 服务响应慢 | Rerank 阶段对全部候选做精排 | 查看每阶段耗时 | 缩小召回 topK 数量,或使用更轻量的 Rerank 模型 |
12. 最佳实践与合规建议
RAG 项目做的是知识库增强的大模型应用,越接近生产环境,越要重视工程规范和合规边界。
- 先跑通小规模评测集再上全量数据。第一次用 50 条问题验证优化方向,不要直接压上几百万条文档。
- 保留一套最小可运行的基线配置。后续怎么改都不怕,随时可以回退。
- 模型文件、评测集、中间结果、输出答案分目录管理,文件命名带上版本号和时间戳。
- 批量评测任务要加日志和失败重试。RAG 评测涉及大量 API 调用,单条超时不能影响整个评测流程。
- 接口服务要限制访问范围。如果 RAG 服务需要对外暴露,务必加鉴权和限流,避免被刷。
- 涉及内部敏感数据、客户数据、个人信息的知识库,必须落实脱敏和数据访问审计。上传到外部大模型 API 前要确认是否符合数据安全要求;建议优先使用本地部署模型或私有化网关。
- 知识库中包含第三方版权文档、来源不明的抓取内容,不能直接用于商业服务。需要在授权范围内使用,并保留引用溯源。
- 商用前要做效果复核。尤其面向用户直接展示的答案,建议加入工单复核机制,让真人对 AI 回答做抽检。
13. 下一步:Agentic RAG 与工程化扩展
当你的 RAG 系统通过上述优化稳定到达 80% 以上之后,下一步可以考虑引入 Agentic RAG。传统 RAG 是一次检索、一次生成,属于“开环”流程。Agentic RAG 则允许大模型在回答过程中决定要不要继续检索、要不要改写检索词、要不要调工具查询结构化数据。
| 能力 | 传统 RAG | Agentic RAG |
|---|---|---|
| 检索次数 | 一次固定检索 | 可多轮检索、子问题拆分 |
| 查询改写 | 无 | 可改写、可扩展同义词 |
| 工具调用 | 无 | 可调数据库、API、表格查询 |
| 信息融合 | 单段上下文 | 可多段信息交叉验证 |
| 适用场景 | 简单问答、FAQ | 复杂推理、多跳问答、跨库查询 |
如果你的知识库数据源比较杂,既有文档又有数据库、表格、工单系统,Agentic RAG 能把准确率再往上推一截。但需要注意,Agentic RAG 的延迟更高、Token 消耗更大,必须配合评测集做收益评估,不要盲目接入。
最后讲一点最实在的:RAG 从 60% 拉到 80%,靠的不是某一个模型,而是把切块、召回、重排、生成、评测这条链路完整跑一遍。建议收藏这份优化清单,下次调 RAG 的时候,直接按问题排查表逐项过一遍。