改进 retriever step,是提升 RAG 系统准确性和相关性的最有效方式。你需要一套扎实的工具箱,来构建定制化、高效率的系统。本章介绍超越基础 vector search 的高级检索技术。图 7-1 概览了优化 pre-retrieval 和 retrieval steps 的技术。
图 7-1:增强 retrieval process 的技术
真实世界的 RAG workflows 通常会根据 use case 组合多种技术。图 7-2 展示了一个组合多个步骤和高级 retrieval techniques 的示例工作流。该工作流展示了三个关键阶段:(1)将复杂 query 拆解为聚焦的 subqueries;(2)将每个 subquery 路由到合适的 tool 或 data source,以检索相关信息;(3)跨答案进行推理,综合出一个完整 response。
图 7-2:多步 retrieval process
你可以把本章的 recipes 看作 building blocks,用来组合并改进你的 workflow,从而获得更好的搜索结果。你需要一套高级 retrieval techniques 工具箱,并以最适合具体 use case 的方式应用它们。
表 7-1 总结了本章讨论的七种技术。你应该理解每一种技术,并判断哪一种适合你的具体数据集和任务。
表 7-1:Retrieval enhancement techniques
| Technique | Description | Recipe reference |
|---|---|---|
| Metadata filtering | 使用 metadata 根据你对用户的了解过滤搜索结果 | Recipe 7.1 |
| Multiquery retrieval | 创建同一个 prompt 的多个版本,以找到更多相关文档 | Recipe 7.3 |
| Query routing system | 使用 query routing system 识别回答问题时最适合使用的数据源或工具 | Recipe 7.4 |
| Auto-merging retriever | 通过分组相关数据,检索更大、更有意义的 text chunks | Recipe 7.5 |
| Sentence window retrieval | 包含附近句子,为检索文本增加上下文 | Recipe 7.6 |
| Hypothetical document embeddings(HyDE) | 生成 hypothetical documents 以改善搜索结果 | Recipe 7.2 |
| Query decomposition | 将复杂 queries 拆解为更简单的 subqueries | Recipe 7.8 |
| Reranking | 通过 LLMs 审查并评估 retrieved documents 的相关性 | Recipe 7.7 |
你可以在本书 GitHub repository 中找到本章所有代码示例。
7.1 通过 PostgreSQL 中的 Metadata Filtering 优化 Query Results
Problem
你想在运行 semantic search 之前应用 metadata filters,使搜索结果更稳健、更准确。
Solution
将 metadata 与 embeddings 一起存储,可以让你在 semantic search 之前应用 filters。这会把搜索空间缩小到数据中相关的子集。设想你正在为一个覆盖 football、tennis 和 basketball 的体育网站构建 chatbot。用户 query “Who’s the best player of all time?” 太模糊。如果不知道具体是哪项运动,就不可能正确回答。
如果你有用户上下文,就可以将 vector dataset 过滤到相关运动。如果用户是 football 爱好者,就只过滤到 football 内容,并在该子集中搜索。
在图 7-3 中,Jim 是来自 Liverpool 的 football fan。当他询问自己最喜欢俱乐部的最新比赛结果时,用户偏好很可能表明,他想找的是 FC Liverpool 昨晚的比赛结果。
图 7-3:将 metadata filtering 作为 router retriever 的一部分。Router 会在执行搜索前,基于 metadata 预过滤搜索空间
下面用 PostgreSQL 作为 vector database 实现该方法。你只需要 Psycopg 2 library 连接 PostgreSQL,并使用 OpenAI client 生成 vector embeddings:
pip install psycopg2 openai
在这个例子中,设想有一个包含 football 和 tennis 通用知识的数据集。这个 recipe 展示如何根据用户偏好过滤,回答关于最伟大球员的问题。
先设置数据库 schema。为了稍后应用 filters,需要预先存储相关 metadata。创建一个包含 embedding vectors 和 metadata column 的 table,其中 metadata column 保存 JSON object:
# Create the vector extension if it does not exist
cur.execute("""CREATE EXTENSION IF NOT EXISTS vector""")
# Create table with metadata column
cur.execute(
"""
CREATE TABLE IF NOT EXISTS embeddings_table_with_metadata (
id SERIAL PRIMARY KEY,
text_chunk TEXT,
embedding VECTOR(1536),
metadata JSONB
)
"""
)
conn.commit()
Table structure 创建完成后,用 sample data 填充它。你会插入四个 text chunks,其中两个关于 tennis,两个关于 football。每一行包含文本、embedding vector,以及 JSON object 形式的 metadata。在这个基础例子中,唯一 metadata field 是 text chunk 的 topic,设置为 tennis 或 football:
# Define text chunks and metadata
text_chunks = [
{
"text": "Roger Federer has won 20 Grand Slam titles in tennis.",
"topic": "tennis",
},
{
"text": "The FIFA World Cup is the most prestigious "
"football tournament.",
"topic": "football",
},
{
"text": "Serena Williams is one of the greatest tennis "
"players of all time.",
"topic": "tennis",
},
{
"text": "Lionel Messi has won multiple Ballon d'Or "
"awards in football.",
"topic": "football",
},
]
# Initialize OpenAI client
client = OpenAI()
# Insert text chunks with embeddings and metadata
for chunk in text_chunks:
# Get embedding using OpenAI API
response = client.embeddings.create(
input=chunk["text"],
model="text-embedding-3-small"
)
embedding = response.data[0].embedding
metadata = {"topic": chunk["topic"]}
cur.execute(
"INSERT INTO embeddings_table_with_metadata "
"(text_chunk, embedding, metadata) VALUES (%s, %s, %s)",
(chunk["text"], embedding, json.dumps(metadata)),
)
conn.commit()
图 7-4 展示了该 table 在 pgAdmin 中的样子。
图 7-4:在 pgAdmin 中查看已填充的 metadata filtering table
现在数据已经带 metadata 加载完成,可以在 semantic search 之前使用 metadata column 进行 filtering。设想一个用户 query 询问史上最佳球员——如果不知道用户关注哪项运动,这个问题是模糊的。
如果你知道用户偏好,比如他主要是 football fan,就可以对 topic 为 football 的 entries 应用 metadata filter,然后在过滤后的子集上执行 cosine-similarity search:
# Query the table with metadata filtering
query = "Who is the best player?"
response = client.embeddings.create(
input=query,
model="text-embedding-3-small"
)
query_embedding = response.data[0].embedding
topic_filter = "football"
cur.execute(
f"""
SELECT text_chunk, 1 - (embedding <=> %s::vector) AS similarity
FROM embeddings_table_with_metadata
WHERE metadata->>'topic' = %s
ORDER BY similarity DESC
LIMIT 5
""",
(query_embedding, topic_filter),
)
results = cur.fetchall()
图 7-5 展示了结果。关于 Lionel Messi 及其多次获得 Ballon d’Or 的 snippet 排名最高,因此是最相关匹配。
图 7-5:展示 metadata filtering 生效的 query results
该 query 成功检索到 football 相关内容,因为 metadata filter 是基于对用户的先验知识设置为 football。另一种方法是让 LLM 分析问题和 vector database 中可用内容,然后让模型提出最合适的 metadata filters。
为了避免增加延迟,这一步可以使用快速高效模型,因为在许多情况下,只要知识源中可用数据清晰,用户想找什么是显而易见的。更困难的部分是分析复杂 snippets 并得出好结论。对于那个 reasoning step,使用更大、更强的模型是合理的。
Discussion
Metadata filtering 的工作方式,是在 semantic search 运行前缩小搜索空间。当你的 vector database 包含多样内容,而全局搜索会返回无关匹配时,这一点很重要。机制很简单:先按 metadata 过滤,然后只在该子集内运行 cosine similarity。
当你有不同用户分群,例如带偏好的 logged-in users,内容自然按 categories 分区,例如按 department 划分 products、按 topic 划分 articles,或者 queries 在没有上下文时会变得模糊,例如 “best player” 在不同运动中含义不同,可以使用 metadata filtering。
当整个数据集主题高度同质、没有可用于过滤的 metadata,或者维护 metadata tags 的开销超过准确率收益时,可以跳过该方法。
主要取舍是前期 metadata collection effort 与长期 accuracy gains。可测试性会显著提升:当你按 metadata 分段 retrieval,例如 football 与 tennis,你可以为每个 topic 构建 evaluation datasets,更容易度量性能并识别薄弱点。
从一个 category 开始,验证 retrieval quality,然后逐步扩展。通常最困难的是收集合适 metadata。Recipe 4.1 展示了如何从文档中提取已有 metadata,并通过让 LLM 分析内容来生成新的 metadata。
See Also
Chroma metadata filtering documentation 展示了使用 $eq、$gt 和 $lt 等 operators 的实践示例。
Pinecone metadata filtering guide 演示了如何将 vector search 与 metadata filters 结合。
PostgreSQL JSONB documentation 覆盖了 metadata storage 的 JSON querying 和 indexing。
7.2 使用 HyDE 提升 Retrieval Accuracy
Problem
你想通过创建与数据库中真实内容风格匹配的 pseudodocuments 来改善搜索结果。
Solution
Hypothetical document embeddings(HyDE)会生成与用户 query 匹配的 hypothetical documents。随后,这些 hypothetical documents 会用于 semantic search。
这些 hypothetical documents 并不要求事实正确。它们只是看起来像文章或书籍中可能包含答案的 plausible passages,并且以与你数据库中真实文档类似的风格写成。这种 style matching 可以改善 retrieval results。
流程很直接:
- 用户提出问题。
- LLM 生成一个可能回答用户问题的 hypothetical document。
- 你对 hypothetical document(s) 做 embedding,并搜索 vector database。
- 你使用检索到的真实文档生成最终答案。
图 7-6 使用最早提出 HyDE 概念的论文中的两个示例,可视化这些步骤。该图展示了一个 medical query 生成关于 wisdom tooth removal time 的 passage,以及一个 scientific query 生成关于 COVID 对心理健康影响统计数据的 passage。两者都展示了 HyDE 如何创建风格上与目标语料匹配的文本。
图 7-6:LLM 生成一个 hypothetical paragraph,用于辅助回答问题
在这个 recipe 中,你使用 OpenAI 模型生成 hypothetical documents。
较小且高效的模型通常足够,因为它只需要生成与 query 匹配的 plausible text snippets。模型应该只返回 hypothetical documents,不返回其他内容。
为了确保模型以可预测格式返回 hypothetical documents 列表,使用 Pydantic 定义预期 response schema。安装如下:
pip install openai pydantic
然后为一个询问 Company X 在 2024 年收入的 query 生成 hypothetical documents:
from pydantic import BaseModel
from openai import OpenAI
user_query = "What is the revenue of Company X in 2024?"
client = OpenAI()
class HypotheticalDocuments(BaseModel):
documents: list[str]
prompt = f"""
You are an AI assistant. Based on the user query below, generate
three hypothetical text chunks that contain relevant information to
answer the query.
"""
completion = client.beta.chat.completions.parse(
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": user_query},
],
model="gpt-5-mini",
response_format=HypotheticalDocuments,
)
hypothetical_documents = completion.choices[0].message.parsed.documents
图 7-7 展示了生成的 hypothetical documents。LLM 创建了类似财务报告的段落,这可以帮助从 vector database 检索更好的结果。
图 7-7:为回答 “What is the revenue of Company X in 2024?” 生成的 hypothetical documents
下一步遵循基础 RAG 方法:使用 hypothetical documents 生成 embeddings,搜索 vector database,然后围绕 retrieved content 构建 prompt。
Hypothetical documents 不会包含在最终 prompt 中。它们会被从 vector database 检索出的真实文档替代。
Discussion
HyDE 的工作方式,是将 queries 转换成与你语料风格匹配的文本。如果你的文档是使用正式语言的 scientific papers,HyDE 会生成正式的 hypothetical passages。如果你的文档是休闲博客文章,HyDE 会生成休闲文本。Embedding similarity 会变成 style + topic,而不只是 topic。
当用户 queries 与已存储文档之间存在 vocabulary 或 style mismatch 时,可以使用 HyDE。例如,用户问 “How do I fix my car?”,但你的文档写的是 “automotive repair procedures”。HyDE 通过生成你语料风格的文本来弥合这种差距。
当 query 和 document styles 已经匹配、baseline retrieval 已经表现良好,应先用 eval metrics 验证,或者额外延迟,也就是每个 query 多一次 LLM call,和成本使 trade-off 不划算时,可以跳过该方法。
主要风险是生成 plausible 但错误的 hypothetical documents,从而导致错误 retrieval。在模型缺乏知识的专门领域中,这种情况更容易发生。部署生产前需要充分测试。
TIP
HyDE 不同于 query expansion:query expansion 会生成同一问题的替代表达,而 HyDE 会生成 hypothetical answer passages。当跨领域 retrieval 中语言风格比精确关键词匹配更重要时,HyDE 最有效。
See Also
LlamaIndex HyDE implementation 提供了可直接改造的完整工作示例。
LangChain RAG tutorial 包含 production pipeline 中的 HyDE 集成。
原始 HyDE paper 描述了该技术和多个领域上的评估结果。
7.3 使用 Multiquery Retrieval 改善搜索结果
Problem
你想向 vector database 发送多个 queries,覆盖原始用户 query 的多个方面,以确保检索到所有相关内容。
Solution
Multiquery retrieval 会创建原始用户问题的多个变体,对每个变体执行 semantic search,然后合并结果,以生成更全面的答案。
流程很直接,如图 7-8 所示:
- 获取原始 query,并生成两到三个替代表达。
- 使用每个 query 搜索 vector database。
- 合并每次 search run 检索到的 documents,并发送给 LLM。
每个 query variant 都可能发现不同的相关文档,从而更广泛地覆盖知识库内容。
图 7-8:Multiquery retrieval
示例用户 query 可能是 “What are the benefits of renewable energy?” Multiquery retrieval 会把这个看似直接的问题拆成多个对于良好回答很重要的方面。
在这个例子中,下面是从原始 query 派生出的三个 sample queries:
- What are the environmental benefits of renewable energy?
- How does renewable energy contribute to economic growth?
- What are the drawbacks of renewable energy?
每个 query 都针对回答原始问题时可能相关的不同方面。基于 LLM 对世界的了解,环境影响、经济效果和潜在挑战,都是生成完整答案时可能重要的因素。
这个 recipe 的实践示例中,你使用 OpenAI LLM 生成原始 query 的多个变体。为了确保结果严格是字符串列表,代码使用 Pydantic 定义 retrieval template。要运行代码,安装 OpenAI SDK 和 Pydantic:
pip install openai pydantic
你使用 prompt template 指示 LLM 将原始 query 改写成多个 subqueries。这个 prompt 有意保持简单:它要求生成同一 query 的多样化变体,并说明目标是从 vector database 中检索相关文档,以及缓解基于距离的 similarity search 的局限:
from openai import OpenAI
from pydantic import BaseModel
import os
client = OpenAI()
question = "What are the benefits of renewable energy?"
query_prompt = f"""You are an AI language model assistant. Your task is
to create three alternative versions of the provided user query to
enhance the retrieval of relevant documents from a vector database.
By offering diverse variations of the query, your goal is to help
mitigate the limitations of distance-based similarity search. Provide
these alternative queries, each on a new line.
Original query: {question}
"""
# Send the query prompt to OpenAI
class QueryVariations(BaseModel):
queries: list[str]
completion = client.beta.chat.completions.parse(
model="gpt-5-mini",
messages=[
{
"role": "user",
"content": query_prompt,
},
],
response_format=QueryVariations,
)
queries = completion.choices[0].message.parsed.queries
图 7-9 展示了输出。在这个例子中,模型生成了原始问题 “What are the benefits of renewable energy?” 的三个替代版本。
图 7-9:query 的派生版本
接下来,retrieval step 会对三个生成 queries 加上原始 query 都执行 semantic search。也就是说,你总共有四次 vector searches,返回四组结果。然后可以使用 merger 合并这些结果;如有需要,再应用 reranking 方法对 retrieved snippets 打分,并只保留最佳匹配项。Reranking 在 Recipe 7.7 中介绍。
Discussion
Multiquery retrieval 通过将一个 query 扩展为 N 个 variants,运行 N 次 vector searches,并合并结果来工作。该机制利用了这样一个事实:不同表达方式会发现不同文档。当原始 query 因 vocabulary mismatch 而错过相关内容时,这可以提升 recall。
NOTE
Recall 衡量 retriever 找到的相关文档数量,相对于数据库中所有存在的相关文档数量。对于 RAG 系统,高 recall 意味着 retriever 成功找到了大多数有用信息,即使它也检索到一些之后会被过滤的无关内容。
当用户问题宽泛或模糊,例如 “renewable energy benefits” 可能意味着环境、经济或社会方面;单个 query 经常错过 eval set 显示存在的相关文档;或者提升 recall 能 justify 多次 search 的更高成本时,可以使用 multiquery retrieval。
当用户提出狭窄、具体且单次 query 就能很好处理的问题、retrieval latency budget 无法承受 N 次并行 searches 加一次 LLM call,或者 eval metrics 显示 single-query retrieval 已经达到目标 recall 时,可以跳过该方法。
主要取舍是成本和延迟 versus recall。运行三次 searches 加一次用于 query generation 的 LLM call,会大约让 retrieval time 和 compute cost 增加三倍。这适合复杂研究 queries,但不适合简单 FAQ lookups。
Multiquery 与 query decomposition 不同:multiquery 生成的是同一问题的语义相似变体,而 decomposition 会将复杂问题拆成需要分别回答的不同 subquestions。当复杂 multipart questions 的每个部分也受益于多种表达时,可以组合两者。
See Also
LlamaIndex multiquery retriever tutorial 展示了如何合并来自多个 queries 和 indexes 的 retrieval results。
LangChain MultiQueryRetriever 提供了另一种带 result merging 的实现。
7.4 通过设计 Query Routing System 处理复杂请求
Problem
你想通过将复杂用户问题导向正确数据源来处理它们。
Solution
Query routing system 会选择最合适的 tool 或 data source 来解决用户 query。例如,用户要求计算 87 乘以 99,这是一个基础数学运算,最适合由 calculator tool 处理。相比之下,关于 Google 某一年收入的问题,更适合通过 vector search 处理,因为答案很可能写在 Google 的某份财务报告中。
如果你的 RAG 系统连接了多个 data sources 和 tools,并且这些工具可以执行某种操作,那么你需要一个 orchestrator,将每个问题路由到正确组件。
图 7-10 展示了这类系统的一个基础示例。它可以访问多家公司财务报告,例如一个包含 Microsoft 收入数据和 Google 财务数据的 knowledge store。此外,query routing system 还连接到一个 SQL database,其中包含每家公司股价等 time-series data。
图 7-10:使用 function calling models 进行 query routing。在这里,agent 可以查询 vector database 或 SQL database。Agent 应决定哪条路径最合适。
Router 会选择最适合用户问题的数据源。对于复杂问题,router 还可以将任务拆成多个 subquestions,每个 subquestion 可以使用不同 data sources 和 tools 回答,然后将不同 queries 的结果合并成最终答案。
这个 recipe 使用 LLM-based router。一个 efficient model 会接收包含可用 functions、tools 和 data sources 的 prompt,并决定从哪里检索信息。
通常,每个可以提供某类信息的 tool 或 database 都会暴露为一个 function。Router 选择调用哪个 function,Python 代码执行该 function。本 recipe 中的 router 使用 OpenAI LLM 作出路由决策,同时 Pydantic 确保输出只包含要调用的 function name。用以下命令安装 required dependencies:
pip install openai pydantic
你将定义一个 prompt,指示模型从这三个 tools 中选择:
- 一个连接到 football 通用知识 vector database 的 tool,用于回答 “What is the offside rule in football?” 这样的问题。
- 一个连接到 tennis 相关 vector database 的 tool。
- 一个模拟连接到 SQL database 的 tool,用于保存最新 football match results。
你通过定义 Pydantic schema 实现该 router,该 schema 约束 LLM 只能选择这三个 tools 之一。基于用户问题,router step 调用 LLM,并让它决定使用哪个 tool。LLM 只返回一个用于识别所选 tool 的字符串 label。代码如下:
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
user_queries = [
{
"query": "Who is the all-time top scorer in the FIFA World Cup?",
"selected_data_source": None,
},
{
"query": "What are the four Grand Slam tennis tournaments?",
"selected_data_source": None,
},
{
"query": "Did Manchester United win their last game?",
"selected_data_source": None,
},
]
prompt = f"""
You are an expert at routing a user question to the appropriate
data source. Given a user question choose which of the data sources
in list_of_data_sources is the best to answer the question.
"""
from typing import Literal
from pydantic import Field
class RouterDecision(BaseModel):
data_source: Literal[
"general_football_knowledge",
"general_tennis_knowledge",
"latest_football_results_sql",
] = Field(
...,
description="The best data source to answer the question."
)
for user_query in user_queries:
completion = client.beta.chat.completions.parse(
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": user_query["query"]},
],
model="gpt-5-mini",
response_format=RouterDecision,
)
user_query["selected_data_source"] = completion.choices[
0
].message.parsed.data_source
图 7-11 展示了 query routing system 的结果。
图 7-11:Query routing system results
第一个关于 FIFA World Cup top scorer 的 query,被路由到 football 通用知识。第二个关于 Grand Slam tournaments 的 query,被路由到 tennis 知识。最后一个关于 Manchester United 最近比赛的问题,被路由到包含最新结果的 SQL database。
对于更复杂的问题,也可以指示模型将整体 query 拆分为多个 subqueries。每个 subquery 可以路由到不同 tool,最后通过 LLM reasoning step 整合结果。
Discussion
Query routing 的工作方式,是把 retrieval decisions 委托给一个 classifier,由它选择合适 data source 或 tool。机制很简单:分析 query,选择最佳 source,然后执行 retrieval。前面的代码展示了 routing decision。在完整实现中,你会基于 router selection 执行合适 retrieval function,例如对知识库执行 search_vector_db(query),或对结构化数据执行 query_sql_db(query)。
当你有多个 distinct data sources,例如 vector stores、SQL databases、APIs,且 queries 可以清楚映射到某个 source;query types 足够可预测,可以可靠分类;或者搜索错误 sources 的成本,例如无关结果、浪费计算,高于 routing overhead 时,可以使用 routing。
当你只有一个 data source,或者“搜索所有内容再过滤”比先 route 更快,或者 queries 太模糊而无法可靠分类时,可以跳过 routing。
LLM-based routing 每个 request 会增加 500ms 到 2 秒的延迟。虽然 2 秒不一定无法接受,但对于用户期待快速响应的 user-facing chat applications 来说,会明显增加延迟。Routing 更适合 asynchronous workflows,例如 batch processing 或 email automation。有两种更快替代方案:
Majority vote
搜索所有 collections,并路由到结果最多的 collection。延迟接近单次 search,但会增加计算成本。
Embedding classifier
训练一个 lightweight model,根据 query embeddings 预测最佳 collection。它会增加 10 到 50ms 延迟,计算 overhead 很小。
先用 LLM routing 验证 routing logic。如果延迟成为问题,且 collections 主题明确区分,就切换到 embedding classifier。如果 collections 高度重叠,尽管有延迟成本,LLM routing 可能是唯一可靠选择。
Routing 与 metadata filtering 不同:routing 在搜索前从不同 data sources 中选择;filtering 则在单一 source 内缩小结果范围。当 queries 需要 source selection,也就是 routing,然后还需要在该 source 内 category filtering,也就是 metadata 时,可以组合二者。
图 7-12 展示了构建 query routing system 的替代方法。
图 7-12:构建 query routing system 的替代方法
TIP
LangChain、LlamaIndex、Amazon Bedrock Agents 和 Azure AI Agent Service 等 frameworks 提供预构建 routing solutions。不过,由于 router 通常只是一个精心设计的 prompt,在添加 framework dependencies 前,应考虑是否真的需要额外复杂性。
See Also
LlamaIndex router query engine 为 multisource systems 提供完整 routing 实现。
LangChain multiagent router 提供 agent 和 workflows 的 routing components。
AWS Bedrock Agents documentation 展示了企业系统中的生产 routing patterns。
7.5 通过设计 Auto-Merging Retriever 增强 Retrieved Documents
Problem
你想在 semantic search 阶段使用较小 text chunks,同时在 generation step 向 LLM 提供更大的 parent chunks,以优化 retrieval 和 generation。
Solution
Auto-merging retriever 会将文本组织成较小 child chunks,并链接到较大的 parent chunks。Child chunks 用于 vector search,因为它们足够聚焦,可以捕捉不同 information units。检索后,系统会统计属于同一个 parent 的 child chunks 数量。如果来自某个 parent 的 child chunks 被识别为相关的数量达到某个阈值,系统就会将整个 parent chunk 提供给 LLM,为 generation 提供更广上下文。
图 7-13 展示了一个三层 tree structure:顶部是 parent nodes,中间是 child nodes,底部是 leaf nodes。Leaf nodes 用于 semantic search。如果来自某个 child 或 parent node 的 leaf nodes 中达到阈值数量被检索到,系统就会合并它们,并将整个 child 或 parent chunk 放入 prompt。例如,如果 leaf nodes 1、2 和 5 是相关的,其中两个属于 child node 1,系统会检索 child node 1 的完整文本;但对于 child node 2,只检索 leaf node 5。
图 7-13:使用 parent 和 child nodes 的 auto-merging retriever
这个 recipe 使用 PostgreSQL 作为 vector database,并使用 OpenAI embedding models,安装如下:
pip install psycopg2 openai
接下来,创建一个简单两层结构。每个 child chunk 大约包含 250 characters,四个 child chunks 合并成一个 parent chunk,因此每个 parent chunk 大约 1,000 characters。每个 leaf node 都明确只属于一个 parent node。
为了后续使用这些 relationships,每个 parent node 和每个 leaf node 都有 ID。此外,每个 child chunk 会在单独列中同时存储 child node ID 和 parent node ID:
file_path = (
"../datasets/text_files/"
"random-text-about-5-different-stories-with-paragraphs.txt"
)
with open(file_path, "r", encoding="utf-8") as f:
text = f.read()
leaf_node_size = 250 # Size of the leaf nodes in characters
parent_merge = 4 # Number of leaf nodes to merge into a parent node
parent_node_size = (
parent_merge * leaf_node_size
) # Size of the parent nodes in characters
text_chunks = [] # List to store the text chunk dictionaries
for leaf_node_id in range(0, len(text) // leaf_node_size, 1):
parent_node_id = leaf_node_id // parent_merge
leaf_chunk_start = leaf_node_id * leaf_node_size
parent_chunk_start = parent_node_id * parent_node_size
text_chunk = {
"leaf_node_id": leaf_node_id,
"leaf_chunk": text[
leaf_chunk_start : leaf_chunk_start + leaf_node_size
],
"parent_node_id": parent_node_id,
"parent_chunk": text[
parent_chunk_start : parent_chunk_start + parent_node_size
],
}
text_chunks.append(text_chunk)
创建 table,用于存储 leaf node、leaf chunk、leaf node ID、parent node、parent node ID,以及 leaf chunk 的 embeddings:
# Create a new table for storing chunks and embeddings
cur.execute("""CREATE EXTENSION IF NOT EXISTS vector""")
cur.execute(
"""
CREATE TABLE auto_merging_retriever_text_chunks (
leaf_node_id SERIAL PRIMARY KEY,
leaf_chunk TEXT,
parent_node_id INTEGER,
parent_chunk TEXT,
leaf_chunk_embedding VECTOR(1536)
)
"""
)
conn.commit()
然后使用前面创建的 dictionary 填充 table:
from openai import OpenAI
client = OpenAI() # Initialize OpenAI client
# Insert chunks and their embeddings into the table
for text_chunk in text_chunks:
leaf_embedding = (
client.embeddings.create(
input=[text_chunk["leaf_chunk"]],
model="text-embedding-3-small"
)
.data[0]
.embedding
)
cur.execute(
"""
INSERT INTO auto_merging_retriever_text_chunks
(leaf_node_id, leaf_chunk, parent_node_id, parent_chunk,
leaf_chunk_embedding)
VALUES (%s, %s, %s, %s, %s)
""",
(
text_chunk["leaf_node_id"],
text_chunk["leaf_chunk"],
text_chunk["parent_node_id"],
text_chunk["parent_chunk"],
leaf_embedding,
),
)
conn.commit()
当用户现在提出问题时,对 leaf chunks 执行 vector search。Vector search 之后,统计是否有某个 parent node 有超过两个 leaf nodes 被标记为相关。对于这些 parent nodes,通过 parent node ID 检索 parent text chunk,并将 parent chunk 提供给 generation model。
Discussion
Auto-merging 会在 child level 进行搜索,以获得 precision;当同一 parent 中有多个 children 相关时,再扩展到 parent chunks。
当你的文档具有强层级结构,例如 books with chapters and sections、research papers with sections;retrieval quality 因信息跨多个 chunks 而受损;或者评估显示更大的 context chunks 能提升 generation quality 且不会造成过度 prompt bloat 时,可以使用 auto-merging。
当 simple flat chunking 已经效果很好、文档缺乏清晰 hierarchy,例如 FAQs、product descriptions、更大的 prompts 增加成本却没有质量收益,或维护 parent-child relationships 的额外复杂度无法被 evaluation metrics 证明合理时,可以跳过该方法。
主要取舍是 storage complexity versus context quality。你需要同时存储 parent 和 child chunks,维护 ID relationships,并实现 merge logic。作为回报,当 queries 匹配多个相关 fragments 时,你可以检索到更连贯的 context。
See Also
LlamaIndex auto-merging retriever 会与 hierarchical node parser 一起处理多层 chunk hierarchies。
Haystack auto-merging implementation 使用 hierarchical document splitter 处理 parent-leaf structures。
pgvector documentation 覆盖在 PostgreSQL 中高效存储和检索 hierarchical embeddings。
7.6 使用 Sentence Window Retriever 检索更完整的 Text Chunks
Problem
你想使用较小 text chunks 做 semantic search,但仍然向 LLM 提供足够的周围上下文,以避免遗漏 retrieved sentences 附近的重要信息。
Solution
Sentence window retriever 会在最相关 chunk 的前后添加几句话,以提供额外上下文。图 7-14 展示了一个关于 FC Bayern Munich 的问题示例。Semantic search 可能识别出一段关于近期成功和冠军的 paragraph,但会切掉解释俱乐部是什么、名字来源是什么、所在地在哪里的历史上下文。加入周围句子可以确保这些相关上下文进入 prompt。
图 7-14:Sentence window retrieval
在图 7-15 中,sentence window 由三个 text chunks 定义。原始文档被拆分为小 text elements,用于 vector search。如果 element 4 被识别为相关,发送给 LLM 的完整 window 就包含 elements 3、4 和 5。
图 7-15:Sentence window retriever 的实践方式
你需要一个 vector store,能够存储 text chunks、它们的 embeddings,以及 chunks 在源文档中的原始顺序。这个 recipe 使用 PostgreSQL 作为 vector database,并使用 Psycopg 2 package 从 Python 连接。安装 Psycopg 2:
pip install psycopg2
接下来,定义需要创建的 table。它会存储 chunk ID、chunk text、chunk embedding 和 document ID。
将文档加载到 vector database 时,确保 chunk ID 反映原始顺序。第一个文本片段应为 chunk ID 1,第二个为 chunk ID 2,依此类推。这样之后才能检索 semantic search 返回 chunk 前后的 chunks。用以下代码定义 table:
cur.execute(
"""
CREATE TABLE sentence_window_retriever_text_chunks (
chunk_id SERIAL PRIMARY KEY,
chunk TEXT,
chunk_embedding VECTOR(1536)
)
"""
)
conn.commit()
填充 table 时,像前面的 recipes 一样,拆分文本、生成 embeddings 并插入。关键是 chunk IDs 必须是连续的,以体现文本在文档中的顺序。将文档拆分成大约 250 tokens 的小 chunks。当某个 chunk 在 semantic search 中被识别为相关时,选择它前后的 chunks,然后将大约 750 tokens 的组合 window 提供给 LLM。
这有助于确保重要信息不会被拆分到 chunk boundaries 两侧,也会增加包含相关周围上下文的可能性。
Discussion
Sentence window retrieval 的工作方式,是使用原始文档中的相邻上下文扩展 retrieved chunks。机制很简单:通过 semantic search 识别最相关 chunk,然后根据它在源文档中的顺序,包含前后 N 个 chunks。
当你的 chunks 很小,例如 200–500 tokens,以追求 retrieval precision;相邻上下文经常对理解有帮助,例如 legal docs、technical manuals、narrative text;并且扩展一两个 chunks 不会相对于模型 context limit 造成过度 prompt 膨胀时,可以使用 sentence window retrieval。
当 chunks 已经包含 500+ tokens 且有足够上下文、文档是 FAQ-style 且每个 chunk 都自包含,或 evaluation metrics 显示增加复杂性没有质量提升时,可以跳过该方法。
主要取舍是 prompt size versus completeness。从每个 retrieved chunk 1,000 tokens 扩展到 3,000 tokens,会让 context length 增加三倍,从而增加成本和延迟。收益是降低 boundary effects,也就是相关信息被拆分到 chunk edges 两边的问题。
Sentence window retrieval 与 auto-merging 不同:前者使用固定位置扩展,也就是总是添加相邻 N 个 chunks,不考虑内容;后者使用 relevance-based expansion,只有当多个 child chunks 匹配时,才将 parent chunk 添加到 retrieved context。Sentence window retrieval 实现更简单,但适应性较弱。对于简单位置上下文需求,使用它;当文档结构和相关性模式应该驱动 expansion decisions 时,使用 auto-merging。
See Also
LlamaIndex sentence window retriever 展示了使用 metadata replacement 进行 context expansion 的完整实现。
LangChain ParentDocumentRetriever 提供类似方法,用 child chunks 检索 parent documents。
7.7 使用 Reranking Methods 提升 Retrieval Relevancy
Problem
你的系统会从多个 databases 和 tools 检索文档,你想通过 reranking retrieved results,过滤出最相关文档。
Solution
本章前面的 recipes 描述了执行多个 queries 的技术,每个 query 都可能返回不同结果。挑战在于合并这些结果,同时识别哪些 documents 对最终答案真正重要。
使用 HyDE 时,你会生成多个 hypothetical documents,并运行多次 vector search。Multiquery retrieval 会并行执行多个 reformulated queries。Agent systems 可能查询多个 tools,例如 SQL databases、vector stores、APIs 或 Python calculations。这些方法都会产生一个需要过滤的 combined result set。
Reranking 通过对合并结果进行 relevance 打分,只保留 top items 来解决该问题。这会减少噪声并提升最终 prompt 的质量。图 7-16 可视化了这个过程。
图 7-16:对 retrieved documents 应用 reranking
图中展示了三次 semantic searches,但实践中你可以使用任何 retrieval step:SQL databases、vector stores、external APIs 或 Python calculations。你会将所有结果合并成一个 list,然后在 LLM step 之前过滤 combined set。
这个 recipe 使用 LLM-based reranker。一个 efficient LLM 会审查 retrieved documents 并评估其 relevance。这个 recipe 也使用 OpenAI model 对结果 rerank,并通过定义为 Pydantic model 的 response schema 返回结果。安装这些 dependencies:
pip install openai pydantic
这个 recipe 会把所有 retrieved documents 提供给 reranker。原始用户 query 是关于 Tesla 是否会继续保持电动汽车市场领导地位。在上游,你可能使用 multiquery 方法派生五个 subqueries,为每个 subquery 执行 vector search,然后选择每个 query 的最佳结果。随后,将这些结果合并成一个集合。
这里定义一个 prompt template,指示 LLM 根据用户问题对 retrieved text chunks 的相关性重新排序:
import textwrap
from openai import OpenAI
from pydantic import BaseModel
text_chunks = {
1: "Tesla's Supercharger network and tech lead face rising "
"competition from BYD and established automakers.",
2: "Tesla's production grows, but price competition threatens "
"its market share.",
3: "The automotive industry is shifting to EVs due to climate "
"change and regulations.",
4: "Semiconductor shortages are disrupting automotive supply "
"chains.",
5: "Consumer demand for autonomous driving and advanced tech "
"impacts EV competition.",
}
prompt = textwrap.dedent(
f""""
Query: Will Tesla remain the market leader in electric vehicles?
Documents:
1. {text_chunks[1]}
2. {text_chunks[2]}
3. {text_chunks[3]}
4. {text_chunks[4]}
5. {text_chunks[5]}
Instructions:
Please assess the relevance of each document to the query and
provide a relevance score from 1 to 5, where 5 is the most relevant.
Relevance Scores:
"""
)
图 7-17 展示了 reranking output,也就是 documents 的新排序。根据 LLM 判断,第五个 text chunk 看起来与用户问题最相关,其次是 chunks 4、2 和 1。
图 7-17:Reranking results
你仍然拥有同样五个 retrieved documents,但现在知道哪些最相关。为了提升 response time 并避免给最终 generation model 添加过多噪声,可以只选择 top three text chunks 并将它们组合进最终 prompt。
Discussion
Reranking 会在 generation 之前对合并后的 retrieval results 进行打分和过滤,只保留最相关的 top-K items。
当你从多个 sources 合并 20+ candidates、初始 retrieval 噪声较多且包含许多边缘相关结果,或者 reranking 后选择 top five 相比从每个 source 分别取 top five 质量更好时,可以使用 reranking。
当你只有一个 retrieval source 且 native ranking 已经很好、retrieval 已经返回高度相关结果,或者额外 LLM call 或 cross-encoder 成本无法通过 precision@k metrics 测得的质量收益证明合理时,可以跳过 reranking。
主要取舍是 latency 和 cost versus relevance。LLM-based reranking 会增加一次 inference call,用于处理所有 candidates。Cross-encoder models 更快,但仍会增加 overhead。作为交换,你获得更相关的最终结果,并可以有信心地从 50+ candidates 中过滤到 5–10 个高质量 items。
Reranking 与 initial retrieval scoring 不同:initial scoring,例如 cosine similarity、BM25,很快,但会独立处理 query 和 document;reranking 则使用 query 和 candidates 之间的 cross-attention 进行更深入 relevance assessment。两阶段方法在速度,也就是 initial retrieval,和准确率,也就是 reranking,之间取得平衡。应将 reranking 用作 merged results 的第二遍处理,而不是替代快速初始 retrieval。
See Also
LangChain RankLLM Reranker 为 document relevance scoring 提供 LLM-based reranking。
LlamaIndex ColbertRerank 使用 ColBERT models 进行快速且准确的 reranking。
Haystack SentenceTransformersSimilarityRanker 使用 Hugging Face cross-encoder models 进行 document ranking。
7.8 将复杂 Queries 拆解为多个 Subqueries
Problem
你想通过将复杂问题拆解为更简单、可管理的部分来回答它们。
Solution
图 7-18 展示了端到端流程:
- 将复杂 query 拆解为更简单的 subqueries。
- 每个 subquery 触发一个 retrieval step。
- Retrieved context 为每个 subquery 生成一个答案。
- 最后的 reasoning step 将所有内容组合起来,生成一个答案。
图 7-18:Decomposing complex queries
这个 recipe 使用 OpenAI LLM 将复杂用户 query 拆解成更简单的 subqueries。你还使用 OpenAI SDK,并将 response template 定义为 Pydantic model。安装 dependencies:
pip install openai pydantic
你定义一个 Pydantic model 来捕捉 question-answer pairs。最终 response 是一个 questions 列表,其中每个 question 实际上都是一个包含 question 和 answer 的 Python object。
下面是定义 prompt instructions 和 Pydantic response template 的代码:
from pydantic import BaseModel
from typing import Optional
from openai import OpenAI
class Question(BaseModel):
question: str
answer: Optional[str] = None
class Questions(BaseModel):
questions: list[Question]
splitter_prompt = """
You are a helpful assistant for a RAG chatbot.
Your job is to break down complex questions into simpler ones that
are easy to answer. When the answers to these simpler questions are
combined, they should fully answer the original question. If the
question is already simple, leave it as it is. Handle one question
at a time.
Example:
1. Query: Did Microsoft or Google make more money last year?
Decomposed Questions:
1. How much profit did Microsoft make last year?
2. How much profit did Google make last year?
"""
接下来,将这些 templates 应用于一个关于 renewable energy 相比 fossil fuels 优势的 sample query。该问题并不算高度复杂,但不清楚用户是从哪个角度询问。
由于 query 比较模糊,人类通常会从多个角度探索它,以提供完整答案。这个 RAG process 也会这样做:将原始 query 拆分为从不同角度切入该主题的 subqueries:
client = OpenAI()
query = (
"What are the benefits of renewable energy compared to "
"fossil fuels?"
)
completion = client.beta.chat.completions.parse(
model="gpt-5-mini",
messages=[
{"role": "system", "content": splitter_prompt},
{"role": "user", "content": query},
],
response_format=Questions,
)
decomposed_questions = completion.choices[0].message.parsed.questions
图 7-19 展示了 decomposition step 的结果。LLM 将原始关于 renewable energy 优势的问题拆分成五个更简单的 subqueries。例如,第一个 subquery 询问 renewable energy 的好处,第二个询问使用 fossil fuels 的好处。其他 subqueries 覆盖 environmental impact、cost 和 availability 等关键方面。
图 7-19:拆解后的 subqueries
RAG 系统的下一步,是为每个 subquery 执行 semantic search 并生成单独答案。当所有 subqueries 都得到回答后,将 subqueries 和对应答案输入最终 reasoning prompt。该 prompt 会指示模型将这些洞察综合成一个 consolidated answer。
Discussion
Query decomposition 会将复杂问题拆解为更简单 subquestions,为每个 subquestion 独立检索 context,并将 sub-answers 综合为最终 response。
当问题天然需要从多个 sources 聚合信息,例如 “Compare X and Y” 需要分别获取 X 数据和 Y 数据;拆解 query 能让每个 subsearch 更精确;可以并行运行 subqueries 以限制延迟影响;或者 intermediate answers 有助于 debug 最终答案为何失败时,可以使用 decomposition。
当问题简单直接,例如 “What is X?”;单次 retrieval call 已经返回足够 context;额外 LLM calls,包括 decomposition 和 final reasoning,使响应时间不可接受;或者 evaluation 显示 single-step retrieval 同样表现良好时,可以跳过 query decomposition。
主要取舍是 complexity 和 latency versus completeness。Decomposition 会增加两次 LLM calls,一次拆解,一次综合,加上多次 retrieval steps。作为交换,你可以回答单次 vector search 无法很好处理的问题,例如 “Which company was more profitable: Microsoft or Google?”
Query decomposition 与 multiquery retrieval 不同:decomposition 创建需要分别回答并组合的不同 subquestions,例如 “How much did Microsoft earn?” + “How much did Google earn?”;multiquery 则创建同一问题的语义相似变体,以提升 recall。当复杂问题的每个 subquestion 也受益于多种表达时,可以组合两者。
See Also
LlamaIndex Query Transform Cookbook 提供 subquestion decomposition 示例和实现模式。
Haystack query decomposition guide 提供带代码示例的实践 walkthrough。
Demonstrate-Search-Predict paper 将 multihop question decomposition 引入 retrieval systems。