Chapter 08

查询变换与高级 RAG · HyDE/多查询/子问题

Ch7 的组合已经能到 B+ 的水平,但要再往上——从 query 本身入手。HyDE 先假设答案再检索,SubQ 把复杂问题拆小,Self-RAG 自我反思,Step-back 先抽象再具体。这章讲"查询变换"流派。

一、为什么 query 要变换

用户的原始 query 有三类"病":

  1. 太抽象——"我要优化数据库性能",embedding 匹配不到具体方案
  2. 太复杂——"公司 2024 年 Q3 和 Q4 营收对比,利润率变化原因",一问多答
  3. 信息量少——"退款",没上下文的向量很难定位

查询变换就是让 query 更接近文档的语言或分解到可检索的粒度

二、HyDE:假设文档嵌入

经典思路:让 LLM 先写出"如果答案存在应该长什么样",然后用这个假设答案去检索。

query: "双因素认证怎么配?" ↓ LLM 生成假设文档 hypo: "双因素认证需要在设置页面启用,常见方法有 TOTP(Google Authenticator)、短信验证码。..." ↓ embed(hypo) 去查 ↓ 召回的是实际配置文档(语义更接近"假设答案"而非原 query)
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
from llama_index.core.query_engine import TransformQueryEngine

hyde = HyDEQueryTransform(include_original=True)   # 同时保留原 query 更稳
base_qe = index.as_query_engine()
hyde_qe = TransformQueryEngine(base_qe, hyde)

ans = hyde_qe.query("双因素认证怎么配?")
什么时候用 HyDE:
✅ 学术/专业领域(医学、法律、科研)——用户不会用文档里的专业术语
✅ query 很短("疟疾怎么治")对应长文档
❌ 精确关键词问题(产品型号、代号)——HyDE 反而会被 LLM 的幻觉污染
❌ 简单 FAQ——没必要多一次 LLM 调用

三、SubQuestionQueryEngine:把大问题拆小

"对比 2024 Q3 和 Q4 的营收"——两个子问题分别检索后再综合:

from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata

tools = [
    QueryEngineTool(
        query_engine=qe_2024q3,
        metadata=ToolMetadata(name="q3_2024", description="2024 年 Q3 财报"),
    ),
    QueryEngineTool(
        query_engine=qe_2024q4,
        metadata=ToolMetadata(name="q4_2024", description="2024 年 Q4 财报"),
    ),
]

sqe = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=tools,
    use_async=True,       # 子问题并发跑
    verbose=True,
)

ans = sqe.query("对比 Q3 和 Q4 的营收变化并说明原因")
# LLM 先拆成:"2024 Q3 营收是多少" + "2024 Q4 营收是多少",各自查,最后合成

SubQ 特别适合跨文档对比、多维度聚合的问题。缺点:每个子问题一次 LLM,问题越复杂成本线性上涨

四、MultiStepQueryEngine:多步推理

SubQ 是并行分解,MultiStep 是串行——上一步的答案变成下一步的 query:

from llama_index.core.query_engine import MultiStepQueryEngine
from llama_index.core.indices.query.query_transform import StepDecomposeQueryTransform

step_transform = StepDecomposeQueryTransform(llm=llm, verbose=True)

mqe = MultiStepQueryEngine(
    query_engine=base_qe,
    query_transform=step_transform,
    num_steps=3,
)

ans = mqe.query("CEO 是谁?他上个公司是哪家?那家公司主营什么?")
# 第一步:查 CEO → 得到"张三"
# 第二步:查 张三 上个公司 → 得到"Acme"
# 第三步:查 Acme 主营业务

五、Step-Back Prompting:先抽象再具体

Google DeepMind 提的技巧:先让 LLM 从具体问题抽象出原则性问题,查一次;再回到具体问题查一次,两个结果融合

# LlamaIndex 没有直接 API,自己实现(也可以用 Workflow 包)
from llama_index.core.retrievers import BaseRetriever

class StepBackRetriever(BaseRetriever):
    def __init__(self, base, llm):
        self.base, self.llm = base, llm

    def _retrieve(self, query_bundle):
        abstract = self.llm.complete(
            f"把下面的具体问题抽象成一个更一般性的问题。只输出一句。\n\n原题:{query_bundle.query_str}"
        ).text
        orig_nodes = self.base.retrieve(query_bundle.query_str)
        abs_nodes  = self.base.retrieve(abstract)
        return orig_nodes + abs_nodes    # 简单并集,生产上配 RRF

效果明显的场景:专业咨询、概念性问题。"怎么防止内存泄漏?"先抽象成"什么是内存泄漏、有哪些成因",更容易召回理论章节,最后答案也更有深度。

六、Self-RAG:自我反思

Self-RAG 的核心:检索完后让 LLM 先判断检索结果够不够用,不够用就再次检索或直接说"不知道"。LlamaIndex 没有现成的 SelfRAG QueryEngine,但可以用 Workflow 搭(Ch10 会讲):

