ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG项目落地必看:5个常见问题与实战解决方案

RAG项目落地必看:5个常见问题与实战解决方案

1. RAG 项目落地前必须想清楚的几个核心问题

RAG 这个词这两年热度一直没降过,从最早的 naive RAG 到现在的 agentic rag、ontology rag,各种概念层出不穷。但我在实际带团队做 rag 知识库项目的过程中发现,真正让项目翻车的往往不是什么高深的技术难题,而是几个反复出现的基础问题。这篇文章我整理了 RAG 最常遇到的 5 个问题,每个问题都给出我在实战中验证过的解决办法,附带可以直接抄的参数和配置。

先说清楚这篇文章适合谁看。如果你正在做 rag 项目,或者准备用 langchain4j easy rag、ollama + 简易本地 rag 知识库这类方案搭一个能用的知识库,那这篇内容基本覆盖了你 80% 会踩的坑。如果你还在选型阶段,纠结用 agentscope 2.0 rag as service 还是自己搭,那也可以先看看这些问题,评估一下自己的团队能不能扛住。

RAG 的本质其实不复杂,就是给大模型外挂一个知识库,让它回答问题时先去查资料再组织语言。但就是这么一个看似简单的流程,从文档解析、切片、向量化、检索、重排到最终生成,每一步都有坑。下面我按问题出现的频率和破坏力排序,逐个拆解。

2. 问题一:检索结果不相关,答非所问

这是 RAG 最致命也最常见的问题。用户问了一个问题,系统检索回来的文档片段跟问题八竿子打不着,大模型拿着这些不相关的上下文硬编,最后输出的答案要么是胡扯,要么是“根据已知信息无法回答”。这个问题不解决,整个 RAG 系统就是废的。

2.1 为什么会检索不相关

根本原因通常出在三个环节。第一是切片策略太粗暴,固定长度切片把一段完整的语义切碎了,导致每个片段都缺胳膊少腿。第二是 embedding 模型选得不对,中文场景用了英文为主的模型,语义空间对不上。第三是检索策略太单一,只用了向量相似度,没有结合关键词匹配。

我见过一个典型的案例,一个法律知识库项目,文档切片设成了固定 512 字符,结果一个法条被切成了三段,用户问“劳动合同解除的赔偿标准”,检索回来的片段只有“用人单位应当向劳动者支付经济补偿”这半句话,后面的计算标准在下一个片段里,但没被检索到。大模型拿到这半句话,只能瞎猜。

2.2 切片策略的调整方案

切片不是越细越好,也不是越粗越好。我的经验是,切片大小要跟文档类型和查询粒度匹配。对于结构化的文档,比如产品手册、法律条文,最好按标题层级切,保证每个片段是一个完整的语义单元。对于非结构化的长文本,可以用递归切片,先按段落切,段落太长再按句子切。

具体参数上,我实测下来比较稳的配置是:chunk_size 设在 256 到 512 之间,chunk_overlap 设在 chunk_size 的 10% 到 20%。overlap 的作用是防止关键信息刚好落在切片边界上被切断。比如 chunk_size 是 400,overlap 设 80,这样每个片段跟下一个片段有 80 个字符的重叠,边界信息不会丢。

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], length_function=len, )

注意 separators 的顺序很重要,中文场景一定要把中文标点加进去,而且优先级要放在空格前面。很多教程直接用默认的英文分隔符,中文文档切出来全是乱的。

2.3 Embedding 模型的选择

embedding 模型直接决定了语义检索的质量。中文场景我推荐几个经过验证的选项:BGE 系列的中文模型、M3E 模型、以及 text-embedding-ada-002 这类通用模型。如果做本地部署,BGE-large-zh 是性价比很高的选择,显存占用大概 1.5G,检索效果比很多商业 API 都好。

选 embedding 模型的时候要注意一点,不同模型输出的向量维度不一样,BGE-large-zh 是 1024 维,M3E-base 是 768 维。维度越高表达能力越强,但存储和检索成本也越高。如果你的知识库规模在十万条以内,1024 维完全扛得住。超过百万条就要考虑用量化或者降维了。

还有一个坑是,embedding 模型换了之后,整个知识库必须重新向量化。我见过有人换了模型只重新处理了新文档,旧文档还用原来的向量,结果检索出来的结果乱七八糟。这个一定要避免。

2.4 混合检索的引入

单纯靠向量检索有个天然缺陷,就是对精确匹配不敏感。比如用户问“XX-2000 型号的参数”,向量检索可能返回一堆“XX 系列”的文档,但就是找不到精确的“XX-2000”。这时候就需要引入关键词检索,把 BM25 或者全文索引的结果跟向量检索的结果融合。

