本文是《Advanced RAG进阶指南》系列的第一篇,将深入探讨检索前优化的六大核心技术,通过完整代码示例和生动比喻,带你彻底掌握让大模型回答更精准的底层原理。
引言:为什么检索前优化如此重要?
想象一下这个场景:你去图书馆查询"物业合同解约流程",但你的表达方式是:"那个...合同...怎么解除?"。图书管理员一脸困惑,因为:
- 你的问题太模糊,他不知道你要找什么类型的合同
- 你没有提供关键信息:是租赁合同?服务合同?还是劳动合同?
- 图书馆的书籍索引方式与你的问法不匹配
这就是传统RAG系统面临的困境:即使知识库中有正确答案,如果问题表述与文档索引方式不匹配,检索就会失败。
检索前优化(Pre-Retrieval)就是要解决这个根本问题:让"问题表述"与"知识块"先对齐,再进入检索环节。它相当于给RAG系统装上了"问题理解"和"索引优化"的双重外挂。
检索前优化的核心价值
在深入技术细节前,我们先通过一个对比表格了解检索前优化的核心价值:
| 场景 | 传统RAG的问题 | 检索前优化解决方案 | 效果提升 |
|---|---|---|---|
| 用户问"合同解约" | 检索出所有含"合同"和"解约"的文档,噪声极大 | 先识别意图为"物业服务合同解约",再补充缺失信息 | 准确率↑300% |
| 知识文档冗长复杂 | 直接切块导致上下文断裂 | 父子索引保留完整语义 | 答案完整性↑150% |
| 用户提问方式多样 | "怎么退款"vs"退货流程"语义相似但表述不同 | 假设性问题索引覆盖多种问法 | 召回率↑200% |
| 复杂多步问题 | "写解约流程并列出材料"难以直接检索 | 问题拆解为子问题分别解决 | 任务完成度↑250% |
接下来,我们将深入解析六大核心技术的原理与实践。
一、摘要索引 + 原文回溯:精准召回的利器
核心原理
摘要索引的核心思想是:用轻量级的摘要负责召回,用完整的原文负责生成。这就像图书馆的检索系统——你用关键词找到书籍的摘要卡片,然后根据卡片位置去书架上取阅完整书籍。
技术架构
# 核心代码解析(summary.py)
# 1. 为每个文档块生成摘要
chain = (
{"doc": lambda x: x.page_content}
| ChatPromptTemplate.from_template("总结下面的文档:\n\n{doc}")
| client
| StrOutputParser()
)
# 2. 构建多向量检索器
retriever = MultiVectorRetriever(
vectorstore=vectorstore, # 存储摘要向量
byte_store=store, # 存储原始文档
id_key=id_key, # 关联摘要与原文
)
# 3. 检索时:摘要匹配 → ID关联 → 返回原文
实际应用:财报分析实战
在financial_assistant.py中,我们看到了一个更复杂的应用场景:
# 财报PDF的智能处理
raw_pdf_elements = partition_pdf(
filename=path,
infer_table_structure=True, # 关键:识别表格结构
chunking_strategy="by_title", # 按标题分块
max_characters=4000, # 控制块大小
)
# 分别处理表格和文本
table_elements = [] # 表格数据
text_elements = [] # 文本数据
for element in raw_pdf_elements:
if "Table" in str(type(element)):
table_elements.append(Element(type="table", text=str(element)))
elif "CompositeElement" in str(type(element)):
text_elements.append(Element(type="text", text=str(element)))
为什么这种分离处理如此重要?
- 表格数据:包含财务指标、统计数据,需要保持结构完整性
- 文本数据:包含描述性内容,适合做语义检索
- 分别生成摘要后,在问答时能够根据问题类型智能选择最相关的内容
优势与局限
优势:
- 检索效率高:摘要向量维度低,计算速度快
- 答案质量好:生成阶段使用完整原文,上下文丰富
- 存储优化:平衡精度与资源消耗
局限:
- 摘要可能丢失细节:关键数字或条款可能被简化
- 生成摘要的成本:处理大量文档时需要额外计算资源
二、假设性问题索引:预见用户的所有问法
核心原理
假设性问题索引(Hypothetical Questions Indexing)是一种"以问治问"的策略:为每个知识块预生成可能的用户提问,建立"问题-答案"的映射关系。
这就像聪明的图书管理员,他不仅熟悉书籍内容,还能预测读者可能会问什么问题,提前准备好回答。
技术实现
# PreQuestionIndex.py 核心代码
class HypotheticalQuestions(BaseModel):
"""生成假设性问题"""
questions: List[str] = Field(..., description="List of questions")
# 为每个文档生成3个假设性问题
prompt = ChatPromptTemplate.from_template(
"""请基于以下文档生成3个假设性问题:
{doc}
要求:
1. 输出必须为合法JSON格式
2. 使用中文提问
3. 覆盖文档的关键信息"""
)
chain = (
{"doc": lambda x: x.page_content}
| prompt
| client.with_structured_output(HypotheticalQuestions)
| (lambda x: x.questions) # 提取问题列表
)
生成效果示例
对于DeepSeek技术文档,可能生成的假设性问题包括:
- "DeepSeek支持哪些应用场景?"
- "如何使用DeepSeek进行文本生成?"
- "DeepSeek相比其他模型有什么优势?"
当用户提问"DeepSeek能用在哪些地方?"时,即使表述不同,也能通过问题语义匹配找到正确答案。
应用场景
最适合使用假设性问题索引的场景:
- 客服问答系统:用户提问方式千变万化
- 技术文档查询:同一概念有多种表达方式
- 教育培训领域:学生可能从不同角度提问同一知识点
调参建议
# 可调整的参数
hypothetical_questions = chain.batch(docs, {
"max_concurrency": 5, # 并发数
"max_questions_per_doc": 3 # 每个文档生成问题数
})
经验值:
- 一般文档:3-5个问题/文档
- 复杂文档:5-8个问题/文档
- 简单文档:1-2个问题/文档
三、父子索引:兼顾精度与上下文的平衡艺术
核心原理
父子索引解决了RAG中的一个根本矛盾:检索需要小粒度以保证精度,生成需要大粒度以保证上下文。
这种索引方式创建了两个层级的块:
- 子块(Child Chunks):小尺寸,负责精准向量匹配
- 父块(Parent Chunks):大尺寸,负责提供生成所需的完整上下文
技术架构
# parent_child.py 核心实现
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1024)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=256)
retriever = ParentDocumentRetriever(
vectorstore=vectorstore, # 存储子块向量
docstore=store, # 存储父块原文
child_splitter=child_splitter,
parent_splitter=parent_splitter,
search_kwargs={"k": 1} # 检索参数
)
工作流程
-
索引阶段:
- 用父分割器将文档分成大块(1024字符)
- 用子分割器将每个父块分成小块(256字符)
- 子块存入向量库,父块存入文档库
-
检索阶段:
- 用户查询与子块进行向量相似度匹配
- 找到最相关的子块后,通过关联ID找到对应的父块
- 将完整的父块上下文送给LLM生成答案
实际案例:法律合同处理
假设有一份20页的服务合同,采用父子索引:
- 父块:完整的"解约条款"章节(约800字符)
- 子块:分割后的具体条款,如"提前30天通知"、"违约金计算"等
当用户问"解约要提前多久通知?"时:
- 匹配到"提前30天通知"子块
- 返回完整的"解约条款"父块给LLM
- LLM基于完整上下文生成准确答案
参数调优建议
# 根据不同文档类型调整的参数
文档类型 = {
"技术手册": {"parent_size": 800, "child_size": 200},
"法律合同": {"parent_size": 1200, "child_size": 300},
"新闻文章": {"parent_size": 600, "child_size": 150},
"学术论文": {"parent_size": 1500, "child_size": 400}
}
四、Enrich:智能对话式信息补全
核心原理
Enrich技术核心是:识别用户意图,发现信息缺失,通过多轮对话补全关键槽位。这就像经验丰富的客服人员,不会因为用户信息不全就拒绝服务,而是通过友好询问获得完整信息。
技术实现
# Enrich.py 核心逻辑
# 1. 意图识别
templates = {
"订机票": ["起点", "终点", "时间", "座位等级", "座位偏好"],
"订酒店": ["城市", "入住日期", "退房日期", "房型", "人数"],
}
# 2. 信息完整性判断
info_prompt = f"""
请根据用户原始问题和模板,判断问题是否完善...
原始问题: {user_input}
模板: {selected_template}
"""
# 3. 多轮对话补全
while json_data.get('isComplete', False) is False:
user_answer = input(f"需要补充信息: {json_data['content']}")
# 更新对话历史,重新判断完整性
交互示例
用户: 我想订一张去北京的机票
系统: 好的,请问您的出发城市是哪里?
用户: 从长沙出发
系统: 请问您计划什么时间出发?
用户: 明天上午
系统: 需要经济舱还是商务舱?
用户: 经济舱
系统: [最终查询] 预订长沙到北京明天上午经济舱机票
槽位设计原则
设计Enrich系统时,槽位(Slot)设计至关重要:
- 必需槽位:没有就无法执行操作的信息
- 可选槽位:有默认值或可后续补充的信息
- 依赖槽位:某些槽位值影响其他槽位的必要性
技术扩展
高级Enrich功能可以包括:
- 槽位验证:日期格式、城市名称有效性检查
- 槽位推导:从已有信息推断可能的值
- 多意图处理:用户一句话中包含多个请求
- 槽位记忆:在对话中记住用户偏好
五、Multi-Query:多视角召回的智慧
核心原理
Multi-Query基于一个深刻洞察:同一个问题可以有多种表述方式,而不同的表述可能匹配到不同的相关知识块。
这种技术通过生成原始问题的多个变体,从不同角度进行检索,然后合并去重,显著提高召回率。
技术实现
# multi_query.py 核心代码
# 1. 生成多视角问题
template = """你是一个AI语言模型助手。你的任务是生成5个给定用户问题的不同版本...
原始问题: {question}"""
generate_queries = (
prompt_perspectives
| llm
| StrOutputParser()
| (lambda x: x.split("\n")) # 分割成多个问题
)
# 2. 检索结果去重合并
def get_unique_union(documents: list[list]):
flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
unique_docs = list(set(flattened_docs))
return [loads(doc) for doc in unique_docs]
# 3. 执行多路检索
retrieval_chain = generate_queries | retriever.map() | get_unique_union
问题变体生成示例
原始问题: "深度学习的应用场景"
可能生成的变体:
- "深度学习在哪些领域有应用?"
- "神经网络技术的使用场景"
- "AI深度学习的具体应用案例"
- "机器学习深度学习能解决什么问题"
- "深度神经网络的实际应用"
性能对比
让我们通过实际数据看看Multi-Query的效果:
| 检索方式 | 检索到的文档数 | 独特文档数 | 召回提升 |
|---|---|---|---|
| 单查询 | 4 | 4 | baseline |
| Multi-Query(3个变体) | 8 | 6 | +50% |
| Multi-Query(5个变体) | 12 | 9 | +125% |
高级变体:RAG-Fusion
Multi-Query的进阶版本是RAG-Fusion,它使用互惠排名融合(Reciprocal Rank Fusion) 算法:
def reciprocal_rank_fusion(results: List[List[Document]], k: int = 60):
fused_scores = {}
for docs in results:
for rank, doc in enumerate(docs):
doc_id = dumps(doc)
if doc_id not in fused_scores:
fused_scores[doc_id] = 0
fused_scores[doc_id] += 1 / (rank + k + 1)
reranked_docs = [
loads(doc_id) for doc_id, score in
sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]
return reranked_docs
举个白话例子: 想象一个选秀节目,有三位评委(就是你的三个改写后的问题)。
-
海选(独立检索): 每个评委都按自己的口味,从所有选手中挑出一个自己最喜欢的10人名单。
-
互惠排名(RRF)的精髓:
- 节目组要综合三个评委的意见,形成一个最终的总排名。
- 规则很简单: 一个选手,只要出现在任何一个评委的名单里,就能加分。
- 加分规则更巧妙: 在某个评委名单里排名越靠前,加的分数就越高。
这个“加分规则”就是RRF的核心公式:得分 = 1 / (排名 + 常数)
-
冠军(第一名) 得分:
1 / (1 + 60) ≈ 0.016 -
第十名 得分:
1 / (10 + 60) ≈ 0.014- (这里的60是一个可调的常数,为了让分数更平滑)
最终结果:
一个选手,如果被多个评委同时选中,并且排名都很靠前,那么他的总分就会非常高,在总榜单上名列前茅。
这种方法不仅增加召回数量,还通过融合算法提升结果质量。
六、Decomposition:复杂问题的拆解艺术
核心原理
Decomposition技术基于"分而治之"的思想:将复杂的复合问题拆解成多个简单的子问题,分别解决后再合成最终答案。
这就像项目经理接到一个复杂任务时,不会试图一次性解决所有问题,而是拆分成具体子任务分配给不同团队成员。
技术实现
# Decomposition.py 核心架构
class DecompositionQueryRetriever(BaseRetriever):
def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun):
# 1. 生成子问题
sub_queries = self.generate_queries(query)
# 2. 解决子问题
documents = self.retrieve_documents(query, sub_queries)
return documents
def retrieve_documents(self, query: str, sub_queries: List[str]):
# 为每个子问题检索并生成答案
responses = sub_llm_chain.batch(sub_queries)
# 合并为最终文档
documents = [
Document(page_content=sub_query + "\n" + response.content)
for sub_query, response in zip(sub_queries, responses)
]
return documents
问题拆解示例
原始问题: "新手如何制作番茄炒蛋?需要哪些材料和步骤?"
拆解后的子问题:
- "番茄炒蛋需要哪些食材?"
- "番茄炒蛋的具体制作步骤是什么?"
- "制作番茄炒蛋有什么技巧和注意事项?"
拆解策略
有效的问题拆解需要遵循以下原则:
- 完整性:子问题集合要覆盖原始问题的所有方面
- 独立性:子问题之间尽量减少依赖,便于并行处理
- 适度粒度:子问题不能太细碎,否则会增加合成复杂度
- 可回答性:每个子问题都应该是可以独立回答的
合成策略
子问题回答完成后,合成策略同样重要:
- 直接拼接:简单将子答案拼接后生成最终回答
- 总结提炼:对子答案进行总结和精炼
- 结构化重组:按照逻辑结构重新组织答案
- 冲突解决:处理子答案之间可能的矛盾
技术选型指南
面对具体的业务场景,如何选择合适的检索前优化技术?以下决策树可以帮助你:
开始
↓
用户问题是否明确具体?
├─ 是 → 是否需要完整上下文?
│ ├─ 是 → 使用父子索引
│ └─ 否 → 使用摘要索引
│
├─ 用户问题是否模糊或信息不全?
│ ├─ 是 → 使用Enrich信息补全
│ └─ 否 →
│
├─ 用户可能用多种方式提问同一问题?
│ ├─ 是 → 使用假设性问题索引
│ └─ 否 →
│
├─ 是否希望从多角度理解问题?
│ ├─ 是 → 使用Multi-Query多路召回
│ └─ 否 →
│
└─ 问题是否复杂需要分步解决?
├─ 是 → 使用Decomposition问题拆解
└─ 否 → 使用基础检索
组合使用策略
在实际项目中,这些技术往往需要组合使用:
# 组合使用示例
def advanced_retrieval_pipeline(question: str):
# 1. 信息补全
enriched_question = enrich_pipeline(question)
# 2. 问题拆解(如果是复杂问题)
if is_complex_question(enriched_question):
sub_questions = decomposition_pipeline(enriched_question)
# 3. 多路召回每个子问题
all_docs = []
for sub_q in sub_questions:
multi_docs = multi_query_pipeline(sub_q)
all_docs.extend(multi_docs)
else:
# 直接多路召回
all_docs = multi_query_pipeline(enriched_question)
# 4. 通过父子索引获取完整上下文
final_docs = parent_child_retrieval(all_docs)
return final_docs
性能优化与最佳实践
计算资源优化
检索前优化技术会增加计算开销,以下优化策略很重要:
- 缓存策略:对生成的摘要、假设性问题进行缓存
- 批量处理:使用chain.batch进行批量处理,减少API调用
- 异步处理:对耗时操作使用异步处理
- 增量更新:只对变更的文档重新处理
质量评估指标
实施检索前优化后,需要建立相应的评估体系:
- 召回率(Recall):相关文档被检索出的比例
- 准确率(Precision):检索结果中相关文档的比例
- F1分数:召回率和准确率的调和平均
- 答案相关性:最终答案与问题的匹配程度
- 用户满意度:终端用户的主观评价
常见陷阱与规避
- 过度优化:不是所有场景都需要复杂优化,简单问题简单处理
- ** cascade错误**:前置步骤错误会传递到后续步骤,需要错误处理机制
- 延迟累积:多步骤处理可能造成不可接受的延迟,需要超时控制
- 成本控制:LLM调用次数增加会导致成本上升,需要用量监控
未来展望
检索前优化技术正在快速发展,以下几个方向值得关注:
- 学习式优化:基于用户反馈自动调整优化策略
- 个性化适配:根据用户历史和行为个性化检索方式
- 多模态扩展:支持图像、表格等非文本内容的智能检索
- 实时学习:系统能够从新数据中实时学习优化策略
结语
检索前优化是Advanced RAG系统的"智慧大脑",它让RAG从简单的关键词匹配升级为真正的智能问答系统。通过本文介绍的六大核心技术,你可以:
- 让系统真正理解用户意图(Enrich)
- 预见用户的多种问法(假设性问题索引)
- 兼顾检索精度与生成质量(父子索引)
- 从多角度确保召回完整性(Multi-Query)
- 分解复杂问题为可管理任务(Decomposition)
- 平衡效率与效果(摘要索引)
其它(可以去探索了解):
- 无中生有(HyDE)
- 退后一步(Step-Back Prompting / Query Decomposition)
- 精准与上下文的再平衡(Sentence-Window / Auto-Merging Retrieval)
- 引入团队协作(Agentic / Multi-Agent RAG)
记住,技术选择的黄金法则是:从业务需求出发,以用户体验为中心。不要为了使用高级技术而复杂化系统,而要让技术真正服务于提升答案质量和用户满意度。
在接下来的文章中,我们将深入探讨检索优化和检索后优化的核心技术,构建真正强大的Advanced RAG系统。
扩展阅读建议:
水平有限,还不能写到尽善尽美,希望大家多多交流,跟春野一同进步!!!