RAG 检索增强
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 token | 50–100(10–20%) | 混合内容的稳妥默认 |
| 长法律 / 技术 / 合同 | ~1024 token | 100–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,倒数排名融合) 绕开了这个问题:只用排名,不用分数。
其中
k=60 来自 2009 年 Cormack、Clarke、Büttcher 的论文,是经验取值。取大的 k 会把分数曲线压平,避免某一路的 top-1 自动主导整个结果。
手算一遍就明白了
| 情形 | 计算 | 得分 |
|---|---|---|
| 向量排 #1、BM25 排 #5 | 0.0318 | |
| 向量排 #3、BM25 排 #2 | 0.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 是纯算术、不调模型,延迟可忽略 —— 这是它成为混合检索默认融合步骤的主要原因。实现要点
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会让比应有的值偏大
加权变体用于合并不同提供方的重排结果:Σ (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-v2 | late-interaction 多向量重排——打分是 O(tokens) 而不是 O(docs),是延迟与精度之间的折中 |
查询变换:问题不在检索,而在查询本身
「那个新政策变化的事怎么样了?」这种查询里没有任何具体术语,嵌入出来是模糊的,再好的检索器也找不对。两个方向:
MQE:同一问题的多种说法
多查询扩展(Multi-Query Expansion) 的洞察:同一个问题可以有多种表述,不同表述能匹配到不同的相关文档。例:「如何学习 Python」→「Python 入门教程」「Python 学习方法」「Python 编程指南」,并行检索再合并。
prompt = [
{"role": "system", "content": "你是检索查询扩展助手。生成语义等价或互补的多样化查询。使用中文,简短,避免标点。"},
{"role": "user", "content": f"原始查询:{query}\n请给出{n}个不同表述的查询,每行一个。"}
]它解决的是用词多样性问题,对模糊查询和专业术语查询效果显著。
HyDE:用答案找答案
假设文档嵌入(Hypothetical Document Embeddings) 的出发点更根本:
问题通常是疑问句,文档内容是陈述句。两者在语义空间里的分布本来就不同。
传统方法拿问题去匹配文档,一开始就吃了这个分布差。HyDE 的做法是让 LLM 先写一段假设性的答案段落,再用这段答案去检索真实文档:
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 整合进统一流程,核心是「扩展 → 检索 → 合并」三步:
# 扩展
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] = hcandidate_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。
相关
- 07-模型幻觉 —— RAG 是三层缓解手段里推理生成层的手段之一
- 10-智能体记忆系统 —— 共享同一套嵌入与向量存储,靠元数据隔离
- Context Engineering —— 检索回来的内容要跟其他五个信息源抢 token 预算
- 02-Agent Loop ——
lost in the middle与循环的上下文增长是同一件事的两面
参考
- 《Hello-Agents》第八章 §8.3
- https://www.scaler.com/topics/reciprocal-rank-fusion/
- https://aiengineeringfromscratch.com/lesson.html?path=phases/11-llm-engineering/07-advanced-rag
- https://www.theagentecosystem.com/blog/rag-chunking-strategies 、https://techsy.io/en/blog/rag-chunking-strategies
- https://www.tech-japan.jp/en/blog/chunking-research/
- https://arxiv.org/html/2604.01733v1
- https://www.apex-logic.net/news/production-rag-architecture-2026
- https://deepwiki.com/weaviate/retrieve-dspy/10.4-reciprocal-rank-fusion-(rrf)
- https://techsy.io/en/blog/rag-chunking-strategies
YJ