Skip to content
Go back

RAG 其实没那么复杂:六种检索架构怎么选

“我们需要一个 RAG 系统”,很多团队的下一步会直接变成:切块、生成 embedding、部署向量数据库、加 reranker,再开始讨论检索质量。

问题在于,用户可能只想找到一篇写着“如何重置密码”的文档。

Lighthouse Newsletter 的原文《RAG Is Simpler Than You Think》给出了一个很实用的判断:先从全文检索和查询改写开始,只有当数据特征和实测结果提出要求时,才逐层增加向量化、混合检索和预计算。

本文把原文的六种方案重新整理成一张可执行的选择表,并补充三件工程工作:

先给结论:RAG 是一条升级路径

六种检索方式可以看作从简单到复杂的路径:

层级方案解决的主要问题主要代价
1全文检索关键词、编号、专有名词和精确匹配同义词与隐含语义覆盖较弱
2查询改写用户表达与文档词汇不一致每次查询增加一次模型调用
3混合检索关键词精度和语义召回需要同时保留需要向量字段、融合和更完整的评测
4在线向量化文档变化快,索引很难及时重建查询延迟更高,调用链更长
5热冷分层热门文档要快,长尾文档要新需要访问统计、分层和两种检索路径
6全量预向量化规模大、查询量高、延迟目标严格存储、重建、模型升级和运维成本高

核心规则可以压缩成一句话:

先建立简单方案的测量基线,再根据具体失败增加一层复杂度。每次只改变一个关键变量,才能知道改进来自哪里。

原文给出的查询量、数据变化比例和延迟数字适合作为起始假设。它们不能直接替代自己的数据集、预算和服务目标。

为什么很多 RAG 项目一开始就变复杂

RAG 通常包含两段工作:

  1. 从知识库找出相关内容;
  2. 把这些内容放进上下文,让模型生成回答。

第二段再强,也无法弥补第一段找错文档。检索问题却经常被误判成“需要更强的 embedding 模型”,于是团队先投入到切块、向量库和重排器,查询本身的表达问题反而没有被检查。

这里有三个常见原因:

用户说的是自然语言,文档写的是工程术语

用户会问“怎么修复启动失败”,文档可能写着“解决 worker 初始化异常”。两句话含义接近,词面却不一致。查询改写有机会把用户表达转换成文档更容易匹配的词。

用户输入里包含必须精确保留的词

产品编号、错误码、内部框架名、客户名称和 API 名称都需要精确匹配。通用 embedding 对内部术语缺少上下文时,可能把它映射到更常见的含义。全文检索或查询改写里的词汇表可以保留这些词。

一个问题里混着多个动作

“读取 CSV、清理缺失值、画出结果”包含三个子问题。把整句送进一次检索,结果可能只覆盖其中一个动作。拆分、分别检索,再按依赖顺序组织答案,通常更容易控制。

因此,RAG 的第一项架构决策应当是:当前失败来自词面、语义、新鲜度、规模,还是查询本身包含多个意图?

四个选择维度

原文把选择条件归纳成数据新鲜度、语料特征、查询模式、规模和团队能力。实际做方案判断时,可以先填写下面这张表:

维度需要观察什么倾向的方案
数据新鲜度文档每天变化很多,还是数周甚至数月才变化高频变化倾向在线向量化;稳定语料可以预先计算
语料访问是否有明显热门文档,是否存在大量几乎不访问的长尾内容热冷分层,或对长尾按需处理
查询词形编号、姓名、产品码、专有名词是否很多全文检索和查询改写优先
查询语义用户是否大量使用“类似”“怎么解决”“有什么替代方案”混合检索或向量检索
查询结构单意图还是包含多个目标与依赖关系多意图拆分与并行检索
查询量和延迟每日请求量、可接受延迟、峰值并发从简单方案起步,按测量结果增加优化
团队能力是否有检索、机器学习和索引运维经验先选择团队能够解释和维护的方案

原文给出的经验分界包括每天少于 1000 次查询、1000 到 10000 次查询、超过 10000 次查询,以及每天超过 10% 的文档变化。这些数字适合帮助团队开始讨论,最终仍需要用真实流量和评测集校准。

方案一:全文检索,先把基线跑起来

全文检索使用倒排索引和词项匹配。Elasticsearch 默认使用 BM25 相似度,官方文档说明了 BM25 的默认行为和可调参数。PostgreSQL 也提供了基于 tsvector 和 tsquery 的全文检索能力

它适合这些场景:

全文检索的优势很直接:

它的短板也很明确:

一个最小基线可以只有这样一个接口:

def retrieve(query, limit=10):
    return full_text_index.search(query, limit=limit)

基线阶段不要急着追求复杂指标。先准备一组真实问题,并为每个问题标记至少一个相关文档,记录:

如果这一步已经能解决大部分查询,后面的向量组件就缺少充分理由。

方案二:查询改写,先处理“问法”问题

查询改写用一个语言模型把自然语言问题转换成更适合全文检索的查询。它的价值在于:许多看起来像语义检索的问题,根源可能只是用户问法与文档词汇不一致。

