在我们的RAG基础篇中,我们学会了构建一个简单但实用的RAG系统。然而,当你真正将其应用到生产环境中时,很快就会发现一些棘手的问题:

  • 当用户询问"请假流程"时,系统却返回了关于"薪资制度"的文档
  • 面对包含表格和图片的PDF文档,检索效果大打折扣
  • 用户用不够精确的词汇提问时,往往找不到相关内容
  • 检索出的文档中夹杂着大量无关信息,影响最终答案质量

这些问题的根源在于朴素RAG在检索精度和上下文质量方面还存在不少局限性。就像从自行车升级到汽车一样,Advanced RAG正是为了解决这些"最后一公里"的问题而诞生的。

Advanced RAG:站在巨人肩膀上的进化

Advanced RAG的核心理念很简单:既然检索是RAG的关键环节,那就从检索的前、中、后三个阶段全方位优化

用户问题

Pre-Retrieval
预检索优化

索引优化

数据增强

Retrieval
检索阶段

查询优化

Post-Retrieval
后检索优化

重新排序

上下文压缩

LLM生成答案

Advanced RAG将传统的"索引-检索-生成"三步曲扩展为了一个更加精细化的多阶段流程:

预检索阶段(Pre-Retrieval):重点优化索引结构和数据质量,包括构建更智能的索引、添加元数据标签、处理半结构化数据等。

查询优化阶段:重点优化用户查询,通过查询扩展、多角度查询等技术提升检索召回率。

后检索阶段(Post-Retrieval):重点优化检索结果,通过重新排序、上下文压缩等技术提升最终答案质量。

这就像是将原本的"粗放式检索"升级为"精准制导系统",每个环节都有专门的优化策略。

Pre-Retrieval:为检索打好地基

预检索优化就像是为房子打地基,地基越牢固,上层建筑就越稳定。这个阶段主要解决的是"如何让向量数据库更智能地存储和组织信息"的问题。

1. 摘要索引:让AI先读懂,再存储

痛点分析:半结构化数据的挑战

想象一下这样的场景:你有一份包含文字说明和数据表格的财务报告。传统RAG在处理时会遇到尴尬的情况:

  • 文本分块可能会把表格切断:原本一个完整的财务数据表被切成了几段,失去了数据的完整性
  • 表格向量化效果很差:表格的行列结构在转换为向量时容易丢失语义关系
  • 检索时语义匹配困难:用户问"公司利润情况",但表格数据很难与这个查询建立语义联系

这就是半结构化数据给RAG带来的挑战。

解决思路:双层存储架构

摘要索引采用了一个巧妙的"双层存储"策略:

原始文档

文档分块

LLM生成摘要

摘要向量化

向量数据库
存储摘要

原始文档块

文档存储
存储原文

用户查询

摘要检索

获取文档ID

回溯原始文档

LLM生成答案

这个架构的核心思想是"用精炼的摘要保证搜索精确性,用完整的原文保证上下文完整性"**。

关键代码实现

让我们看看摘要索引的核心实现逻辑:

from langchain.retrievers import MultiVectorRetriever
from langchain.storage import InMemoryByteStore

# 1. 为每个文档块生成摘要
def generate_summaries(docs, llm):
    chain = (
        {"doc": lambda x: x.page_content}
        | ChatPromptTemplate.from_template("总结下面的文档:\n\n{doc}")
        | llm
        | StrOutputParser()
    )
    return chain.batch(docs, {"max_concurrency": 5})

# 2. 构建双存储架构
def build_summary_retriever(docs, summaries, embeddings_model):
    # 摘要存储在向量数据库中
    vectorstore = Chroma(
        collection_name="summaries",
        embedding_function=embeddings_model
    )

    # 原始文档存储在内存中
    docstore = InMemoryByteStore()

    # 多向量检索器负责协调两个存储层
    retriever = MultiVectorRetriever(
        vectorstore=vectorstore,
        byte_store=docstore,
        id_key="doc_id"
    )

    # 为每个文档生成唯一ID
    doc_ids = [str(uuid.uuid4()) for _ in docs]

    # 摘要与原文通过ID关联
    summary_docs = [
        Document(page_content=s, metadata={"doc_id": doc_ids[i]})
        for i, s in enumerate(summaries)
    ]

    retriever.vectorstore.add_documents(summary_docs)
    retriever.docstore.mset(list(zip(doc_ids, docs)))

    return retriever

为什么这样设计很巧妙?