融合的策略有两种,一种是加权求和,一种是 RRF(Reciprocal Rank Fusion)。RRF 更简单也更稳定,不需要调权重。它的逻辑是,对每个文档在两种检索结果中的排名取倒数再求和,排名越靠前得分越高。

def rrf_fusion(vector_results, keyword_results, k=60): scores = {} for rank, doc in enumerate(vector_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1) for rank, doc in enumerate(keyword_results): scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

k 值一般设 60,这是原论文里的推荐值,我实测下来 40 到 80 之间效果差别不大。

注意:混合检索不是万能的,如果关键词检索的召回质量太差,反而会拉低整体效果。建议先用一批测试 query 分别评估两种检索的召回率,再决定要不要融合。

3. 问题二:大模型拿着正确的资料却答错

这个问题比检索不相关更隐蔽。检索回来的文档是对的,但大模型要么理解错了,要么把多个文档的信息混淆了,要么直接忽略了关键信息。用户看到的是一个自信满满的错误答案,比“不知道”还危险。

3.1 上下文窗口的利用问题

很多人以为把检索到的所有片段一股脑塞进 prompt 就行了,但实际上大模型对上下文的使用是有偏好的。研究表明,大模型对上下文开头和结尾的信息利用得最好,中间部分容易被忽略,这就是所谓的“lost in the middle”现象。

解决办法有两个。一是控制检索返回的片段数量,不要贪多。我一般设 top_k 在 3 到 5 之间,超过 5 个片段之后,新增片段带来的收益急剧下降,反而增加了干扰。二是对片段进行重排序,把最相关的放在最前面和最后面。

重排序可以用专门的 rerank 模型,比如 BGE-reranker 系列,它会对每个片段和 query 的相关性打一个精细的分数,比向量相似度准得多。流程是先用向量检索召回 20 到 50 个候选,再用 rerank 模型精排取 top 3 到 5。

from FlagEmbedding import FlagReranker reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True) pairs = [[query, doc.text] for doc in candidate_docs] scores = reranker.compute_score(pairs) ranked_docs = [doc for _, doc in sorted(zip(scores, candidate_docs), reverse=True)]

rerank 模型的计算开销比 embedding 大,但只对少量候选做精排,整体延迟增加在可接受范围内。我实测下来,加了 rerank 之后,答案准确率能提升 15 到 25 个百分点。

3.2 Prompt 模板的设计

Prompt 模板直接决定了大模型怎么使用检索到的上下文。我见过很多项目用的 prompt 就是简单的一句“根据以下资料回答问题”,这种模板太粗糙了。

一个好的 RAG prompt 应该包含几个要素:明确的角色设定、对上下文的使用指令、对不确定性的处理方式、以及输出格式要求。比如:

你是一个严谨的知识库助手。请根据下面提供的参考资料回答用户问题。 规则: 1. 只使用参考资料中的信息回答,不要编造。 2. 如果参考资料中没有相关信息,直接回答“根据现有资料无法回答”。 3. 如果多个资料之间有冲突,指出冲突并说明。 4. 回答时引用具体的资料来源编号。 参考资料: [1] {doc1} [2] {doc2} [3] {doc3} 用户问题:{query}

这个模板的关键在于第 2 条和第 3 条。很多项目不敢让模型说“不知道”,结果模型就硬编。明确允许模型说不知道,反而能大幅降低幻觉率。

3.3 上下文冲突的处理

当检索回来的多个片段包含矛盾信息时,大模型往往会随机选一个,或者把矛盾的信息糅在一起。这个问题在知识库有多个版本文档时特别常见。

我的处理方式是在 prompt 里明确要求模型指出冲突,同时在检索阶段做去重和版本过滤。具体做法是给每个文档片段打上版本标签和时间戳,检索时优先返回最新版本,如果检测到同一主题有多个版本,在 prompt 里标注出来让模型注意。

实操心得:我习惯在文档入库时就给每个片段加上 metadata,包括来源文件、版本号、更新时间、章节路径。这些 metadata 在检索和 prompt 构建时都能用上,是提升 RAG 效果的低成本手段。

4. 问题三:知识库更新后检索效果变差

RAG 知识库不是建好就完事了,文档会更新,新知识要加入,旧知识要淘汰。很多项目在第一次建库时效果不错,但更新几次之后检索质量就明显下降。

4.1 增量更新的向量一致性问题

前面提到过,embedding 模型换了要全量重建。但即使模型没换,增量更新也有坑。比如你用的 embedding 服务有版本更新,新文档用新版本向量化,旧文档还是旧版本,两个版本的向量空间可能有细微差异,导致检索时新旧文档的相似度不可比。

解决办法是固定 embedding 模型的版本,并且在 metadata 里记录向量化时用的模型版本。如果必须升级模型,就安排一次全量重建,不要做混合。

4.2 文档删除与向量清理

