三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

07 RAG架构设计:我踩过的坑和做过的取舍

07 RAG架构设计:我踩过的坑和做过的取舍

这是”AI Agent落地:业务系统智能化改造”系列的第七篇。第六篇讲了RAG全景——是什么、怎么演进、每个环节的作用。这篇讲我实际搭RAG系统时的设计决策:为什么这样选、哪些work、哪些还没验证。
不是”正确答案”,而是”决策过程”。


目录

引言:架构不是画出来的,是踩出来的

一、整体架构:从需求倒推

二、五个关键设计决策

  • 分块策略 / 检索通道 / 融合策略 / 上下文组织 / 生成控制

三、踩坑清单

四、待优化的问题

五、上手的建议

总结


引言:架构不是画出来的,是踩出来的

第六篇讲了RAG的7层管线、四类索引、混合检索、Rerank。那些是”业界共识”,教科书上能查到。

这篇讲教科书上查不到的东西:面对具体业务,每一层怎么选、为什么选、选错了怎么办。

我搭的RAG系统服务的是乡村产业监测类的应用:800+政策文档、15900+分块、用户是政府管理人员。他们问的问题五花八门——”山东省2024年产业园申报情况”(精确词项)、”哪些产业发展比较好”(语义模糊)、”帮我做个对比分析”(多步推理)。

没有一种检索策略能覆盖所有场景。架构设计的本质是:在有限资源下,为你的业务场景做最优组合。

这篇文章讲五件事:整体架构怎么从需求倒推、五个关键设计决策怎么做取舍、六个真实踩坑、四个还没解决的问题、给读者的五条建议。每个决策讲三件事:选项有哪些、我选了什么、为什么。


一、整体架构:从需求倒推

业务需求分析

在画架构之前,先搞清楚用户怎么用这个系统:

用户行为示例对RAG的要求
精确查询“2024年山东省产业园名单”关键词匹配必须准
语义查询“哪些产业发展势头好”需要语义理解
表格查询“各省产业园数量对比”需要结构化数据检索
多步分析“对比山东和浙江的产业差异”需要多次检索+综合
报告生成“帮我写一份分析报告”需要大量上下文+防幻觉

五种行为,对检索的要求完全不同。单一检索通道不可能覆盖。

架构选择

已验证:最终的架构是”5通道检索 + 3级策略 + 防幻觉体系”:

用户提问 │ ▼ 查询分析(意图识别 + 查询改写) │ ├──→ FTS全文检索(BM25,精确词项) ├──→ 向量检索(Embedding,语义匹配) ├──→ 表格检索(结构化数据查询) ├──→ 事实表检索(预构建的事实对) └──→ 文档目录检索(按文档结构定位) │ ▼ RRF融合排序 │ ▼ Rerank精排 │ ▼ 上下文组织(裁剪 + 去重 + 分层) │ ▼ 生成控制(引用溯源 + 语料收缩 + answer_guard) │ ▼ 输出

有把握:5通道的设计逻辑是对的——不同类型的查询需要不同的检索方式。

待验证:当文档量增长到2000+时,5通道的检索延迟是否还能保持在可接受范围内(目前800+文档时延迟约200-500ms)。


二、五个关键设计决策

决策1:分块策略——固定 vs 语义 vs 结构

选项

策略做法优势劣势
固定长度按500字符切分简单、可控容易在句子中间截断
语义切片按语义相似度断开语义完整计算量大,阈值敏感
按文档结构以标题、章节为边界保留文档逻辑单节过长时需二次切分
Parent-Child小块检索,大块生成兼顾精度和上下文需要维护两级索引

我选了什么:按文档结构切分 + 固定长度二次切分。

为什么

我的文档主要是政策文件和案例报告,特点是章节结构清晰、段落逻辑完整。按标题切分能保留”一个块≈一个主题”的语义完整性。但有些章节太长(比如一个案例报告可能有3000字),所以超长的章节用固定长度二次切分,chunk_size=500,overlap=50。

没选语义切片的原因:计算成本太高。800+文档、15900+块,逐句向量化再比较相似度,离线索引时间会从分钟级变成小时级。对于政策文档这种结构化程度高的文本,结构切分的效果已经够用。

没选Parent-Child的原因:增加了系统复杂度(两级索引、检索时需要回查Parent),而我的场景中,500字的块已经能提供足够的上下文。如果后续发现上下文不够,再升级到Parent-Child。

踩过的坑:最初用固定长度500字符切分,不看文档结构。结果一个政策文件的”第三条”被切成了两半,前半段在块A,后半段在块B。用户问”第三条是什么”,检索命中了块A,但答案不完整。改成结构切分后解决了。

确定性:结构切分+二次切分的组合,在800+文档的场景下效果验证过。但不同文档类型(比如纯表格、纯文本)可能需要不同策略,还没做对比实验。

