RAG 系列(十三):查询优化——让问题问得更好

问法不同,结果截然不同

向量检索有一个容易被忽视的脆弱性:同一个问题换个说法,检索结果可能完全不同。

"BGE 模型在中文上效果怎么样" 和 "中文 Embedding 推荐哪个" 语义高度相似,但它们的 embedding 向量在高维空间中的位置可能相差不少,导致检索到的文档集合差异很大。

这是 Bi-Encoder 的结构性问题:embedding 是在 query 和 doc 互不知情的情况下各自计算的,对措辞的微妙变化非常敏感。

上一篇我们优化了文档侧 ------更好的分块策略让文档更容易被找到。本篇从问题侧入手:在把问题送进向量库之前,先对问题本身做一些处理,让它的召回效果更稳定、更全面。

三种策略:

  1. Multi-Query:生成多个问法,多角度检索后合并
  2. HyDE:先生成假设答案,用答案去检索
  3. Query Decomposition:把复杂问题拆成多个子问题

Multi-Query:多角度问法扩大召回面

核心思路

一个问题对应向量空间中的一个点,这个点可能恰好和某些相关文档距离较远。如果能从多个方向逼近,就能覆盖更大的区域。

css 复制代码
原始问题 → LLM 改写 → [问法1, 问法2, 问法3]
                              ↓
                    分别检索,合并去重
                              ↓
                     取前 TOP-K 返回

代码实现

python 复制代码
from langchain_classic.retrievers import MultiQueryRetriever

MULTI_QUERY_PROMPT = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的问题改写助手。"),
    ("human",
     "请将以下问题改写为 3 个不同的表达方式,从不同角度提问,"
     "以便在向量数据库中检索到更多相关内容。\n"
     "每行输出一个问题,不要编号,不要解释。\n\n"
     "原始问题:{question}"),
])

# 方式一:使用 LangChain 内置封装
retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
    llm=llm,
)

# 方式二:手动实现,可以控制 Prompt 和合并逻辑
multi_query_chain = MULTI_QUERY_PROMPT | llm | StrOutputParser()
variants_text = multi_query_chain.invoke({"question": question})
variants = [q.strip() for q in variants_text.strip().split("\n") if q.strip()]

all_docs = base_retriever.invoke(question)   # 原始问题
for variant in variants:
    all_docs.extend(base_retriever.invoke(variant))

return dedup_docs(all_docs)[:TOP_K]

适用场景: 问法多样、用户可能用不同术语描述同一概念的场景。代价是每次查询多调用 3 次检索(LLM 改写 + 3 次向量检索)。


HyDE:用假设答案去检索

核心思路

HyDE(Hypothetical Document Embeddings,假设文档嵌入)是 2022 年提出的方法,它的出发点是:

问题的 embedding 和答案的 embedding 天生处于不同的语义空间。

向量库里存的是文档(答案空间),但检索时用的是 query(问题空间),两者的 embedding 分布本来就不完全重叠。

HyDE 的解法:让 LLM 先生成一段假设性的答案,用这个假设答案的 embedding 去检索------假设答案和真实答案处于同一个语义空间,比问题更接近文档。

markdown 复制代码
query → LLM 生成假设答案(~100字)→ 假设答案 embedding
                                            ↓
                                    向量检索(找最近的文档)
                                            ↓
                                    返回真实文档给 LLM

假设答案不需要准确,只需要语义上接近真实答案,让 embedding 落在正确的区域就够了。

代码实现

python 复制代码
HYDE_PROMPT = ChatPromptTemplate.from_messages([
    ("system", "你是一个技术知识助手。"),
    ("human",
     "请为以下问题写一段假设性的回答,约 100 字。"
     "这段回答将用于向量检索,不需要完全准确,"
     "只需要在语义上与真实答案接近。\n\n"
     "问题:{question}"),
])

hyde_chain = HYDE_PROMPT | llm | StrOutputParser()
hypothetical_answer = hyde_chain.invoke({"question": question})

# 用假设答案的 embedding 检索
hyp_embedding = embeddings.embed_query(hypothetical_answer)
results = vectorstore.similarity_search_by_vector(hyp_embedding, k=TOP_K)

适用场景: 问题措辞和文档措辞差异较大的场景,比如用户用口语提问、文档是技术语言写的。代价是额外一次 LLM 调用 + 一次 embedding 计算。


Query Decomposition:拆解复杂问题

核心思路

有些问题天然是多跳的:

"RAG 系统在中文场景下,应该选什么 Embedding 模型和向量数据库?"

这个问题包含两个独立的子问题:

  1. 中文场景推荐哪个 Embedding 模型?
  2. 企业级应用推荐哪个向量数据库?

用一次检索试图同时覆盖两个问题,往往两个都覆盖得不完整。

Query Decomposition 的思路:先拆,再各自检索,最后把所有检索结果合并给 LLM 一起回答。

css 复制代码
复杂问题 → LLM 拆解 → [子问题1, 子问题2, 子问题3]
                              ↓
                    分别检索,合并去重
                              ↓
                 所有子问题的文档汇总给 LLM

代码实现

python 复制代码
DECOMPOSE_PROMPT = ChatPromptTemplate.from_messages([
    ("system", "你是一个问题分析助手。"),
    ("human",
     "请将以下复杂问题拆分为 2-3 个简单的子问题,"
     "每个子问题可以独立检索。\n"
     "每行输出一个子问题,不要编号,不要解释。\n\n"
     "原始问题:{question}"),
])