例如:

改写器要有一份领域词汇表,明确哪些词必须原样保留。建议让模型输出结构化结果:

{
  "search_terms": ["Atlas", "batch jobs", "data processing"],
  "must_keep": ["Atlas"],
  "sub_queries": [],
  "reason": "preserve internal framework name"
}

这里的 must_keep 很重要。内部框架、产品代号、错误码和接口名被模型替换后,全文检索可能失去最有价值的精确命中。

查询改写的优点:

它也有自己的风险:

可以把改写器放在检索前,并给它一个有限的重试次数:

def retrieve_with_rewrite(question, max_attempts=2):
    rewritten = rewrite_question(question)
    results = full_text_index.search(rewritten, limit=50)

    for attempt in range(max_attempts):
        if retrieval_quality_is_enough(results):
            return results

        rewritten = refine_query(
            original=question,
            previous=rewritten,
            results=results,
        )
        results = full_text_index.search(rewritten, limit=50)

    return results

实际服务中,quality_is_enough 可以结合规则和人工标注,例如结果里是否出现必须命中的文档、前几项是否来自允许的产品范围、用户是否点击了结果。不要让模型凭空判断“看起来不错”就结束循环。

方案三:混合检索,合并词面与语义

混合检索同时运行全文检索和向量检索,再把两边的结果合并或重排。它适合这样的查询:

混合检索有两种常见组织方式:

先全文召回,再向量重排

先用 BM25 找出 50 到 100 个候选,再对候选文档做向量相似度计算,最后返回前 10 个。候选集合限制了向量计算范围,也让全文检索承担精确过滤。

全文与向量并行,再做排名融合

两条检索路径分别返回结果列表,再用 Reciprocal Rank Fusion 等方法合并排名。Azure AI Search 的混合检索文档说明了这种并行执行和排名融合的结构;其相关性文档也说明了文本检索使用 BM25、向量结果使用向量近邻算法,混合结果通过 RRF 形成统一排序。

混合检索解决了两类入口的互补问题:

它也会引入新的工作:

不要直接比较 BM25 分数和向量相似度分数的大小。两种算法的分值含义和范围不同,应当使用排名融合、归一化或经过验证的加权方式。

方案四:在线向量化,优先保证新鲜度

在线向量化把文档或候选文档的向量计算推迟到查询时。它适合高频变化的内容,例如新闻、社交内容、实时状态和持续更新的内部数据。

原文给出的思路是:先用 BM25 找到少量候选,再对这些候选做在线向量化,避免为很少被访问的全部文档提前存向量。

它的特点是:

原文用每次查询处理约 50 个文档、增加 200 到 500 毫秒延迟作为示例。实际延迟取决于文档长度、并发、模型服务和网络,不能直接当成服务承诺。

在线向量化适合“新鲜度比速度更重要”的场景。以下情况需要谨慎:

一个可靠的实现应当限制候选数、设置超时、保留全文检索结果,并在在线向量化失败时返回可解释的降级结果。

方案五:热冷分层,照顾访问分布

很多知识库并不会平均访问所有文档。一部分高频文档承载了大多数请求,另一部分文档很少被访问。原文用 Pareto 分布和 20/80 作为理解这种现象的启发式。

热冷分层的做法是:

它适合:

热冷分层的关键问题不在于把文档分成两组,而在于维护这两组:

如果系统还没有稳定的访问日志,直接上热冷分层会增加许多难以验证的状态。先建立查询、文档命中和访问时延记录更稳妥。

方案六:全量预向量化,把复杂度换成查询速度

全量预向量化会提前处理所有文档,把向量存入向量索引,查询时使用近似近邻或其他向量检索算法。

它通常适合:

原文把每天超过 10000 次查询、目标延迟低于 50 毫秒、每月变化低于 5% 作为一种高规模场景的起点。它还提醒,全量预向量化会把模型更换变成一次大规模重建工作。

需要提前算清楚的内容包括:

全量预向量化适合有充分规模证据的系统。小规模、低查询量或仍在探索数据结构的项目,先使用全文检索与查询改写通常更容易发现真正问题。

多意图查询:先拆,再检索

考虑这个问题:

如何读取 CSV,清理缺失数据,然后画出结果?

它包含至少三个意图:

  1. 读取 CSV;
  2. 清理缺失数据;
  3. 绘制结果。

它们还有依赖关系:先读取,才能清理;先清理,才能绘图。可以让查询理解步骤输出结构化计划:

{
  "query_type": "multi_intent",
  "sub_queries": [
    {
      "id": "read",
      "text": "pandas read csv",
      "depends_on": []
    },
    {
      "id": "clean",
      "text": "pandas handle missing data",
      "depends_on": ["read"]
    },
    {
      "id": "plot",
      "text": "matplotlib plot dataframe",
      "depends_on": ["clean"]
    }
  ]
}

执行时可以按依赖关系调度:

并行执行的总延迟接近最慢的一支,再加上拆分和合并开销;串行执行则会累加每一支的延迟。前提是子查询之间确实没有依赖。对有依赖的步骤强行并行,可能得到互相矛盾的上下文。

这种路由方式还有一个好处:昂贵的向量检索只出现在确实需要的子查询上,简单问题继续使用低成本路径。

一个可执行的升级流程

第一步:定义检索目标

先写清楚系统要找什么:

不同目标需要不同的评测方式。唯一文档查找关注前几名命中;多文档问答还要关注覆盖范围和引用完整性。

第二步:建立全文检索基线

用真实用户问题建立评测集,至少记录:

{
  "query_id": "q-001",
  "question": "怎么修复登录后白屏",
  "relevant_doc_ids": ["doc-17"],
  "must_match_terms": ["登录"],
  "expected_intent": "troubleshooting"
}

运行一段时间后,按失败类型分类:

第三步:只针对失败增加一层

可以按下面的顺序尝试:

  1. 词面不一致:先加查询改写;
  2. 内部术语被改坏:加领域词汇表和 must_keep;
  3. 精确词与语义查询同时存在:加混合检索;
  4. 内容更新太快:考虑在线向量化;
  5. 热门与长尾差异明显:考虑热冷分层;
  6. 查询量和延迟目标足够高:评估全量预向量化;
  7. 一个问题包含多个目标:加入意图拆分和路由。

每次改动都保留旧路径作为对照。一次同时换模型、切块方式、索引和排序,最终很难解释结果变化。

第四步:把检索过程记录下来

建议为每次查询记录最小事件:

{
  "query_id": "q-001",
  "strategy": "rewrite_then_bm25",
  "original_query": "怎么修复登录后白屏",
  "rewritten_query": "authentication blank screen login",
  "candidate_doc_ids": ["doc-17", "doc-23"],
  "retrieval_latency_ms": 34,
  "answer_doc_ids": ["doc-17"],
  "user_feedback": "accepted"
}

有了这类记录,团队可以回答:

选型表:什么时候停,什么时候继续

当前情况建议
还没有搜索能力先做全文检索和基础结果页
结果中有相关文档,但用户问法差异大加查询改写,保留领域词汇
精确术语和语义问题并存做混合检索并使用排名融合
文档每天频繁变化评估在线向量化,限制候选数量
热门文档和长尾文档差异明显评估热冷分层
查询量高、语料稳定、延迟目标严格评估全量预向量化
查询由多个动作组成先拆意图,再按依赖检索
当前指标已经满足目标先停在当前方案,把时间用在产品功能和内容质量上

原文最后给出的方向很清楚:很多系统在全文检索加查询改写这一层就已经足够;少数系统才需要把所有文档提前向量化。真正重要的判断依据是自己的查询日志、评测集、延迟预算和更新频率。

常见陷阱

把“有 RAG”直接等同于“必须有向量数据库”

RAG 的关键是找到并使用相关上下文。全文检索同样可以提供上下文,向量数据库只是其中一种检索基础设施。

只看最终回答,不看检索结果

回答有时会用模型常识补全,看起来流畅,却没有使用正确文档。评测时应把检索命中、排序、引用和最终回答分开看。

让查询改写器随意修改专有名词

模型应该知道哪些词必须保留。把产品码、错误码和内部项目名放入词汇表,并在结果里记录原查询和改写查询。

直接相加 BM25 分数和向量分数

两种分数没有天然可比性。先做排名融合或归一化,再用评测集选择权重。

预先处理所有文档,却没有更新方案

向量索引需要面对新增、修改、删除、模型升级和重建期间的切换。没有这些流程时,向量质量可能很快落后于原文。

把经验数字当成硬规则

“每天 1000 次”“变化 10%”“延迟 50 毫秒”都可以帮助开始讨论,实际阈值需要结合文档规模、查询峰值、模型服务和用户容忍度。

多意图查询全部串行执行

没有依赖的子查询可以并行,串行会放大延迟。先建依赖关系,再决定执行顺序。

最后:把复杂度留给真正需要它的地方

一套可维护的 RAG 检索系统,往往从下面这条路径开始:

  1. 用全文检索建立可解释的基线;
  2. 用查询改写处理自然语言问法;
  3. 用混合检索补足语义召回;
  4. 用在线向量化回应高频变化内容;
  5. 用热冷分层平衡热门请求和长尾新鲜度;
  6. 用全量预向量化服务稳定的大规模查询;
  7. 对多意图问题做拆分、路由和依赖调度;
  8. 用查询日志和评测集证明每一层增加了价值。

如果当前问题只是“用户找不到已经存在的文档”,先检查全文检索和查询改写。向量索引、重排器和复杂代理流程都有适用场景,但它们应该由检索失败、数据变化和性能目标共同推动。

如果你正在构建 AI 助手、开发工具或知识库系统,欢迎关注 Aide Hub,后续会继续分享检索、评测和软件工程实践。

参考


Tags


Previous

Uno Platform如何用MCP让AI检查自己的应用

Next

C# 获取机器逻辑处理器总数