决策2:检索通道——为什么需要5通道

背景:大多数RAG教程只讲两种检索——BM25和向量。但我发现两种不够。

用户查询的真实分布

查询类型占比只用BM25只用向量混合检索
精确词项(地名、年份、政策名)~30%✅ 能命中❌ 容易漏
语义模糊(”发展好的”)~25%❌ 命中不了✅ 能命中
表格数据(”各省数量”)~20%⚠️ 看分词❌ 向量不懂表格⚠️
事实查询(”谁负责”)~15%⚠️⚠️⚠️
结构导航(”第三章”)~10%

后三类查询,BM25+向量的混合检索覆盖不了。所以加了三个专用通道:

已验证:5通道比2通道(BM25+向量)在120个评测查询上的Recall@5从72%提升到89%。

待验证:事实表的维护成本。每新增一批文档,需要重新构建事实表。目前是手动+半自动,还没做到全自动化。

决策3:融合策略——RRF vs加权

选项

策略做法优势劣势
RRF按排名位置融合,score = Σ 1/(k+rank)不需要调权重,对不同分数尺度天然兼容丢失绝对分数信息
加权融合对各通道分数归一化后加权求和保留分数信息,可调权重需要归一化,BM25和向量分数的归一化本身引入误差

我选了什么:RRF,k=60。

为什么

  1. RRF不需要调权重。5个通道的分数尺度完全不同——BM25的分数是一个体系,向量相似度是另一个体系,表格检索可能返回精确匹配的布尔值。归一化这些异构分数本身就是个难题。
  2. RRF的工程实现简单,几行代码就够。
  3. k=60是业界通用经验值,在我的场景下没有调过,效果已经够用。

代价:RRF只有相对排序,没有绝对置信度。也就是说,我知道”这个结果排第一”,但不知道”这个结果有多好”。如果需要设”置信度低于0.3就不返回”的阈值,RRF做不到。

待验证:k=60是否是最优值。理论上应该在评测集上做网格搜索(k=10,20,30,…,100),但还没做。

决策4:上下文组织——TopK直接塞 vs 压缩 vs 分层

问题:检索返回了10个块,每个500字,总共5000字。全部塞给模型?还是压缩后再塞?

选项

策略做法优势劣势
TopK直接塞取前K个块直接拼接简单token消耗大,噪声多
上下文压缩用LLM提取每个块中和查询相关的部分减少噪声多一次LLM调用,增加延迟
分层披露先给摘要,需要时再给详情省token实现复杂

我选了什么:TopK直接塞 + 简单去重。

为什么:在当前800+文档的规模下,简单方案已经够用。上下文压缩需要额外的LLM调用,延迟翻倍;分层披露需要设计摘要策略,工程量大。如果后续文档量增长到万级,再考虑升级。

效果:在800+文档的规模下,Top5(2500字)已经能覆盖大多数查询的上下文需求。token消耗在可接受范围内。

代价:有时候5个块里有2个是重复信息(同一个政策被不同分块包含),浪费了token。简单去重(按文本相似度>0.9过滤)能解决一部分,但不彻底。

待验证:上下文压缩是否能显著提升生成质量。理论上应该做A/B测试——同样的查询,一组用TopK直接塞,一组用压缩后的上下文,比较答案质量。还没做。

决策5:生成控制——防幻觉的三层机制

RAG最大的风险不是检索不到,而是检索到了但模型不用,反而自己编

三层防幻觉机制

第一层:Prompt约束

你是一个乡村产业数据分析助手。请严格基于以下检索结果回答问题。 如果检索结果中没有相关信息,请明确说"暂无相关数据"。 回答时请标注信息来源。

已验证:Prompt约束能减少60%以上的幻觉。但不是100%——有些情况下模型还是会”创造性地”解读检索结果。

第二层:引用溯源

每个检索结果带元数据(文档名、页码、章节),生成答案时要求模型引用来源。用户可以点进去验证。

已验证:引用溯源本身不减少幻觉,但让幻觉变得可检测——如果引用的来源和答案对不上,用户能发现。

第三层:answer_guard

在答案返回前,用另一个LLM调用做一轮检查:

def answer_guard(question, answer, contexts):"""检查答案是否基于检索结果(简化示例,生产中用异步调用)""" prompt=f"""请判断以下回答是否完全基于提供的检索结果。 如果有任何信息不在检索结果中,请标记为"存在幻觉"。 问题:{question}回答:{answer}检索结果:{contexts}只返回:基于检索 / 存在幻觉 / 部分基于检索"""returnllm.invoke(prompt).content

有把握:这个机制能捕获大部分幻觉。但有两个问题:(1)多一次LLM调用,延迟增加;(2)guard模型本身也可能误判。

