返回洞察
RAG · 检索增强

高级 RAG 技术:HyDE、查询扩展与自反思检索

AI Workforce Factory·2026-07-20·10 分钟·来源 · superml.org

高级 RAG:HyDE、查询扩展与自反思检索

应用查询重写、假设性文档嵌入(hypothetical document embeddings)与自我反思式检索。

🎯 你将学到

  • 理解并应用本课涵盖的核心概念

优秀与卓越之间的质量鸿沟

一个基线 RAG 系统(递归分块 + 稠密检索 + GPT-4o-mini)通常能达到如下 RAGAS 分数:

  • Faithfulness(忠实度):0.82
  • Answer Relevancy(答案相关性):0.78
  • Context Precision(上下文精度):0.61

这还行——比没有好,对许多内部工具有用。但它有明显的失败模式:

  • 简短查询: 用户敲了「SSO setup?」——过于模糊,不利于检索
  • 非对称问题: 「How do I configure SSO?」这个问题,与标题为「Single Sign-On Configuration Walkthrough」的文档段落,具有不同的嵌入签名
  • 听起来对、实则错: 答案听起来很自信,却缺乏依据

本教程覆盖三种能把分数推到 0.90+ 的技术。每种都增加一些复杂度;在决定是否采用前,都值得理解。


技术一:HyDE——假设性文档嵌入

非对称问题

标准 RAG 有个微妙的问题:问题与答案处在嵌入空间的不同位置。

当用户问「How do I configure SSO?」,那是个问题。相关的文档块写着「To configure SSO, navigate to Settings > Security and enter your Identity Provider metadata.」,那是个答案。

问题与答案具有不同的语言结构。「How do I…?」与「To configure…」有不同的嵌入签名。这意味着,即便在讨论同一主题,查询向量与相关块向量也不如它们本可以的那么接近。

HyDE 的解法: 与其嵌入问题,不如生成一个_假设性答案_并嵌入它。

直觉

想象一个图书管理员的调研技巧。你进来问:「我在找关于配置 SSO 的信息。」图书管理员不会只搜索「configuring SSO」。相反,他们会想:「一篇讲这个的文章大概会这么说——『单点登录(SSO)配置需要一个身份提供方 URL、一张证书和属性映射……』让我去搜听起来像这样的文章。」

图书管理员构建的这个假设性摘要,在嵌入空间里比原始问题更接近真实文档。

实现

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# Setup
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)  # slight temperature for variety
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)

# Step 1: Generate a hypothetical answer
hyde_prompt = ChatPromptTemplate.from_template("""
Generate a concise, informative paragraph that would appear in technical documentation
and directly answer the following question.
Write as if you are the documentation — authoritative and specific.
Do not say "the documentation says" — just write the content directly.

Question: {question}

Documentation excerpt:""")

hyde_chain = hyde_prompt | llm | StrOutputParser()

def hyde_retrieve(question: str, k: int = 5) -> list:
    """Use HyDE: embed a hypothetical answer instead of the question."""

    # Generate hypothetical answer
    hypothetical_answer = hyde_chain.invoke({"question": question})
    print(f"Hypothetical answer: {hypothetical_answer[:200]}...")

    # Embed the hypothetical answer (not the question!)
    results = vectorstore.similarity_search(
        hypothetical_answer,  # key difference from standard RAG
        k=k
    )

    return results

# Compare standard vs HyDE retrieval
question = "What happens when I exceed rate limits?"

print("=== Standard Retrieval ===")
standard_results = vectorstore.similarity_search(question, k=3)
for doc in standard_results:
    print(f"  {doc.page_content[:150]}")

print("\n=== HyDE Retrieval ===")
hyde_results = hyde_retrieve(question, k=3)
for doc in hyde_results:
    print(f"  {doc.page_content[:150]}")

HyDE 帮助最大的场景

HyDE 带来的提升最大,当:

  • 查询简短而简略(「SSO config?」「rate limits?」)
  • 领域有专门的行话——假设性答案使用与文档相同的术语
  • 你的文档像文档一样结构化(陈述性的、第三人称散文)

HyDE 每次查询只增加一次 LLM 调用(约 50ms,按 GPT-4o-mini 价格约 $0.0001)。这几乎总是值得。


技术二:查询扩展

歧义问题

用户敲了「can't connect」。连什么?数据库?API?网络?他们的 VPN?

一个「can't connect」的单一查询向量会落在所有这些含义的中间某处,并检索出对每一种都平庸的块。

查询扩展生成查询的多个改写版本,对每个版本检索,然后合并结果。你是在跨多个语义角度拓宽搜索网。

并行检索实现

import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

