一、为什么 query 要变换
用户的原始 query 有三类"病":
- 太抽象——"我要优化数据库性能",embedding 匹配不到具体方案
- 太复杂——"公司 2024 年 Q3 和 Q4 营收对比,利润率变化原因",一问多答
- 信息量少——"退款",没上下文的向量很难定位
查询变换就是让 query 更接近文档的语言或分解到可检索的粒度。
二、HyDE:假设文档嵌入
经典思路:让 LLM 先写出"如果答案存在应该长什么样",然后用这个假设答案去检索。
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("双因素认证怎么配?")
✅ 学术/专业领域(医学、法律、科研)——用户不会用文档里的专业术语
✅ 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 & 反事实
更进一步的工业级技巧:
- Ensemble Retrieval:同一份数据建两个 embedding 模型的索引,分别检索后 RRF——用多样性换召回
- 反事实检索:除了查"是什么",同时查"不是什么",把反例一起给 LLM 避免 overclaim
- Chain-of-Note:先让 LLM 给每个检索结果写"笔记"(相关度、关键信息),再基于笔记合成——更好地对抗噪声
九、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 根据问题自选工具,达到"工具 = 变换策略"的融合
十二、反模式
- HyDE 用在精确查询:LLM 幻觉反而污染向量。
- SubQ 问题太简单还拆:一个问题 → N 次 LLM,延迟和成本都爆。有 问题复杂度检测 才用 SubQ。
- Self-RAG 没设最大循环次数:不停重写查询,死循环。
- GraphRAG 所有数据都建图:建图成本 LLM 一次每 chunk,不分领域烧钱。
- 多种变换叠加用:HyDE + SubQ + Step-Back 全上,延迟 30s+,不值。
- 拿 query_transform 替代好的 embedding:变换是锦上添花,基础召回差就先换 embedding / rerank。
十三、本章小结
① 查询变换是 RAG 的"最后 10 分"——先把基础(召回 + rerank)做到 B+,再上变换冲 A。
② HyDE(短抽象问题)、SubQ(多对象)、MultiStep(多跳)、Step-Back(概念)——对症下药,别乱叠。
③ Self-RAG/CRAG 用 Workflow(Ch10)实现更自然,循环 + 兜底 + 幻觉检测 一体。
④ GraphRAG 适合实体关系密集的领域——普通 RAG 够用时不值得引入。