Skip to content

RAG 检索增强 ​

标签
AI/agent/检索
字数
7405 字
阅读时间
29 分钟

RAG(Retrieval-Augmented Generation,检索增强生成)是缓解 07-模型幻觉 最有效的方法之一:生成之前先从外部知识库检索相关信息,把它作为上下文,引导模型基于事实回答。原理上它把模型的「回忆」换成了「查询」——而查询是可以验证的。

但它不是「接个向量库就完了」。召回质量由一条五阶段的链路决定,每一段都有各自的参数和失败模式。

① 分块 Chunking
   决定系统「找得到什么」:切点直接决定查询时能匹配到什么
   参数:chunk size、overlap、切分策略(固定 / 递归 / 语义 / 滑动窗口 / 父子)
        │
        ▼
② 嵌入 Embedding → 向量索引
   文本经 tokenizer → 编码器前向 → 池化 → 归一化 → 向量
   参数:池化方式、是否 L2 归一化、距离度量
        │
        ▼
③ 检索 Retrieval(稠密 / 稀疏 / 混合,取 top-k)
   k 不是常数:k = n × 放大倍数,并设一个绝对下限
        │
        ▼
④ 融合 Fusion(RRF 合并多路排名)
   只用排名、不用分数 —— 两路的分数量纲不可比
        │
        ▼
⑤ 重排 Rerank(cross-encoder 精排)→ 最终 top-n
   召回约 20 条、重排到 5 条、送 3–5 条给模型
        │
        ▼
⑥ 上下文组装 → LLM 生成
   最强的块放最前与最后 —— 模型对长上下文中间部分的注意力更低

分块:决定系统「找得到什么」 ​

分块的切点直接决定查询时能匹配到什么。五种策略:

策略原理优点缺点适用
固定大小按 token / 字符数均匀切最快,零模型调用,尺寸可预测会切断句子和表格结构化文档(日志、代码);高吞吐量场景
递归走分隔符优先级,切在「能放下的最大自然边界」保持自然语义边界,索引时零模型调用成本需要调参通用文档的默认首选
语义逐句嵌入,与当前块的相似度跌破阈值就开新块语义连贯性最好每句一次嵌入调用;阈值要调高连贯性要求的问答;主题多样的语料
滑动窗口固定大小 + overlap保留跨块上下文冗余度高,索引膨胀需要上下文连续性
父子分块小块检索(100–200 token)+ 大块喂给模型(1000–2000 token)检索精度与上下文完整性兼得实现复杂度高生产环境的进阶选择

递归分块之所以成为默认:它拿到了结构感知分块的大部分好处,却没付语义分块的嵌入成本。

chunk size 的实测结论 ​

区间效果
< 50 token准确率掉到 54%——碎片化丢了逻辑关系
256–512 token通用最佳区间(接近自然段,连贯性保得住)
512–768 token技术、法律文本(密集定义术语和引用条款)更好
> 2500 token上下文悬崖效应,质量断崖式下跌

问题粒度会影响最优值:事实型问题偏好更小的块,摘要/解释型问题偏好更大的块。所以「最优 chunk size」不是一个常数,它取决于你的问题分布和文档类型。

按内容类型的起点(供 A/B 测试,不是定律):

内容类型起点overlap备注
短问答 / FAQ / 聊天~256 token~25(10%)检索更准,单次命中的上下文更少
文章 / 文档 / 产品页~512 token50–100(10–20%)混合内容的稳妥默认
长法律 / 技术 / 合同~1024 token100–200(10–20%)保住完整条款,代价是精度被稀释
表格 / 财务 PDF按页或按节边界对齐不要切表格

按 token 数计数,不要按字符数——否则块长和嵌入模型的限制对不上。

chunk size 与效果的关系不是单调的。