待验证:guard的准确率。理论上应该用标注数据集测precision/recall,但还没做。


三、踩坑清单

这些是我实际踩过的坑,按严重程度排序。

坑1:分块大小选错了,检索精度直接废了

现象:用户问”2024年产业园申报条件”,返回的5个块里只有1个真正相关,其他4个是噪声。

原因:chunk_size=1000太大。一个1000字的块里可能包含3个不同主题的内容,向量检索命中了其中1个主题,但其他2个主题是噪声。

解决:chunk_size从1000改到500,overlap从100改到50。块小了,语义更纯,检索精度提升。

效果:Recall@5从65%提升到78%。

教训:不要网上抄一个”最佳chunk_size”就用。拿你自己的数据,准备50个真实查询,跑评测。chunk_size的选择和文档类型强相关——政策文档500字合适,FAQ可能200字更好。

坑2:向量检索对精确词项不稳定

现象:用户问”山东省2024年产业集群名单”,向量检索返回的结果里没有”山东”,而是”河北”——因为”山东”和”河北”在向量空间里很近(都是省份)。

原因:Embedding模型对专有名词(地名、年份、编号)的区分度不如关键词检索。”2024”和”2023”在向量空间里的距离可能很近,但在业务上完全不同。

解决:加了BM25全文检索通道。精确词项走BM25,语义查询走向量,两路并行。

效果:精确词项查询的准确率从60%提升到92%。

教训:向量检索不是万能的。它擅长”意思相近”,不擅长”字面精确”。大多数生产场景推荐混合检索。

坑3:Rerank延迟太高,用户体验差

现象:加了Rerank模型后,检索延迟从200ms变成了800ms。用户感觉”卡了一下”。

原因:Rerank是Cross-Encoder模型,需要把查询和每个候选结果一起输入模型计算相关性分数。10个候选就是10次推理。

解决:两个优化——(1)Rerank的输入从Top10减少到Top5(先粗排再精排);(2)Rerank模型从大模型换成小模型(bge-reranker-base),精度损失约3%,延迟减少60%。

效果:总延迟从800ms降到350ms。

教训:Rerank是”精度vs延迟”的trade-off。不是候选越多越好,也不是模型越大越好。找到你的业务能接受的平衡点。

坑4:多轮对话的指代消解

现象

用户:山东省产业园有哪些? 系统:[返回12个产业园列表]用户:第三个的详细情况呢? 系统:[返回了一堆不相关的内容]

原因:”第三个”指代的是上一轮返回的列表中的第三个,但检索模块收到的查询是”第三个的详细情况呢”,没有上下文。

解决:在检索前,用LLM把指代消解成完整问题。把对话历史和当前问题一起传给LLM,让它改写成独立可检索的问题。

def resolve_reference(history, current_query): prompt=f"""对话历史:{history}当前问题:{current_query}请将当前问题改写为一个独立的、不含指代的问题。只返回改写后的问题。"""returnllm.invoke(prompt).content# "第三个的详细情况呢" → "山东省兰陵县国家现代农业产业园的详细情况"

有把握:这个方案能解决大部分指代问题。但增加了每次查询的LLM调用次数。

待验证:指代消解的准确率。有些复杂的指代(比如”那个去年说的政策”)可能消解不了。

坑5:评测集没覆盖边界场景

现象:系统在常规查询上效果不错,但遇到边界情况就翻车——比如空查询(用户只发了一个”?”)、超长查询(用户粘贴了一整段话)、多语言查询(中英混杂)。

原因:最初的评测集只有50个”正常”查询,没有覆盖边界场景。

解决:扩充评测集到120个查询,增加了30个边界场景:

教训:评测集的质量决定了你能发现多少问题。50个”正常”查询的评测集,给你一种”效果很好”的错觉。真正的考验在边界场景。

补充一点:评测集本身的代表性也很重要。如果120个查询都是你自己想出来的,可能和真实用户的查询分布有偏差。更好的做法是从线上日志中采样真实查询,按场景分类后构建评测集。每月校准一次,确保评测集覆盖了新出现的查询模式。

坑6:表格数据检索是个大坑

现象:用户问”各省产业园数量排名”,系统返回了一堆文字描述,没有返回表格数据。

原因:表格在文档里是以图片或PDF表格形式存在的,切片后变成了纯文本,丢失了行列结构。向量检索把表格当普通文本处理,自然检索不到结构化信息。

解决:表格单独处理——在文档解析阶段提取表格为结构化格式(DataFrame),单独建索引,检索时支持按列名、数值范围查询。

有把握:这个方向是对的。但表格解析的准确率依赖文档质量——有些PDF的表格解析出来行列错位。

待验证:自动化的表格提取管线。目前部分表格还是手动处理的。


四、还没解决的问题

