LlamaIndex 深度学习笔记
0. 一句话定位
LlamaIndex 的核心价值:把企业内外部数据转换为 LLM 可高效检索、可组合查询、可追踪执行的知识索引层。
如果用传统后端类比:
- 数据库 负责将结构化数据组织为可查询表、索引、执行计划。
- LlamaIndex 负责将文档、网页、PDF、数据库记录、API 返回值等异构数据组织为 Documents / Nodes / Indices / Query Engines,让 LLM 能基于检索结果进行推理和回答。
它不是简单的"向量数据库封装器",而是一个面向 LLM 应用的数据中间层:
text
原始数据源
↓ Data Connectors
Documents
↓ Node Parser / Text Splitter
Nodes
↓ Embedding / Index Struct / Vector Store
Indices
↓ Retriever / Query Engine / Response Synthesizer
最终答案 / 工具调用 / Agent 执行结果
1. LlamaIndex 与传统数据库索引的架构对比
1.1 传统数据库索引解决什么问题
传统数据库索引,例如 B+Tree、Hash Index、Bitmap Index,本质目标是:
- 减少扫描范围;
- 优化谓词过滤;
- 提升精确查询性能;
- 为执行计划提供代价模型依据。
典型 SQL:
sql
SELECT * FROM user_order WHERE user_id = 10001 AND status = 'PAID';
数据库索引适合处理:
- 字段值精确匹配;
- 范围查询;
- 排序;
- Join;
- 聚合。
它的前提是:数据结构化、Schema 明确、查询意图可形式化表达。
1.2 LlamaIndex 解决什么问题
LLM 应用的问题通常不是:
text
user_id = 10001
而是:
text
"总结一下过去三个月用户投诉退款失败的主要原因,并指出是否和支付网关升级有关。"
这个问题具备以下特征:
- 查询意图是自然语言;
- 数据源可能分散在工单、日志、Confluence、数据库、PDF、代码仓库;
- 结果需要综合、归纳、推理;
- 语义相关性比字段等值匹配更重要。
LlamaIndex 的核心思路是:
先把外部数据切成语义单元,再对语义单元建立索引,查询时先召回相关上下文,再由 LLM 做答案合成。
2. LlamaIndex 底层工作流
2.1 总体流程
text
Data Source
↓
Data Connector / Reader
↓
Document
↓
Node Parser / Splitter
↓
Node
↓
Embedding Model
↓
Index / Vector Store / DocStore / IndexStore
↓
Retriever
↓
Query Engine
↓
Response Synthesizer
↓
Answer
2.2 Data Connectors:数据加载层
Data Connectors 负责从外部数据源加载原始内容,并转换为 LlamaIndex 的 Document。
常见数据源:
- 本地文件:Markdown、TXT、PDF、HTML;
- 数据库:MySQL、PostgreSQL、MongoDB;
- SaaS:Notion、Slack、Google Drive;
- Web:网页、RSS、API;
- 代码仓库:GitHub、GitLab。
极简示例:
python
from llama_index.core import SimpleDirectoryReader
# 从本地目录加载文件,返回 Document 列表
documents = SimpleDirectoryReader(input_dir="./data").load_data()
print(type(documents[0]))
print(documents[0].text[:200])
工程理解:
Document类似数据库中的一条原始记录;- 它通常粒度较大,可能是一篇文章、一个 PDF、一份接口文档;
- 后续不会直接把完整
Document塞给 LLM,而是切成更细的Node。
2.3 Documents 与 Nodes:解析与切分层
2.3.1 Document
Document 表示原始文档对象,包含:
- 文本内容;
- 元数据;
- 文档 ID;
- 数据来源信息。
示例:
python
from llama_index.core import Document
document = Document(
text="LlamaIndex 是一个面向 LLM 应用的数据框架。",
metadata={"source": "manual", "category": "rag"},
)
print(document.text)
print(document.metadata)
2.3.2 Node
Node 是 LlamaIndex 更核心的检索单元。
它通常由 Document 切分而来,包含:
- 当前文本片段;
- 所属文档 ID;
- 前后关系;
- metadata;
- embedding;
- hash;
- relationship。
示例:
python
from llama_index.core import Document
from llama_index.core.text_splitter import SentenceSplitter
texts = [
Document(text="LlamaIndex 支持文档加载、文本切分、索引构建、检索增强生成。" * 20)
]
splitter = SentenceSplitter(chunk_size=128, chunk_overlap=20)
nodes = splitter.get_nodes_from_documents(texts)
print(len(nodes))
print(nodes[0].text)
工程理解:
Document是数据加载粒度;Node是检索、Embedding、索引构建的基本粒度;- Chunk 过大影响召回精度;
- Chunk 过小影响上下文完整性和答案合成质量。
2.3.3 Node 关系与类型系统
2.3.3.1 Node 关系 (Relationships)
Node 通过 relationships 字段维护与其他节点的关系,这是 LlamaIndex 实现上下文追溯和文档结构化的核心机制。
关系类型说明:
| 关系类型 | 枚举值 | 含义 | 用途 |
|---|---|---|---|
| SOURCE | 1 | 指向原始文档 | 追溯 Node 来自哪个 Document |
| PREVIOUS | 2 | 指向前一个文本块 | 获取上文,支持上下文扩展 |
| NEXT | 3 | 指向下一个文本块 | 获取下文,支持上下文扩展 |
| PARENT | 4 | 指向父节点 | 支持层级结构(如 TreeIndex) |
| CHILD | 5 | 指向子节点 | 支持层级结构(如 TreeIndex) |
关系结构示例:
json
{
"relationships": {
"1": {
"node_id": "ecd1e5a0-c0db-4cc3-80d3-e2c0a1ea0914",
"node_type": "4",
"metadata": {...},
"hash": "ca91cab2...",
"class_name": "RelatedNodeInfo"
},
"3": {
"node_id": "66a93d09-08fd-4e97-a089-fab13d548c07",
"node_type": "1",
"metadata": {},
"hash": "fc216585...",
"class_name": "RelatedNodeInfo"
}
}
}
关系图解:
NEXT
NEXT
SOURCE
SOURCE
SOURCE
📄 Document (原始文档)
ID: ecd1e5a0-c0db-4cc3-80d3-e2c0a1ea0914
node_type: 4 (DOCUMENT)
✂️ 切分 (SentenceSplitter)
Node 0
node_type: 1 (TEXT)
Node 1
node_type: 1 (TEXT)
Node 2
node_type: 1 (TEXT)
工程价值:
- 追溯来源: 从任何 Node 都能找到它来自哪个原始文档
- 获取完整上下文: 通过 PREVIOUS/NEXT 关系获取前后文
- 文档级别操作: 可以找到同一文档的所有 Node
- 引用溯源: 在回答问题时,可以标注答案来自哪个文档
- 层级结构: 支持 TreeIndex 等层级索引结构
验证代码:
python
import json
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
# 加载文档
documents = SimpleDirectoryReader(input_dir="./data").load_data()
print("原始 Document ID:", documents[0].doc_id)
# 切分为 Node
splitter = SentenceSplitter(chunk_size=128, chunk_overlap=20)
nodes = splitter.get_nodes_from_documents(documents)
print("第一个 Node ID:", nodes[0].node_id)
# 获取 SOURCE 关系
source_relationship = nodes[0].relationships.get("1")
if source_relationship:
print("SOURCE 关系的 node_id:", source_relationship.node_id)
print("SOURCE 关系的 node_type:", source_relationship.node_type)
print("是否等于原始 Document ID:", source_relationship.node_id == documents[0].doc_id)
2.3.3.2 Node 类型 (node_type)
node_type 字段标识节点的类型,用于区分不同的对象类型。
核心类型枚举:
| node_type | 类型名称 | 说明 | 典型用途 |
|---|---|---|---|
| 1 | TEXT | 文本节点 | 最常见的 Node 类型,存储文本片段 |
| 2 | IMAGE | 图像节点 | 存储图像数据和元数据 |
| 3 | INDEX | 索引节点 | 指向另一个索引结构 |
| 4 | DOCUMENT | 文档对象 | 原始文档,是 Node 的来源 |
类型判断示例:
python
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
documents = [Document(text="LlamaIndex 支持多种节点类型。")]
splitter = SentenceSplitter(chunk_size=128)
nodes = splitter.get_nodes_from_documents(documents)
# 查看 Node 类型
print("Node 类型:", nodes[0].node_type) # 输出: ObjectType.TEXT
# 查看 SOURCE 关系指向的类型
source_rel = nodes[0].relationships.get("1")
if source_rel:
print("SOURCE 类型:", source_rel.node_type) # 输出: ObjectType.DOCUMENT
工程理解:
- 类型安全 : 通过
node_type可以在运行时判断节点类型,避免类型错误 - 多模态支持: 支持文本、图像等多种数据类型
- 关系验证: 在建立关系时可以验证类型是否匹配
- 索引组合: 支持索引嵌套和组合查询
完整 Node 结构示例:
json
{
"id_": "ea90bd8c-1a0a-4fd2-843f-1fc4ca85724a",
"embedding": null,
"metadata": {
"file_path": "E:\\data\\document.txt",
"file_name": "document.txt",
"file_type": "text/plain",
"file_size": 7915,
"creation_date": "2026-04-25",
"last_modified_date": "2026-04-25"
},
"excluded_embed_metadata_keys": ["file_name", "file_type", "file_size"],
"excluded_llm_metadata_keys": ["file_name", "file_type", "file_size"],
"relationships": {
"1": {
"node_id": "af49ea29-59dd-4979-ad96-b443bf83569a",
"node_type": "4",
"metadata": {...},
"hash": "ca91cab2...",
"class_name": "RelatedNodeInfo"
},
"3": {
"node_id": "66a93d09-08fd-4e97-a089-fab13d548c07",
"node_type": "1",
"metadata": {},
"hash": "fc2165851...",
"class_name": "RelatedNodeInfo"
}
},
"text": "智研咖啡馆合作伙伴信息\n\n一、咖啡豆供应商\n\n1.",
"start_char_idx": 0,
"end_char_idx": 25,
"class_name": "TextNode"
}
字段说明:
id_: Node 的唯一标识符embedding: 向量嵌入(生成后为浮点数组)metadata: 元数据信息relationships: 与其他节点的关系text: 实际文本内容start_char_idx/end_char_idx: 在原文档中的位置class_name: 类名,通常为 "TextNode"
生产建议:
- 充分利用关系: 在需要上下文时,通过 PREVIOUS/NEXT 关系获取前后文
- 追溯来源: 在回答时通过 SOURCE 关系标注答案来源
- 类型检查 : 在处理多模态数据时,先检查
node_type再处理 - 元数据设计 : 合理设计
metadata,支持业务过滤和权限控制
3. 安装与最小可运行环境
3.1 推荐安装
bash
pip install llama-index llama-index-llms-openai llama-index-embeddings-openai
如使用本地向量库:
bash
pip install llama-index-vector-stores-chroma chromadb
3.2 环境变量
bash
export OPENAI_API_KEY="你的 API Key"
Windows PowerShell:
powershell
$env:OPENAI_API_KEY="你的 API Key"
3.3 全局 Settings
LlamaIndex 新版本推荐通过 Settings 配置全局 LLM 与 Embedding。
python
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
4. 核心索引结构解析
LlamaIndex 的索引不是单一结构,而是一组围绕不同查询模式设计的数据结构。
核心索引包括:
- VectorStoreIndex:语义向量检索;
- SummaryIndex:顺序扫描与摘要聚合;
- TreeIndex:树状摘要与层级归纳;
- KeywordTableIndex:关键词倒排;
- KnowledgeGraphIndex / PropertyGraphIndex:实体关系图谱。
本文重点分析前三类。
5. VectorStoreIndex:向量索引
5.1 核心价值
VectorStoreIndex 是生产 RAG 中最常用的索引结构,用于语义相似度检索。
它适合回答:
- "与问题最相关的文档片段是什么?"
- "这段报错和哪个知识库条目最相似?"
- "用户问题应该召回哪些上下文?"
5.2 底层结构
构建时:
text
Node.text
↓ Embedding Model
Dense Vector
↓ Vector Store
向量索引结构(HNSW / IVF / Flat / DiskANN 等,取决于后端)
查询时:
text
Query Text
↓ Query Embedding
Query Vector
↓ Top-K 相似度搜索
Relevant Nodes
↓ Response Synthesizer
Answer
5.3 与数据库索引类比
| 维度 | 数据库 B+Tree | VectorStoreIndex |
|---|---|---|
| 查询方式 | 精确匹配 / 范围查询 | 语义相似度查询 |
| Key | 字段值 | Embedding 向量 |
| 距离度量 | 比较大小 | Cosine / Dot Product / L2 |
| 返回结果 | 符合谓词的行 | Top-K 相似节点 |
| 适合场景 | 订单查询、用户查询 | FAQ、知识库、文档问答 |
5.4 可运行代码:最小 RAG
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [
Document(text="LlamaIndex 的 VectorStoreIndex 适合语义检索和 RAG。"),
Document(text="SummaryIndex 适合需要遍历全文并做整体摘要的任务。"),
Document(text="TreeIndex 通过树状摘要结构支持层级归纳。"),
]
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=2)
response = query_engine.query("哪种索引适合 RAG 语义检索?")
print(response)
5.5 适用业务场景
适合:
- 企业知识库问答;
- 技术文档检索;
- FAQ 匹配;
- 工单相似问题推荐;
- 代码语义搜索;
- RAG 问答主链路。
不适合:
- 精确数值计算;
- 全表聚合统计;
- 必须 100% 精确匹配的审计查询;
- 复杂 Join 查询。
6. SummaryIndex:摘要 / 顺序扫描索引
6.1 核心价值
SummaryIndex 更接近"全表扫描 + 汇总归纳",适合对所有节点进行遍历后综合回答。
它不是为了 Top-K 精准召回,而是为了:
- 对完整文档做摘要;
- 汇总所有输入节点;
- 在数据量不大时保证覆盖率;
- 处理"全局性问题"。
6.2 底层结构
SummaryIndex 的底层结构可以理解为一个顺序列表:
text
IndexStruct
├── Node 1
├── Node 2
├── Node 3
└── Node N
查询时通常会遍历多个或全部节点,再交给 LLM 合成答案。
6.3 类比数据库
类似:
sql
SELECT SUM(amount) FROM orders;
如果没有合适索引,数据库需要扫描大量行。
SummaryIndex 也类似:牺牲检索速度,换取覆盖完整性。
6.4 可运行代码:全文摘要
python
from llama_index.core import Document, Settings, SummaryIndex
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
documents = [
Document(text="第一季度系统主要问题是登录超时,原因包括缓存击穿和数据库连接池耗尽。"),
Document(text="第二季度系统主要问题是支付失败,原因包括支付网关升级和签名算法不兼容。"),
Document(text="第三季度系统主要问题是消息积压,原因包括消费者扩容不足和重试策略不合理。"),
]
index = SummaryIndex.from_documents(documents)
query_engine = index.as_query_engine(response_mode="tree_summarize")
response = query_engine.query("总结三个季度系统问题的共同规律。")
print(response)
6.5 适用业务场景
适合:
- 文档摘要;
- 会议纪要总结;
- 多段材料归纳;
- 小规模数据全集分析;
- "请总结全部内容"类问题。
不适合:
- 大规模知识库实时问答;
- 低延迟 Top-K 检索;
- 高并发 RAG 主链路。
7. TreeIndex:树状层级索引
7.1 核心价值
TreeIndex 通过层级摘要组织节点,适合层级归纳、从粗到细的查询与长文档总结。
它的思想类似:
text
Leaf Nodes 原文片段
↓ summarize
Parent Summary Nodes
↓ summarize
Root Summary Node
7.2 底层结构
text
Root Summary
/ \
Summary A Summary B
/ \ / \
Node 1 Node 2 Node 3 Node 4
构建 TreeIndex 时,LlamaIndex 会对底层节点逐层摘要,形成树状结构。
7.3 查询方式
TreeIndex 查询通常有两种思路:
- 从根节点向下选择相关分支;
- 从叶子节点向上合成摘要。
它适合处理:
- 长文档全局理解;
- 多章节报告摘要;
- 层级结构明显的知识库;
- 需要先粗筛再细读的问题。
7.4 可运行代码:树状摘要
python
from llama_index.core import Document, Settings, TreeIndex
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
documents = [
Document(text="第一章:RAG 的核心是检索增强生成,关键在于召回质量。"),
Document(text="第二章:向量数据库通过 embedding 相似度完成语义检索。"),
Document(text="第三章:查询引擎负责把检索结果合成为最终答案。"),
Document(text="第四章:生产系统需要关注延迟、成本、权限和可观测性。"),
]
index = TreeIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("这份材料主要讲了什么?")
print(response)
7.4.1 TreeIndex 性能与成本警告 ⚠️
重要: TreeIndex 构建极其耗时,不推荐用于大文档或生产环境。
性能问题分析
TreeIndex 的构建过程需要为每个 Node 调用 LLM 生成摘要:
text
原始文档 (10,000 字)
↓ 切分 (chunk_size=512)
约 20 个 Node
↓ 第一层: 为每个 Node 调用 LLM 生成摘要 (20 次 LLM 调用)
约 5 个摘要 Node
↓ 第二层: 为每个摘要 Node 再调用 LLM (5 次 LLM 调用)
1 个根摘要 Node
↓ 总计: 25+ 次 LLM 调用
实际案例:
python
# 6 个文档,每个约 5000 字
documents = SimpleDirectoryReader(input_dir="./data").load_data()
# 假设每个文档切分成 50 个 Node
# 总共 300 个 Node
# 需要调用 LLM 300+ 次!
index = TreeIndex.from_documents(documents)
# ⚠️ 这一行可能需要 10-30 分钟!
# ⚠️ 成本: 300 次 LLM 调用,约 $0.5-$2 (取决于模型)
时间成本估算:
| 文档数量 | 总字数 | Node 数量 | LLM 调用次数 | 预计耗时 | 预计成本 (GPT-4o-mini) |
|---|---|---|---|---|---|
| 1 个 | 1,000 字 | 5 个 | 5-10 次 | 30 秒 | $0.01 |
| 1 个 | 5,000 字 | 25 个 | 25-50 次 | 2-5 分钟 | $0.05 |
| 6 个 | 30,000 字 | 300 个 | 300-500 次 | 10-30 分钟 | 0.5-2 |
| 50 个 | 250,000 字 | 2,500 个 | 2,500-4,000 次 | 2-5 小时 | 5-20 |
工程建议:
- 仅用于小规模文档 (< 5,000 字)
- 不要用于实时查询场景
- 构建后持久化索引,避免重复构建
- 优先考虑 VectorStoreIndex 或 SummaryIndex
减少构建时间的方法
方法1: 限制文档长度
python
# 只取文档前 1000 字符
original_docs = SimpleDirectoryReader(input_dir="./data").load_data()
documents = [
Document(text=doc.text[:1000], metadata=doc.metadata)
for doc in original_docs
]
index = TreeIndex.from_documents(documents)
方法2: 只加载部分文件
python
# 只加载一个文件进行测试
documents = SimpleDirectoryReader(
input_files=["./data/sample.txt"]
).load_data()
index = TreeIndex.from_documents(documents)
方法3: 持久化索引,避免重复构建
python
from llama_index.core import StorageContext, load_index_from_storage
# 第一次构建
index = TreeIndex.from_documents(documents)
index.storage_context.persist(persist_dir="./tree_index_storage")
# 后续直接加载
storage_context = StorageContext.from_defaults(persist_dir="./tree_index_storage")
index = load_index_from_storage(storage_context)
7.4.2 TreeIndex 的两种查询模式
TreeIndex 支持两种查询模式,触发条件和所需配置不同。
模式1: 树状遍历模式 (默认,推荐)
触发条件: 不传 similarity_top_k 参数
工作原理:
text
Query
↓
从根节点开始
↓
根据 LLM 判断选择相关分支
↓
递归向下遍历
↓
返回叶子节点
代码示例:
python
from llama_index.core import TreeIndex, Settings
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
# 不需要配置 embed_model
index = TreeIndex.from_documents(documents)
# 方式1: 使用 query_engine (推荐)
query_engine = index.as_query_engine() # 不传 similarity_top_k
response = query_engine.query("这份材料主要讲了什么?")
# 方式2: 使用 retriever
retriever = index.as_retriever() # 不传 similarity_top_k
nodes = retriever.retrieve("这份材料主要讲了什么?")
所需配置:
- ✅ 必须:
Settings.llm(用于判断分支选择) - ❌ 不需要:
Settings.embed_model
优点:
- 不需要向量化
- 遵循 TreeIndex 的设计理念
- 适合层级归纳和摘要
模式2: 向量相似度模式 (不推荐)
触发条件: 传入 similarity_top_k 参数
工作原理:
text
Query
↓ Embedding
Query Vector
↓ 向量相似度计算
Top-K 相似节点
↓
返回结果
代码示例:
python
from llama_index.core import TreeIndex, Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small") # 必须配置!
index = TreeIndex.from_documents(documents)
# 使用向量相似度检索
query_engine = index.as_query_engine(similarity_top_k=3) # 需要 embed_model
response = query_engine.query("这份材料主要讲了什么?")
retriever = index.as_retriever(similarity_top_k=3) # 需要 embed_model
nodes = retriever.retrieve("这份材料主要讲了什么?")
所需配置:
- ✅ 必须:
Settings.llm - ✅ 必须:
Settings.embed_model(用于向量化)
缺点:
- 违背 TreeIndex 的设计理念
- 需要额外配置 embedding 模型
- 不如直接使用 VectorStoreIndex
两种模式对比
| 维度 | 树状遍历模式 | 向量相似度模式 |
|---|---|---|
| 触发条件 | 不传 similarity_top_k |
传入 similarity_top_k |
| 需要 LLM | ✅ 是 | ✅ 是 |
| 需要 Embedding | ❌ 否 | ✅ 是 |
| 查询方式 | 层级遍历 | 向量相似度 |
| 符合设计理念 | ✅ 是 | ❌ 否 |
| 推荐使用 | ✅ 推荐 | ❌ 不推荐 |
工程建议:
- TreeIndex 应该使用树状遍历模式 (不传
similarity_top_k) - 如果需要向量检索,直接使用 VectorStoreIndex
- 不要混用 TreeIndex 和向量相似度检索
常见错误
错误1: 使用 TreeIndex 做向量检索
python
# ❌ 错误: TreeIndex 不适合向量检索
index = TreeIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=3) # 不推荐!
正确做法:
python
# ✅ 正确: 向量检索用 VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=3)
错误2: 忘记配置 embed_model
python
# ❌ 错误: 使用 similarity_top_k 但没有配置 embed_model
Settings.llm = OpenAI(model="gpt-4o-mini")
# Settings.embed_model = ... # 忘记配置!
index = TreeIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=3) # 会报错!
正确做法:
python
# ✅ 正确: 配置 embed_model
Settings.llm = OpenAI(model="gpt-4o-mini")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
index = TreeIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=3)
错误3: Document.text 是只读属性
python
# ❌ 错误: 不能直接修改 Document.text
documents = SimpleDirectoryReader(input_dir="./data").load_data()
documents[0].text = documents[0].text[:1000] # AttributeError!
正确做法:
python
# ✅ 正确: 创建新的 Document 对象
original_docs = SimpleDirectoryReader(input_dir="./data").load_data()
documents = [
Document(text=doc.text[:1000], metadata=doc.metadata)
for doc in original_docs
]
7.5 适用业务场景
适合:
- 长文档总结;
- 章节型报告;
- 法务合同归纳;
- 技术白皮书摘要;
- 研究论文综述。
不适合:
- 实时低延迟问答;
- 需要精确引用某个片段的场景;
- 超大规模动态知识库频繁更新。
8. 三类核心索引对比
| 索引 | 底层结构 | 查询模式 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|---|
| VectorStoreIndex | 向量空间 Top-K | 语义相似度 | 快、适合大规模 RAG | 可能漏召回 | 知识库问答 |
| SummaryIndex | 节点顺序列表 | 全量扫描汇总 | 覆盖完整 | 慢、成本高 | 全文摘要 |
| TreeIndex | 摘要树 | 层级归纳 | 适合长文档 | 构建成本高 | 报告总结 |
工程建议:
- 默认 RAG 主链路优先使用 VectorStoreIndex。
- 需要全文覆盖时使用 SummaryIndex。
- 长文档、多章节归纳使用 TreeIndex。
- 复杂业务可以组合多个索引,而不是强行用一种索引解决所有问题。
9. Retriever:检索器抽象
9.1 Retriever 的位置
Retriever 是 Index 和 Query Engine 之间的桥梁。
text
Index
↓ as_retriever()
Retriever
↓ retrieve(query)
Nodes
它只负责召回,不负责最终回答。
9.2 可运行代码:只检索不回答
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [
Document(text="Redis 适合缓存热点数据。"),
Document(text="Kafka 适合构建高吞吐消息系统。"),
Document(text="MySQL 适合事务型关系数据存储。"),
]
index = VectorStoreIndex.from_documents(documents)
retriever = index.as_retriever(similarity_top_k=2)
nodes = retriever.retrieve("如何处理高吞吐异步消息?")
for node in nodes:
print("score=", node.score)
print(node.text)
9.3 工程意义
拆出 Retriever 后可以实现:
- 自定义排序;
- 多路召回;
- 混合检索;
- 权限过滤;
- 召回结果调试;
- 召回结果缓存。
10. Query Engine:查询引擎
10.1 Query Engine 的职责
QueryEngine 负责完整查询链路:
text
用户问题
↓
Retriever 召回 Nodes
↓
Response Synthesizer 合成答案
↓
返回 Response
最小示例:
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [Document(text="Spring Boot 默认使用内嵌 Tomcat 作为 Web 容器。")]
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("Spring Boot 默认 Web 容器是什么?")
print(response)
10.2 Response Synthesizer
ResponseSynthesizer 决定如何把多个 Node 合成为答案。
常见模式:
compact:压缩上下文后回答;refine:逐段迭代修正答案;tree_summarize:树状摘要;simple_summarize:简单总结。
示例:
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [
Document(text="A 服务负责订单创建。"),
Document(text="B 服务负责库存扣减。"),
Document(text="C 服务负责支付确认。"),
]
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(response_mode="refine", similarity_top_k=3)
response = query_engine.query("订单链路包含哪些服务?")
print(response)
10.3 Response Synthesizer 深度解析
10.3.1 response_mode 的本质
核心问题 : response_mode 优化的是什么?
答案 : ✅ 优化的是 AI 回答的生成过程,而不是检索结果!
工作流程:
text
检索阶段 (Retriever)
↓
召回 Top-K Nodes (例如 3 个)
↓
Response Synthesizer (response_mode 在这里起作用!)
↓
如何把这 3 个 Nodes 合成为最终答案?
↓
最终回答
实现方式 : ✅ 通过额外调用 LLM 进行优化,不是内置算法!
10.3.2 四种 response_mode 的底层机制
模式 1: compact (压缩上下文)
工作原理:
text
Node 1: 文本 A (500 tokens)
Node 2: 文本 B (600 tokens)
Node 3: 文本 C (400 tokens)
↓ 合并压缩
Context: 文本 A + 文本 B + 文本 C (1500 tokens)
↓ 一次 LLM 调用
Prompt:
"""
基于以下上下文回答问题:
{Context}
问题: {Query}
"""
↓ LLM 生成
答案
特点:
- LLM 调用次数: 1 次
- Token 消耗: 最低
- 延迟: 最低
- 质量: 中等
- 适用场景: 实时问答、高并发、成本敏感
模式 2: refine (逐段迭代修正) ⭐
工作原理:
text
第 1 轮 LLM 调用:
Prompt:
"""
基于以下上下文回答问题:
Node 1: 文本 A
问题: {Query}
"""
↓
初步答案 Answer_1
第 2 轮 LLM 调用:
Prompt:
"""
原始问题: {Query}
已有答案: {Answer_1}
新上下文: Node 2: 文本 B
请根据新上下文优化答案,如果新上下文无关则保持原答案。
"""
↓
优化答案 Answer_2
第 3 轮 LLM 调用:
Prompt:
"""
原始问题: {Query}
已有答案: {Answer_2}
新上下文: Node 3: 文本 C
请根据新上下文优化答案,如果新上下文无关则保持原答案。
"""
↓
最终答案 Answer_3
特点:
- LLM 调用次数: N 次 (N = 召回的 Nodes 数量)
- Token 消耗: 最高
- 延迟: 最高
- 质量: 最高
- 适用场景: 对答案质量要求极高、成本不敏感
优势:
- ✅ 逐步精炼,质量高
- ✅ 可以处理更多上下文(不受单次窗口限制)
- ✅ 每个 Node 都能充分影响答案
劣势:
- ❌ 慢,成本高
- ❌ 需要多次 LLM 调用
- ❌ 串行执行,无法并行
模式 3: tree_summarize (树状摘要)
工作原理:
text
Node 1 + Node 2 → LLM 调用 1 → 摘要 A
Node 3 + Node 4 → LLM 调用 2 → 摘要 B
摘要 A + 摘要 B → LLM 调用 3 → 最终答案
特点:
- LLM 调用次数: log(N) 次 (树的层数)
- Token 消耗: 中等
- 延迟: 中等
- 质量: 高
- 适用场景: 召回 Nodes 非常多(10+ 个)、需要处理超长上下文
优势:
- ✅ 可以处理大量 Nodes
- ✅ 并行化潜力(同层可并行)
- ✅ 避免单次上下文过长
劣势:
- ❌ 复杂度高
- ❌ 可能丢失细节
- ❌ 中间摘要可能引入偏差
模式 4: simple_summarize (简单总结)
工作原理:
text
Node 1 + Node 2 + Node 3 → 一次 LLM 调用 → 答案
特点:
- LLM 调用次数: 1 次
- Token 消耗: 低
- 延迟: 低
- 质量: 中等
- 适用场景: 类似
compact,但 Prompt 更侧重总结
10.3.3 实际例子对比
假设召回了 3 个 Nodes:
- Node 1: "A 服务负责订单创建。"
- Node 2: "B 服务负责库存扣减。"
- Node 3: "C 服务负责支付确认。"
问题: "订单链路包含哪些服务?"
使用 compact 模式:
text
LLM 调用 1 次:
Prompt:
"""
基于以下上下文回答问题:
- A 服务负责订单创建。
- B 服务负责库存扣减。
- C 服务负责支付确认。
问题: 订单链路包含哪些服务?
"""
答案: "订单链路包含 A、B、C 三个服务,分别负责订单创建、库存扣减和支付确认。"
使用 refine 模式:
text
LLM 调用 1:
Prompt: "基于 'A 服务负责订单创建' 回答问题"
答案 1: "订单链路包含 A 服务,负责订单创建。"
LLM 调用 2:
Prompt: "已有答案: '订单链路包含 A 服务...'
新上下文: 'B 服务负责库存扣减'
请优化答案"
答案 2: "订单链路包含 A 服务(订单创建)和 B 服务(库存扣减)。"
LLM 调用 3:
Prompt: "已有答案: '订单链路包含 A、B 服务...'
新上下文: 'C 服务负责支付确认'
请优化答案"
答案 3: "订单链路包含 A 服务(订单创建)、B 服务(库存扣减)、C 服务(支付确认)。"
对比:
compact: 1 次调用,快速但可能遗漏细节refine: 3 次调用,逐步完善,质量更高
10.3.4 成本与性能对比
假设每次 LLM 调用消耗 1000 tokens:
| response_mode | LLM 调用次数 | Token 消耗 | 延迟 | 质量 | 推荐场景 |
|---|---|---|---|---|---|
compact |
1 次 | ~1000 | 最低 | 中等 | 实时问答、高并发 |
simple_summarize |
1 次 | ~1000 | 最低 | 中等 | 快速总结 |
refine |
N 次 | ~N×1000 | 高 | 最高 | 高质量要求 |
tree_summarize |
log(N) 次 | ~log(N)×1000 | 中等 | 高 | 大量 Nodes |
实际成本示例 (召回 10 个 Nodes):
compact: 1 次调用refine: 10 次调用tree_summarize: 4 次调用
10.3.5 工程选型建议
什么时候用 compact?
- ✅ 实时问答场景
- ✅ 高并发场景
- ✅ 成本敏感
- ✅ 召回的 Nodes 较少(2-3 个)
- ✅ 对答案质量要求不极致
什么时候用 refine?
- ✅ 对答案质量要求极高
- ✅ 召回的 Nodes 较多(5-10 个)
- ✅ 成本不敏感
- ✅ 延迟不敏感
- ✅ 需要充分利用每个 Node 的信息
什么时候用 tree_summarize?
- ✅ 召回的 Nodes 非常多(10+ 个)
- ✅ 需要处理超长上下文
- ✅ 可以接受中等成本
- ✅ 需要全局理解
什么时候用 simple_summarize?
- ✅ 需要快速总结
- ✅ 对格式要求不高
- ✅ 成本敏感
11. 高级检索策略一:Router Query Engine
11.1 核心思想
Router Query Engine 根据用户问题自动选择最合适的 Query Engine。
典型场景:
- 有些问题适合向量检索;
- 有些问题适合全文摘要;
- 有些问题适合 SQL 查询;
- 有些问题适合工具调用。
Router 类似后端中的策略路由:
text
Query
↓ LLM Selector
选择 QueryEngineTool
↓
目标 Query Engine
↓
Answer
11.2 业务类比
类似 Spring 中根据请求类型路由到不同 Handler:
text
/order/detail → OrderDetailHandler
/order/statistics → OrderStatisticsHandler
/document/search → DocumentSearchHandler
LlamaIndex Router 是语义级路由:
text
"总结全文" → SummaryIndex Query Engine
"查找某个概念" → VectorStoreIndex Query Engine
11.3 可运行代码:向量检索 vs 全文总结自动路由
python
from llama_index.core import Document, Settings, SummaryIndex, VectorStoreIndex
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import LLMSingleSelector
from llama_index.core.tools import QueryEngineTool
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [
Document(text="VectorStoreIndex 适合基于 embedding 的语义检索。"),
Document(text="SummaryIndex 适合对所有节点进行遍历和整体摘要。"),
Document(text="TreeIndex 适合长文档的层级摘要和归纳。"),
]
vector_index = VectorStoreIndex.from_documents(documents)
summary_index = SummaryIndex.from_documents(documents)
vector_engine = vector_index.as_query_engine(similarity_top_k=2)
summary_engine = summary_index.as_query_engine(response_mode="tree_summarize")
query_engine = RouterQueryEngine(
selector=LLMSingleSelector.from_defaults(),
query_engine_tools=[
QueryEngineTool.from_defaults(
query_engine=vector_engine,
description="适合回答具体概念、局部事实、精准语义检索问题。",
),
QueryEngineTool.from_defaults(
query_engine=summary_engine,
description="适合回答全文总结、整体归纳、全局分析问题。",
),
],
)
response = query_engine.query("请整体总结这些索引结构的区别。")
print(response)
11.4 适用场景
适合:
- 多知识库问答;
- 多索引组合;
- 文档问答 + SQL 查询混合;
- 根据问题类型自动选择工具;
- Agent 前置工具路由。
风险:
- 路由依赖 LLM 判断,可能选错;
- 工具描述质量直接影响路由准确率;
- 多工具场景下调试复杂度上升。
调优建议:
- 工具描述要互斥、清晰、短;
- 对高频问题建立规则路由兜底;
- 记录 selector 的选择结果;
- 将 Router 决策过程纳入日志和可观测性。
12. 高级检索策略二:Sub-Question Query Engine
12.1 核心思想
Sub-Question Query Engine 会把复杂问题拆成多个子问题,分别查询不同工具,再综合答案。
与 RouterQueryEngine 的关系
SubQuestionQueryEngine = 问题拆分 + 路由查询 + 答案综合
| 维度 | RouterQueryEngine | SubQuestionQueryEngine |
|---|---|---|
| 问题处理 | 单个问题 → 路由到一个工具 | 复杂问题 → 拆分成多个子问题 |
| 工具调用 | 选择一个最合适的工具 | 可能调用多个工具 |
| 执行方式 | 单次路由决策 | 多次查询 + 结果综合 |
| 适用场景 | 问题明确、单一数据源 | 问题复杂、需要多源对比 |
技术实现关系:
python
# SubQuestionQueryEngine 内部流程(伪代码)
class SubQuestionQueryEngine:
def query(self, complex_question):
# 步骤1: 拆分问题
sub_questions = question_generator.generate(complex_question)
# 步骤2: 每个子问题进行路由(类似 Router)
answers = []
for sub_q in sub_questions:
tool = selector.select(sub_q, tools) # 路由逻辑
answer = tool.query(sub_q)
answers.append(answer)
# 步骤3: 综合答案
return synthesizer.synthesize(answers)
适合问题示例
Router 适合:
text
"VectorStoreIndex 的底层结构是什么?"
→ 直接路由到 vector_index_tool
SubQuestion 适合:
text
"比较 VectorStoreIndex 和 SummaryIndex 的底层结构、查询性能和适用场景。"
它会拆成:
text
1. VectorStoreIndex 的底层结构是什么? → vector_index_tool
2. SummaryIndex 的底层结构是什么? → summary_index_tool
3. 二者性能差异是什么? → 可能两个工具都调用
4. 二者分别适合什么场景? → 综合两个工具的结果
12.2 执行流程
text
Complex Query
↓ Question Generator
Sub Questions
↓ QueryEngineTool 并行或串行查询
Intermediate Answers
↓ Response Synthesizer
Final Answer
12.3 可运行代码:复杂问题拆解
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
vector_docs = [
Document(text="VectorStoreIndex 使用 embedding 向量表示 Node,并通过相似度 Top-K 检索相关文本。"),
Document(text="VectorStoreIndex 适合知识库问答、FAQ、语义搜索和 RAG 主链路。"),
]
summary_docs = [
Document(text="SummaryIndex 将节点组织为列表,查询时适合遍历全部节点并进行整体摘要。"),
Document(text="SummaryIndex 适合全文总结、会议纪要、全局归纳,但大规模场景成本较高。"),
]
vector_engine = VectorStoreIndex.from_documents(vector_docs).as_query_engine()
summary_engine = VectorStoreIndex.from_documents(summary_docs).as_query_engine()
tools = [
QueryEngineTool(
query_engine=vector_engine,
metadata=ToolMetadata(
name="vector_index_tool",
description="回答 VectorStoreIndex 的结构、性能和适用场景。",
),
),
QueryEngineTool(
query_engine=summary_engine,
metadata=ToolMetadata(
name="summary_index_tool",
description="回答 SummaryIndex 的结构、性能和适用场景。",
),
),
]
query_engine = SubQuestionQueryEngine.from_defaults(query_engine_tools=tools)
response = query_engine.query("比较 VectorStoreIndex 和 SummaryIndex 的底层结构、性能和业务适用场景。")
print(response)
12.4 适用场景
适合:
- 多源复杂问答;
- 对比分析;
- 财报多维分析;
- 多文档交叉验证;
- 技术方案选型。
不适合:
- 简单 FAQ;
- 极低延迟场景;
- 子问题拆解错误成本高的场景。
调优建议:
- 限制子问题数量;
- 明确工具边界;
- 缓存子问题结果;
- 对关键问题增加人工规则拆解;
- 为每个子问题记录 trace。
13. 高级检索策略三:Metadata Filter
13.1 为什么需要 Metadata Filter
纯向量检索只看语义相似度,可能召回权限不符、时间不符、业务域不符的内容。
Metadata Filter 的作用类似于 SQL 中的 WHERE 子句,在向量检索前先进行结构化筛选。
例如:
text
查询:"退款失败原因"
约束:只查 2025 年支付系统文档
这不是 embedding 能稳定解决的问题,应该使用 metadata filter。
工作原理示例
python
# 1. 构建带 metadata 的文档
documents = [
Document(text="支付网关升级导致签名验证失败。", metadata={"system": "payment", "year": "2025"}),
Document(text="登录系统缓存击穿导致接口超时。", metadata={"system": "auth", "year": "2025"}),
Document(text="库存系统锁竞争导致扣减失败。", metadata={"system": "stock", "year": "2024"}),
]
# 2. 定义过滤条件(类似 SQL WHERE system = 'payment')
filters = MetadataFilters(
filters=[ExactMatchFilter(key="system", value="payment")]
)
# 3. 查询时应用过滤器
# 执行流程: 先过滤 metadata → 再进行向量检索 → 返回结果
# 相当于: SELECT * FROM docs WHERE system = 'payment' AND vector_similarity(query, text) > threshold
query_engine = index.as_query_engine(filters=filters, similarity_top_k=2)
执行顺序:
text
原始文档集合
↓ Metadata Filter (WHERE system = 'payment')
符合条件的文档子集
↓ Vector Similarity Search (语义检索)
Top-K 最相关结果
13.2 可运行代码:按 metadata 过滤
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.vector_stores import ExactMatchFilter, MetadataFilters
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [
Document(text="支付网关升级导致签名验证失败。", metadata={"system": "payment", "year": "2025"}),
Document(text="登录系统缓存击穿导致接口超时。", metadata={"system": "auth", "year": "2025"}),
Document(text="库存系统锁竞争导致扣减失败。", metadata={"system": "stock", "year": "2024"}),
]
index = VectorStoreIndex.from_documents(documents)
filters = MetadataFilters(
filters=[ExactMatchFilter(key="system", value="payment")]
)
query_engine = index.as_query_engine(filters=filters, similarity_top_k=2)
response = query_engine.query("失败原因是什么?")
print(response)
13.3 工程建议
元数据设计应该像数据库字段设计一样认真。
推荐 metadata:
tenant_id:租户隔离;user_id:用户隔离;department:部门;system:业务系统;source:来源;created_at:创建时间;doc_type:文档类型;permission_level:权限级别。
14. 高级检索策略四:持久化与向量数据库
14.1 本地持久化
默认情况下,索引可能在内存中。生产环境必须考虑持久化。
python
from llama_index.core import Document, Settings, StorageContext, VectorStoreIndex, load_index_from_storage
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = [Document(text="LlamaIndex 支持索引持久化。")]
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir="./storage")
storage_context = StorageContext.from_defaults(persist_dir="./storage")
loaded_index = load_index_from_storage(storage_context)
query_engine = loaded_index.as_query_engine()
print(query_engine.query("LlamaIndex 支持什么?"))
14.2 Chroma 向量库存储
python
import chromadb
from llama_index.core import Document, Settings, StorageContext, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
# 1. Chroma 层: 创建数据库连接和集合
chroma_client = chromadb.PersistentClient(path="./chroma_db") # 类似 MySQL 连接
chroma_collection = chroma_client.get_or_create_collection("llamaindex_demo") # 类似创建表
# 2. 适配器层: 将 Chroma 适配到 LlamaIndex
vector_store = ChromaVectorStore(chroma_collection=chroma_collection) # 类似 JDBC 驱动
# 3. LlamaIndex 层: 管理存储上下文
storage_context = StorageContext.from_defaults(vector_store=vector_store) # 类似 Spring DataSource
documents = [
Document(text="Chroma 可以作为 LlamaIndex 的向量存储后端。"),
Document(text="生产环境中向量库需要关注索引构建、召回速度和持久化。"),
]
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
)
query_engine = index.as_query_engine(similarity_top_k=1)
response = query_engine.query("Chroma 在这里的作用是什么?")
print(response)
补充:
| 组件 | 层级 | 职责 | 类比 |
|---|---|---|---|
| chroma_client | Chroma 层 | 数据库连接,持久化 | MySQL 连接 |
| chroma_collection | Chroma 层 | 存储向量,提供检索 | MySQL 表 |
| ChromaVectorStore | 适配器层 | 适配 Chroma 到 LlamaIndex | JDBC 驱动 |
| StorageContext | LlamaIndex 层 | 管理所有存储组件 | Spring DataSource |
| LlamaIndex 组件 | MySQL/MyBatis 对应物 | 职责 |
|---|---|---|
| Chroma 数据库 | MySQL 数据库 | 实际存储数据 |
| chroma_collection | MySQL 表 (table) | 存储向量数据 |
| VectorStoreIndex | MySQL 索引 (B+Tree/IVF) | 加速查询的数据结构 |
| ChromaVectorStore | JDBC Driver | 数据库驱动 |
| StorageContext | DataSource | 数据源管理 |
| QueryEngine | MyBatis Mapper | 提供查询接口 |
| Retriever | MyBatis Executor | 执行检索逻辑 |
14.3 生产选型建议
| 向量库 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Chroma | 简单、本地友好 | 大规模能力有限 | PoC、小型项目 |
| Milvus | 高性能、分布式 | 运维复杂 | 大规模向量检索 |
| Qdrant | Rust 实现、过滤能力强 | 生态相对小 | 中大型 RAG |
| Weaviate | GraphQL、模块化 | 部署复杂 | 知识平台 |
| Elasticsearch | 混合检索强 | 向量能力依版本而定 | 传统搜索融合 |
| pgvector | 直接复用 PostgreSQL | 超大规模性能受限 | 中小数据、低运维 |
15. Chunk 设计:RAG 性能的第一关键参数
15.1 Chunk Size 的影响
chunk_size 决定 Node 文本大小。
| Chunk 大小 | 优点 | 缺点 |
|---|---|---|
| 小 Chunk | 召回精准、噪声少 | 语义上下文不足 |
| 大 Chunk | 上下文完整 | 召回不精准、成本高 |
| 中等 Chunk | 平衡精度和上下文 | 需要按业务调参 |
15.2 推荐起点
经验值:
- FAQ:
128 ~ 256 tokens; - 技术文档:
512 ~ 1024 tokens; - 法务合同:
512 ~ 1500 tokens; - 代码文档:按函数 / 类 / Markdown 标题切分;
- 表格数据:按行组或业务实体切分。
15.3 常见文本切割方式
15.3.1 按字符长度切分 (TokenTextSplitter)
适用场景: 通用文本、不关心语义边界、追求均匀分布
python
from llama_index.core import Document
from llama_index.core.text_splitter import TokenTextSplitter
text = """LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。"""
documents = [Document(text=text)]
splitter = TokenTextSplitter(
chunk_size=50, # 按 token 数量切分
chunk_overlap=10, # 重叠 10 个 token
)
nodes = splitter.get_nodes_from_documents(documents)
print("=== TokenTextSplitter 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i} ({len(node.text)} 字符):")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: "LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。合理的 chunk_size 会显著影响"
# Node 1: "chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。"
切分过程:
text
原始文本 (120 字符)
↓ 按 token 数量切分 (chunk_size=50)
Node 0: [0:50 tokens] + overlap
Node 1: [40:90 tokens] (包含 10 token 重叠)
Node 2: [80:120 tokens]
15.3.2 按句子切分 (SentenceSplitter)
适用场景: 保持语义完整性、FAQ、技术文档、对话记录
python
from llama_index.core import Document
from llama_index.core.text_splitter import SentenceSplitter
text = """LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。Embedding 模型决定检索效果。"""
documents = [Document(text=text)]
splitter = SentenceSplitter(
chunk_size=80, # 最大字符数
chunk_overlap=20, # 重叠字符数
separator=" ", # 句子分隔符
)
nodes = splitter.get_nodes_from_documents(documents)
print("=== SentenceSplitter 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i} ({len(node.text)} 字符):")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: "LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。"
# Node 1: "合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。"
# Node 2: "Embedding 模型决定检索效果。"
切分过程:
text
原始文本
↓ 按句子边界切分 (保持句子完整)
Node 0: 句子1 + 句子2 (不超过 80 字符)
Node 1: 句子2(重叠) + 句子3 + 句子4
Node 2: 句子4(重叠) + 句子5
15.3.3 按段落切分 (SentenceSplitter with paragraph)
适用场景: 长文档、文章、报告、保持段落完整性
python
from llama_index.core import Document
from llama_index.core.text_splitter import SentenceSplitter
text = """第一段:LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。
第二段:合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。
第三段:Embedding 模型决定检索效果。生产环境需要考虑成本和性能。"""
documents = [Document(text=text)]
splitter = SentenceSplitter(
chunk_size=200,
chunk_overlap=50,
paragraph_separator="\n\n", # 按段落分隔
)
nodes = splitter.get_nodes_from_documents(documents)
print("=== 按段落切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: "第一段:LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。"
# Node 1: "第二段:合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。"
# Node 2: "第三段:Embedding 模型决定检索效果。生产环境需要考虑成本和性能。"
15.3.4 按语义切分 (SemanticSplitterNodeParser)
适用场景: 高质量切分、主题变化明显的文档、需要精准召回
python
from llama_index.core import Document, Settings
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
text = """LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。
支付系统升级导致签名验证失败。需要检查网关配置和证书有效期。
Embedding 模型决定检索效果。生产环境需要考虑成本和性能。"""
documents = [Document(text=text)]
splitter = SemanticSplitterNodeParser(
buffer_size=1, # 缓冲区大小
breakpoint_percentile_threshold=95, # 语义变化阈值
embed_model=Settings.embed_model,
)
nodes = splitter.get_nodes_from_documents(documents)
print("=== SemanticSplitter 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: "LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。"
# Node 1: "支付系统升级导致签名验证失败。需要检查网关配置和证书有效期。"
# Node 2: "Embedding 模型决定检索效果。生产环境需要考虑成本和性能。"
切分过程:
text
原始文本
↓ 计算相邻句子的 embedding 相似度
句子1 ←→ 句子2: 相似度 0.92 (同主题)
句子2 ←→ 句子3: 相似度 0.45 (主题变化) ← 切分点
句子3 ←→ 句子4: 相似度 0.88 (同主题)
↓ 按语义边界切分
Node 0: 句子1 + 句子2
Node 1: 句子3 + 句子4
15.3.5 按代码结构切分 (CodeSplitter)
适用场景: 代码文档、API 文档、技术教程
python
from llama_index.core import Document
from llama_index.core.node_parser import CodeSplitter
code_text = """
def calculate_total(items):
'''计算订单总价'''
total = sum(item.price for item in items)
return total
class OrderService:
'''订单服务类'''
def create_order(self, user_id, items):
total = calculate_total(items)
return Order(user_id=user_id, total=total)
"""
documents = [Document(text=code_text)]
splitter = CodeSplitter(
language="python",
chunk_lines=10, # 每个 chunk 最多 10 行
chunk_lines_overlap=2, # 重叠 2 行
max_chars=500,
)
nodes = splitter.get_nodes_from_documents(documents)
print("=== CodeSplitter 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: "def calculate_total(items):\n '''计算订单总价'''\n total = sum(item.price for item in items)\n return total"
# Node 1: "class OrderService:\n '''订单服务类'''\n def create_order(self, user_id, items):\n total = calculate_total(items)\n return Order(user_id=user_id, total=total)"
15.3.6 父子文档切分 (Parent-Child Document Splitting)
适用场景: 需要兼顾召回精度和上下文完整性的生产环境
核心思想: 用小 chunk 做检索(精准),用大 chunk 做生成(完整上下文)
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
text = """LlamaIndex 是一个数据框架。它支持数据加载、文本切分、索引构建和查询引擎。
合理的 chunk_size 会显著影响 RAG 召回质量。向量检索依赖语义相似度。
Embedding 模型决定检索效果。生产环境需要考虑成本和性能。
支付系统升级导致签名验证失败。需要检查网关配置和证书有效期。"""
documents = [Document(text=text)]
# 1. 创建父文档 (大 chunk)
parent_splitter = SentenceSplitter(chunk_size=200, chunk_overlap=50)
parent_nodes = parent_splitter.get_nodes_from_documents(documents)
# 2. 为每个父文档创建子文档 (小 chunk)
child_nodes = []
for parent_node in parent_nodes:
child_splitter = SentenceSplitter(chunk_size=80, chunk_overlap=20)
child_docs = [Document(text=parent_node.text, metadata=parent_node.metadata)]
children = child_splitter.get_nodes_from_documents(child_docs)
# 建立父子关系
for child in children:
child.relationships["parent"] = parent_node.node_id
child_nodes.extend(children)
# 3. 构建索引 (只对子文档建索引)
child_index = VectorStoreIndex(child_nodes)
# 4. 创建递归检索器
retriever = RecursiveRetriever(
"vector",
retriever_dict={"vector": child_index.as_retriever(similarity_top_k=2)},
node_dict={parent.node_id: parent for parent in parent_nodes},
)
# 5. 查询 (检索子文档,返回父文档)
query_engine = RetrieverQueryEngine.from_args(retriever)
response = query_engine.query("支付系统失败的原因是什么?")
print(response)
print("\n=== 父子文档结构 ===")
print(f"父文档数量: {len(parent_nodes)}")
print(f"子文档数量: {len(child_nodes)}")
for i, parent in enumerate(parent_nodes):
print(f"\n父文档 {i} ({len(parent.text)} 字符):")
print(parent.text[:100] + "...")
工作流程:
text
原始文档 (400 字符)
↓ 父切分器 (chunk_size=200)
父文档 A (200 字符) | 父文档 B (200 字符)
↓ 子切分器 (chunk_size=80)
子 A1 (80) | 子 A2 (80) | 子 A3 (40) | 子 B1 (80) | 子 B2 (80) | 子 B3 (40)
↓ 检索阶段
用户查询 → 检索子文档 (精准匹配) → 找到子 B1
↓ 生成阶段
返回父文档 B (完整上下文 200 字符) → LLM 生成答案
优势:
- ✅ 召回精准 (小 chunk 语义聚焦)
- ✅ 上下文完整 (大 chunk 提供完整信息)
- ✅ 兼顾精度和质量
15.3.7 层级切分 (Hierarchical Node Parser)
适用场景: 结构化文档 (Markdown、HTML、技术文档)
核心思想: 按文档结构层级切分,保持层级关系
python
from llama_index.core import Document
from llama_index.core.node_parser import HierarchicalNodeParser, SentenceSplitter
text = """# LlamaIndex 教程
## 1. 安装指南
### 1.1 环境要求
需要 Python 3.8+ 版本。建议使用虚拟环境。
### 1.2 安装步骤
使用 pip install llama-index 命令安装。
## 2. 快速开始
### 2.1 加载数据
使用 SimpleDirectoryReader 加载本地文件。
### 2.2 构建索引
使用 VectorStoreIndex 构建向量索引。"""
documents = [Document(text=text)]
# 定义三层切分器
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128], # 三层: 章节 → 小节 → 段落
)
nodes = node_parser.get_nodes_from_documents(documents)
print("=== 层级切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i} (层级 {node.metadata.get('level', 'unknown')}):")
print(node.text[:100] + "..." if len(node.text) > 100 else node.text)
print(f"父节点: {node.relationships.get('parent', 'None')}")
print("-" * 50)
层级结构:
text
Level 0 (2048 tokens): 整个文档
├─ Level 1 (512 tokens): ## 1. 安装指南
│ ├─ Level 2 (128 tokens): ### 1.1 环境要求
│ └─ Level 2 (128 tokens): ### 1.2 安装步骤
└─ Level 1 (512 tokens): ## 2. 快速开始
├─ Level 2 (128 tokens): ### 2.1 加载数据
└─ Level 2 (128 tokens): ### 2.2 构建索引
优势:
- ✅ 保持文档结构
- ✅ 支持多粒度检索
- ✅ 适合结构化文档
15.3.8 Markdown 标题切分 (MarkdownNodeParser)
适用场景: Markdown 文档、技术博客、Wiki
python
from llama_index.core import Document
from llama_index.core.node_parser import MarkdownNodeParser
markdown_text = """# LlamaIndex 核心概念
## Document
Document 是原始文档对象,包含文本和元数据。
## Node
Node 是检索的基本单元,由 Document 切分而来。
## Index
Index 是对 Node 的组织结构,支持高效检索。
## Query Engine
Query Engine 负责查询执行和答案合成。"""
documents = [Document(text=markdown_text)]
parser = MarkdownNodeParser()
nodes = parser.get_nodes_from_documents(documents)
print("=== Markdown 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(f"标题: {node.metadata.get('header', 'N/A')}")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: 标题: # LlamaIndex 核心概念
# Node 1: 标题: ## Document, 内容: Document 是原始文档对象...
# Node 2: 标题: ## Node, 内容: Node 是检索的基本单元...
切分规则:
text
按 Markdown 标题层级切分:
# 一级标题 → 独立 Node
## 二级标题 → 独立 Node (包含下面的内容直到下一个标题)
### 三级标题 → 独立 Node
15.3.9 JSON 结构化切分 (JSONNodeParser)
适用场景: API 文档、配置文件、结构化数据
python
from llama_index.core import Document
from llama_index.core.node_parser import JSONNodeParser
import json
json_data = {
"user": {
"id": 10001,
"name": "张三",
"orders": [
{"order_id": "O001", "amount": 299.0, "status": "paid"},
{"order_id": "O002", "amount": 599.0, "status": "pending"}
]
},
"payment": {
"gateway": "alipay",
"config": {"timeout": 30, "retry": 3}
}
}
documents = [Document(text=json.dumps(json_data, ensure_ascii=False))]
parser = JSONNodeParser()
nodes = parser.get_nodes_from_documents(documents)
print("=== JSON 切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(f"路径: {node.metadata.get('json_path', 'N/A')}")
print(node.text)
print("-" * 50)
# 输出示例:
# Node 0: 路径: user, 内容: {"id": 10001, "name": "张三", ...}
# Node 1: 路径: user.orders[0], 内容: {"order_id": "O001", ...}
# Node 2: 路径: payment, 内容: {"gateway": "alipay", ...}
15.3.10 窗口切分 (SentenceWindowNodeParser)
适用场景: 需要动态扩展上下文的场景
核心思想: 检索时用小窗口,生成时动态扩展前后文
python
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
text = """句子1:LlamaIndex 是一个数据框架。句子2:它支持数据加载和文本切分。句子3:索引构建是核心功能。句子4:查询引擎负责检索。句子5:RAG 需要合理的 chunk 设计。"""
documents = [Document(text=text)]
# 窗口切分: 每个句子为一个 Node,但保存前后各 2 个句子的上下文
parser = SentenceWindowNodeParser.from_defaults(
window_size=2, # 前后各保留 2 个句子
window_metadata_key="window",
original_text_metadata_key="original_sentence",
)
nodes = parser.get_nodes_from_documents(documents)
print("=== 窗口切分结果 ===")
for i, node in enumerate(nodes):
print(f"\nNode {i}:")
print(f"原始句子: {node.metadata.get('original_sentence', 'N/A')}")
print(f"窗口内容: {node.metadata.get('window', 'N/A')}")
print("-" * 50)
# 构建索引并查询
index = VectorStoreIndex(nodes)
# 使用后处理器替换为窗口内容
postprocessor = MetadataReplacementPostProcessor(target_metadata_key="window")
query_engine = index.as_query_engine(node_postprocessors=[postprocessor])
response = query_engine.query("索引构建的作用是什么?")
print(f"\n查询结果: {response}")
工作流程:
text
原始文本: 句子1 句子2 句子3 句子4 句子5
↓ 窗口切分 (window_size=2)
Node 0: 原始=句子1, 窗口=[句子1, 句子2, 句子3]
Node 1: 原始=句子2, 窗口=[句子1, 句子2, 句子3, 句子4]
Node 2: 原始=句子3, 窗口=[句子1, 句子2, 句子3, 句子4, 句子5]
Node 3: 原始=句子4, 窗口=[句子2, 句子3, 句子4, 句子5]
Node 4: 原始=句子5, 窗口=[句子3, 句子4, 句子5]
↓ 检索阶段
用户查询 → 检索原始句子 (精准)
↓ 生成阶段
返回窗口内容 (包含前后文) → LLM 生成答案
优势:
- ✅ 检索精准 (单句匹配)
- ✅ 上下文动态扩展
- ✅ 灵活控制窗口大小
15.4 切分方式对比与选型
| 切分方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| TokenTextSplitter | 简单快速、均匀分布 | 可能切断句子、语义不完整 | 通用文本、不关心边界 |
| SentenceSplitter | 保持句子完整、语义连贯 | 可能产生过大/过小 chunk | FAQ、技术文档、对话 |
| 段落切分 | 保持段落完整、上下文丰富 | chunk 大小不均匀 | 长文档、文章、报告 |
| SemanticSplitter | 按主题切分、召回精准 | 需要 embedding、速度慢 | 高质量切分、主题明确 |
| CodeSplitter | 保持代码结构、函数完整 | 仅适用代码 | 代码文档、API 文档 |
| 父子文档切分 | 兼顾精度和上下文 | 实现复杂、存储翻倍 | 生产环境、高质量 RAG |
| 层级切分 | 保持文档结构、多粒度检索 | 需要结构化文档 | Markdown、HTML、Wiki |
| Markdown 切分 | 按标题切分、结构清晰 | 仅适用 Markdown | 技术博客、文档、笔记 |
| JSON 切分 | 保持数据结构、路径追溯 | 仅适用 JSON | API 文档、配置文件 |
| 窗口切分 | 动态扩展上下文、灵活 | 存储开销大 | 需要前后文的场景 |
选型建议:
- FAQ/客服对话 → SentenceSplitter (chunk_size=128-256)
- 技术文档/Wiki → Markdown 切分 或 层级切分
- 长文章/报告 → 段落切分 (chunk_size=1024-2048)
- 代码文档 → CodeSplitter (按函数/类切分)
- 高精度场景 → SemanticSplitter 或 父子文档切分
- 通用场景 → TokenTextSplitter (快速简单)
- 结构化数据 → JSON 切分
- 需要前后文 → 窗口切分
- 生产环境 → 父子文档切分 (最佳实践)
工程实践:
python
# 根据文档类型选择切分器
def get_splitter(doc_type: str):
if doc_type == "faq":
return SentenceSplitter(chunk_size=256, chunk_overlap=50)
elif doc_type == "article":
return SentenceSplitter(chunk_size=1024, chunk_overlap=200, paragraph_separator="\n\n")
elif doc_type == "code":
return CodeSplitter(language="python", chunk_lines=20)
elif doc_type == "markdown":
return MarkdownNodeParser()
elif doc_type == "json":
return JSONNodeParser()
elif doc_type == "high_quality":
return SemanticSplitterNodeParser(breakpoint_percentile_threshold=95)
elif doc_type == "production":
# 生产环境推荐: 父子文档切分
return {
"parent": SentenceSplitter(chunk_size=1024, chunk_overlap=200),
"child": SentenceSplitter(chunk_size=256, chunk_overlap=50)
}
elif doc_type == "context_aware":
return SentenceWindowNodeParser(window_size=3)
else:
return TokenTextSplitter(chunk_size=512, chunk_overlap=100)
其他常见切分方式:
还有一些特定场景的切分方式:
- HTML 切分 (HTMLNodeParser) - 按 HTML 标签结构切分
- 表格切分 (TableNodeParser) - 专门处理表格数据
- PDF 切分 (按页/按段落) - 保持 PDF 原始结构
- 多模态切分 - 同时处理文本、图片、表格
核心原则:
- 精度优先 → 小 chunk + 语义切分
- 上下文优先 → 大 chunk + 段落切分
- 兼顾两者 → 父子文档切分 (生产推荐)
- 结构化文档 → 按结构切分 (Markdown/JSON/HTML)
- 成本敏感 → TokenTextSplitter (最快最便宜)
15.5 调优方法
不要凭感觉调 Chunk,应该建立评测集。
评测集至少包含:
- 问题;
- 标准答案;
- 标准引用文档;
- 期望召回 Node;
- 是否要求精确事实。
核心指标:
- Recall@K;
- MRR;
- Answer Faithfulness;
- Context Precision;
- Latency;
- Token Cost。
16. Embedding 成本与性能优化
16.1 成本来源
Embedding 成本主要来自:
- 初次构建索引时对所有 Node 生成向量;
- 新增文档的增量 embedding;
- 查询时 query embedding;
- 多路召回时重复 embedding。
16.2 优化策略
方案一:Embedding 缓存
优点:
- 避免重复计算;
- 降低成本;
- 提升构建速度。
缺点:
- 需要缓存一致性策略;
- 模型升级后缓存需要失效。
方案二:增量索引
优点:
- 避免全量重建;
- 适合频繁更新的知识库。
缺点:
- 需要文档版本、hash、删除同步机制。
方案三:本地 Embedding 模型
优点:
- 成本可控;
- 数据不出内网;
- 可离线部署。
缺点:
- 效果可能低于商业模型;
- GPU / 推理服务运维成本高。
方案四:分层召回
text
关键词 / BM25 粗召回
↓
向量召回
↓
Reranker 重排
↓
LLM 合成
优点:
- 提升召回率;
- 降低单一路径风险。
缺点:
- 链路复杂;
- 调参成本增加。
17. 检索召回率低的常见原因
17.1 原因一:Chunk 切分破坏语义
表现:
- 答案在原文里,但召回不到;
- 召回片段缺少上下文;
- 一个完整概念被切到多个 Node。
解决:
- 增大
chunk_overlap; - 按 Markdown 标题切分;
- 按业务实体切分;
- 对表格、代码、日志使用专用 parser。
17.2 原因二:Embedding 模型不匹配
表现:
- 中文召回差;
- 专业术语召回差;
- 缩写、代码、错误码召回差。
解决:
- 选择支持中文和技术语料的 embedding;
- 使用领域微调 embedding;
- 增加关键词检索兜底;
- 对术语维护 synonym map。
17.3 原因三:Top-K 设置不合理
Top-K 太小:漏召回。
Top-K 太大:噪声多、成本高。
推荐:
- FAQ:
top_k = 2 ~ 5; - 技术文档:
top_k = 5 ~ 10; - 多文档分析:
top_k = 10 ~ 20; - 配合 reranker 时可先召回
20 ~ 50再重排。
17.4 原因四:缺少 Metadata Filter
解决:
- 把业务约束结构化;
- 先 filter 再 vector search;
- 权限、租户、时间范围必须走 metadata,不要交给 LLM 猜。
18. Reranker:提升最终上下文质量
18.1 为什么需要 Reranker
向量检索负责召回,Reranker 负责精排。
text
Vector Search Top 50
↓
Reranker Top 5
↓
LLM Answer
优势:
- 减少噪声上下文;
- 提升事实准确性;
- 改善长尾问题表现。
18.2 工程建议
- 高价值问答链路建议加 reranker;
- 低延迟链路谨慎使用;
- 可对 rerank 结果做缓存;
- reranker 输入不宜过长。