准确率
  │           ╭───────────────╮
  │        ╱                     ╲
  │      ╱                         ╲
  │    ╱                             ╲╲
  │  ╱                                  ╲╲╲╲
  │╱                                          ╲╲╲╲╲
  └─────────────────────────────────────────────────▶ chunk size(token)
   <50           256–512        512–768          >2500
   54%           通用最佳区间     技术 / 法律       上下文悬崖效应
   碎片化丢了     接近自然段      密集定义术语       质量断崖式下跌
   逻辑关系       连贯性保得住    与引用条款更好

问题粒度也会影响最优值
   事实型问题   ──▶ 偏好更小的块
   摘要 / 解释型 ──▶ 偏好更大的块
   └─ 「最优 chunk size」不是一个常数,取决于问题分布与文档类型

按内容类型的起点(供 A/B 测试,不是定律)
   短问答 / FAQ / 聊天    ~256 token,overlap ~25(10%)
   文章 / 文档 / 产品页    ~512 token,overlap 50–100(10–20%)   ← 混合内容的稳妥默认
   长法律 / 技术 / 合同    ~1024 token,overlap 100–200(10–20%)
   表格 / 财务 PDF        按页或按节,边界对齐 —— 不要切表格
   └─ 按 token 数计数,不要按字符数,否则块长与嵌入模型的限制对不上

overlap:一个小额保险,但有成本 ​

10–15% 的 overlap 能减少边界处的信息损失——一个跨两个块的概念至少在一个块里是完整的。

但超过 20% 反而因为冗余拉低性能。 而且 2026 年 1 月的一项研究发现在稀疏检索(SPLADE)下 overlap 没有可测收益,却仍在推高索引成本。所以:稠密检索留一点 overlap,偏稀疏的路线要重新评估是否值得。

两个容易被忽略的坑 ​

代码要按 AST 边界切,不是按 token 数。 按函数和类定义切,每个函数保持 256–512 token,前置文件的 import 块和所属类签名,零 overlap(函数是自包含单元)。按 token 硬切会把一个函数劈成两半,两边都不可用。

token 数不是语言中立的。 同一句话在土耳其语和日语里比英语多 2.7 倍 token,阿拉伯语多 3.9 倍(tiktoken 的 cl100k_base)。一个固定的 512-token 预算会静默地给非英语块更少的语义内容——不报错,只是效果变差。

还有一条与嵌入模型强相关的:512-token 输入上限的模型(BGE、Cohere v3)要求块长明显低于 512,因为截断是静默的。不报错,只是后半段内容没进向量。

别急着换分块器 ​

先做块富化,再重新调分块器。 Anthropic 的 contextual retrieval 做法是在索引时给每个块前置一段 LLM 生成的上下文摘要(含关键实体、报告期、指标),实测让失败率从 5.7% 降到 3.7%——这个收益比在分块策略之间换来换去大得多。

嵌入链路与向量检索 ​

从文本到向量,中间有四步 ​

「embedding 一下」这个说法掩盖了四步:

文本
   │  ① tokenizer:按模型自己的词表切分
   │     坑:嵌入模型的 tokenizer 与生成模型的不是同一套 ——
   │         用 A 的 tokenizer 数 B 的 token 预算,长度估算就错了
   ▼
token ids
   │  ② 编码器前向:Transformer 逐层计算,位置编码在这一步注入
   │     坑:位置编码决定了模型能处理多长的输入,
   │         绝对位置编码超过训练长度直接失效
   ▼
每个 token 一个向量
   │  ③ 池化:把「每 token 一个向量」压成「整段一个向量」
   │     三种做法:取 [CLS] 位 / mean pooling / 按注意力加权
   │     坑:必须用模型训练时用的那一种,换错会让向量完全失去语义
   ▼
整段一个向量
   │  ④ 归一化:L2 到单位长度
   │     坑:归一化后内积等于余弦,不归一化时两者不同 ——
   │         模型卡说按内积训练的就照着做
   ▼