expansion_prompt = ChatPromptTemplate.from_template("""
Generate {n} different phrasings of the following question.
Each rephrasing should capture a different interpretation or emphasis.
Return only the questions, one per line, no numbering.

Original question: {question}

Alternative phrasings:""")

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
expansion_chain = expansion_prompt | llm | StrOutputParser()

def expand_query(question: str, n: int = 4) -> list[str]:
    """Generate multiple query reformulations."""
    response = expansion_chain.invoke({"question": question, "n": n})
    alternatives = [q.strip() for q in response.strip().split('\n') if q.strip()]
    return [question] + alternatives[:n]  # include original + n alternatives

def deduplicate_docs(doc_lists: list[list]) -> list:
    """Remove duplicate documents across multiple retrieval results."""
    seen_content = set()
    unique_docs = []

    for doc_list in doc_lists:
        for doc in doc_list:
            content_key = doc.page_content[:100]  # use first 100 chars as fingerprint
            if content_key not in seen_content:
                seen_content.add(content_key)
                unique_docs.append(doc)

    return unique_docs

def query_expansion_retrieve(question: str, k: int = 5) -> list:
    """
    Retrieve with query expansion:
    1. Generate multiple query variants
    2. Retrieve for each variant
    3. Deduplicate and return top results
    """
    # Generate query variants
    queries = expand_query(question, n=3)
    print(f"Expanded queries:")
    for q in queries:
        print(f"  - {q}")

    # Retrieve for each query
    all_results = []
    for query in queries:
        results = vectorstore.similarity_search(query, k=k)
        all_results.append(results)

    # Deduplicate
    unique_results = deduplicate_docs(all_results)

    # Return top k unique results (they're roughly ordered by first-retrieval rank)
    return unique_results[:k * 2]  # return more candidates for re-ranking

# Example
question = "can't connect"
results = query_expansion_retrieve(question, k=5)

# Expanded queries will include:
# - "can't connect"
# - "connection failure troubleshooting"
# - "how to resolve connection errors"
# - "troubleshooting network connectivity issues"
# - "why is my connection being refused"

异步版本(用于生产吞吐量)

顺序运行多个检索查询会增加延迟。生产环境应并行运行:

async def query_expansion_retrieve_async(question: str, k: int = 5) -> list:
    """Parallel query expansion retrieval."""
    queries = expand_query(question, n=3)

    async def retrieve_single(query: str) -> list:
        # Note: most vector stores are synchronous; run in thread pool
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(
            None,
            lambda: vectorstore.similarity_search(query, k=k)
        )

    # Run all retrievals concurrently
    all_results = await asyncio.gather(*[retrieve_single(q) for q in queries])

    unique_results = deduplicate_docs(list(all_results))
    return unique_results[:k * 2]

# Usage
results = asyncio.run(query_expansion_retrieve_async("can't connect"))

并行检索把延迟从 n×T 降到约 T(单次检索的时间),这使查询扩展在墙上时钟时间上几乎免费。


技术三:Self-RAG

验证问题

标准 RAG 回答一次问题就停。但如果检索到的上下文其实不包含答案呢?LLM 要么说「我不知道」(好),要么编造点东西(坏)。

Self-RAG 增加了一个验证循环:生成答案后,让模型评估它自己的答案。如果答案在上下文中依据不足,就用更精细的查询重新检索。

Self-RAG 循环

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# Grounding check prompt
grounding_check_prompt = ChatPromptTemplate.from_template("""
You are a fact-checking assistant. Your job is to determine if an answer is well-grounded
in the provided context.

Context:
{context}

Question: {question}

Generated Answer: {answer}

Evaluation:
1. Does the answer rely only on information from the context? (Yes/No)
2. Is every specific claim in the answer supported by the context? (Yes/No)
3. If no to either: what information is the answer claiming that isn't in the context?

Verdict (respond with exactly "GROUNDED" or "NOT_GROUNDED"):""")

grounding_chain = grounding_check_prompt | llm | StrOutputParser()

# Query refinement prompt (used when answer is not grounded)
refinement_prompt = ChatPromptTemplate.from_template("""
The following answer to a question was not well-supported by the retrieved context.

Original question: {question}
Poor answer: {answer}
Missing information: {missing_info}

Write a more specific search query that would retrieve the missing information:""")

refinement_chain = refinement_prompt | llm | StrOutputParser()

