在 RAG(检索增强生成)系统开发中,查询转换是连接用户问题与知识库的关键桥梁。合理的查询转换能显著提升检索精度和回答质量,是从基础应用迈向高级开发的必经之路。今天我们就来系统拆解 RAG 中四种核心查询转换技术,结合 LlamaIndex 框架的实践经验,带大家深入理解背后的技术逻辑。

一、简单查询转换:用模板生成语义丰富的关联问题

我们首先接触到的是简单查询转换,它的核心思路是通过提示模板让大语言模型生成多个相关查询。比如下面这段代码:

python

from llama_index.core import PromptTemplate
from llama_index.llms.openai import OpenAI

prompt_rewrite_temp = """
你是一个聪明的查询生成器,请生成与以下查询相关的{num_queries}个查询问题
注意每个查询问题都占一行
我的查询{query}
生成查询列表:
"""
prompt_rewrite = PromptTemplate(prompt_rewrite_temp)
llm = OpenAI(model="gpt-3.5-turbo")

def rewrite_query(query: str, num: int = 3):
    response = llm.predict(prompt_rewrite, num_queries=num, query=query)
    queries = response.split('\n')
    return queries

这里我们定义了一个提示模板,告诉模型 “生成相关的查询问题”,然后通过llm.predict让模型基于用户输入生成多个子查询。这种方法的优势在于简单直接,但也存在明显不足 ——缺乏上下文时生成结果容易发散。所以我们更建议在数据准备阶段使用,比如用原始知识生成相似问题做语义丰富,或者抽取元数据生成假设性查询用于嵌入。

二、HyDE 查询:用假设性答案提升检索精度

接下来是效果拔群的 HyDE 技术(Hypothetical Document Embeddings),它的核心是 “先生成假设性答案,再用答案嵌入检索”。看代码实现:

python

from llama_index.core import PromptTemplate
from llama_index.llms.openai import OpenAI
from llama_index.core.indices.query_query_transform import HyDEQueryTransform

hyde_prompt_temp = """
请生成一段文字来回答输入问题
尽可能含有更多的关键细节
{context_str}
生成内容:
"""
hyde_prompt = PromptTemplate(hyde_prompt_temp)
llm = OpenAI(model="gpt-3.5-turbo")
hyde = HyDEQueryTransform(llm=llm)
hyde.update_prompts({'hyde_prompt': hyde_prompt})
query_bundle = hyde.run("请介绍苹果手机的主要配置")

HyDE 的巧妙之处在于:当用户问 “苹果手机主要配置” 时,模型先模拟生成一段详细答案(比如包含处理器、内存、摄像头等细节的描述),然后将这段答案嵌入向量空间进行检索。这样做的好处是:假设性答案包含了更多潜在的检索关键词,能让向量匹配更精准。实际使用时,我们可以通过TransformQueryEngine直接为现有查询引擎添加 HyDE 能力,无需改动底层检索逻辑。

三、多步查询转换:复杂问题的递进式分解

面对复杂问题,我们需要更精细的分解策略 —— 多步查询转换。它的核心是通过历史上下文递进式拆解问题,来看关键实现:

python

from llama_index import (
    SimpleDirectoryReader,
    VectorStoreIndex,
    ServiceContext,
    QueryBundle
)
from llama_index.llms import OpenAI
from llama_index.query_engine import MultiStepQueryEngine
from llama_index.prompts import PromptTemplate
from llama_index.query_engine.transform_query_engine import StepDecomposeQueryTransform

# 初始化大模型与服务上下文
llm = OpenAI(model="gpt-3.5-turbo", temperature=0.7)
service_context = ServiceContext.from_defaults(llm=llm)

# 加载数据并构建索引(假设 data 目录有相关文档)
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents, service_context=service_context)

# 定义自然语言风格的分解模板(核心!)
STEP_DECOMPOSE_PROMPT = PromptTemplate(
    """
    【用户输入】:{query}
    【当前已知信息】:{context}  // 前序步骤的子问题及答案
    
    【分解要求】:
    1. 请将用户输入分解为2-3个递进的子问题,每个子问题需基于已知信息逐步深入
    2. 每个子问题需明确“当前需要解决的具体问题”和“对最终答案的贡献”
    3. 用自然语言描述子问题,格式为:“子问题X:[问题描述](贡献:[说明该问题如何帮助回答原始问题])”
    
    【示例输出格式】:
    子问题1:...
    子问题2:...
    """
)

# 初始化查询转换器并绑定模板
step_decompose = StepDecomposeQueryTransform(
    llm=llm,
    step_decompose_prompt=STEP_DECOMPOSE_PROMPT  # 注入自定义模板
)

# 创建多步查询引擎
query_engine = index.as_query_engine()
multi_step_engine = MultiStepQueryEngine(
    query_engine=query_engine,
    query_transform=step_decompose,
    max_steps=3  # 限制最大分解步骤
)

# 定义用户输入并执行多步查询
user_input = "如何优化LlamaIndex在长文档场景下的检索效率?"
response = multi_step_engine.query(user_input)

这里的关键是STEP_DECOMPOSE_PROMPT模板,它要求模型在分解时考虑 “当前已知信息”,比如第一步分解出 “LlamaIndex 长文档检索瓶颈”,第二步就会结合第一步答案继续追问 “如何解决分块过细的语义断裂问题”。这种层层递进的方式,让复杂问题的解决过程更可控,尤其适合需要多维度信息整合的场景。

四、子问题转换:基于工具约束的精准拆解

