前情回顾
到目前为止,我们构建的RAG系统,其检索流程本质上是“一锤子买卖”:用户提问 -> 系统检索 -> 返回结果。如果第一次检索返回的都是不相关的垃圾信息,整个流程就宣告失败。
这就像一个经验不足的侦探,跟丢了第一条线索后,就直接放弃了整个案件。而一个经验丰富的老侦探,则会重新审视案情,分析失败原因,然后从一个新的、更巧妙的角度再次切入。
我们能让我们的RAG系统也成为一名“老侦探”吗?答案是肯定的。核心思想就是:不要把用户的原始问题当成一成不变的“圣旨”,而是要授权给RAG系统,让它在检索前或检索失败后,对原始问题进行“改写”和“优化”。
这项技术,统称为查询转换(Query Transformation)。
为什么要改写用户的查询?
用户的原始查询可能存在以下问题:
- 过于宽泛或模糊:“请谈谈人工智能。”包含多个子问题:“对比一下Refine和Map-Reduce策略的优缺点。”语言风格与文档库不匹配:用户用口语提问,而文档库是专业术语。
对这些查询进行改写,可以大大提高检索的精准度。下面我们介绍两种强大且常用的查询改写技术。
技术一:查询分解 (Query Decomposition)
当一个问题包含多个方面时,一次性检索很难找到一个“完美”的文档能覆盖所有方面。查询分解的思想就是将一个复杂问题,拆解成多个独立的、更简单的子问题,然后对每个子问题分别进行检索,最后将所有结果汇总。
- 原始问题:“对比一下Refine和Map-Reduce策略的优缺点。”分解后的子问题:
- “RAG中的Refine策略有什么优点?”“RAG中的Refine策略有什么缺点?”“RAG中的Map-Reduce策略有什么优点?”“RAG中的Map-Reduce策略有什么缺点?”
我们可以用一个LLM来自动完成这个分解任务。
分解Prompt模板示例:
DECOMPOSITION_PROMPT_TEMPLATE = """你是一个查询分析专家。请将用户提出的复杂问题,分解成一系列更简单的、可以独立回答的子问题。请直接返回一个Python列表(List)格式的字符串。复杂问题: "{question}"分解后的子问题列表:"""
工作流程:复杂问题 -> LLM(分解) -> [子问题1, 子问题2, ...] -> 分别检索 -> 汇总结果 -> LLM(最终生成)
技术二:假设性文档嵌入 (HyDE)
HyDE (Hypothetical Document Embeddings) 是一种脑洞大开、但效果惊人的技术。
我们之前的思路是:计算“问题”和“文档”的向量相似度。但问题通常很短,而文档很长,这种不对称性有时会影响相似度计算的准确性。
HyDE反其道而行之,它的流程是:
- 拿到用户的问题后,不直接去检索。而是先让一个LLM凭空想象并生成一个“假设性的、完美的”答案。这个答案是模型“猜”出来的,很可能是错的。我们不关心这个答案内容的对错,而是将这个长长的、包含丰富上下文的“假设性答案”进行向量化(Embedding)。用这个“假设性答案的向量”去向量数据库里进行搜索。
核心思想:一个“完美的答案”的向量,在向量空间中,理应与存储着“真实答案”的文档块的向量,极为接近。我们通过生成一个虚构的答案,来创造一个更优质的“查询探针”。
HyDE Prompt模板示例:
HYDE_PROMPT_TEMPLATE = """请根据以下问题,生成一个理想中的、详细的回答。请注意,这个回答是用于后续检索的,不需要保证事实的绝对正确性,但需要包含可能的相关信息和关键词。问题: "{question}"生成的理想回答:"""
工作流程:问题 -> LLM(生成假设性答案) -> Embedding(假设性答案) -> 用新向量进行检索 -> LLM(用真实文档生成最终答案)
上手实战:两种改写策略的代码逻辑
我们用代码逻辑来展示这两种策略如何实现。
# 模拟函数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开发效率的革命性一步!