decompose_chain = DECOMPOSE_PROMPT | llm | StrOutputParser()
sub_questions_text = decompose_chain.invoke({"question": question})
sub_questions = [q.strip() for q in sub_questions_text.strip().split("\n") if q.strip()]

all_docs = []
for sub_q in sub_questions:
    all_docs.extend(base_retriever.invoke(sub_q))

return dedup_docs(all_docs)[:TOP_K]

适用场景: 涉及多个概念或跨主题的复杂问题。代价是多次 LLM 调用(拆解 + N 次检索)。


实验结果

diff 复制代码
==================================================================================
  RAGAS 指标对比(四种查询优化策略)
==================================================================================

  指标                    Naive   Multi-Query    HyDE   Decomposed
  ────────────────────────────────────────────────────────────────
  context_recall          0.625       0.625      0.750     0.875 ◀
  context_precision       0.583       0.583      0.726 ◀   0.590
  faithfulness            0.833       0.883      0.946 ◀   0.911
  answer_relevancy        0.406       0.412      0.377     0.474 ◀
==================================================================================

数字解读:

Multi-Query(context_recall = 0.625,与 Naive 持平)

出乎意料------在这个知识库上,改写问法并没有带来召回提升。原因在于:知识库只有 8 篇文档,向量检索本身就很准,改写出来的 3 个变体和原始问题检索到的是同一批文档,合并去重后没有增量。Multi-Query 的价值在大型知识库上会更加明显,那时候不同问法能触达向量空间的不同区域。

HyDE(context_recall = 0.750,+0.125;context_precision = 0.726,+0.143)

两个指标都有改善,且 faithfulness 也跳到了 0.946------是四种策略里最高的。假设答案的 embedding 确实落在了更接近文档的语义空间,不仅找到了更多相关文档,而且排序质量也更好。

Query Decomposition(context_recall = 0.875,+0.250)

召回率提升最显著。拆解问题后,每个子问题单独检索,覆盖了原始问题一次检索无法触及的文档。最终合并的文档集合更全面,faithfulness 也相应提升到 0.911。


三种策略的本质差异

Multi-Query HyDE Query Decomposition
解决的问题 措辞敏感、问法单一 问答语义空间不匹配 多跳/多概念问题
改造对象 问题措辞(多角度) 问题形态(问→答) 问题结构(整→部分)
额外 LLM 调用 1次(改写) 1次(生成假设答案) 1次(拆解)
额外检索次数 3次 0次 2-3次
本实验最优指标 --- context_precision、faithfulness context_recall
最适合的场景 大知识库、口语化查询 专业文档、问答风格差异大 多概念、需要综合多篇文档

核心区别在于变换维度:

  • Multi-Query 在"如何问"上做文章------同一个意思,多种说法
  • HyDE 在"用什么检索"上做文章------用答案代替问题
  • Query Decomposition 在"问什么"上做文章------把一个大问题拆成几个小问题

可以叠加使用

这三种策略并不互斥,可以根据场景组合:

python 复制代码
# 示例:HyDE + Multi-Query 叠加
# 1. 生成假设答案 embedding
hyp_embedding = embeddings.embed_query(hyde_answer)

# 2. 同时对改写后的 3 个问题检索
all_docs = vectorstore.similarity_search_by_vector(hyp_embedding, k=4)
for variant in multi_query_variants:
    all_docs.extend(base_retriever.invoke(variant))

return dedup_docs(all_docs)[:TOP_K]

叠加后召回面更大,但 API 调用次数也随之增加。生产环境里建议根据查询类型自适应选择策略,而不是无差别叠加。


完整代码

代码已开源:

github.com/chendongqi/...

核心文件:

  • query_optimization.py --- 四种查询优化策略的完整对比实验

运行方式:

bash 复制代码
git clone https://github.com/chendongqi/llm-in-action
cd 13-query-optimization
cp .env.example .env
pip install -r requirements.txt
python query_optimization.py

小结

本文对比了三种查询优化策略的效果:

  1. Multi-Query:对措辞多样性有帮助,在大知识库上价值更突出;本实验因知识库较小效果不明显
  2. HyDE:用假设答案替代问题做检索,跨越了问答语义空间的鸿沟,context_precision 和 faithfulness 均有明显提升
  3. Query Decomposition:拆解复杂问题,多路检索后合并,context_recall 提升最大(+0.250)

这三种优化都在问题进入向量库之前完成,不需要改动分块策略、不需要调整 Embedding 模型,是 RAG 系统里成本最低的一类优化。


参考资料

相关推荐
回眸&啤酒鸭10 分钟前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
博图光电18 分钟前
Libra 27105相关技术参数
人工智能·数码相机
浮链序31 分钟前
怎么证明"你这个模型是偷我的"——把蒸馏变成取证工具
算法·安全·llm
IT_陈寒43 分钟前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
yumgpkpm1 小时前
Acceldata ODP(Open Data Platform)3.3.6.4(RHEL9)保姆级完整安装手册
大数据·人工智能·hive·hadoop·kafka·hbase·cloudera
程序员cxuan1 小时前
腾讯又来一王炸,开源版 WorkBuddy 太夯了!
人工智能·后端·程序员
邓工说电1 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
阿里云大数据AI技术1 小时前
Lance 数据检索怎么选,当然阿里云 Milvus 向量湖
人工智能
出海客1 小时前
跨境电商多语言客服知识库怎么建:资料结构、检索边界与人工升级
大数据·人工智能
xsd202411181 小时前
从自主导航到视觉读表:一台工业巡检机器人的全栈技术链路拆解
人工智能