RAG比你想象的简单:从BM25到智能体的实用指南

大家好,我是拉斐尔——作为一名AI系统开发工程师,我每周都会分享近期遇到的有趣挑战与行业进展。
订阅即可每周获取我的见解👇
如今,多数人搭建RAG(检索增强生成)系统时都存在过度设计的问题。他们一上来就直奔嵌入模型、向量数据库和重排序流程,可用户真正想要的,不过是找到写着“如何重置密码”的文档而已。
工程领域向来是“一把钥匙开一把锁”,AI检索系统也不例外。
决策考量因素
在介绍具体方案前,我们先明确不同方案的适用场景,核心考量因素包括:
1. 数据时效性要求 - 实时更新的内容(如新闻、社交媒体)适合易重新索引的方案;每日或每周更新的内容适配混合检索;数据稳定(每月或每季度更新)的场景则适合预嵌入处理。
2. 语料库特征 - 高变动率(每日变更超10%)的语料库应避免全量预嵌入;文档稳定的场景可放心使用预嵌入;长尾分布(90%文档从未被访问)的情况更适合实时嵌入。
3. 查询模式 - 关键词密集型查询优先选择全文检索;语义或对话式查询适合嵌入模型;混合模式则需采用混合检索方案。
4. 规模与性能 - 每日查询量不足1000次时,简单方案足够应对;1000至10000次需针对性优化;超过10000次则值得投入全量优化。
5. 团队能力 - 无机器学习经验的团队应采用“全文检索+查询改写”方案;具备一定机器学习能力可尝试混合检索;拥有专业ML团队则可落地高级方案。
接下来是具体方案手册。请从最基础的方案开始,只有当数据证明现有方案无法满足需求时,再向下尝试更复杂的方案。
方案1:最简可行版——仅用全文检索
方案概述
经典的BM25算法、Elasticsearch、Postgres全文检索——这些都是“嵌入”成为技术热词之前就已成熟的方案。
适用场景
项目刚起步;用户习惯用关键词式查询(如“pandas merge dataframe”);精确匹配至关重要(如“发票#12345”);希望完全规避机器学习复杂度;语料库包含专有术语(下文会详细说明)。
优势
无API调用成本;响应速度快(低于10毫秒);易于调试(可清晰看到文档匹配的原因);效果远超预期(能覆盖多数使用场景);无需文档拆分策略——直接支持完整文档检索;评估复杂度低——测试与验证简单;无模型过时风险(BM25算法稳定不变)。
劣势
无法识别同义词(如“car”与“automobile”);无法处理语义查询(如“我该如何……?”);只能理解关键词,无法识别深层意图。
根据我的经验,该方案能覆盖相当大比例的使用场景,切勿跳过这一步——它的作用可能远超你的想象。
如果直接上手嵌入模型,你立刻会面临一系列问题:文档拆多大?(512词元?1024词元?)重叠部分设多少?(50词元?100词元?)按语义拆分还是固定长度?如何评估拆分效果?
而使用全文检索,这些问题都不存在。文档无需改动,检索直接可用。
方案2:智能查询改写
方案概述
借助大语言模型(LLM),将用户的模糊查询转化为精准的关键词检索语句。
核心思路
多数所谓的“语义检索”问题,本质上都是查询表述不当的问题。

适用场景
用户习惯用对话式提问;存在词汇差异(用户说“修复bug”,文档写“调试”);内部有专属术语(如自研框架“Atlas”);希望快速迭代查询策略。
成本
约0.001美元/次查询(使用GPT-4o-mini进行改写)
核心优势
大语言模型可自动去除停用词(如“我该如何”这类无意义表述);补充同义词(如将“car”扩展为“car automobile vehicle”);转换领域术语(如将“加速代码运行”改写为“优化性能”);拆分复杂查询(如将“读取CSV并绘图”拆分为["读取CSV", "绘制数据"]);还可通过系统提示词学习你的专业术语表。
比嵌入模型更灵活的原因
使用嵌入模型时,若检索效果不佳,你需要调整文档拆分策略、重新嵌入全量语料、在测试集上运行回归测试,然后祈祷效果有所改善。
而使用查询改写,只需调整系统提示词即可,修改后可立即测试。
多轮智能改写
更优的方式是构建循环机制:
def agentic_search(query, max_iterations=3):
for i in range(max_iterations):
# 改写查询语句
optimized = query_rewriter.rewrite(query, iteration=i)
# 执行检索
results = bm25_search(optimized)
# 评估结果质量
quality = evaluate_results(results, query)
if quality > threshold:
return results
# 基于反馈优化查询
query = refine_based_on_feedback(query, results, quality)
return results
智能体可自主迭代、学习、适配——全程无需重新嵌入任何文档。
示例:专有术语难题
假设你的公司有一款名为“Atlas”的Python框架。若使用通用嵌入模型:
通用嵌入模型(基于互联网数据训练):
“Atlas” = [指向希腊神话、地图、地理的向量]
你的Atlas文档 = [指向数据处理的向量]
相似度得分:0.15(极差!)
模型根本不知道你所说的“Atlas”是什么,只会依赖训练时学到的通用含义。但使用查询改写:
system_prompt = """
领域专有术语(绝对不可修改,需作为精确关键词使用):
- Atlas:我们内部的数据处理框架
- Mercury:我们的消息系统
- Zeus:我们的认证服务
请完整保留上述术语,并优化查询语句的其余部分。
"""
# 用户提问:"我该如何用Atlas处理批量任务?"
# 智能体输出:"Atlas batch jobs data processing pipeline"
# BM25检索:完美匹配"Atlas" ✓
对于专有术语,精确关键词匹配的效果远胜于语义理解。
方案3:混合检索(稀疏检索+密集重排序)
方案概述
先用BM25筛选候选文档(前50-100条),再用嵌入模型进行重排序(选出前10条)。
为什么有效
BM25速度快,擅长关键词匹配;嵌入模型则能理解语义。二者结合可互补短板。
适用场景
用户常提语义类问题(如“寻找X的替代方案”);BM25加查询改写的组合无法满足需求(有数据证明);可接受100-500毫秒的延迟;语料库相对稳定(不会每分钟都变化)。
流程示意图

成本测算
以当前定价(OpenAI text-embedding-3-small,每100万词元0.02美元)为例:
- 每次查询嵌入50篇文档(平均每篇500词元):50×500=25000词元
- 单次查询成本:25000×0.00002≈0.0005美元;每日1000次查询×30天≈每月15美元
成本其实相当合理,但有一个关键问题:延迟。
实时嵌入50篇文档会给每次查询增加200-500毫秒的延迟,对于面向用户的检索系统来说,这个延迟是可感知的。真正的权衡点并非成本,而是速度。
重要提示:文档拆分问题重现
一旦引入嵌入模型,你就需要决定文档拆分方式(固定长度?语义拆分?按章节拆分?),确定拆分长度与重叠部分,还要处理跨重要上下文的拆分块。
这会带来纯全文检索所没有的复杂度。
方案4:实时嵌入(高时效性数据方案)
核心思路
如果数据频繁更新,何必花钱重新嵌入全量文档?
方案概述

适用场景
文档高变动率(每日更新超10%);实时内容(新闻、社交媒体、直播动态);正在测试嵌入模型(无需重新索引);数据时效性要求极高(文档必须实时更新);重排序候选集较小(20-50篇文档)。
成本与性能测算
实时嵌入(每日1000次查询,每次50篇文档):
- 嵌入成本:约15美元/月(持续支出)
- 存储成本:0美元(仅存储文本)
- 延迟:200-500毫秒/次查询
- 时效性:完美(始终最新)
- 模型切换:简单(只需修改API调用)
模型过时问题
嵌入模型会被淘汰。
OpenAI已停用text-embedding-ada-002,转而推出text-embedding-3。如果你用旧模型预嵌入了1000万篇文档,现在就需要用新模型重新嵌入全部文档、更新向量数据库、在测试集上运行回归测试、验证效果未下降、处理切换过渡期,还要应对API变更。
实时嵌入的优势
你只需修改一行代码,一切就搞定了。
劣势
延迟问题。每次查询都要实时嵌入文档,仅适用于可接受200-500毫秒延迟、候选集较小(重排序20-50篇而非500篇)、且时效性优先于速度的场景。
方案5:冷热分层预嵌入
方案概述
对高频访问的文档(“热层”)进行预嵌入,对低频访问的文档(“冷层”)进行实时嵌入。
核心思路
访问模式遵循帕累托分布:20%的文档占据80%的访问量。
# 追踪访问模式
access_counts = Counter()
def adaptive_search(query):
# BM25筛选候选文档
candidates = bm25_search(query, top_k=100)
# 区分冷热层文档
hot = [d for d in candidates if d.id in hot_tier]
cold = [d for d in candidates if d.id not in hot_tier]
# 热层文档:使用预计算嵌入(速度快)
hot_scores = vector_db.similarity_search(query_emb, hot)
# 冷层文档:实时嵌入(速度慢,但访问频率低)
cold_scores = embed_and_score(cold, query_emb)
return merge_and_rank(hot_scores, cold_scores)
## 每周更新冷热分层
def update_tiers_weekly():
frequently_accessed = [doc_id for doc_id, count
in access_counts.items()
if count > threshold]
# 仅对新晋升热层的文档进行嵌入
newly_hot = set(frequently_accessed) - set(hot_tier)
embed_and_index(newly_hot)
适用场景
访问模式清晰(部分文档访问量远高于其他);语料库规模中等至较大(超过10万篇文档);内容兼具稳定性与变动性;需保证高频查询的低延迟;希望减少模型更新时的重新嵌入工作量。
优势
80%的查询速度快(命中预嵌入缓存);低频访问文档始终保持最新;模型更新时仅需重新嵌入热层文档(仅占语料库的20%);可适应访问模式的变化;在延迟、成本与灵活性之间实现最佳平衡。
模型更新成本对比
当嵌入模型被淘汰时:
全量预嵌入:重新嵌入100万篇文档 × 0.01美元 = 10000美元 + 停机时间
冷热分层:重新嵌入20万篇文档 × 0.01美元 = 2000美元 + 极少停机时间
实时嵌入:修改一行代码 = 0美元 + 无停机时间
方案6:全量预嵌入(大规模场景方案)
方案概述
提前嵌入所有文档,存储在向量数据库中,使用近似最近邻(ANN)算法检索。
适用场景
查询量极高(每日超过10000次);需实现低于50毫秒的延迟;语料库非常稳定(每月变动率低于5%);访问模式分散(无明显长尾);拥有专业ML团队管理基础设施。
成本明细
全量预嵌入(100万篇文档):
- 一次性嵌入成本:100万篇×500词元×0.00002美元 = 10美元
- 存储成本:100万×1536维度×4字节 = 6GB(约10-30美元/月)
- 检索延迟:低于50毫秒(极快!)
- 时效性:仅与上次重新索引的时间同步
不适用场景
文档频繁变更(每周变动超10%);正在测试嵌入模型;查询量低(每日不足1000次);尚未尝试过更简单的方案。
模型过时的噩梦
全量预嵌入最大的痛点就在这里。当需要切换模型时,你将面临停机时间(重新嵌入期间检索效果下降)、计算成本(重新嵌入数百万篇文档)、测试负担(对新嵌入结果进行全量回归测试)、文档拆分重评估(新模型可能需要不同的拆分方式?),以及风险(新模型在你的领域表现更差怎么办?)。
大多数系统根本不需要这么复杂的方案。我见过不少团队花数月时间优化向量数据库,而其实查询改写就能解决90%的问题。
但如果你是Pinterest、Shopify这类需要处理大规模稳定语料库的企业,这才是适合你的方案。
多意图查询难题
我们之前讨论的都是单意图查询,比如“如何合并数据框?”
但真实用户的提问往往是这样的:“我该如何读取CSV文件、清理缺失数据并绘制结果图表?”
这包含三个独立的意图。用一个查询来检索,就像找一家同时卖披萨、寿司和 tacos 的餐厅——难度极大。
Perplexity解决方案
现代智能RAG系统(如Perplexity、ChatGPT检索功能)能巧妙处理这类问题:
查询理解智能体
拆分复杂查询:
# 输入:"读取CSV、清理数据、绘制结果"
# 智能体输出:
{
"query_type": "complex",
"sub_queries": [
"pandas read csv file",
"pandas clean missing data",
"matplotlib plot dataframe"
],
"dependencies": ["read > clean > plot"]
}
并行自适应处理
为每个子查询选择最优检索方式:
子查询1(简单):
"pandas read csv"
去除停用词+词形还原,再用BM25检索
成本:0美元,延迟:15毫秒
子查询2(中等):
"pandas clean missing data"
同义词扩展,再用BM25检索
成本:0美元,延迟:20毫秒
子查询3(复杂):
"matplotlib plot dataframe"
LLM改写,再用混合检索
成本:0.001美元,延迟:250毫秒
总计(并行执行):0.002美元,250毫秒(而非285毫秒!)
结果合成
将各子查询的结果整合成连贯的答案:
以下是完整操作流程:
1. 读取CSV文件
[来自子查询1的相关文档]
2. 清理缺失数据
[来自子查询2的相关文档]
3. 绘制结果图表
[来自子查询3的相关文档]
[整合三步操作的代码示例]
为什么有效
每个子查询都聚焦精准,检索效果更好;并行执行可降低延迟(取最长耗时而非总和);自适应路由能降低成本(仅复杂查询需调用LLM);结构化输出提升用户体验。
成本对比
未拆分查询
- LLM改写整个复杂查询:0.005美元
- 嵌入50篇文档:0.025美元
- 总计:0.03美元
拆分查询后
- 拆分查询:0.001美元
- 子查询1(简单):0美元
- 子查询2(简单):0美元
- 子查询3(复杂):0.001美元
- 总计:0.002美元
成本降低至1/15,效果还更好。
这正是智能检索的优势所在:智能体可自主判断哪些子查询需要昂贵的处理(嵌入模型),哪些用低成本方法(简单预处理+BM25)即可解决。
决策树(或:该选哪种方案?)
读到这里,你可能只想知道:“我该搭建哪种系统?”
第一步:你有检索系统吗? 如果没有,先搭建BM25。真的,别再往下看了,先动手做。如果已有检索系统,继续下一步。
第二步:评估基准效果。 让当前检索系统运行2-4周,收集用户反馈。用户对结果满意吗?如果是,就到此为止,去开发新功能吧。如果不满意,继续下一步。
第三步:核心痛点是什么?
如果用户反馈“明明存在的文档却搜不到”,先尝试查询改写。单次查询仅0.001美元,且无需重新索引,值得测试。做两周A/B测试,若效果提升明显,就采用这个方案,任务完成。若效果仍不足,继续下一步。
如果用户反馈“结果还行但不够好”,对混合检索(稀疏检索+嵌入重排序)做A/B测试。增加的延迟是否值得?如果是,根据实际情况选择实现方式:数据频繁更新则用实时嵌入;有明确高频文档则用冷热分层;语料库稳定且规模大则用全量预嵌入。如果延迟不可接受,就进一步优化查询改写方案。
如果用户反馈“需要更好的语义理解”,采用混合检索,并根据场景选择具体方案:高变动率(每日超10%)选实时嵌入;中等规模且访问模式清晰选冷热分层;大规模稳定语料选全量预嵌入。
核心决策要点:
- 全文检索+查询改写:数据时效性完美,搭建复杂度低,查询延迟低于50毫秒,模型切换简单,无需文档拆分,覆盖多数场景。
- 实时嵌入:数据时效性完美,搭建复杂度低,但查询延迟达200-500毫秒,模型切换简单,需文档拆分,适合高变动率场景。
- 冷热分层:数据时效性均衡,搭建复杂度中等,查询延迟50-100毫秒,模型切换简便,需文档拆分,适合需求多样的场景。
- 全量预嵌入:数据时效性依赖重新索引,搭建复杂度高,但查询延迟低于50毫秒,模型切换成本极高,需文档拆分,专为大规模场景设计。
80/20法则: 60%的系统只需停留在“全文检索+查询改写”阶段;25%的系统需要混合检索(实时嵌入或冷热分层);10%的系统需要全量预嵌入;仅5%的系统需要定制方案。
核心结论:别用5%场景的方案,去解决60%场景的问题。