检索用的向量 ──▶ 再往下是索引层(距离度量、ANN、top-k)
步骤做什么容易踩的坑
① tokenizer按模型自己的词表切分,把文本变成 token id嵌入模型的 tokenizer 与生成模型的不是同一套。用 A 模型的 tokenizer 数 B 模型的 token 预算,长度估算就错了。切分算法见 05-文本分词与子词算法:BPE、WordPiece 与 Unigram
② 编码器前向Transformer 逐层计算,位置编码在这一步注入位置编码决定了模型能处理多长的输入——绝对位置编码超过训练长度就失效,RoPE 靠缩放 base 外推(见 09-位置编码)
③ 池化把「每个 token 一个向量」压成「整段一个向量」三种做法:取 [CLS] 位、所有 token 取平均(mean pooling)、按注意力加权。必须用模型训练时用的那种,换错池化方式会让向量完全失去语义
④ 归一化L2 归一化到单位长度归一化后内积就等于余弦;不归一化时两者不同。模型卡如果说是按内积训练的,照着做

再往下就是索引层——距离度量怎么选、为什么必须用近似检索、HNSW / IVF / PQ 各自在做什么、top-k 怎么取,这些在 12-向量检索与 ANN 索引 里单独讲。

「top-k」不是一个常数 ​

这是最容易被拍脑袋决定的参数,但它实际上被四件事牵制:下游要几条、重排器的容量、efSearch / nprobe 的下界、延迟预算。

工程上的取法是三步:定最终条数 n(通常 3–5)→ 取 k = n × 放大倍数 给重排留选择余地 → 设一个绝对下限防候选池过小。

HelloAgents 里的 candidate_pool_multiplier 默认 4,下限 20,正是这个套路。细节见 12-向量检索与 ANN 索引 的 top-k 一节。

维度与索引的约束 ​

不同嵌入模型的输出维度不同,不同模态的维度更是完全不同——混在同一个向量集合里没法算距离。这也是 10-智能体记忆系统 里感知记忆按模态分离存储的原因。

索引侧精确检索(如 FAISS 的 IndexFlatIP,穷举最近邻,无近似误差)和近似检索(ANN)的选择要分场景:做评测时应该用精确检索,把嵌入模型的效果和索引近似的误差分开,否则分不清是哪一环拖了后腿;生产环境则必须用 ANN,否则百万级以上的数据根本撑不住。

向量检索补不上的那部分:稀疏检索 ​

BM25 这类稀疏检索靠词项匹配,在查询和文档逐字出现相同术语时特别强——公司名、财务指标、会计期间标识符这类专有名词。它的经典默认参数是 k1=1.2、b=0.75(Okapi 原始值)。

稠密检索擅长语义,稀疏检索擅长精确词项。所以生产上通常是混合检索。

融合:RRF ​

两路检索的分数不可比——余弦相似度在 [-1, 1],BM25 是无上界的正数。硬要归一化再相加(如 min-max)对离群值很脆弱。

RRF(Reciprocal Rank Fusion,倒数排名融合) 绕开了这个问题:只用排名,不用分数。

RRF(d)=∑i1k+ri(d)

其中 ri(d) 是文档 d 在第 i 份排名里的位置(1-indexed),k 是平滑常数,通常取 60。

k=60 来自 2009 年 Cormack、Clarke、Büttcher 的论文,是经验取值。取大的 k 会把分数曲线压平,避免某一路的 top-1 自动主导整个结果。

手算一遍就明白了 ​

情形计算得分
向量排 #1、BM25 排 #51/(60+1)+1/(60+5)=0.0164+0.01540.0318
向量排 #3、BM25 排 #21/(60+3)+1/(60+2)=0.0159+0.01610.0320

两路都靠前的文档赢了。 而一个在某一路排 #1、在另一路完全没出现的文档只能拿到中等分数。这就是 RRF 的意图:奖励共识,不奖励单路的极端置信。

