第12章:高级 RAG 技术 —— 让检索更精准、更全面

本章目标:深入理解基础 RAG 的三大核心缺陷,掌握 Multi-Query(多查询扩展)、Contextual Compression(上下文压缩)、HyDE(假设文档嵌入)三种高级检索策略,并能根据场景选择合适方案。
前期回顾

AI入门开发系列文章合集


一、基础 RAG 的三大痛点

学完第6章的基础 RAG 后,你可能已经在实际项目中遇到了这些问题:

痛点一:语义漂移(召回率低)

用户问:"Python 程序跑得太慢了,怎么优化?"

知识库文档里写的是:"使用 cProfile 分析 性能瓶颈 ......""NumPy 向量化操作 替代 Python 循环......"

用户说"跑得慢",文档说"性能瓶颈"和"向量化"------语义相关,但向量相似度可能不够高,好文章被漏掉了

痛点二:文档噪声(精确率低)

检索到了一篇 3000 字的 Python 优化指南,但你只问了 GIL 锁的问题。LLM 收到的上下文里 90% 都是无关内容,干扰了 LLM 的判断,还白白消耗 Token。

痛点三:专业表述差距(专业域问题)

普通用户问:"那个让大模型回答风格像人一样的技术是什么?"

技术文档里写的是:"RLHF(基于人类反馈的强化学习)......"

问题和答案所用的词汇完全不同,向量相似度极低,找不到对应文档


二、三种高级 RAG 策略全景

策略 解决的痛点 额外 LLM 调用 适合场景
Multi-Query 召回率低(语义漂移) +1次(改写查询) 通用知识库,用户问题表述多样
Contextual Compression 精确率低(文档噪声) +N次(每个文档压缩一次) 文档块大,信息密度低
HyDE 专业域表述差距 +1次(生成假设答案) 技术/医疗/法律等专业领域

三、方案一:Multi-Query RAG

代码文件:12_advanced_rag/01_multi_query_rag.py

3.1 核心思路

Multi-Query(多查询检索):让 LLM 把用户问题改写成多个不同角度的查询,分别检索,合并去重后送给 LLM 回答。

arduino 复制代码
用户问:"Python 程序跑得太慢了,有什么方法可以提速?"

LLM 改写为:
  1. "Python 代码运行慢的解决方法"          ← 从"解决方法"角度
  2. "Python 性能瓶颈分析工具"              ← 从"分析工具"角度
  3. "Python 并发与并行处理提升速度"        ← 从"并发优化"角度
  4. "CPython 解释器性能优化技巧"           ← 从"底层原理"角度

4路检索 → 去重合并 → 覆盖更多相关文档

3.2 完整实现

python 复制代码
import os                                         # 读取环境变量
import re                                         # 正则表达式,用于解析查询列表

from langchain_core.documents import Document     # 文档数据结构
from langchain_core.output_parsers import BaseOutputParser, StrOutputParser
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_openai import ChatOpenAI           # LLM
from langchain_milvus import Milvus               # 向量数据库(Milvus Lite)
from langchain_openai import OpenAIEmbeddings     # 嵌入模型
from dotenv import load_dotenv
# ★ 已移除未使用的 `from typing import Union`

load_dotenv()

# ── 初始化 LLM 和 Embeddings ──────────────────────────────────
llm = ChatOpenAI(
    model="qwen-plus",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.3,                              # 查询改写允许少量创意性
)