当我们需要结合特定工具时,子问题转换就派上用场了。它的特点是:根据可用工具的能力生成可解答的子问题。看这个例子:

python

from llama_index import (
    SimpleDirectoryReader,
    VectorStoreIndex,
    ServiceContext,
    QueryBundle
)
from llama_index.llms import OpenAI
from llama_index.tools import ToolMetadata
from llama_index.question_gen.openai import OpenAIQuestionGenerator
from llama_index.prompts import PromptTemplate

# 初始化大语言模型
llm = OpenAI(model="gpt-3.5-turbo")
service_context = ServiceContext.from_defaults(llm=llm)

# 加载数据
documents = SimpleDirectoryReader('data').load_data()
index = VectorStoreIndex.from_documents(documents, service_context=service_context)

# 定义可用工具及其元数据
tools = [
    ToolMetadata(
        name="document_search",
        description="在文档中搜索相关信息",
        parameters={"query": "要搜索的查询语句"}
    ),
    ToolMetadata(
        name="statistics_calculation",
        description="进行统计计算,如平均值、总和等",
        parameters={"data": "要进行计算的数据列表", "operation": "要进行的计算操作,如 'mean', 'sum' 等"}
    )
]

# 定义子问题生成提示模板
SUBQUESTION_PROMPT_TMPL = """
原始问题: {query}
可用工具: {tool_descriptions}
请根据可用工具将原始问题转换为多个子问题,每个子问题要能使用一个工具来解答。
子问题列表:
"""
subquestion_prompt = PromptTemplate(SUBQUESTION_PROMPT_TMPL)

# 初始化问题生成器
question_generator = OpenAIQuestionGenerator(
    llm=llm,
    prompt=subquestion_prompt
)

# 定义原始查询
original_query = "如何提高 LlamaIndex 应用的性能?"

# 创建 QueryBundle 对象
query_bundle = QueryBundle(query=original_query)

# 生成子问题
subquestions = question_generator.generate(
    query_bundle,
    tools=tools
)

# 打印子问题
print("生成的子问题:")
for subquestion in subquestions:
    print(subquestion)

# 模拟使用工具解答子问题
query_engine = index.as_query_engine()
for subquestion in subquestions:
    if "document_search" in subquestion:
        # 假设子问题适合使用 document_search 工具
        result = query_engine.query(subquestion)
        print(f"子问题 '{subquestion}' 的解答结果: {result}")

这里通过OpenAIQuestionGenerator告诉模型 “只能用给定工具生成子问题”,比如面对 “如何提高 LlamaIndex 性能”,可能生成 “LlamaIndex 性能优化的文档建议有哪些(适合 document_search 工具)” 和 “各优化方案的效率数据对比(适合 statistics_calculation 工具)”。这种带约束的分解,让子问题与工具能力精准匹配,避免生成无法落地的无效问题。

五、汇总生成

在完成子问题的解答后,我们还需要将这些答案汇总成最终的回答。以下是实现汇总生成的代码:

python

# 定义汇总提示模板
SUMMARY_PROMPT_TMPL = """
原始问题: {original_query}
子问题回答如下:
{answers}
请根据以上子问题的回答,总结出原始问题的最终答案。
最终答案:
"""
summary_prompt = PromptTemplate(SUMMARY_PROMPT_TMPL)

# 假设 subquestion_answers 是子问题的回答列表
subquestion_answers = []
# 这里需要将子问题的解答结果添加到 subquestion_answers 列表中
# 例如:
# for subquestion in subquestions:
#     if "document_search" in subquestion:
#         result = query_engine.query(subquestion)
#         subquestion_answers.append(str(result))

# 构建汇总提示
answers_str = "\n".join(subquestion_answers)
prompt = summary_prompt.format(original_query=original_query, answers=answers_str)

# 调用大模型生成最终答案
final_answer = llm.predict(prompt)
print("最终答案:")
print(final_answer)

汇总生成的过程就是将子问题的答案整合起来,形成对原始问题的完整回答。通过定义汇总提示模板,我们可以引导大模型根据子问题的答案进行总结和提炼,得到最终的结果。

技术选型与实践建议

回顾这四种技术,我们可以总结出清晰的适用场景:

  • 简单查询转换:适合数据准备阶段做语义扩展,不建议直接用于实时查询
  • HyDE 查询:推荐在需要提升检索精度的场景优先使用,尤其适合长文本或语义模糊的查询
  • 多步查询转换:复杂问题的标配方案,通过递进分解实现深度语义解析
  • 子问题转换:当系统集成了特定工具(如搜索、计算模块)时,用工具约束生成可执行的子任务

在工程实现上,LlamaIndex 提供的QueryBundle类非常方便,它能统一管理原始查询和转换后的子查询信息。而ServiceContext的使用,则让大语言模型与检索组件的交互更标准化,降低了系统整合的复杂度。

总结:让查询转换成为 RAG 的智能入口

检索前的查询转换,本质上是在构建用户问题与知识库之间的 “翻译层”。通过简单改写、假设性生成、多步分解、工具约束等不同策略,我们可以让系统更智能地理解用户意图,从而实现更精准的知识召回。

这些技术并非孤立存在,实际项目中我们可以组合使用:比如先用 HyDE 生成假设性嵌入提升召回,再通过多步分解实现答案的逻辑整合。关键是根据具体场景选择合适的转换策略,让查询转换成为 RAG 系统的智能起点。

如果你在实际开发中遇到查询转换的难题,欢迎在评论区交流。觉得内容有帮助的话,别忘了点赞收藏,后续我们会分享更多 RAG 实战技巧,关注我获取最新内容~

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