RRF 只看排名、不看分数,因为两路的分数量纲本来就不可比。

   向量检索排名          稀疏(BM25)排名          融合(k = 60)
   #1  文档 A            #2  文档 B               A:1/61 + 1/64
   #2  文档 C            #4  文档 A                  = 0.0164 + 0.0156
   #3  文档 B            #5  文档 D                  = 0.0320
   …                     …

两个对照(同一份数据)
   向量 #1  + BM25 #5  ──▶  1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318
   向量 #3  + BM25 #2  ──▶  1/(60+3) + 1/(60+2) = 0.0159 + 0.0161 = 0.0320
   └─ 两路都靠前的文档赢了
      在某一路排 #1、在另一路完全没出现的,只能拿到中等分数
      这就是 RRF 的意图:奖励共识,不奖励单路的极端置信

k 取 60 的作用是把分数曲线压平,避免某一路的 top-1 自动主导整个结果。

三个实现细节
   同一文档在多份列表里各计一次分 —— 这正是「共识」的数学表达
   doc_map 保留首次出现的实例,后续出现的只加分数、不覆盖内容
   索引从 1 开始 —— rank = 0 会让 1/(60+0) 比应有的值偏大

RRF 是纯算术、不调模型,延迟可忽略 —— 这是它成为混合检索默认融合步骤的主要原因。

实现要点 ​

python
def rrf(rank, k=60):
    return 1 / (k + rank)

scores = defaultdict(float)
doc_map = {}

for results in result_sets:              # 多份排名列表
    for rank, doc in enumerate(results, start=1):
        scores[doc.id] += rrf(rank)
        doc_map.setdefault(doc.id, doc)  # 保留首次出现的实例

ranked = sorted(scores, key=scores.get, reverse=True)[:top_k]

三个细节:

  • 用 defaultdict(float) 累加,同一文档在多份列表里各计一次分(这正是「共识」的数学表达)
  • doc_map 保留首次出现的文档实例(后续出现的只加分数、不覆盖内容)
  • 索引从 1 开始——rank=0 会让 1/(60+0) 比应有的值偏大

加权变体用于合并不同提供方的重排结果:Σ (weight_provider × 1/(k + rank + 1)),权重默认 0.5。

谁支持它 ​

Elasticsearch(rrf retriever,参数名 rank_constant,默认 60)、Azure AI Search(混合查询里自动应用)、Weaviate(可选的融合类型)、Qdrant、Milvus、OpenSearch、MongoDB Atlas。Postgres + pgvector 没有内置,但要实现它只是一个很普通的 SQL join。

RRF 是纯算术,不调模型,延迟可忽略——这是它成为混合检索默认融合步骤的主要原因。

RRF 和重排不是一回事 ​

RRF重排(Rerank)
输入多份排名列表单个 query-document 对
依据只有位置模型读两段文本
成本纯算术有一次模型推理
目的合并多路结果精排单个结果

生产系统通常两步都做:先用 RRF 融合,再对融合后的 top-k 重排。

重排:为什么要两阶段 ​

检索是快但粗糙的。 它用的是 bi-encoder:query 和每个文档各自独立嵌入,然后比相似度。嵌入可以预计算并缓存,所以能扩到百万级文档。

重排用的是 cross-encoder:query 和候选文档一起喂进一个模型,输出相关性分数。模型同时看到两段文本,能捕捉它们之间的细粒度交互。

差别有多大:cross-encoder 能理解「What were Q3 earnings?」和含有「$47.2M in Q3」的块高度相关,即使 bi-encoder 没抓到这层连接。

代价是 cross-encoder 比 bi-encoder 慢 100–1000 倍——因为要联合处理 query-document 对,不可能给百万文档预计算分数。