正如我们之前讨论的,这种方法**“很考验摘要的准确性”**。但其优势是显而易见的:

  • 检索精度高:摘要包含了文档的核心语义,匹配效果更好
  • 上下文完整:最终给LLM的是完整的原始文档,信息不会丢失
  • 处理复杂结构:表格、图片等复杂内容可以通过摘要进行语义化表达

2. 父子索引:解决"鱼和熊掌"的矛盾

在RAG系统中,我们经常面临一个矛盾:

  • 小块更精准:文档块越小,向量化后的语义表示越精准,检索时更容易找到相关内容
  • 大块更完整:文档块越大,包含的上下文信息越完整,LLM生成的答案质量越高

这就像是摄影中的"景深"问题:焦点越集中,细节越清晰,但视野越窄;焦点越分散,视野越广,但细节越模糊。

解决思路:层级化存储

父子索引采用了一个类似"望远镜"的层级化策略:

原始长文档

父文档切分
chunk_size=1000

子文档切分
chunk_size=400

子文档向量化

向量数据库
存储子文档

父文档存储
存储完整上下文

用户查询

子文档检索
精准匹配

获取父文档ID

返回父文档
完整上下文

LLM生成答案

核心思想:用子文档的精确性定位内容,用父文档的完整性提供上下文。

实现细节
from langchain.retrievers import ParentDocumentRetriever

def build_parent_child_retriever(docs, embeddings_model):
    # 父文档分割器(保留更多上下文)
    parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000)

    # 子文档分割器(精准检索)
    child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

    vectorstore = Chroma(
        collection_name="parent_child",
        embedding_function=embeddings_model
    )

    docstore = InMemoryStore()

    retriever = ParentDocumentRetriever(
        vectorstore=vectorstore,
        docstore=docstore,
        child_splitter=child_splitter,
        parent_splitter=parent_splitter,
        search_kwargs={"k": 1}
    )

    retriever.add_documents(docs)
    return retriever

关键问题解答

Q: 标准实现只有两级(父/子),能实现多级吗?
A: 理论上可以,但会增加复杂性且收益递减。两级架构在检索精度和上下文完整性之间取得了最佳平衡。

Q: 是否需要多个子文档命中才返回父文档?
A: 在LangChain的默认实现中,只要有一个子文档命中就返回对应的父文档。如果需要更严格的逻辑(如N个子文档命中),需要自定义实现。

Q: 如果子文档尺寸设得比父文档还大会怎样?
A: 程序不会报错,但子文档会和父文档完全一样,整个层级策略失效。

3. 假设性问题索引:预设答案的艺术

核心思想

假设性问题索引基于一个有趣的假设:如果我们能提前为每个文档片段预设几个可能的相关问题,那么检索效果会更好

这就像是为每个文档写一份"常见问题FAQ",用户的查询更容易与这些预设问题匹配上。

原始文档分块

LLM生成假设性问题
每块生成3个问题

问题向量化

向量数据库
存储问题

原始文档存储

用户查询

问题向量检索
用问题匹配问题

获取文档ID

返回原始文档

LLM生成答案

实现要点
from pydantic import BaseModel, Field
from typing import List

class HypotheticalQuestions(BaseModel):
    """生成假设性问题的数据模型"""
    questions: List[str] = Field(..., description="List of questions")

def generate_hypothetical_questions(docs, llm):
    prompt = ChatPromptTemplate.from_template(
        """请基于以下文档生成3个假设性问题(必须使用JSON格式):
        {doc}

        要求:
        1. 输出必须为合法JSON格式,包含questions字段
        2. questions字段的值是包含3个问题的数组
        3. 使用中文提问"""
    )

    chain = (
        {"doc": lambda x: x.page_content}
        | prompt
        | llm.with_structured_output(HypotheticalQuestions)
        | (lambda x: x.questions)
    )

    return chain.batch(docs, {"max_concurrency": 5})

为什么要用Pydantic类?

正如我们讨论的,这个看似简单的HypotheticalQuestions类实际上扮演了"数据格式契约"和"验证器"的角色:

  1. 自动生成指令with_structured_output会利用这个类自动告诉LLM应该如何格式化输出
  2. 自动解析验证:确保LLM返回的数据符合预期格式,避免解析错误

这就像Java反序列化时需要.class文件作为蓝图一样,是一个精巧的设计。

4. 元数据索引:给文档贴上智能标签

痛点分析

想象你在一个巨大的医学文献数据库中查找"糖尿病足"相关资料,但数据库中充斥着各种糖尿病并发症的信息。仅仅依靠语义相似性,可能会检索出许多不够精准的文档。