def self_rag(
    question: str,
    vectorstore: Chroma,
    max_iterations: int = 3
) -> dict:
    """
    Self-RAG loop: generate → verify → refine → repeat if needed.
    Returns the final answer with iteration count.
    """
    current_query = question

    for iteration in range(max_iterations):
        print(f"\nIteration {iteration + 1}: Query = '{current_query}'")

        # Retrieve
        docs = vectorstore.similarity_search(current_query, k=5)
        context = "\n\n".join(doc.page_content for doc in docs)

        # Generate answer
        answer_prompt = ChatPromptTemplate.from_template("""
Answer based ONLY on the context. Be specific and cite details from the context.
If the context doesn't contain the answer, say "The context does not contain this information."

Context: {context}
Question: {question}
Answer:""")

        answer = (answer_prompt | llm | StrOutputParser()).invoke({
            "context": context,
            "question": question
        })

        print(f"Answer: {answer[:200]}...")

        # Check grounding
        grounding_result = grounding_chain.invoke({
            "context": context,
            "question": question,
            "answer": answer
        })

        print(f"Grounding check: {grounding_result[:100]}")

        if "GROUNDED" in grounding_result:
            print(f"Answer grounded on iteration {iteration + 1}")
            return {
                "answer": answer,
                "iterations": iteration + 1,
                "final_query": current_query,
                "sources": [doc.metadata.get('source') for doc in docs]
            }

        # Answer not grounded — refine the query
        # Extract what's missing from the grounding check
        missing_info = grounding_result.split("NOT_GROUNDED")[0].strip()

        current_query = refinement_chain.invoke({
            "question": question,
            "answer": answer,
            "missing_info": missing_info
        })

        print(f"Refined query: {current_query}")

    # Return best answer after max iterations
    print(f"Reached max iterations ({max_iterations})")
    return {
        "answer": answer,
        "iterations": max_iterations,
        "final_query": current_query,
        "warning": "Max iterations reached; answer may not be fully grounded"
    }

# Usage
result = self_rag(
    question="What is the SLA for the Enterprise tier?",
    vectorstore=vectorstore
)

print(f"\nFinal answer: {result['answer']}")
print(f"Iterations needed: {result['iterations']}")

Self-RAG 的权衡

维度标准 RAGSelf-RAG
延迟~300ms~600-1500ms(每次迭代)
LLM 调用12-6
忠实度0.82~0.91
成本2-5 倍更高
最佳用例实时聊天高风险查询

Self-RAG 在错误答案有真实后果的高风险应用中价值最大:医疗信息系统、法律研究工具、金融建议平台。对于延迟更重要的随意问答,坚持用标准 RAG 或 HyDE。


三者结合:高级管道

下面是一个结合全部三种技术的管道:

async def advanced_rag_pipeline(
    question: str,
    vectorstore,
    use_hyde: bool = True,
    use_expansion: bool = True,
    use_self_rag: bool = False  # enable for high-stakes queries
) -> str:
    """Full advanced RAG pipeline with configurable techniques."""

    # Step 1: HyDE — Generate hypothetical answer for better query embedding
    if use_hyde:
        hypothetical = hyde_chain.invoke({"question": question})
        retrieval_query = hypothetical
    else:
        retrieval_query = question

    # Step 2: Query expansion — multiple retrieval angles
    if use_expansion:
        queries = expand_query(retrieval_query, n=3)
        all_docs = []
        for q in queries:
            docs = vectorstore.similarity_search(q, k=8)
            all_docs.extend(docs)
        candidates = deduplicate_docs([all_docs])[:15]
    else:
        candidates = vectorstore.similarity_search(retrieval_query, k=15)

    # Step 3: Re-rank (from previous lesson)
    reranked = reranker.rerank(question, candidates, top_k=5)
    top_docs = [doc for doc, score in reranked]

    # Step 4: Generate answer
    context = "\n\n".join(doc.page_content for doc in top_docs)
    answer_prompt = ChatPromptTemplate.from_template("""
Answer based ONLY on the context. Be specific and cite document sections.

Context:
{context}

Question: {question}
Answer:""")
    answer = (answer_prompt | llm | StrOutputParser()).invoke({
        "context": context, "question": question
    })

    # Step 5: Self-RAG verification (optional)
    if use_self_rag:
        grounding = grounding_chain.invoke({
            "context": context, "question": question, "answer": answer
        })
        if "NOT_GROUNDED" in grounding:
            result = self_rag(question, vectorstore, max_iterations=2)
            return result["answer"]

    return answer

# Usage
answer = asyncio.run(advanced_rag_pipeline(
    "What are the retry policies for API calls?",
    vectorstore=vectorstore,
    use_hyde=True,
    use_expansion=True,
    use_self_rag=False
))

何时实现每种技术

技术RAGAS 提升延迟成本复杂度实现时机
HyDE忠实度 +0.05-0.10+50ms始终——轻松的胜手
查询扩展上下文精度 +0.05-0.08+0ms(并行)查询常简略时
Self-RAG忠实度 +0.08-0.15+300-1000ms需要高风险准确性时

Capstone 项目把所有这些整合成一个连贯的系统。我们开始构建吧。

#AI#tutorial#superml#rag#hyde#query-expansion#self-rag#embedding

有类似的场景想落地?聊聊看。