所以解法是两阶段:

   全量文档(百万级)
        │  ① 混合检索(bi-encoder)
        │     query 与每个文档各自独立嵌入,再比相似度
        │     └─ 嵌入可预计算并缓存 ──▶ 能扩到百万级文档
        ▼
   top-50 候选
        │  ② Cross-Encoder 重排
        │     query 与候选文档一起喂进模型,输出相关性分数
        │     └─ 联合处理,不可能给百万文档预计算 ──▶ 只能对少量候选做
        ▼
   top-5 最终结果
        │  ③ 取 3–5 条进 prompt
        ▼
   上下文组装

生产规则:召回约 20 条、重排到 5 条、送 3–5 条给模型。
重排 100 条以上的候选很少划算;轻量重排器对 20 个文档约加 80–120 ms。

生产规则:召回约 20 条,重排到 5 条,送 3–5 条给模型。重排 100 条以上的候选很少划算。

延迟上,轻量重排器对 20 个文档大约加 80–120 ms。

一条实测的收益曲线 ​

配置Context precision
纯稠密检索0.61
混合检索0.71
混合 + 重排0.79

两阶段各贡献了约 0.10 和 0.08。 这也说明「跳过重排器然后怪模型答得不好」是最常见的误判——问题出在召回侧,改 prompt 没用。

常见重排模型 ​

模型类型
Cohere Rerank 3.5 / 4.0 Pro托管 API,多语言;4.0 Pro 针对金融领域检索做过基准
Voyage rerank-2.5托管 API,托管方案里延迟最低
Jina-Reranker-v2 Multilingual开源权重,100+ 语言
bge-reranker-v2-m3开源权重,强基线
cross-encoder/ms-marco-MiniLM-L-6-v2开源权重,CPU 可跑,适合原型
ColBERTv2 / Jina-ColBERT-v2late-interaction 多向量重排——打分是 O(tokens) 而不是 O(docs),是延迟与精度之间的折中

查询变换:问题不在检索,而在查询本身 ​

「那个新政策变化的事怎么样了?」这种查询里没有任何具体术语,嵌入出来是模糊的,再好的检索器也找不对。两个方向:

MQE:同一问题的多种说法 ​

多查询扩展(Multi-Query Expansion) 的洞察:同一个问题可以有多种表述,不同表述能匹配到不同的相关文档。例:「如何学习 Python」→「Python 入门教程」「Python 学习方法」「Python 编程指南」,并行检索再合并。

python
prompt = [
    {"role": "system", "content": "你是检索查询扩展助手。生成语义等价或互补的多样化查询。使用中文,简短,避免标点。"},
    {"role": "user", "content": f"原始查询:{query}\n请给出{n}个不同表述的查询,每行一个。"}
]

它解决的是用词多样性问题,对模糊查询和专业术语查询效果显著。

HyDE:用答案找答案 ​

假设文档嵌入(Hypothetical Document Embeddings) 的出发点更根本:

问题通常是疑问句,文档内容是陈述句。两者在语义空间里的分布本来就不同。

传统方法拿问题去匹配文档,一开始就吃了这个分布差。HyDE 的做法是让 LLM 先写一段假设性的答案段落,再用这段答案去检索真实文档:

python
prompt = [
    {"role": "system", "content": "根据用户问题,先写一段可能的答案性段落,用于向量检索的查询文档(不要分析过程)。"},
    {"role": "user", "content": f"问题:{query}\n请直接写一段中等长度、客观、包含关键术语的段落。"}
]

关键在于:假设答案即使内容不完全正确也管用。它携带的关键术语、概念和表述风格与真实文档同属「陈述性文本」,能有效把检索引向正确区域。有一种解释是:生成文档可能包含幻觉,但稠密编码器会把它映射到与真实文档同一嵌入空间,从而被语料「锚住」。

反过来说代价:HyDE 每次检索都要多调一次 LLM,且那份假设文档本身可能是错的(但它不需要对,只需要「像」)。

什么时候该用这两个 ​