文档删除了,对应的向量也要删。但很多向量数据库的删除操作不是实时的,或者删除后索引没有及时重建,导致已删除的文档还能被检索到。这个问题在用 FAISS 这类内存索引时特别明显。

我的做法是维护一个文档 ID 到向量 ID 的映射表,删除文档时先标记删除,检索后过滤掉已标记的向量,然后定期做一次索引重建彻底清理。这样既保证了检索结果的正确性,又避免了频繁重建索引的开销。

# 检索后过滤已删除文档 def filter_deleted(results, deleted_ids): return [r for r in results if r.doc_id not in deleted_ids]

4.3 知识库膨胀后的检索精度下降

知识库越大,检索的难度越高。当知识库从几千条膨胀到几十万条时,原本好用的 top_k=5 可能就不够了,因为相似度分布变得更分散,真正相关的文档可能排在十几名开外。

这时候有几个应对策略。一是提高召回数量,top_k 从 5 提到 20,再用 rerank 精排。二是引入元数据过滤,让用户在提问时能指定范围,比如“在 2024 年的文档中查找”。三是做聚类索引,把知识库按主题分成多个子库,检索时先路由到相关子库再检索。

元数据过滤是最实用的手段。比如一个企业知识库,可以按部门、文档类型、时间范围做过滤。用户问“技术部的报销标准”,就先过滤出技术部相关的文档再检索,召回率和精度都能大幅提升。

5. 问题四:响应速度慢,用户体验差

RAG 系统的延迟主要来自三个环节:检索、rerank、生成。检索和 rerank 通常在几百毫秒内,生成是大头,尤其是用大参数模型时,首 token 延迟可能好几秒。用户等太久就会觉得系统卡。

5.1 检索环节的优化

向量检索的延迟跟索引类型和向量维度有关。用 HNSW 索引比暴力检索快几个数量级,但会损失一点召回率。我一般用 HNSW,参数 ef_search 设 64 到 128 之间,在召回和速度之间取平衡。

如果知识库规模不大,几万条以内,其实暴力检索也能接受,延迟在几十毫秒。规模上去了就必须用近似索引。另外,向量维度也可以优化,1024 维降到 256 维,检索速度能提升 3 到 4 倍,召回率损失在 5% 以内,性价比很高。

5.2 生成环节的流式输出

生成环节的优化最有效的就是流式输出。用户不需要等完整答案生成完,首 token 一出来就可以开始展示,体感延迟从几秒降到几百毫秒。现在主流的大模型 API 都支持流式,本地部署的模型用 vLLM 这类推理框架也能开流式。

# 流式输出示例 for chunk in llm.stream(prompt): print(chunk.content, end="", flush=True)

流式输出配合前端打字机效果,用户感知到的响应速度会快很多。这个改动成本很低,但体验提升非常明显。

5.3 缓存策略

RAG 系统里有很多可以缓存的东西。embedding 结果可以缓存,同一个 query 第二次来就不用重新向量化了。检索结果可以缓存,热门问题的检索结果直接复用。生成结果也可以缓存,对于完全相同的 query,直接返回上次的答案。

我用 Redis 做了一层语义缓存,把 query 的 embedding 存起来,新 query 来了先算相似度,如果跟缓存里的某个 query 相似度超过 0.95,就直接返回缓存的答案。这个策略对 FAQ 类场景特别有效,命中率能到 30% 以上,平均延迟直接砍半。

注意:语义缓存要设过期时间,知识库更新后缓存要失效,否则会返回过时的答案。我一般设 24 小时过期,知识库更新时主动清空相关缓存。

6. 问题五:评估困难,不知道效果好不好

RAG 系统做完了,怎么知道它好不好?很多团队就是找几个人随便问几个问题,觉得答案还行就上线了。这种评估方式太主观,上线后遇到 bad case 才发现问题。

6.1 建立评估数据集

评估的第一步是有一批标注好的测试数据。我一般会准备 100 到 200 个问题,每个问题标注好标准答案和相关的文档片段。这批数据要覆盖不同的查询类型:事实型查询、比较型查询、多跳查询、否定型查询等。

构建评估集的方式有两种。一种是从真实用户日志里采样,这种方式最贴近实际场景。另一种是让大模型基于知识库自动生成问题和答案,这种方式成本低但质量参差不齐,需要人工审核。

6.2 检索和生成分开评估

RAG 的评估要拆成检索和生成两部分,否则出了问题不知道是哪一环的锅。

检索评估用 recall@k 和 MRR 指标。recall@k 衡量的是前 k 个结果里包含相关文档的比例,MRR 衡量的是第一个相关文档的排名。我一般看 recall@5 和 recall@10,recall@5 低于 0.8 就说明检索有问题。

生成评估用 faithfulness 和 answer relevance 两个指标。faithfulness 衡量答案是否忠于检索到的上下文,answer relevance 衡量答案是否切题。这两个指标可以用大模型来打分,虽然不如人工准,但胜在成本低、可批量。

