RAG 每日一技(十三):检索一次不够?学习查询改写与迭代式检索!

前情回顾

到目前为止,我们构建的RAG系统,其检索流程本质上是"一锤子买卖":用户提问 -> 系统检索 -> 返回结果。如果第一次检索返回的都是不相关的垃圾信息,整个流程就宣告失败。

这就像一个经验不足的侦探,跟丢了第一条线索后,就直接放弃了整个案件。而一个经验丰富的老侦探,则会重新审视案情,分析失败原因,然后从一个新的、更巧妙的角度再次切入。

我们能让我们的RAG系统也成为一名"老侦探"吗?答案是肯定的。核心思想就是:不要把用户的原始问题当成一成不变的"圣旨",而是要授权给RAG系统,让它在检索前或检索失败后,对原始问题进行"改写"和"优化"。

这项技术,统称为查询转换(Query Transformation)

为什么要改写用户的查询?

用户的原始查询可能存在以下问题:

  1. 过于宽泛或模糊:"请谈谈人工智能。"
  2. 包含多个子问题:"对比一下Refine和Map-Reduce策略的优缺点。"
  3. 语言风格与文档库不匹配:用户用口语提问,而文档库是专业术语。

对这些查询进行改写,可以大大提高检索的精准度。下面我们介绍两种强大且常用的查询改写技术。

技术一:查询分解 (Query Decomposition)

当一个问题包含多个方面时,一次性检索很难找到一个"完美"的文档能覆盖所有方面。查询分解的思想就是将一个复杂问题,拆解成多个独立的、更简单的子问题,然后对每个子问题分别进行检索,最后将所有结果汇总。

  • 原始问题:"对比一下Refine和Map-Reduce策略的优缺点。"
  • 分解后的子问题
    1. "RAG中的Refine策略有什么优点?"
    2. "RAG中的Refine策略有什么缺点?"
    3. "RAG中的Map-Reduce策略有什么优点?"
    4. "RAG中的Map-Reduce策略有什么缺点?"

我们可以用一个LLM来自动完成这个分解任务。

分解Prompt模板示例:

python 复制代码
DECOMPOSITION_PROMPT_TEMPLATE = """
你是一个查询分析专家。请将用户提出的复杂问题,分解成一系列更简单的、可以独立回答的子问题。
请直接返回一个Python列表(List)格式的字符串。

复杂问题: "{question}"

分解后的子问题列表:
"""

工作流程: 复杂问题 -> LLM(分解) -> [子问题1, 子问题2, ...] -> 分别检索 -> 汇总结果 -> LLM(最终生成)

技术二:假设性文档嵌入 (HyDE)

HyDE (Hypothetical Document Embeddings) 是一种脑洞大开、但效果惊人的技术。

我们之前的思路是:计算"问题"和"文档"的向量相似度。但问题通常很短,而文档很长,这种不对称性有时会影响相似度计算的准确性。

HyDE反其道而行之,它的流程是:

  1. 拿到用户的问题后,不直接去检索
  2. 而是先让一个LLM凭空想象并生成一个"假设性的、完美的"答案。这个答案是模型"猜"出来的,很可能是错的。
  3. 我们不关心 这个答案内容的对错,而是将这个长长的、包含丰富上下文的"假设性答案"进行向量化(Embedding)
  4. 用这个"假设性答案的向量"去向量数据库里进行搜索。

核心思想 :一个"完美的答案"的向量,在向量空间中,理应与存储着"真实答案"的文档块的向量,极为接近。我们通过生成一个虚构的答案,来创造一个更优质的"查询探针"。

HyDE Prompt模板示例:

python 复制代码
HYDE_PROMPT_TEMPLATE = """
请根据以下问题,生成一个理想中的、详细的回答。请注意,这个回答是用于后续检索的,不需要保证事实的绝对正确性,但需要包含可能的相关信息和关键词。

问题: "{question}"

生成的理想回答:
"""

工作流程: 问题 -> LLM(生成假设性答案) -> Embedding(假设性答案) -> 用新向量进行检索 -> LLM(用真实文档生成最终答案)

上手实战:两种改写策略的代码逻辑

我们用代码逻辑来展示这两种策略如何实现。