只在普通混合检索不够用时才加——它们各自增加一次 LLM 推理的延迟和成本。还有一个更轻的替代:查询重写(把口语化的 query 改写成更好的搜索 query),不生成假设答案,成本低得多。

查询本身有问题时,再好的检索器也找不对。三个方向,成本递增。

症状:查询是「那个新政策变化的事怎么样了?」
     里面没有任何具体术语,嵌入出来是模糊的

   ┌─ ① 查询重写(最轻)
   │     把口语化的 query 改写成更好的搜索 query
   │     不生成假设答案,成本低得多
   │
   ├─ ② MQE 多查询扩展
   │     同一问题生成多种表述,并行检索再合并
   │     例:如何学习 Python ──▶ Python 入门教程 / 学习方法 / 编程指南
   │     解决的是「用词多样性」问题
   │     判据:查询词与文档词对不上
   │
   └─ ③ HyDE 假设文档嵌入
         让 LLM 先写一段假设性的答案段落,再用它去检索真实文档
         出发点:问题通常是疑问句,文档内容是陈述句 ——
                 两者在语义空间里的分布本来就不同,
                 拿问题去匹配文档一开始就吃了这个分布差
         关键:假设答案即使内容不完全正确也管用 ——
               它携带的术语与表述风格与真实文档同属陈述性文本
         判据:查询是疑问句而文档是陈述句
         代价:每次检索多调一次 LLM,且那份假设文档本身可能是错的

合起来的判据:只在普通混合检索不够用时才加 ——
它们各自增加一次 LLM 推理的延迟与成本。

上下文组装:顺序也被研究过 ​

拿到最终几条之后,装配 prompt 也有讲究:

要点原因
最强的块放最前和最后模型对长上下文中间部分的注意力更低——这就是 lost in the middle(见 02-Agent Loop)
注入引用标记让答案能指回来源,也是可验证性的基础
明确指示「只依据给定上下文回答,上下文不足时说不知道」这一条指令单独就能消掉很大一部分编造的答案

另外要权衡 RAG 与「直接把大量文本塞进长上下文窗口」:检索把答案锚在正确事实上,同时压低成本;长上下文能帮助精化,但更慢、更贵,而且会稀释注意力。

两阶段各贡献一段收益。

Context precision

纯稠密检索      ████████████                 0.61
混合检索        ██████████████               0.71   ← +0.10,来自稀疏那一路
混合 + 重排     ████████████████             0.79   ← +0.08,来自 cross-encoder

   └─ 两阶段各贡献约 0.10 和 0.08。所以「跳过重排器、然后怪模型答得不好」
      是最常见的误判 —— 问题出在召回侧,改 prompt 没用。

分块基准的公开分数互相矛盾,不能横向比较
   Chroma 472 条查询     递归 88.5% recall / 语义 89.0%
   2026-02 的另一份测试   递归 69% / 语义 54%
   NAACL 2025 Findings   固定 200 词分块持平甚至优于语义分块
   └─ 三者用的语料、嵌入器、指标都不同。
      唯一能迁移到项目上的分数,是自己文档上测出来的那个。

失败的两种来源(这个分类决定了你该修什么) ​

来源表现该修什么
检索失败正确的块根本没被抓到,模型只能退回自身记忆分块 / 嵌入 / 混合检索 / 重排 / 查询变换
生成噪声上下文里有正确答案,模型仍说错prompt 指令 / 只依据上下文回答的约束 / 引用校验

知道是哪一个,才知道该动哪一段。 对应的三层护栏:置信度门控(top 检索分数太低就直接拒答而不是猜)、强制引用、验证链(第二个模型逐条断言核对上下文——生产试验里这能让幻觉率降约 40%)。

失败有两个来源,分类决定了该修哪一段。

输出不对
   │
   ├─ 检索失败
   │     表现:正确的块根本没被抓到,模型只能退回自身记忆
   │     该修:分块 / 嵌入 / 混合检索 / 重排 / 查询变换
   │
   └─ 生成噪声
         表现:上下文里有正确答案,模型仍说错
         该修:prompt 指令 / 只依据上下文回答的约束 / 引用校验