解决思路:结构化过滤

元数据索引的核心是在向量检索之前先进行结构化过滤,就像是先按分类找书,再按内容找章节。

渲染错误: Mermaid 渲染失败: Parse error on line 2: ... TB A[用户查询
"2023年评分超过8分的机器学习论文"] ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'
实现示例
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.schema import AttributeInfo

# 定义元数据字段
metadata_field_info = [
    AttributeInfo(
        name="genre",
        description="技术领域: ['AI', 'Blockchain', 'Cloud', 'Big Data']",
        type="string",
    ),
    AttributeInfo(
        name="year",
        description="发布年份",
        type="integer",
    ),
    AttributeInfo(
        name="rating",
        description="技术价值评分 (1-10分)",
        type="float"
    )
]

# 创建自查询检索器
retriever = SelfQueryRetriever.from_llm(
    llm,
    vectorstore,
    document_content_description="技术文章简介",
    metadata_field_info=metadata_field_info,
    enable_limit=True
)

# 智能查询解析
results = retriever.invoke("我想了解一篇评分在9分以上的文章")

SelfQueryRetriever的工作原理

  1. 查询理解:LLM将自然语言查询解析为结构化条件和语义关键词
  2. 元数据过滤:根据结构化条件筛选文档子集
  3. 语义搜索:在过滤后的子集中进行向量相似性搜索
  4. 结果融合:综合元数据匹配度和语义相关性进行排序

注意:如果故意用Prompt让LLM生成错误的查询语法,整个机制会直接崩溃并报错,因为它依赖于严格的"语法约定"。

5. 混合检索:取长补短的智慧

两种检索方式的特点对比
特性 向量检索 关键词检索(BM25)
擅长处理 语义理解、同义词、概念匹配 精确术语、专有名词、字面匹配
搜索速度 现代ANN算法较快 (O(log N)) 传统但高效
内存占用 较高(需存储向量) 较低(倒排索引)
示例场景 “汽车"能匹配"车辆” "COVID-19"精确匹配
为什么需要混合检索?

正如我们讨论的,BM25有一个致命弱点:完全不懂语义。它无法处理:

  • 同义词:汽车 vs. 车辆
  • 概念映射:美国内战 vs. 南北战争
  • 口语化表达:电脑卡死了 vs. 应用程序无响应

而向量检索在处理专有名词和精确术语时也有局限性。

混合检索架构

用户查询

BM25检索器

向量检索器

关键词匹配结果
精确术语强相关

语义匹配结果
概念相关强相关

EnsembleRetriever
加权融合

最终检索结果
取两者优势

实现代码
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever

def build_hybrid_retriever(documents, embeddings_model, weights=[0.5, 0.5]):
    # BM25检索器(关键词匹配)
    bm25_retriever = BM25Retriever.from_documents(documents, k=3)

    # 向量检索器(语义匹配)
    vectorstore = Chroma.from_documents(documents, embeddings_model)
    vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

    # 混合检索器(加权融合)
    ensemble_retriever = EnsembleRetriever(
        retrievers=[bm25_retriever, vector_retriever],
        weights=weights
    )

    return ensemble_retriever

权重调整策略

  • 法律文档:更依赖精确术语,BM25权重可以更高
  • 创意内容:更需要语义理解,向量检索权重更高
  • 通用场景:平衡权重 [0.5, 0.5]

查询优化:让问题变得更好回答

在Real-world应用中,用户的查询往往不够精确或者表达不够完整。查询优化就是要解决"如何让模糊的问题变得更容易检索"的问题。

Multi-Query:一个问题,多个角度

核心思想

当用户问一个问题时,AI自动生成多个相关的变体问题,从不同角度进行检索,最后汇总所有检索结果。

