1. 先搞清楚:烂大街的到底是什么
这两年但凡跟 AI 沾边的团队,简历上不写个 RAG 项目都不好意思跟人打招呼。我面过不少人,十个里有八个说自己做过 RAG 知识库,细问下去,基本都是同一套流水线:文档切块、向量化、存进向量库、检索 Top-K、拼进 Prompt、丢给大模型生成。这套东西确实烂大街了,因为它的门槛已经低到任何一个会调 API 的人,花一个周末就能跑通一个 Demo。
但问题在于,Demo 能跑通和线上能用,中间隔着的不是一条河,是一片海。我见过太多团队兴冲冲上线了 RAG 系统,结果用户问三个问题就开始胡言乱语,检索回来的内容驴唇不对马嘴,多跳推理直接歇菜,表格和图片里的信息完全丢失。然后他们就开始怀疑 RAG 这条路是不是走不通。
不是 RAG 走不通,是你只走了那条最平庸的流水线。真正决定一个 RAG 系统能不能打的分水岭,从来不在“有没有做 RAG”,而在下面这六个地方。这篇文章我会把这六处分水岭一个一个拆开,讲清楚每一处到底难在哪、为什么大多数方案会在这里翻车、以及我实际踩过坑之后总结出来的应对思路。不管你是刚入门想搞清楚 RAG 到底该怎么深入,还是已经做过几个项目但总觉得效果差口气,这篇应该都能给你一些能直接抄作业的东西。
2. 分水岭一:检索质量——从“能查到”到“查得准”
2.1 为什么 Top-K 向量检索经常不靠谱
大部分 RAG 教程教你的检索方式,就是把用户问题向量化,然后在向量库里做相似度搜索,取 Top-K 个最相似的块。这个方法在简单场景下能用,但它有一个致命的假设:语义相似等于内容相关。
实际业务里这个假设经常不成立。举个例子,用户问“A 产品的退款政策是什么”,向量检索可能会召回一段讲“B 产品退款流程”的内容,因为两段文本在语义空间里非常接近,都包含“退款”“政策”“流程”这些概念。但用户要的是 A 产品,你给他 B 产品的信息,生成出来的答案就是错的。
更麻烦的是,向量检索对精确匹配类的问题几乎无能为力。用户问“错误码 E5023 是什么意思”,向量模型很可能把“E5023”这个关键标识在编码过程中稀释掉了,召回一堆讲错误处理的通用文档,就是不给你那条精确包含 E5023 的记录。
2.2 混合检索:把关键词召回和语义召回拼起来
我目前最稳定的做法是混合检索,也就是把 BM25 这类稀疏检索和向量稠密检索的结果融合。BM25 负责精确匹配,向量负责语义泛化,两路召回后用 RRF 做融合排序。
具体操作上,Elasticsearch 和 Milvus 都支持原生混合检索,如果你用的是 LangChain,可以直接用EnsembleRetriever把两个 retriever 拼起来。关键参数是两路的权重,我的经验是初始阶段 BM25 权重给 0.3 到 0.4,向量给 0.6 到 0.7,然后根据实际 badcase 调。如果业务里专有名词、型号、错误码特别多,BM25 的权重可以再往上提。
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import Milvus bm25_retriever = BM25Retriever.from_documents(docs, k=10) vector_retriever = Milvus.from_documents(docs, embedding).as_retriever(search_kwargs={"k": 10}) ensemble = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6] )注意:混合检索的收益在文档量大、专有名词多的场景下最明显。如果你的知识库只有几十页通用文档,混合检索的提升可能感知不到,别为了用而用。
2.3 Rerank 模型:检索后的第二道闸
混合检索解决了召回率的问题,但 Top-K 里仍然会混进不相关的内容。这时候需要一个 Rerank 模型来做精排。Rerank 模型和向量模型的区别在于,它不是分别编码 query 和 document 再算距离,而是把 query 和 document 拼在一起做交叉编码,能捕捉到更细粒度的相关性。
我常用的 Rerank 方案是 BGE-Reranker 系列,中文场景下 bge-reranker-v2-m3 效果很稳。流程是先用混合检索召回 20 到 50 条,再用 Rerank 精排出 Top-5 送给大模型。这样既保证了召回率,又保证了送入上下文的内容质量。
实测下来,加了 Rerank 之后,答案准确率能提升 15 到 25 个百分点,尤其是那些“看起来像但实际不相关”的干扰项会被大幅过滤掉。代价是增加了一次模型推理,延迟大概多 100 到 300 毫秒,这个开销在大多数场景下完全值得。
3. 分水岭二:切块策略——别再用固定长度硬切了
3.1 固定长度切块的三个致命伤
几乎所有入门教程都会教你按 512 或 1024 个 token 固定长度切块,加一点 overlap 防止语义断裂。这个方法不是不能用,但它有三个致命伤。
第一个是语义截断。一个完整的论述段落可能刚好在切块边界被劈成两半,前半段在块 A,后半段在块 B,检索到块 A 的时候信息不完整,生成出来的答案缺胳膊少腿。
第二个是上下文丢失。固定切块出来的片段是孤立的,没有文档结构信息。用户问“第三章第二节讲了什么”,你的检索系统根本不知道哪个块属于第三章第二节,因为切块的时候这些结构信息全丢了。
第三个是表格和代码被切碎。一个表格被切成三块,每块只有几行数据,列头可能只在第一块里,后面两块的数据没有列头就完全没法理解。
3.2 结构化切块:按文档自身结构来
我的做法是优先按文档自身结构切块。Markdown 按标题层级切,HTML 按 DOM 结构切,PDF 先做版面分析再按段落和表格切。LangChain 里有MarkdownHeaderTextSplitter可以直接用,它会根据标题层级把文档切成树状结构,每个块都带着自己的标题路径。
from langchain_text_splitters import MarkdownHeaderTextSplitter headers_to_split_on = [ ("#", "h1"), ("##", "h2"), ("###", "h3"), ] splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on) chunks = splitter.split_text(markdown_content)这样切出来的每个块都带着h1、h2、h3的元数据,检索的时候可以把标题路径一起拼进上下文,大模型就能知道这段内容在文档里的位置和层级关系。
3.3 父子块与句子窗口:兼顾精度和上下文
结构化切块解决了层级问题,但还有一个矛盾:块太大检索不精准,块太小上下文不够。父子块策略就是来解决这个矛盾的。
具体做法是,把文档切成大块(父块)和小块(子块),子块用于检索,命中子块后把对应的父块送给大模型。这样检索的精度靠子块保证,上下文的完整性靠父块保证。LangChain 里的ParentDocumentRetriever就是干这个的。
另一个类似的思路是句子窗口检索,命中某个句子后,把前后各 N 个句子一起送给大模型。这个方案在对话式问答里特别好用,因为答案往往需要前后文的语境才能理解。
实操心得:父子块的父块大小建议控制在 2000 到 4000 token,子块 200 到 500 token。父块太大容易引入噪声,太小又失去了上下文补充的意义。这个参数没有万能值,得拿你的实际数据跑评测集调。
4. 分水岭三:上下文增强——Contextual Retrieval 到底怎么落地
4.1 问题的根源:块是孤立的
Anthropic 提出的 Contextual Retrieval 思路,核心洞察是:每个切块在向量化之前,先让大模型给它生成一段上下文说明,把这块内容在整篇文档里的位置和背景补上,然后再做 embedding。这样检索的时候,块不再是孤立的,而是带着上下文信息的。
举个例子,原始块内容是“该功能默认关闭,需要在配置文件中手动开启”。这句话单独看,你不知道“该功能”是什么功能。Contextual Retrieval 会让大模型生成一段前缀,比如“本文档介绍的是数据同步模块的配置,其中增量同步功能的默认状态和开启方式如下”,然后把这段前缀拼到块前面一起做 embedding。
4.2 落地成本与优化方案
这个方案的效果确实好,Anthropic 官方数据是检索失败率降低 35%,结合 Rerank 能降低 67%。但它的成本问题也很现实:每个块都要调一次大模型生成上下文,如果你的知识库有十万个块,那就是十万次调用。
我的优化方案是批量处理加缓存。把同一篇文档的所有块一次性送给大模型,让它批量生成每块的上下文说明,这样比逐块调用省很多 token。另外,文档更新频率低的话,生成的上下文可以缓存起来,下次直接用。
def generate_context(chunk, full_doc, client): prompt = f"""以下是整篇文档的内容: <document> {full_doc} </document> 以下是需要处理的片段: <chunk> {chunk} </chunk> 请用一两句话说明这个片段在整篇文档中的位置和背景,不要重复片段内容本身。""" response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}] ) return response.choices[0].message.content注意:生成上下文的模型不需要用最强的,gpt-4o-mini 或者本地的小模型就够用,因为这个任务本身不难。用便宜模型能把成本压到原来的十分之一。
5. 分水岭四:多跳推理——Agentic RAG 和 GraphRAG 的用武之地
5.1 单次检索搞不定的问题长什么样
传统 RAG 是单次检索:用户问一个问题,检索一次,生成答案。但实际业务里大量问题需要多跳推理。比如“我们公司去年营收增长最快的产品线,它的负责人是谁”,这个问题需要先查营收数据找到增长最快的产品线,再查组织架构找到负责人。单次检索要么只能召回营收数据,要么只能召回组织架构,没法把两跳串起来。
再比如“A 方案和 B 方案在安全性上的差异是什么”,这需要分别检索 A 和 B 的安全文档,然后做对比。单次检索如果只召回了一边的文档,答案就是片面的。
5.2 Agentic RAG:让模型自己决定怎么查
Agentic RAG 的思路是把检索变成 Agent 的一个工具,让模型自己决定什么时候查、查什么、查几次。用 LangGraph 可以很自然地实现这个流程:定义一个 Agent 节点和一个工具节点,Agent 决定调用检索工具,拿到结果后判断信息是否足够,不够就继续查,够了就生成答案。
from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode def should_continue(state): last_message = state["messages"][-1] if last_message.tool_calls: return "tools" return END workflow = StateGraph(AgentState) workflow.add_node("agent", agent_node) workflow.add_node("tools", ToolNode([retriever_tool])) workflow.set_entry_point("agent") workflow.add_conditional_edges("agent", should_continue) workflow.add_edge("tools", "agent") app = workflow.compile()这个方案的好处是灵活,能处理各种复杂的查询模式。代价是延迟增加,因为可能要多次调用大模型和检索。我的经验是给 Agent 设置一个最大迭代次数,比如 5 次,防止它陷入死循环。
5.3 GraphRAG:当关系比内容更重要
GraphRAG 解决的是另一类问题:实体之间的关系推理。它先从文档里抽取实体和关系,构建知识图谱,然后基于图谱做检索和推理。适合的场景是“A 和 B 有什么关系”“哪些实体同时满足条件 X 和 Y”这类问题。
微软开源的 GraphRAG 框架做了完整的实现,包括实体抽取、社区检测、层次化摘要。但它的构建成本很高,因为要对全量文档做实体关系抽取,token 消耗量很大。我的建议是只在关系密集型场景用 GraphRAG,比如法律条文、医疗知识、企业组织架构,普通文档问答用 Agentic RAG 就够了。
6. 分水岭五:多模态——图片和表格不能当不存在
6.1 图片和表格为什么是 RAG 的盲区
大部分 RAG 流水线只处理纯文本,PDF 里的图片直接跳过,表格被当成普通文本切碎。但实际业务文档里,图表和表格往往承载着最关键的信息。一份财报的核心数据在表格里,一份产品手册的关键参数在图表里,你把这些丢了,RAG 系统能回答的问题就少了一大半。
6.2 多模态 RAG 的三种落地路径
第一种是图片转文字。用多模态模型对图片生成描述,把描述文本存进向量库。这个方案实现简单,但会丢失图片里的细节信息,适合对精度要求不高的场景。
第二种是图文联合嵌入。用 CLIP 这类模型把图片和文本映射到同一个向量空间,检索的时候文本 query 可以直接召回相关图片。这个方案保留了图片的原始信息,但需要多模态嵌入模型和对应的向量库支持。
第三种是多模态大模型直接理解。检索到图片后,把图片和文本一起送给多模态大模型,让它直接看图回答。这个方案效果最好,但成本也最高,适合关键场景。
表格的处理相对成熟一些,用unstructured或者camelot做表格抽取,把表格转成 Markdown 或 HTML 格式保留结构,然后单独做 embedding。检索到表格后,把完整的表格结构送给大模型,它就能理解行列关系了。
实操心得:多模态 RAG 不要一上来就追求全链路多模态,先把表格处理好,收益最明显。图片处理根据业务价值排优先级,核心图表优先处理,装饰性图片直接跳过。
7. 分水岭六:评测与迭代——没有度量就没有优化
7.1 为什么大多数 RAG 项目没有评测
我见过太多团队做 RAG 就是凭感觉,上线之后用户说不好用,但具体哪里不好用、哪个环节出了问题,完全不知道。没有评测体系,优化就是盲人摸象,今天调调切块大小,明天换换 embedding 模型,全凭运气。
RAG 的评测要分两个层面:检索质量和生成质量。检索质量看召回率、精确率、MRR 这些指标,生成质量看答案的忠实度、相关性、完整性。
7.2 用 RAGAS 搭建自动化评测流水线
RAGAS 是目前最成熟的 RAG 评测框架,它定义了几个核心指标:Faithfulness 衡量答案是否忠实于检索到的上下文,Answer Relevancy 衡量答案和问题的相关程度,Context Precision 和 Context Recall 衡量检索质量。
from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall result = evaluate( dataset=eval_dataset, metrics=[faithfulness, answer_relevancy, context_precision, context_recall] ) print(result)搭建评测集的关键是构造高质量的问答对。我的做法是从真实用户问题里采样,人工标注标准答案和相关的文档块,然后定期跑评测,看每次改动对各项指标的影响。
7.3 持续迭代的节奏
我的迭代节奏是这样的:每周跑一次全量评测,看整体指标趋势;每次上线新功能前跑一次回归评测,确保没有退化;日常收集 badcase,每周分析一次,找出共性问题,排优先级修复。
评测集本身也要迭代,随着业务变化补充新的问题类型。一个覆盖全面的评测集,比任何调参技巧都值钱。
8. 常见问题与排查技巧实录
8.1 检索相关但答案不对
这是最常见的问题。检索回来的内容确实相关,但大模型生成的答案就是不对。排查思路是先把检索到的上下文打印出来,人工看一遍,确认信息是否真的足够回答问题。如果信息足够但答案不对,问题出在 Prompt 上,需要调整 Prompt 让模型更忠实地使用上下文。如果信息不够,问题出在检索环节,需要检查切块策略和检索参数。
8.2 多轮对话中检索漂移
用户在多轮对话里,后续问题往往省略了主语,比如第一轮问“A 产品的价格”,第二轮问“那 B 产品呢”。如果直接用第二轮的问题去检索,很可能召回一堆不相关的内容。解决方案是在检索前做 query 改写,把多轮对话的上下文压缩成一个独立的检索 query。LangChain 里的ConversationalRetrievalChain内置了这个能力,或者用 LLM 做 query rewriting 也行。
8.3 知识库更新后检索结果不一致
文档更新后,旧的向量还在库里,新的向量也进去了,检索的时候新旧内容混在一起。解决方案是给每个块打上版本号和时间戳,检索时过滤掉过期版本。Milvus 和 Qdrant 都支持标量字段过滤,可以在检索时加上时间范围条件。
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 检索相关但答案不对 | Prompt 不够忠实或上下文不足 | 打印检索上下文人工检查 | 调整 Prompt 或优化检索 |
| 多轮对话检索漂移 | Query 缺少上下文 | 检查检索 query 是否完整 | 加 Query 改写环节 |
| 更新后结果不一致 | 新旧向量混杂 | 检查是否有版本过滤 | 加时间戳和版本过滤 |
| 表格信息丢失 | 切块时表格被切碎 | 检查表格抽取逻辑 | 表格单独处理保留结构 |
| 专有名词召回差 | 向量模型稀释了关键词 | 检查 BM25 是否启用 | 启用混合检索 |
8.4 延迟太高用户等不及
RAG 的延迟主要来自三块:检索、Rerank、生成。检索和 Rerank 的延迟可以通过缓存和并行化优化,生成的延迟取决于大模型。我的做法是把检索和 Rerank 做成异步,生成用流式输出,用户能尽快看到第一个 token。另外,高频问题的答案可以做缓存,命中缓存直接返回,不用走完整流程。
9. 我踩过的坑和最后几条建议
第一个坑是过度依赖向量检索。我早期做的项目全部用纯向量检索,结果在专有名词密集的场景下召回率惨不忍睹。后来加了 BM25 混合检索,效果立竿见影。如果你的知识库里有大量型号、编码、人名,混合检索几乎是必选项。
第二个坑是切块参数拍脑袋定。我试过 256、512、1024 各种切块大小,最后发现没有万能值,必须拿实际数据跑评测。而且不同文档类型的最优切块大小不一样,技术文档适合大块,FAQ 适合小块,得分类处理。
第三个坑是忽略评测。我有个项目上线三个月没做评测,全靠用户反馈驱动优化,结果就是一直在修修补补,没有系统性提升。后来搭了 RAGAS 评测流水线,才发现瓶颈根本不在我以为的地方。
最后一条建议:RAG 的优化是个系统工程,六个分水岭不是孤立的,检索质量、切块策略、上下文增强、多跳推理、多模态、评测迭代,每一处都会影响最终效果。不要指望换一个 embedding 模型或者调一个参数就能解决问题,要系统性地看整个链路,找到真正的瓶颈再动手。我个人的经验是,先把评测体系搭起来,有了度量之后,优化方向自然就清晰了。