embeddings = OpenAIEmbeddings(
    model="text-embedding-v3",                    # 百炼的文本嵌入模型
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

# ── 构建知识库 ─────────────────────────────────────────────────
documents = [
    Document(
        page_content="使用 cProfile 和 line_profiler 分析 Python 性能瓶颈。"
                     "常见性能问题:不必要的循环、字符串拼接、频繁 I/O 操作。",
        metadata={"section": "性能分析工具"},    # 元数据,可用于过滤
    ),
    Document(
        page_content="Python GIL 限制多线程并行。CPU 密集型任务用 multiprocessing,"
                     "I/O 密集型任务用 asyncio 协程。",
        metadata={"section": "并发优化"},
    ),
    Document(
        page_content="NumPy 向量化操作将 Python 循环转为底层 C 实现,速度提升 10-100 倍。"
                     "避免 Python 层面 for 循环,优先使用 NumPy ufunc。",
        metadata={"section": "数值计算"},
    ),
    Document(
        page_content="内存优化:__slots__ 减少实例内存,生成器替代列表推导处理大数据集,"
                     "lru_cache 缓存函数结果。",
        metadata={"section": "内存优化"},
    ),
    Document(
        page_content="连接池是数据库性能优化关键,避免频繁创建连接。"
                     "asyncpg、aiomysql 配合 asyncio 大幅提升 I/O 吞吐量。",
        metadata={"section": "数据库优化"},
    ),
]

# Milvus.from_documents:从文档列表创建向量数据库(Milvus Lite 本地文件模式)
# connection_args={"uri": "..."} 指定本地 .db 文件路径,无需启动独立 Milvus 服务
# drop_old=True 保证每次运行从空集合开始,避免重复写入
vectorstore = Milvus.from_documents(
    documents=documents,
    embedding=embeddings,
    connection_args={"uri": "./milvus_ch12_01.db"},
    drop_old=True,
)

# ── 自定义输出解析器:将多行文本解析为字符串列表 ──────────────
class LineListOutputParser(BaseOutputParser[list[str]]):
    """把 LLM 改写的多行查询解析为列表。
    
    LLM 输出格式:
      1. Python 代码运行慢的解决方法
      2. Python 性能瓶颈分析工具
      ...
    
    解析后:["Python 代码运行慢的解决方法", "Python 性能瓶颈分析工具", ...]
    """

    def parse(self, text: str) -> list[str]:
        lines = text.strip().split("\n")          # 按换行拆分
        result = []
        for line in lines:
            line = line.strip()                   # 去除首尾空格
            if not line:
                continue                          # 跳过空行
            # 去除 "1. " "2. " "- " "• " 等前缀
            line = re.sub(r"^[\d]+[\.\)]\s*", "", line)
            line = re.sub(r"^[-•]\s*", "", line)
            if line:                              # 确保去前缀后不为空
                result.append(line)
        return result


# ── Multi-Query 检索核心函数 ──────────────────────────────────
def multi_query_retrieve(
    vectorstore: Milvus,
    llm: ChatOpenAI,
    original_question: str,
    n_queries: int = 4,                           # 改写查询的数量
) -> list[Document]:
    """执行 Multi-Query 检索。
    
    Args:
        vectorstore: 向量数据库实例(Milvus)
        llm: 用于改写查询的语言模型
        original_question: 用户原始问题
        n_queries: 改写查询数量(建议 3-5 个)
    
    Returns:
        去重合并后的相关文档列表
    """
    # ── 第一步:改写查询 ──────────────────────────────────────
    rewrite_prompt = PromptTemplate.from_template(
        "你是 AI 问题改写专家。将用户问题改写成 {n} 个不同角度的版本,"
        "以提升向量检索的召回率。\n\n"
        "原始问题:{question}\n\n"
        "生成 {n} 个改写版本,每行一个,直接输出查询内容,不要编号:"
    )

    # 构建改写链:提示词 → LLM → 解析为列表
    rewrite_chain = rewrite_prompt | llm | LineListOutputParser()
    generated_queries = rewrite_chain.invoke({
        "n": n_queries,
        "question": original_question,
    })

    # 改写查询 + 原始查询 = 全部查询
    all_queries = [original_question] + generated_queries
    print(f"📝 原始查询:{original_question}")
    print(f"🔄 改写查询({len(generated_queries)} 个):")
    for q in generated_queries:
        print(f"   • {q}")

    # ── 第二步:多路并行检索 ──────────────────────────────────
    retriever = vectorstore.as_retriever(search_kwargs={"k": 2})  # 每次取2个
    all_docs: list[Document] = []
    seen_contents: set[int] = set()               # 用集合存内容哈希,实现去重

    for query in all_queries:
        docs = retriever.invoke(query)            # 向量检索
        for doc in docs:
            content_hash = hash(doc.page_content) # 用内容哈希判断重复
            if content_hash not in seen_contents:
                seen_contents.add(content_hash)
                all_docs.append(doc)

    print(f"\n📦 多路检索后去重,共 {len(all_docs)} 个不重复文档块")
    return all_docs


# ── 完整 Multi-Query RAG 流程 ─────────────────────────────────
def multi_query_rag(question: str) -> str:
    """完整的 Multi-Query RAG 问答。"""
    # 第一步:多查询检索
    docs = multi_query_retrieve(vectorstore, llm, question, n_queries=3)

    # 第二步:拼接上下文
    context = "\n\n---\n".join([doc.page_content for doc in docs])

    # 第三步:LLM 生成最终答案
    qa_prompt = ChatPromptTemplate.from_messages([
        ("system",
         "你是 Python 技术专家。请根据以下知识库内容,为用户提供全面专业的解答。\n\n"
         "相关知识库内容:\n{context}"),
        ("human", "{question}"),
    ])
    qa_chain = qa_prompt | llm | StrOutputParser()
    return qa_chain.invoke({"context": context, "question": question})


# ── 执行示例 ───────────────────────────────────────────────────
question = "Python 程序跑得太慢了,有什么方法可以提速?"
answer = multi_query_rag(question)
print(f"\n💡 最终回答:\n{answer}")

3.3 效果对比验证

python 复制代码
# ── 基础检索 vs Multi-Query 对比 ─────────────────────────────
question = "Python 程序跑得太慢了,有什么方法可以提速?"

# 基础检索:只用原始问题
basic_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
basic_docs = basic_retriever.invoke(question)
print(f"基础检索:{len(basic_docs)} 个文档")
for doc in basic_docs:
    print(f"  • {doc.metadata.get('section')}: {doc.page_content[:60]}...")

# Multi-Query 检索
multi_docs = multi_query_retrieve(vectorstore, llm, question)
print(f"\nMulti-Query:{len(multi_docs)} 个文档(覆盖更多主题)")
for doc in multi_docs:
    print(f"  • {doc.metadata.get('section')}: {doc.page_content[:60]}...")

# 预期结果:Multi-Query 能覆盖"性能分析"、"并发优化"、"内存优化"等多个主题
# 基础检索可能只返回与"跑得慢"最相近的2个文档块

四、方案二:Contextual Compression(上下文压缩)

代码文件:lessons/12_advanced_rag/02_contextual_compression.py

4.1 核心思路

Contextual Compression(上下文压缩):在向量检索之后,用 LLM 对检索结果进行二次过滤/压缩,只保留真正与查询相关的部分。

markdown 复制代码
原始检索返回:
  文档1(2000字):Python 完整优化指南
    - GIL 介绍(200字,与问题相关)
    - 数据结构选择(300字,无关)
    - 异步编程(500字,部分相关)
    - 内存管理(400字,无关)
    - ...

压缩后:
  文档1(200字):仅保留 GIL 相关内容

4.2 两种压缩策略

LLMChainExtractor(抽取式):从文档中抽取相关段落,去掉无关部分。优点是保留原文措辞,缺点是 LLM 调用成本较高。

LLMChainFilter(过滤式):对每个文档做 Yes/No 判断,过滤掉整体不相关的文档。优点是快速低成本,缺点是粒度粗(保留或删除整个文档)。

python 复制代码
import os
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor, LLMChainFilter
from langchain_milvus import Milvus
from langchain_openai import OpenAIEmbeddings
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(
    model="qwen-plus",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0,                                # 压缩/过滤任务要确定性
)

embeddings = OpenAIEmbeddings(
    model="text-embedding-v3",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

# ── 构建包含"噪声"的文档(模拟真实知识库的文档块) ─────────────
# 注意:每个文档故意包含多个主题,模拟真实的"大而全"文档块
noisy_documents = [
    Document(
        page_content=(
            "Python 性能优化综合指南。"
            "一、GIL(全局解释器锁):GIL 是 CPython 的互斥锁,同一时刻只允许一个线程执行 Python 字节码。"
            "对 CPU 密集型任务影响大,推荐使用 multiprocessing 模块绕过 GIL。"
            "二、代码规范:遵循 PEP 8 风格指南,使用 flake8 进行代码检查,保持代码可读性。"
            "三、虚拟环境:使用 venv 或 conda 管理项目依赖,避免包版本冲突。"
            "四、异步 I/O:对于网络请求等 I/O 密集型任务,asyncio 协程可大幅提升并发性能。"
        ),
        metadata={"source": "python_guide", "topic": "综合指南"},
    ),
    Document(
        page_content=(
            "Python 数据科学生态介绍。"
            "NumPy:高性能数值计算库,向量化操作比纯 Python 循环快 100 倍。"
            "Pandas:数据处理和分析,支持 DataFrame 操作。"
            "Matplotlib:数据可视化,支持静态图表绘制。"
            "Scikit-learn:机器学习算法库,包含分类、回归、聚类等算法。"
            "注意:NumPy 和 Pandas 的底层计算均已优化,大量计算任务应优先使用它们而非纯 Python。"
        ),
        metadata={"source": "data_science", "topic": "数据科学"},
    ),
]

# Milvus Lite:本地文件存储,drop_old=True 每次从空集合开始
base_vectorstore = Milvus.from_documents(
    documents=noisy_documents,
    embedding=embeddings,
    connection_args={"uri": "./milvus_ch12_02.db"},
    drop_old=True,
)
base_retriever = base_vectorstore.as_retriever(search_kwargs={"k": 2})

# ── 方案 A:LLMChainExtractor(抽取式压缩) ───────────────────
# 从检索到的文档中,提取与查询相关的文本片段
# 完整流程:查询 → 向量检索(粗筛) → LLM 抽取相关段落(精筛)
extractor = LLMChainExtractor.from_llm(llm)       # 用 LLM 做抽取
extractor_retriever = ContextualCompressionRetriever(
    base_compressor=extractor,                    # 压缩器:LLMChainExtractor
    base_retriever=base_retriever,                # 基础检索器:向量检索
)

question = "Python 的 GIL 是什么,如何解决?"
compressed_docs = extractor_retriever.invoke(question)

print("【LLMChainExtractor 结果】")
for i, doc in enumerate(compressed_docs, 1):
    print(f"文档 {i}(压缩后 {len(doc.page_content)} 字):")
    print(f"  {doc.page_content}")                # 只剩下 GIL 相关内容
    # 对比:原始文档有约 500 字,压缩后只剩 GIL 相关的 ~100 字

# ── 方案 B:LLMChainFilter(过滤式) ─────────────────────────
# 对每个文档进行相关性判断,过滤掉整体不相关的文档
# 适合:快速过滤明显不相关的文档块
doc_filter = LLMChainFilter.from_llm(llm)         # 用 LLM 做过滤
filter_retriever = ContextualCompressionRetriever(
    base_compressor=doc_filter,                   # 压缩器:LLMChainFilter
    base_retriever=base_retriever,
)

filtered_docs = filter_retriever.invoke(question)
print(f"\n【LLMChainFilter 结果】({len(filtered_docs)} 个文档通过过滤)")
for doc in filtered_docs:
    print(f"  来源:{doc.metadata.get('source')}  内容:{doc.page_content[:80]}...")

4.3 压缩检索的成本权衡

指标 基础检索 压缩检索
LLM 调用次数 1次(最终回答) 1 + k次(k = 检索文档数)
上下文质量 包含噪声 高度相关
Token 消耗 文档块全文 仅相关片段
延迟 较高(多次 LLM 调用串行)
建议 k 值 k=4-8 k=2-4(压缩器会增加成本)

五、方案三:HyDE(假设文档嵌入)

代码文件:lessons/12_advanced_rag/03_hyde_rag.py

5.1 核心思路

HyDE(Hypothetical Document Embeddings,假设文档嵌入):不直接用问题的向量去检索,而是先让 LLM 生成一段"假设性答案",再用假设答案的向量去检索知识库。

为什么这样有效?

yaml 复制代码
问题:      "那个让大模型跟人说话风格一样的技术是什么原理?"
问题向量:  [0.12, -0.34, 0.89, ...](口语化,偏向"说话风格")

知识库文档:"RLHF(基于人类反馈的强化学习)通过人工评分来对齐模型输出......"
文档向量:  [0.45, -0.21, 0.76, ...](技术性,偏向"RLHF")

→ 直接检索:问题向量与文档向量相似度低,找不到!

---

HyDE 假设答案:"基于人类反馈的强化学习(RLHF)是实现对话风格对齐的关键技术,
                通过收集人类对不同回答的偏好评分,训练奖励模型......"
假设答案向量:  [0.44, -0.20, 0.77, ...](接近文档向量)

→ HyDE 检索:假设答案向量与文档向量高度相似,成功找到!

5.2 完整 HyDE 实现

python 复制代码
import os
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_milvus import Milvus
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(
    model="qwen-plus",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.3,
)

embeddings = OpenAIEmbeddings(
    model="text-embedding-v3",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

# ── 构建专业技术知识库 ─────────────────────────────────────────
tech_documents = [
    Document(
        page_content=(
            "RLHF(基于人类反馈的强化学习)是对齐大语言模型的核心技术。"
            "流程:先进行监督微调(SFT),再训练奖励模型(RM),"
            "最后使用 PPO 算法对 LLM 进行强化学习,使其输出更符合人类偏好。"
            "ChatGPT、Claude 等产品均使用了 RLHF 技术。"
        ),
        metadata={"source": "rlhf_paper"},
    ),
    Document(
        page_content=(
            "LoRA(Low-Rank Adaptation)是参数高效微调(PEFT)技术。"
            "核心思想:预训练权重的更新矩阵可以用低秩矩阵分解近似。"
            "LoRA 冻结原始权重,只训练低秩矩阵,参数量减少 99%+,"
            "可在消费级 GPU 上微调大型模型(如 Qwen、LLaMA)。"
        ),
        metadata={"source": "lora_paper"},
    ),
    Document(
        page_content=(
            "Flash Attention 是内存高效的注意力计算算法,"
            "通过 IO 感知的块状计算减少 HBM 访问次数。"
            "标准 Attention 内存占用 O(N²),Flash Attention 显著降低内存峰值。"
            "长上下文模型(128k+ tokens)几乎都依赖 Flash Attention 技术。"
        ),
        metadata={"source": "flash_attention"},
    ),
    Document(
        page_content=(
            "RAG(检索增强生成)弥补 LLM 知识截止日期的局限。"
            "流程:Query → 向量检索 → 相关文档 → LLM + 上下文 → 答案。"
            "高级 RAG:Multi-Query、HyDE、Contextual Compression、Re-ranking。"
        ),
        metadata={"source": "rag_overview"},
    ),
]

# Milvus Lite:本地文件存储,drop_old=True 每次从空集合开始
vectorstore = Milvus.from_documents(
    documents=tech_documents,
    embedding=embeddings,
    connection_args={"uri": "./milvus_ch12_03.db"},
    drop_old=True,
)

# ── 第一步:生成假设性答案文档 ────────────────────────────────
def generate_hypothetical_document(question: str) -> str:
    """让 LLM 针对问题生成一段"假设性答案"(不需要完全正确)。
    
    关键点:假设答案不用真实准确,只需要在向量空间中
    与真实知识库文档足够接近即可。LLM 可能有幻觉,
    但这里只用它的向量,不用它的文字内容。
    """
    hyde_prompt = ChatPromptTemplate.from_messages([
        ("system",
         "请针对问题,生成一段详细的技术解释(约100字),作为该问题的理想答案。"
         "用陈述句,语气专业,直接陈述核心内容,不要以'答案是'开头。"),
        ("human", "{question}"),
    ])
    chain = hyde_prompt | llm | StrOutputParser()
    return chain.invoke({"question": question})

# ── 第二步:用假设答案的向量检索 ─────────────────────────────
def hyde_retrieve(question: str, k: int = 2) -> tuple[str, list[Document]]:
    """HyDE 检索:生成假设答案 → 向量化 → 检索知识库。
    
    Returns:
        (假设性答案文本, 检索到的相关文档列表)
    """
    # Step 1: 生成假设答案
    hypothetical_doc = generate_hypothetical_document(question)
    print(f"📝 假设性答案(节选):{hypothetical_doc[:100]}...")

    # Step 2: 将假设答案转为向量(而不是将问题转为向量)
    # embed_query() 返回 list[float],即一个向量
    hyp_vector = embeddings.embed_query(hypothetical_doc)

    # Step 3: 用假设答案的向量在知识库中检索
    # similarity_search_by_vector:直接用向量检索,不再二次嵌入
    docs = vectorstore.similarity_search_by_vector(hyp_vector, k=k)

    return hypothetical_doc, docs

# ★ hyde_rag 在对比测试循环之前定义,确保代码从上到下可读
# ── 完整 HyDE RAG 问答 ─────────────────────────────────────────
def hyde_rag(question: str) -> str:
    """完整的 HyDE RAG 问答流程。"""
    _, docs = hyde_retrieve(question, k=3)                # HyDE 检索
    context = "\n\n".join([d.page_content for d in docs]) # 拼接上下文

    qa_prompt = ChatPromptTemplate.from_messages([
        ("system", "你是 AI 技术专家。基于知识库内容为用户提供准确专业的解答。\n\n"
                   "参考资料:\n{context}"),
        ("human", "{question}"),
    ])
    chain = qa_prompt | llm | StrOutputParser()
    return chain.invoke({"context": context, "question": question})

# ── 对比测试:基础检索 vs HyDE 检索 ──────────────────────────
test_questions = [
    "那个让大模型可以做到跟人类对话风格一样的技术是什么原理?",  # 应找到 RLHF
    "微调大模型但是 GPU 显存不够怎么办?",                       # 应找到 LoRA
]

for question in test_questions:
    print(f"\n{'─' * 50}")
    print(f"查询:{question}")

    # 基础检索(直接用问题向量)
    basic_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
    basic_docs = basic_retriever.invoke(question)
    print(f"\n【基础检索】:")
    for doc in basic_docs:
        # ★ 使用 .get() 避免 metadata 中缺少 'source' 键时抛出 KeyError
        print(f"  来源:{doc.metadata.get('source', '未知')}  {doc.page_content[:60]}...")

    # HyDE 检索
    print(f"\n【HyDE 检索】:")
    _, hyde_docs = hyde_retrieve(question)
    for doc in hyde_docs:
        print(f"  来源:{doc.metadata.get('source', '未知')}  {doc.page_content[:60]}...")

answer = hyde_rag("那个让大模型可以做到跟人类对话风格一样的技术是什么原理?")
print(f"\n💡 HyDE RAG 最终回答:\n{answer}")

六、三种方案组合使用

高级技巧:三种方案可以叠加使用,应对最复杂的场景:

python 复制代码
def advanced_rag(question: str) -> str:
    """组合方案:Multi-Query + Contextual Compression。"""
    # 第一步:Multi-Query 扩展(提升召回率)
    all_docs = multi_query_retrieve(vectorstore, llm, question, n_queries=3)

    # 第二步:手动实现文档压缩(过滤不相关内容)
    # 简化版:用 LLM 评分每个文档的相关性
    compress_prompt = ChatPromptTemplate.from_messages([
        ("system", "判断以下文档是否与查询相关。只回答 '是' 或 '否'。"),
        ("human", "查询:{query}\n\n文档:{document}"),
    ])
    compress_chain = compress_prompt | llm | StrOutputParser()

    relevant_docs = []
    for doc in all_docs:
        relevance = compress_chain.invoke({
            "query": question,
            "document": doc.page_content,
        })
        # ★ Bug fix: "不是" 中也含有 "是",必须先排除否定答复,再判断肯定答复
        # LLM 可能回答 "是"、"是的"、"是,相关" 或 "否"、"不是"、"否,不相关"
        relevance_stripped = relevance.strip()
        is_relevant = (
            relevance_stripped.startswith("是")
            and not relevance_stripped.startswith("是否")  # 排除"是否..."反问句
        ) or relevance_stripped == "Yes"
        if is_relevant:                               # 只保留相关文档
            relevant_docs.append(doc)

    # ★ Bug fix: 若所有文档都被过滤掉,回退到全部文档,避免以空上下文调用 LLM
    if not relevant_docs:
        relevant_docs = all_docs

    # 第三步:生成回答
    context = "\n\n---\n".join([d.page_content for d in relevant_docs])
    qa_prompt = ChatPromptTemplate.from_messages([
        ("system", "基于知识库内容回答问题。\n\n内容:\n{context}"),
        ("human", "{question}"),
    ])
    chain = qa_prompt | llm | StrOutputParser()
    return chain.invoke({"context": context, "question": question})

七、场景选型指南

你的问题 推荐方案 理由
用户提问方式多样(同一问题10种说法) Multi-Query 覆盖更多语义变体
文档块太大,包含大量无关内容 Contextual Compression 精准提取相关片段
专业领域,用户用口语问技术问题 HyDE 桥接日常用语与专业术语
知识库小(<100文档)、问题简单 基础 RAG 额外复杂度不值得
响应速度要求极高(<2秒) 基础 RAG 或 Multi-Query CC 和 HyDE 增加延迟
预算有限,控制 API 成本 Multi-Query 额外 1 次 LLM 调用,性价比高

八、常见错误与排查

错误信息 原因 解决方法
ImportError: langchain_milvus 未安装 Milvus 依赖 uv sync --extra milvuspip install langchain-milvus milvus-lite
Multi-Query 改写的查询意思完全偏离 改写提示词不够清晰 在提示词中加入"改写应保持原始问题的核心意图"
压缩后文档为空([] LLMChainFilter 过滤太严 改用 LLMChainExtractor,或降低 k 值
HyDE 假设答案包含严重错误信息 LLM 幻觉(正常现象) HyDE 设计上允许幻觉,只用向量不用文字,无需担心
向量检索返回结果全部不相关 Embedding 模型与文档语言不匹配 使用多语言 Embedding 模型(如 text-embedding-v3
grpc.RpcError 或空错误信息 pymilvus 版本与 milvus-lite 不兼容 确保使用 pymilvus<2.6.0 + milvus-lite>=2.4.10,与 langchain-milvus<0.3.0 配套
ModuleNotFoundError: pkg_resources milvus-lite 依赖 setuptools pip install setuptools>=78.1.1

下一章预告

掌握了高级 RAG 策略,下一章我们进入生产部署的核心话题。

第13章 将覆盖:

  • 异步编程:同时处理 100 个 LLM 请求,不阻塞服务器
  • FastAPI 集成:把 LangChain 封装成 REST API,支持流式 SSE
  • LLM 缓存:相同问题第二次请求 0 延迟,大幅降低 API 成本

高级 RAG 解决了"找到正确信息"的问题,生产部署解决了"高效稳定地服务用户"的问题。两章合力,构建真正可用的 AI 产品。


AI入门开发系列文章合集
作者:阿聪谈架构

公众号:阿聪谈架构 (分享后端架构 / AI / Java 技术文章)

相关代码关注公众号:【阿聪谈架构】 回复:AI专栏代码

相关推荐
liu****6 小时前
1.Vibe Coding 介绍
人工智能·ai·辅助编程·vibe coding
武子康6 小时前
Java-06 深入浅出 MyBatis 数据库1对1模型实战:从概念到查询实现
java·后端
日月云棠6 小时前
4 AbstractStringBuilder —— 可变字符串的骨架实现
java·后端
日月云棠6 小时前
2 Object —— Java 类体系的根节点
java·后端
零壹AI实验室6 小时前
GPT-5.5 vs 国产大模型:2026年5月AI编程工具横评实测
人工智能·gpt·ai编程
Cosolar6 小时前
2026最新RAG面试题集:45问覆盖全链路
人工智能·系统架构·大模型·agent·rag
小皮咖6 小时前
DeepSeek-Reasonix:缓存命中率高达 90% 的AI编程助手
人工智能
南汁bbj6 小时前
从Prompt到Agent:教育错题分析系统的流程编排设计实践
人工智能·prompt