# 伪代码,Workflow 实现见 Ch10
async def self_rag(query):
    nodes = retriever.retrieve(query)

    # 1. 判断相关性
    relevant = await llm_check(f"这些片段能回答问题吗?", nodes, query)
    if not relevant:
        # 2. 改写 query 再查
        new_q = await llm_rewrite(query)
        nodes = retriever.retrieve(new_q)

    # 3. 生成答案并检查 hallucination
    ans = await synthesizer(nodes, query)
    grounded = await llm_check_grounding(ans, nodes)
    if not grounded:
        return "根据资料无法确定答案"
    return ans

七、CRAG(Corrective RAG)

Self-RAG 的表亲——检索 → 打分 → 低分时回退到 Web 搜索补充:

# 核心逻辑
nodes = retriever.retrieve(q)
score = evaluator.evaluate(q, nodes)   # 内部文档相关度

if score < 0.3:
    # 完全不相关 → Web 搜索
    web_nodes = web_search(q)
    nodes = web_nodes
elif score < 0.7:
    # 有点相关 → 内部 + Web 混合
    web_nodes = web_search(q)
    nodes = nodes + web_nodes
# 否则:直接用内部

适合开放域问答产品——内部知识不够用就去外面搜,而不是硬编。实现上 LlamaIndex 的 TavilyToolSpec / DuckDuckGoSearchToolSpec 可以直接拿来搜网。

八、Ensemble & 反事实

更进一步的工业级技巧:

九、GraphRAG:图增强检索

Microsoft 2024 年提的:不只建向量索引,还用 LLM 构建"实体关系图 + 社区摘要",查询时结合图结构和向量。LlamaIndex 对应:

from llama_index.core import PropertyGraphIndex
from llama_index.core.indices.property_graph import (
    SchemaLLMPathExtractor, ImplicitPathExtractor,
)

kg_extractor = SchemaLLMPathExtractor(
    llm=llm,
    possible_entities=["PERSON", "COMPANY", "PRODUCT"],
    possible_relations=["WORKS_AT", "OWNS", "ACQUIRED"],
    strict=True,
)

index = PropertyGraphIndex.from_documents(
    docs,
    kg_extractors=[kg_extractor, ImplicitPathExtractor()],
    embed_kg_nodes=True,
    show_progress=True,
)

# 查询时 hybrid:向量 + 图路径
qe = index.as_query_engine(include_text=True, similarity_top_k=5)
ans = qe.query("Acme 被谁收购,收购方的 CEO 是?")

适合实体密集的领域(金融、法律、医疗),问题里涉及"谁-关系-谁"的多跳时效果显著。代价:建图贵(每个 chunk 一次 LLM 抽三元组),生产要用 Neo4j/FalkorDB。

十、选型速查

问题特征推荐技术成本
短问题、领域专业HyDE+1 LLM
明显可拆分(多对象)SubQuestionQueryEngine+N LLM(并行)
多步推理(A→B→C)MultiStepQueryEngine+N LLM(串行)
概念/原则性问题Step-Back+1 LLM
开放域、需要兜底CRAG + Web+搜索调用
内部知识不确定性大Self-RAG+2-3 LLM
实体关系密集GraphRAG / PropertyGraphIndex建图贵
模糊的短查询QueryFusion 改写(Ch7)+1 LLM

十一、组合示例:一个健壮的 production RAG

# 这个组合对抗了三种常见失败:
# 1. 用户 query 太抽象 → HyDE
# 2. 复杂多维问题 → SubQ 自动拆(由 Agent 决定,Ch9)
# 3. 内部没答案 → 回退 Web

from llama_index.core.agent import FunctionCallingAgent
from llama_index.tools.tavily_research import TavilyToolSpec

tavily = TavilyToolSpec(api_key="...")

tools = [
    QueryEngineTool.from_defaults(
        query_engine=hyde_qe,
        name="internal_kb",
        description="公司内部知识库,包含产品、流程、合同",
    ),
    *tavily.to_tool_list(),
    QueryEngineTool.from_defaults(
        query_engine=sqe,
        name="compare_analysis",
        description="跨文档对比分析,适合 对比/统计/多维 问题",
    ),
]

agent = FunctionCallingAgent.from_tools(tools, llm=llm, verbose=True)
# Agent 根据问题自选工具,达到"工具 = 变换策略"的融合

十二、反模式

  1. HyDE 用在精确查询:LLM 幻觉反而污染向量。
  2. SubQ 问题太简单还拆:一个问题 → N 次 LLM,延迟和成本都爆。有 问题复杂度检测 才用 SubQ。
  3. Self-RAG 没设最大循环次数:不停重写查询,死循环。
  4. GraphRAG 所有数据都建图:建图成本 LLM 一次每 chunk,不分领域烧钱。
  5. 多种变换叠加用:HyDE + SubQ + Step-Back 全上,延迟 30s+,不值。
  6. 拿 query_transform 替代好的 embedding:变换是锦上添花,基础召回差就先换 embedding / rerank。

十三、本章小结

记住:
① 查询变换是 RAG 的"最后 10 分"——先把基础(召回 + rerank)做到 B+,再上变换冲 A。
② HyDE(短抽象问题)、SubQ(多对象)、MultiStep(多跳)、Step-Back(概念)——对症下药,别乱叠。
③ Self-RAG/CRAG 用 Workflow(Ch10)实现更自然,循环 + 兜底 + 幻觉检测 一体。
④ GraphRAG 适合实体关系密集的领域——普通 RAG 够用时不值得引入。