对应的三层护栏
   置信度门控   top 检索分数太低就直接拒答,而不是猜
   强制引用     让答案能指回来源,也是可验证性的基础
   验证链       第二个模型逐条断言核对上下文
                └─ 生产试验里这能让幻觉率降约 40%

评测要分开做(因为两侧是分开失败的) ​

指标回答什么问题
Recall@k / MRR / NDCG检索有没有抓到正确的块,并把它排在前面
Faithfulness答案的每条断言是否有上下文支撑(常定义为「已验证断言 ÷ 总断言」,高风险场景基线约 > 0.85)
Answer relevance答案是否真的回答了问题
Context precision / recall上下文的信噪比,以及有没有漏掉必需内容

工具上有 RAGAS、DeepEval、Promptfoo 可以自动化打分。

动手调分块之前先建评测集:从真实用户问题里挑 20–50 条,标注应答文件,测 hit@5 和 MRR。改一个变量(尺寸 / overlap / 策略)就跑一遍对比。二十条就够起步。

**公开的分块基准互相矛盾,不能横向比较。** Chroma 的 472 条查询研究给出递归分块 88.5% recall、语义分块 89.0%;另一份 2026-02 的测试给出递归 69%、语义 54%;NAACL 2025 Findings 的一篇论文则得出「固定 200 词分块持平甚至优于语义分块」。这三者用的**语料、嵌入器、指标都不同**。**唯一能迁移到你项目上的分数,是你自己文档上测出来的那个。**

成本与缓存 ​

RAG 的成本藏在四个桶里:嵌入 API、向量存储(按 GB)、重排器(按请求)、生成 token。

最大的节省来自缓存,三个位置:

  • 嵌入缓存——内容没变的文本绝不重新嵌入
  • 查询结果缓存——重复或近似重复的查询直接命中
  • 重排结果缓存——输入重复时复用

大部分超支来自三件事:召回太多、重新嵌入没变的文档、每次调用都塞过大的上下文。

HelloAgents 的实现:扩展检索框架 ​

把 MQE 和 HyDE 整合进统一流程,核心是「扩展 → 检索 → 合并」三步:

python
# 扩展
expansions = [query]
if enable_mqe and mqe_expansions > 0:
    expansions.extend(_prompt_mqe(query, mqe_expansions))
if enable_hyde:
    hyde_text = _prompt_hyde(query)
    if hyde_text:
        expansions.append(hyde_text)

# 去重
uniq = []
for e in expansions:
    if e and e not in uniq:
        uniq.append(e)

# 候选池分配:整体池子放大 4 倍,再平均分给每个扩展查询
pool = max(top_k * candidate_pool_multiplier, 20)   # candidate_pool_multiplier 默认 4
per  = max(1, pool // max(1, len(expansions)))

# 合并:同 id 保留最高分
if mid not in agg or s > float(agg[mid].get("score", 0.0)):
    agg[mid] = h

candidate_pool_multiplier(默认 4)的作用是保证有足够候选供筛选,max(..., 20) 是给 top_k 很小时的兜底。两个下限一起保证扩展查询不会被配额挤到没有结果。

注意过滤器里的元数据字段:memory_type="rag_chunk"、is_rag_data=True、data_source="rag_pipeline"、rag_namespace。RAG 的向量和记忆系统的向量存在同一套存储里,靠这些字段隔离命名空间(见 10-智能体记忆系统)。

场景策略
一般查询启用 MQE
专业领域查询MQE + HyDE 同时启用
性能敏感只做基础检索,或仅启用 MQE

判据是问题出在词法层还是语义层:查询词与文档词对不上 → MQE;查询是疑问句而文档是陈述句 → HyDE。

相关 ​

参考 ​

贡献者 ​

文件历史 ​