渲染错误: Mermaid 渲染失败: Parse error on line 2: ...B A[用户原始问题
"天才AI少女是谁"] --> B[LLM ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'
实现要点
from langchain.retrievers import MultiQueryRetriever
import logging

# 开启日志查看生成的问题
logging.basicConfig(level=logging.INFO)

def build_multi_query_retriever(base_retriever, llm):
    # MultiQueryRetriever会自动生成多个查询变体
    multi_query_retriever = MultiQueryRetriever.from_llm(
        retriever=base_retriever,
        llm=llm,
    )

    return multi_query_retriever

# 使用示例
enhanced_docs = multi_query_retriever.invoke("天才AI少女是谁")

Multi-Query的价值

  • 提升召回率:多角度查询能找到更多相关文档
  • 弥补表达局限:用户表达不准确时,变体问题可能更精准
  • 增强鲁棒性:某个角度检索失败,其他角度仍可能成功

注意:生成的变体问题是中间工具,不会参与最终答案生成,但它们找回的文档会参与。

Post-Retrieval:检索后的精雕细琢

检索到相关文档只是万里长征的第一步,如何从这些文档中提取最有价值的信息并呈现给LLM,决定了最终答案的质量。

1. RAG-Fusion:重新排序的艺术

痛点分析

在Multi-Query检索后,我们得到了大量文档,但这些文档的相关性参差不齐:

  • 某些不相关文档可能排在前面
  • 相同文档在不同查询中的排名不一致
  • 如何综合多个查询的结果是个难题
解决思路:互惠排名融合(RRF)

RAG-Fusion采用了一个类似"奥运裁判打分"的融合算法:一个文档在越多查询结果中排名越靠前,最终得分就越高。

多个查询生成

查询1结果: Doc1,Doc4,Doc3,Doc2

查询2结果: Doc3,Doc1,Doc2,Doc4

查询3结果: Doc4,Doc3,Doc1,Doc2

查询4结果: Doc2,Doc1,Doc4,Doc3

RRF算法
互惠排名融合

Doc1: 综合得分最高

Doc2: 综合得分次高

其他文档按得分排序

Top-K相关文档

RRF算法详解

RRF的计算公式很简单:score = 1 / (rank + k),其中k通常取60。

让我们看一个具体例子:

假设4个查询的排名结果:
查询A: Doc1(rank=0), Doc4(rank=1), Doc3(rank=2), Doc2(rank=3)
查询B: Doc3(rank=0), Doc1(rank=1), Doc2(rank=2), Doc4(rank=3)
查询C: Doc4(rank=0), Doc3(rank=1), Doc1(rank=2), Doc2(rank=3)
查询D: Doc2(rank=0), Doc1(rank=1), Doc4(rank=2), Doc3(rank=3)

Doc1的RRF得分计算:
= 1/(0+60) + 1/(1+60) + 1/(2+60) + 1/(1+60)
= 1/60 + 1/61 + 1/62 + 1/61 ≈ 0.0656
实现代码
from langchain.load import dumps, loads

def reciprocal_rank_fusion(results: list[list], k=60):
    """互惠排名融合算法"""
    fused_scores = {}

    # 遍历每个查询的结果
    for docs in results:
        for rank, doc in enumerate(docs):
            doc_str = dumps(doc)  # 序列化文档用作唯一标识
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            # 累加RRF分数
            fused_scores[doc_str] += 1 / (rank + k)

    # 按融合分数排序
    sorted_docs = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
    return [(loads(doc), score) for doc, score in sorted_docs]

# 集成到查询链中
def build_rag_fusion_chain(retriever, llm):
    # 从langchain hub获取多查询生成prompt
    prompt = hub.pull("langchain-ai/rag-fusion-query-generation")

    generate_queries = (
        prompt | llm | StrOutputParser() | (lambda x: x.split("\n"))
    )

    # 完整的RAG-Fusion链
    chain = generate_queries | retriever.map() | reciprocal_rank_fusion

    return chain

关键问题:用户原始问题(主裁判)和LLM生成的变体问题(分身裁判)权重应该一样吗?

在简单实现中可以一样,但在更精细的实现中,应该给原始问题更高的权重,以确保结果更贴近用户本意。

2. 上下文压缩:去芜存菁的智慧

痛点分析

RAG系统经常遇到这样的问题:

  • 检索出的文档块包含大量与查询无关的信息
  • 文档总长度超出LLM的上下文窗口限制
  • 噪音信息干扰LLM的判断,影响答案质量
解决思路:多层过滤机制

上下文压缩通过多种压缩器的组合,实现"去芜存菁"的效果:

检索到的原始文档

LLMChainExtractor
内容精炼

LLMChainFilter
文档过滤

EmbeddingsFilter
相似度过滤

精炼后的文档内容

过滤后的文档集合

高相似度文档

DocumentCompressorPipeline
组合多个压缩器

TextSplitter
重新分块

EmbeddingsRedundantFilter
去重

EmbeddingsFilter
质量门禁

最终优质文档

四种压缩器对比
压缩器类型 工作方式 优缺点 适用场景
LLMChainExtractor LLM精炼文档内容 质量高但成本高 对质量要求极高的场景
LLMChainFilter LLM判断是否保留文档 快速过滤无用文档 需要语义理解的过滤
EmbeddingsFilter 基于相似度阈值过滤 成本低速度快 大批量文档的初筛
Pipeline组合 串联多个压缩器 效果最佳但复杂 生产环境的精细化处理
实现示例
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import (
    LLMChainExtractor, LLMChainFilter, EmbeddingsFilter,
    DocumentCompressorPipeline
)

def build_compression_retriever(base_retriever, llm, embeddings_model):
    # 方案1:LLM内容精炼
    extractor = LLMChainExtractor.from_llm(llm)
    compression_retriever = ContextualCompressionRetriever(
        base_compressor=extractor,
        base_retriever=base_retriever
    )

    # 方案2:组合多个压缩器
    splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0)
    redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings_model)
    relevant_filter = EmbeddingsFilter(
        embeddings=embeddings_model,
        similarity_threshold=0.66
    )

    pipeline_compressor = DocumentCompressorPipeline(
        transformers=[splitter, redundant_filter, relevant_filter]
    )

    advanced_retriever = ContextualCompressionRetriever(
        base_compressor=pipeline_compressor,
        base_retriever=base_retriever
    )

    return advanced_retriever