python 复制代码
# 模拟函数
def call_llm(prompt):
    print("--- 调用LLM ---")
    print(f"Prompt: {prompt[:150]}...")
    if "分解" in prompt:
        return "['RAG中Refine策略的优缺点是什么?', 'RAG中Map-Reduce策略的优缺点是什么?']"
    if "理想回答" in prompt:
        return "RAG(检索增强生成)是一种将大语言模型与外部知识库结合的技术。它通过检索相关文档来为生成提供依据,从而减少幻觉并提高答案的准确性和时效性。这在问答、内容创作等领域有广泛应用。"
    return "..."

def retrieve(text_for_embedding):
    print(f"--- 正在对文本进行Embedding并检索 ---")
    print(f"文本: {text_for_embedding[:100]}...")
    return ["检索到的文档1", "检索到的文档2"]

# --- 策略一:查询分解 ---
def run_decomposition(question):
    print("\n=== 运行查询分解策略 ===")
    prompt = DECOMPOSITION_PROMPT_TEMPLATE.format(question=question)
    sub_questions_str = call_llm(prompt)
    sub_questions = eval(sub_questions_str) # 将字符串格式的列表转为真实的列表

    all_retrieved_docs = []
    for sub_q in sub_questions:
        docs = retrieve(sub_q)
        all_retrieved_docs.extend(docs)
    
    print("--- 所有子问题检索到的文档 ---")
    print(list(set(all_retrieved_docs))) # 去重

# --- 策略二:HyDE ---
def run_hyde(question):
    print("\n=== 运行HyDE策略 ===")
    prompt = HYDE_PROMPT_TEMPLATE.format(question=question)
    hypothetical_answer = call_llm(prompt)
    
    retrieved_docs = retrieve(hypothetical_answer)
    print("--- HyDE检索到的文档 ---")
    print(retrieved_docs)


complex_question = "对比一下Refine和Map-Reduce策略的优缺点,并简述RAG是什么。"
run_decomposition(complex_question)
run_hyde(complex_question)

总结与预告

今日小结:

  • 优秀的RAG系统不应满足于"一次性"检索,而应具备查询改写迭代检索的能力。
  • 查询分解:将复杂问题拆分为简单的子问题,分别检索,再汇总结果,适合处理多方面的问题。
  • HyDE:通过生成一个"假设性答案"来创造一个更优质的查询向量,适合处理较短或较模糊的问题。
  • 这些技术让RAG系统变得更"聪明",能从失败的检索中自我恢复,并主动优化检索路径。

经过这几天的学习,我们已经探索了混合搜索、Refine、Map-Reduce、查询改写等多种高级组件和策略。现在,我们的工具箱里已经装满了各种强大的"乐高积木"。

问题来了:我们该如何高效、优雅地将这些复杂的组件和逻辑流(比如先做查询改写,再做混合搜索,最后用Refine策略生成答案)串联起来,构建成一个稳定、可维护的应用呢?手动编写大量的"胶水代码"显然是一场噩梦。

明天预告:RAG 每日一技(十四):化繁为简,统揽全局------用LangChain构建高级RAG流程

明天,我们将把目光从"造零件"转向"搭框架"。我们将正式介绍大名鼎鼎的AI应用开发框架LangChain,学习它是如何通过"链(Chain)"的概念,将我们学过的所有RAG组件和策略,像搭乐高一样轻松地编排和组合在一起的。这将是提升你RAG开发效率的革命性一步!

相关推荐
三道杠卷胡3 分钟前
【AI News | 20250804】每日AI进展
人工智能·python·语言模型·github·aigc
夕颜11110 分钟前
Claude AI 编程初体验
后端
程序员爱钓鱼14 分钟前
Go语言实战案例:使用WaitGroup等待多个协程完成
后端·go·trae
蓝屏的钙16 分钟前
从 FastGPT 中浅析 RAG 技术
人工智能·llm
人机与认知实验室19 分钟前
是的,或许这就是意识!
人工智能
微凉的衣柜22 分钟前
GitHub Models:为开源AI项目解决推理难题,让AI更易用、更普及
人工智能·开源·github
程序员海军22 分钟前
告别低质量Prompt!:字节跳动PromptPilot深度测评
前端·后端·aigc
程序员爱钓鱼23 分钟前
Go语言实战案例:任务调度器:定时执行任务
后端·go·trae
沙蒿同学29 分钟前
Golang单例模式实现代码示例与设计模式解析
后端·go
Apifox30 分钟前
如何在 Apifox 中给字段设置枚举(比如字符串、数组等)?
后端·ai编程·测试