评估维度指标合格线优化手段
检索recall@50.8调整切片、换 embedding、加混合检索
检索MRR0.7加 rerank、优化索引
生成faithfulness0.9优化 prompt、加引用要求
生成answer relevance0.85优化 prompt、加 few-shot 示例

6.3 持续监控与 bad case 分析

上线不是终点,要持续监控。我会记录每次查询的检索结果、生成的答案、用户反馈。用户点了“没用”的 query 就是 bad case,定期分析这些 bad case,找出共性问题。

常见的 bad case 模式有几种:检索不到相关文档、检索到了但排序靠后、检索到了但模型没用、模型用了但理解错了。每种模式对应不同的优化方向。我一般每周做一次 bad case 复盘,把高频问题整理成优化任务。

实操心得:bad case 分析不要只看单个案例,要聚类看模式。我习惯把 bad case 按问题类型分组,比如“型号查询失败”“多条件查询失败”“否定查询失败”,然后针对每组设计优化方案。这样比零散地修 bug 效率高得多。

7. 几个容易被忽略的细节问题

除了上面五个大问题,还有一些细节问题容易被忽略,但影响不小。

7.1 文档解析的质量

PDF、Word、PPT 这些格式的文档,解析质量直接决定了后续所有环节的效果。我见过太多项目在文档解析上偷懒,用个简单的库把 PDF 转成文本就完事了,结果表格变成了一堆乱码,图片里的文字全丢了。

PDF 解析我推荐用专门做版面分析的工具,能识别表格、图片、标题层级。如果文档里有大量表格,解析后要单独处理,把表格转成结构化的文本描述,而不是直接丢给切片器。

7.2 多语言场景的处理

如果知识库包含多种语言,embedding 模型要选多语言的,切片策略也要考虑语言差异。中文按字符切,英文按单词切,混在一起切容易出问题。我的做法是按语言分库,检索时根据 query 的语言路由到对应的库。

7.3 权限控制

企业知识库往往有权限要求,不同用户能看到的文档不一样。这个要在检索阶段就做过滤,不能等生成完了再过滤。具体做法是在向量数据库的 metadata 里存权限标签,检索时带上用户的权限条件做过滤。

# 带权限过滤的检索 results = vector_store.search( query_vector, top_k=10, filter={"department": user.department, "level": {"$lte": user.level}} )

权限过滤一定要在检索阶段做,否则检索结果里包含了用户无权查看的文档,即使最后没展示,也存在信息泄露风险。

8. 从 naive RAG 到 agentic RAG 的演进思路

最后聊一下 RAG 的演进方向。naive RAG 就是简单的“检索-生成”两步走,适合简单的事实型问答。但实际业务场景往往更复杂,需要多跳推理、需要调用工具、需要多轮交互,这时候 naive RAG 就不够了。

agentic RAG 的思路是让模型自己决定什么时候检索、检索什么、检索几次。比如用户问“对比 A 产品和 B 产品的参数”,agentic RAG 会先检索 A 的参数,再检索 B 的参数,然后对比。这种模式灵活性强,但实现复杂度也高,需要模型有较强的规划和工具调用能力。

我的建议是,不要一上来就搞 agentic RAG。先把 naive RAG 的五个基础问题解决好,把检索质量和生成质量做扎实,再考虑往 agentic 方向演进。基础不牢,上再高级的架构也是白搭。

ontology rag 是另一个方向,通过构建知识图谱来增强检索。它适合实体关系复杂的场景,比如医疗、金融。但构建和维护知识图谱的成本很高,不是所有场景都值得投入。

langchain4j easy rag 和 ollama + 简易本地 rag 知识库这类方案,适合快速验证和轻量级场景。它们的优势是上手快、依赖少,但灵活性和可扩展性有限。如果只是做个内部工具或者 demo,用这些方案完全够用。如果要上生产环境,还是要自己搭一套可控的流程。

agentscope 2.0 rag as service 这类服务化的方案,适合不想自己维护基础设施的团队。但要注意数据安全和供应商锁定问题,敏感数据场景要谨慎评估。

不管选哪种方案,上面讲的五个问题都是绕不开的。检索不相关、生成不准确、更新后效果变差、响应慢、评估困难,这些问题在任何 RAG 系统里都会出现,只是程度不同。把这些问题解决好,RAG 项目就成功了一大半。

我在实际项目里踩过的坑远不止这些,但上面这五个是最普遍、最影响效果的。每个问题的解决办法我都尽量给出了具体的参数和代码,你可以直接拿去试。RAG 这个领域变化很快,新的模型和工具层出不穷,但底层的逻辑是不变的:好的检索加上好的生成,再加上持续的评估和优化。把这三点做好,效果不会差。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进