深度问题解析

Q: 为什么需要EmbeddingsFilter重新计算相似度?初步检索时不是已经有相似度了吗?

这是一个极其关键的洞察!答案在于两种不同的保证:

  • 初步检索保证"相对最优"(Top-K):从所有文档中找出相似度最高的K个
  • EmbeddingsFilter保证"绝对达标"(阈值过滤):确保文档质量高于设定阈值

这是在初步召回基础上增加的"质量门禁"。

Q: 为什么不直接在初步检索时指定相似度阈值?

因为主流向量数据库的ANN算法(如HNSW)是为Top-K搜索优化的,执行"阈值搜索"效率极低。"先Top-K,再内存过滤"是目前的最佳工程实践。

Q: 如果所有数据库都统一了score字段,EmbeddingsFilter是不是就没用了?

EmbeddingsFilter看似"冗余"的设计,实际上是为了框架的模块化和健壮性。在复杂处理流水线中,文档内容可能被修改,原始分数失效。EmbeddingsFilter通过"重新计算"这一"最笨但最可靠"的方式,保证了组件的通用性。

技术选择指南:因地制宜的智慧

面对如此多的Advanced RAG技术,如何选择适合自己项目的方案?这里提供一个实用的决策框架:

按数据特征选择

数据类型 推荐技术 理由
半结构化文档 (PDF含表格图片) 摘要索引 能够语义化表达复杂结构
长文档 (论文、报告) 父子索引 平衡检索精度和上下文完整性
结构化数据 (带标签的文档) 元数据索引 利用结构化信息精确过滤
用户查询不精确 Multi-Query + 混合检索 多角度查询+关键词补充

按性能要求选择

性能需求 推荐方案 权衡
极致精度 摘要索引 + 上下文压缩 高质量但成本较高
平衡性能 父子索引 + 混合检索 效果好且实现相对简单
高速响应 元数据索引 + EmbeddingsFilter 快速但需要良好的元数据

按业务场景选择

业务场景 推荐组合 核心考虑
法律咨询 元数据索引 + 混合检索 精确术语匹配很重要
技术文档问答 父子索引 + RAG-Fusion 需要完整上下文和多角度理解
企业知识库 假设性问题索引 + Multi-Query 覆盖常见问题场景

Advanced RAG并不是要完全替代朴素RAG,而是在其基础上针对特定问题进行精细化优化。就像从手动档升级到自动档,核心的驾驶原理没变,但驾驶体验显著提升。

关键洞察

  1. 没有银弹:每种技术都有其适用场景,关键是理解业务需求
  2. 渐进优化:从朴素RAG开始,根据实际问题逐步引入Advanced技术
  3. 成本平衡:更好的效果往往意味着更高的计算成本,需要权衡
  4. 框架思维:理解每个组件的作用,灵活组合以适应不同场景

Advanced RAG的真正价值在于让AI系统更好地理解和处理现实世界的复杂信息。从半结构化数据到用户的模糊查询,从海量文档中的精准定位到上下文的智能压缩,每一个技术细节都在服务于一个终极目标:让AI助手变得更加可靠、精准、实用。

这就是从朴素到精准的进化之路——不是为了炫技,而是为了更好地解决实际问题。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