RAG进阶:让AI检索更精准的8大技术
本文介绍了Advanced RAG技术如何优化传统RAG系统的检索性能。针对朴素RAG存在的检索精度低、上下文质量差等问题,Advanced RAG采用三阶段优化策略:预检索阶段通过摘要索引、父子索引等优化数据存储;查询阶段增强用户提问;后检索阶段通过重排序等技术提升结果质量。重点解析了摘要索引的双层存储架构和父子索引的层级化策略,展示了如何平衡检索精度与上下文完整性。这些创新方法有效解决了半结构
在我们的RAG基础篇中,我们学会了构建一个简单但实用的RAG系统。然而,当你真正将其应用到生产环境中时,很快就会发现一些棘手的问题:
- 当用户询问"请假流程"时,系统却返回了关于"薪资制度"的文档
- 面对包含表格和图片的PDF文档,检索效果大打折扣
- 用户用不够精确的词汇提问时,往往找不到相关内容
- 检索出的文档中夹杂着大量无关信息,影响最终答案质量
这些问题的根源在于朴素RAG在检索精度和上下文质量方面还存在不少局限性。就像从自行车升级到汽车一样,Advanced RAG正是为了解决这些"最后一公里"的问题而诞生的。
Advanced RAG:站在巨人肩膀上的进化
Advanced RAG的核心理念很简单:既然检索是RAG的关键环节,那就从检索的前、中、后三个阶段全方位优化。
Advanced RAG将传统的"索引-检索-生成"三步曲扩展为了一个更加精细化的多阶段流程:
预检索阶段(Pre-Retrieval):重点优化索引结构和数据质量,包括构建更智能的索引、添加元数据标签、处理半结构化数据等。
查询优化阶段:重点优化用户查询,通过查询扩展、多角度查询等技术提升检索召回率。
后检索阶段(Post-Retrieval):重点优化检索结果,通过重新排序、上下文压缩等技术提升最终答案质量。
这就像是将原本的"粗放式检索"升级为"精准制导系统",每个环节都有专门的优化策略。
Pre-Retrieval:为检索打好地基
预检索优化就像是为房子打地基,地基越牢固,上层建筑就越稳定。这个阶段主要解决的是"如何让向量数据库更智能地存储和组织信息"的问题。
1. 摘要索引:让AI先读懂,再存储
痛点分析:半结构化数据的挑战
想象一下这样的场景:你有一份包含文字说明和数据表格的财务报告。传统RAG在处理时会遇到尴尬的情况:
- 文本分块可能会把表格切断:原本一个完整的财务数据表被切成了几段,失去了数据的完整性
- 表格向量化效果很差:表格的行列结构在转换为向量时容易丢失语义关系
- 检索时语义匹配困难:用户问"公司利润情况",但表格数据很难与这个查询建立语义联系
这就是半结构化数据给RAG带来的挑战。
解决思路:双层存储架构
摘要索引采用了一个巧妙的"双层存储"策略:
这个架构的核心思想是"用精炼的摘要保证搜索精确性,用完整的原文保证上下文完整性"**。
关键代码实现
让我们看看摘要索引的核心实现逻辑:
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生成的答案质量越高
这就像是摄影中的"景深"问题:焦点越集中,细节越清晰,但视野越窄;焦点越分散,视野越广,但细节越模糊。
解决思路:层级化存储
父子索引采用了一个类似"望远镜"的层级化策略:
核心思想:用子文档的精确性定位内容,用父文档的完整性提供上下文。
实现细节
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",用户的查询更容易与这些预设问题匹配上。
实现要点
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类实际上扮演了"数据格式契约"和"验证器"的角色:
- 自动生成指令:
with_structured_output会利用这个类自动告诉LLM应该如何格式化输出 - 自动解析验证:确保LLM返回的数据符合预期格式,避免解析错误
这就像Java反序列化时需要.class文件作为蓝图一样,是一个精巧的设计。
4. 元数据索引:给文档贴上智能标签
痛点分析
想象你在一个巨大的医学文献数据库中查找"糖尿病足"相关资料,但数据库中充斥着各种糖尿病并发症的信息。仅仅依靠语义相似性,可能会检索出许多不够精准的文档。
解决思路:结构化过滤
元数据索引的核心是在向量检索之前先进行结构化过滤,就像是先按分类找书,再按内容找章节。
"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的工作原理:
- 查询理解:LLM将自然语言查询解析为结构化条件和语义关键词
- 元数据过滤:根据结构化条件筛选文档子集
- 语义搜索:在过滤后的子集中进行向量相似性搜索
- 结果融合:综合元数据匹配度和语义相关性进行排序
注意:如果故意用Prompt让LLM生成错误的查询语法,整个机制会直接崩溃并报错,因为它依赖于严格的"语法约定"。
5. 混合检索:取长补短的智慧
两种检索方式的特点对比
| 特性 | 向量检索 | 关键词检索(BM25) |
|---|---|---|
| 擅长处理 | 语义理解、同义词、概念匹配 | 精确术语、专有名词、字面匹配 |
| 搜索速度 | 现代ANN算法较快 (O(log N)) | 传统但高效 |
| 内存占用 | 较高(需存储向量) | 较低(倒排索引) |
| 示例场景 | “汽车"能匹配"车辆” | "COVID-19"精确匹配 |
为什么需要混合检索?
正如我们讨论的,BM25有一个致命弱点:完全不懂语义。它无法处理:
- 同义词:汽车 vs. 车辆
- 概念映射:美国内战 vs. 南北战争
- 口语化表达:电脑卡死了 vs. 应用程序无响应
而向量检索在处理专有名词和精确术语时也有局限性。
混合检索架构
实现代码
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自动生成多个相关的变体问题,从不同角度进行检索,最后汇总所有检索结果。
"天才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采用了一个类似"奥运裁判打分"的融合算法:一个文档在越多查询结果中排名越靠前,最终得分就越高。
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 | 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,而是在其基础上针对特定问题进行精细化优化。就像从手动档升级到自动档,核心的驾驶原理没变,但驾驶体验显著提升。
关键洞察:
- 没有银弹:每种技术都有其适用场景,关键是理解业务需求
- 渐进优化:从朴素RAG开始,根据实际问题逐步引入Advanced技术
- 成本平衡:更好的效果往往意味着更高的计算成本,需要权衡
- 框架思维:理解每个组件的作用,灵活组合以适应不同场景
Advanced RAG的真正价值在于让AI系统更好地理解和处理现实世界的复杂信息。从半结构化数据到用户的模糊查询,从海量文档中的精准定位到上下文的智能压缩,每一个技术细节都在服务于一个终极目标:让AI助手变得更加可靠、精准、实用。
这就是从朴素到精准的进化之路——不是为了炫技,而是为了更好地解决实际问题。
更多推荐


所有评论(0)