诚实地说,RAG在业务系统还有几个问题需要优化,跟着业务走很多时候需要优先满足核心需求:

问题1:跨文档推理

用户问”对比山东和浙江的产业发展差异”,需要从多个文档中分别提取两个省的数据,再综合对比。当前系统能分别检索到山东和浙江的信息,但没有”对比”的能力——它会把两个省的信息混在一起返回,而不是结构化对比。

可能的解法:Agent编排——先拆分为两个子查询(”山东产业情况”和”浙江产业情况”),分别检索,再用LLM综合对比。但还没实现。

问题2:实时数据接入

当前RAG的知识库是静态的——文档更新后需要重新索引。如果要接入实时数据(比如最新的产业统计数据),需要一个增量索引管线。

可能的解法:监听文档目录变化,触发增量索引。但需要处理文档版本冲突和索引一致性问题。

问题3:评测体系不完整

有评测集(120个查询),但没有自动化评测管线。每次改了Prompt或检索策略,都是手动跑几个查询看效果,没有系统化的指标对比。

可能的解法:用RAGAS框架搭建自动化评测,核心指标:Faithfulness、Answer Relevancy、Context Precision、Context Recall。但还没落地。

问题4:多模态检索

文档里有图片(地图、流程图、数据图表),当前系统只检索文本,图片被忽略了。用户问”产业园分布图”,系统找不到。

可能的解法:图片单独建索引(用CLIP或其他多模态Embedding),检索时文本和图片并行召回。但增加了系统复杂度。


五、上手的建议

看完这些,如果你正准备搭RAG系统,几条建议:

1. 不要照搬架构,理解取舍逻辑

我的5通道架构是在”800+政策文档、多种查询类型”这个场景下的选择。你的场景可能只需要2通道(BM25+向量就够了),也可能需要更多(比如加GraphRAG处理关系推理)。

关键是:先分析你的用户怎么用系统,再决定需要几条检索通道。

2. 先跑通最简单的,再逐步加通道

第一步:纯向量检索,跑通Demo。 第二步:加BM25混合检索。 第三步:加Rerank。 第四步:按业务需求加专用通道(表格、事实表等)。

每加一个通道,都要有评测数据证明”加了之后效果变好”。不要为了”架构好看”而加。

3. 评测集比架构更重要

一个2通道RAG + 100个高质量评测集,比5通道RAG + 10个随便写的测试问题强100倍。

评测集要覆盖:正常查询、精确词项、语义模糊、边界场景、多轮对话。每种至少10个。

4. 分块策略要拿自己的数据测

不要抄网上的”最佳chunk_size”。拿你自己的文档,准备50个真实查询,对比不同chunk_size的Recall@5。你会发现,最佳值和你的文档类型、查询模式强相关。

5. 防幻觉比检索优化更重要

用户能容忍”没检索到”(系统说”暂无数据”),但不能容忍”检索到了但答案是编的”。Prompt约束 + 引用溯源 + answer_guard,三层机制缺一不可。

6. 场景→方案速查

不确定自己需要几个通道?对号入座:

你的场景推荐方案原因
纯FAQ/知识问答纯向量检索查询模式单一,语义匹配够用
政策/法规文档BM25+向量混合检索精确词项多(政策名、年份、编号),推荐加BM25
有大量表格数据混合检索+表格专用通道表格的行列结构向量检索处理不了
多轮对话混合检索+指代消解“那个”“第三个”这类指代需要消解
关系推理(”对比A和B”)混合检索+Agent编排需要拆分子查询分别检索再综合
小库(<100个文档)长上下文就够了文档少直接塞进模型窗口,不需要检索

总结

架构设计的本质是取舍。没有”正确的RAG架构”,只有”适合你业务场景的RAG架构”。每个决策都有代价——5通道比2通道更准但更复杂,RRF比加权融合更简单但丢失绝对分数,TopK直接塞比上下文压缩更快但噪声更多。

踩坑是正常的。分块大小选错、向量检索精确词项漏召回、Rerank延迟太高、多轮对话指代消解——这些问题不是你的架构有问题,是RAG系统天然会遇到的挑战。关键是建立”发现问题→定位原因→验证修复”的闭环。

诚实面对不确定性。我的系统还有跨文档推理、实时数据接入、评测体系、多模态检索四个问题没解决。这不是失败,是工程的常态。知道”什么还没解决”比假装”什么都解决了”更有价值。

评测集是最被低估的武器。花在评测集上的时间,回报率比花在架构优化上高得多。没有评测集,你改了任何东西都不知道是变好还是变差。

欢迎评论区交流~


相关关键词:RAG架构、RAG实战、混合检索、BM25、向量检索、RRF、Rerank、分块策略、防幻觉、RAG评测、生产级RAG

← 返回列表