Files
2026-09-22 15:42:25 +08:00

216 lines
14 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段三 · 小点 3:LlamaIndex(数据感知型 Agent·重点)
> 所属:阶段三 主流 Agent 开发框架
> 定位:LangChain / LangGraph 负责「编排 Agent」,LlamaIndex 负责「喂给 Agent 知识」。当你的 Agent 要回答"某个文档/数据库里"的问题时,LlamaIndex 是全链路最顺的工具。这一讲讲透"加载→分块→索引→检索→生成"这条数据链路,以及生产标配的检索三件套。
## 精简大纲
1. 定位与安装(0.10+ 拆分包)
2. 数据链路:加载 → 分块 → 索引 → 检索 → 生成
3. 进阶检索三件套:查询改写 / 混合检索 / Rerank
4. 引用溯源
5. 与 LangGraph 组合拳
## 学习内容详情
> 定位:LangChain / LangGraph 负责「编排 Agent」,LlamaIndex 负责「喂给 Agent 知识」——两者经常组合使用。
### 1. 安装与全局配置
- `pip install llama-index llama-index-llms-openai llama-index-embeddings-openai llama-index-embeddings-huggingface`(末位包为下方 `HuggingFaceEmbedding` 所需,缺它 import 会失败)
- 中文语料建议开源中文 Embedding:`HuggingFaceEmbedding(model_name="BAAI/bge-small-zh-v1.5")`(免 API 费、中文效果更好)。
- 0.10 前后 API 差异巨大,认准新版写法(Settings 全局配置)。
```python
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
# 全局配置: 一次设置, 全项目生效
Settings.llm = OpenAI(model="gpt-4o", temperature=0.2)
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-zh-v1.5" # 中文推荐
)
```
### 2. 数据链路(Document → Node → Index → Retriever → QueryEngine)
#### 2.1 一张图看懂全链路
```mermaid
graph LR
A[加载 Document] --> B[分块 Node/Chunk]
B --> C[Embedding 向量化]
C --> D[索引 VectorStoreIndex]
D --> E[检索 Retriever top-k]
E --> F[生成 QueryEngine 拼prompt出答案]
style A fill:#e6ffe6
style F fill:#e6ffe6
```
| 环节 | 组件 | 作用 |
|-|-|-|
| 加载 | Document / Reader | 把 PDF/网页/Notion/数据库读成统一 Document |
| 分块 | Node / Chunk | 切成检索单元(默认约 1024 token/块),**自带元数据(来源文件/页码)** |
| 索引 | VectorStoreIndex | 全部 Node 做 Embedding 后存储,按相似度召回 |
| 检索 | Retriever | 只负责找回「相关 Node 列表」,可替换 |
| 生成 | QueryEngine | Retriever + LLM 组合,`as_query_engine()` 一行生成 |
```python
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
# ① 加载: 一行读整个目录
docs = SimpleDirectoryReader("data").load_data()
# ② 分块 + 索引: 默认 1024 token/块; 每个 Node自带元数据(引用溯源靠它)
index = VectorStoreIndex.from_documents(docs)
# ③ 检索 + 生成: 一行生成查询引擎
query_engine = index.as_query_engine(similarity_top_k=3) # 取最相关3块
# ④ 提问
resp = query_engine.query("这份文档讲了什么?")
print(resp.response)
```
> 🔬 **深度理解 · Document / Node / Index 数据抽象**:
> 1. **本质**:LlamaIndex 把「喂给模型的领域知识」做成一串带层级的数据对象,而不是一把原始文本。Document 是最粗粒度的来源单元(一个文件/网页),Node 是真正参与检索的最细单元(一块文本 + 自带元数据),Index 是这些 Node 的「组织方式」(如向量索引)。
> 2. **机制**:加载阶段 Reader 把异构来源(PDF / 网页 / Notion / 数据库)规范化成统一 Document;分块阶段把 Document 切成若干 Node,每个 Node 携带 metadata(来源文件、页码、标题);索引阶段对 Node 做 Embedding 存入 Index;检索阶段把 user query 编码后在 Index 里按相似度召回 top-k 个 Node。
> 3. **为什么重要**:这种分层让「如何切片」「如何存储」「如何召回」各自可替换——改分块策略不影响存储与检索代码;Node 自带元数据是「引用溯源」和「权限过滤」的基础。
> 4. **易错点**:把 Document 和 Node 混为一谈;以为整篇原文直接 Embedding 就能检好——实际分块粒度直接影响召回命中率,块太小上下文不全、块太大精度下降。
- **Semantic Chunking(语义分块)**:按「语义边界」切而非固定字数切(相邻句子相似度低于阈值处断开),完整论点不被腰斩。
- **父子分块(Parent-Child)**:检索用小块(精准命中)、返回用大块(上下文完整),解决「块太小不全 / 块太大不准」的两难。
> ⏳ **短期可不深究**:Semantic Chunking 和 Parent-Child 这种「花式分块」第一遍只需知道「目的都是让切块更贴近语义、别腰斩论点」,具体调参 / 内网评测是真正做 RAG 落地时的事,现在不用钻细节。
- **ChatEngine**:带多轮记忆,自动改写查询补全指代(「它多少钱」→「iPhone 17 多少钱」)。
### 3. 进阶检索三件套(生产标配)
#### 3.1 为什么是三件套
```mermaid
graph LR
A[口语问题] --> B[① 查询改写: 补指代/拆多问/纠错]
B --> C[② 混合检索: 向量+BM25 双路]
C --> D[RRF 融合排序]
D --> E[③ Rerank 重排]
E --> F[精排取 top-3 喂给生成]
```
1. **查询改写:** 检索前先让 LLM 把口语问题改写成检索友好查询(补指代、拆多问、纠错别字)。
- 例:「它多少钱」→「iPhone 17 多少钱」。
2. **混合检索:** 向量(语义)+ BM25(精确词,如型号 / 人名 / 数字)双路召回,再 RRF 融合排序——单路各有盲区,混合是生产标配。
> ⏳ **短期可不深究**:RRF 那个「按排位倒数相加」的具体融合公式第一遍不用背——只需记住「它把向量路和 BM25 路各自的排名融成一个统一排序」即可,真正调库时看文档就好。
3. **Rerank 重排:** 粗召回 top-10/20 → Cross-Encoder 精排 top-3(两段式「先海选再细读」)。
> 🔬 **深度理解 · 两段式检索(Retriever + Rerank)的精度机制**:
> 1. **本质**:检索要做两遍——第一遍「召回」追求全(recall),第二遍「精排」追求准(precision)。单靠 Embedding 相似度无法同时满足两者。
> 2. **机制**:召回层先用向量(语义)+ BM25(精确词)双路取回较多候选,用 RRF(Reciprocal Rank Fusion)把不同路各自排位融合成统一排序;精排层再用 Cross-Encoder 一类的 Reranker 对候选逐对打分、重排,截取 top-3 喂给生成。
> 3. **为什么重要**:向量擅长语义近似,但对型号 / 人名 / 精确数字容易失效,BM25 恰好擅长精确词匹配——互补能覆盖两类查询;Rerank 能显著压「召回了但相关性差」的噪声,直接改善生成质量。
> 4. **易错点**:把召回当精排用(`similarity_top_k` 贪大、无关块干扰答案);RRF 只是「排名融合」,不能替代语义级重排;Reranker 也有上下文上限,候选太多会截断。
```python
# 混合检索 + Rerank 示例(结构示意; 生产接 BM25Retriever + 重排模型)
from llama_index.core.retrievers import VectorIndexRetriever
retriever = VectorIndexRetriever(index=index, similarity_top_k=10)
nodes = retriever.retrieve("iPhone 17 多少钱") # 粗召回 10 块
# ③ 重排: 对粗召回用 Cross-Encoder 精排到 top-3 (略)
# top_nodes = reranker.rerank(nodes, top_n=3)
print(f"粗召回 {len(nodes)} 块, 精排后取 top-3 再喂给生成")
```
### 4. 坑点自查
| 坑 | 现象 | 对策 |
|-|-|-|
| 多次检索冗余 | 每轮都调知识库 | 工具内做缓存 / 去重 |
| 检索 query 生成错误 | 答非所问 | 打印每轮实际用 query 人肉抽查 |
| 无关文档干扰 | 答案被带偏 | `similarity_top_k` 别贪大,加 score 阈值过滤低分块 |
### 5. 引用溯源
```python
# Node 自带元数据(来源文件/页码) → 回答可带引用
resp = query_engine.query("营收增长多少?")
for node in resp.source_nodes: # 遍历命中的文档块
print("来源:", node.metadata.get("file_name")) # 哪个文件
print("页码:", node.metadata.get("page_label")) # 哪一页
print("片段:", node.node.get_text()[:80]) # 原文片段
```
### 6. 与 LangGraph 组合拳(生产最常用)
- **原则**:检索强用 LlamaIndex,流程强用 LangGraph,各干各的擅长事。
- **做法**:把 LlamaIndex 检索引擎封装成 LangChain `@tool`(描述决定模型调用时机),再用 `create_react_agent` 一行创建 ReAct Agent——模型自己决定何时查知识库。
```python
from langchain_core.tools import tool
# 把 LlamaIndex 检索引擎封装成 LangChain @tool
@tool
def query_knowledge(query: str) -> str:
"""查询公司内部知识库。当用户问业务/产品/政策问题时调用。"""
return str(query_engine.query(query).response) # 复用上面建好的 query_engine
# create_react_agent: 一句话把"工具"变成"会自己决定查不查的 Agent"
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools=[query_knowledge])
result = agent.invoke({"messages": [("user", "我们的退款政策是什么?")]})
print(result["messages"][-1].content)
```
- **为什么这样组合**:
- LlamaIndex 的 Agent 编排弱于 LangGraph(无 Checkpointer / 复杂分支)。
- LangChain 的文档解析检索链不如 LlamaIndex 成熟(分块器 / 混合检索 / 重排开箱即用)。
- 二者通过「@tool 包装 QueryEngine」解耦,各管各的。
## 本节自检
- [ ] 能完成文档加载 → 分块 → 索引 → 检索 → 带引用溯源的问答全链路
- [ ] 能实现混合检索 + Rerank 并接入 LangGraph Agent
## 本节配套思考题(快速入门的检验)
1. 为什么"语义块"里的 Node 要自带元数据(来源文件 / 页码)?对生产有什么实际价值?
2. 单路向量检索最容易在哪类查询上翻车?(提示:型号 / 人名 / 精确数字)
3. 混合检索的"混合"到底混了哪两路?各自擅长什么?RRF 在这里起什么作用?
4. 为什么 LlamaIndex 的检索要包成 `@tool` 塞给 LangGraph,而不是直接调用?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:LlamaIndex 为什么把数据拆成 Document → Node → Index → Retriever → QueryEngine 这么多层?各层职责是什么?
- **面试官想考察**:是否理解数据链路由粗到细的分层设计价值,而不是只会调 `as_query_engine()`。
- **专业作答(含深度)**:
1. Document 是粗粒度来源单元(一个文件/网页),负责"拿到原始知识";Node 是细粒度检索单元(一块文本 + 自带元数据),负责"能被精准命中"。
2. Index 管"如何组织这些 Node"(向量索引 / 树 / 关键词索引等),Retriever 只负责"召回相关 Node 列表",QueryEngine 把 Retriever 结果 + LLM 拼成最终回答。
3. 分层的核心价值是「可替换性」:换分块策略 / 换存储 / 换召回方式互不影响;同时 Node 元数据支撑引用溯源与权限过滤。
4. 边界:Retriever 不负责"回答",只负责"找材料";真正生成在 QueryEngine(或阶段四略过之外的 Agent 里)。
- **加分亮点 / 深度追问**:主动讲"分块粒度如何同时影响 recall 与精度",以及 QueryEngine 可被替换成任意 Agent 编排;追问常是"Retriever 和 Reranker 的区别"——答:Retriever 粗召回求全、Reranker 精排求准。
### 面试题 2:单路向量检索在哪类查询上最容易翻车?生产如何用「混合检索 + Rerank」提升精度?
- **面试官想考察**:是否真正理解不同检索路的互补盲区,以及"召回 vs 精排"的两段式道理。
- **专业作答(含深度)**:
1. 单路向量检索对型号、人名、精确数字等"必须逐字命中"的查询最容易翻车——Embedding 抓语义但允许近似,常把相似但不精确的内容排前面。
2. 生产方案是混合检索:向量(语义)+ BM25(精确词)双路召回,再用 RRF 融合两者的排名,覆盖两类查询盲区。
3. 召回层之后接 Rerank(如 Cross-Encoder)对候选逐对精排,截取 top-k 喂给生成,压低"召回对但排序差"的噪声。
4. 常见工程技巧:给 `similarity_top_k` 设合理上限并加 score 阈值,避免无关块干扰答案。
- **加分亮点 / 深度追问**:主动提"召回追求 recall、精排追求 precision,两者必须分开做";追问常是"RRF 能替代 Rerank 吗"——答:不能,RRF 是排名融合不含语义相关性打分。
### 面试题 3:为什么要把 LlamaIndex 检索引擎包成 `@tool` 塞给 LangGraph,而不是直接调用 LlamaIndex 自己的 Agent?
- **面试官想考察**:对「检索层与编排层各司其职」的组合架构判断,以及对工具封装边界的理解。
- **专业作答(含深度)**:
1. 职责划分:检索(语义分块 / 混合检索 / Rerank / 引用溯源)强在 LlamaIndex;流程编排(循环 / 分支 / 持久化 / 审批 / 多智能体)强在 LangGraph。
2. 桥接方式:把 QueryEngine 封装成 `@tool`,用 docstring 描述决定"模型何时该查知识库",再用 `create_react_agent` 让模型自主决定调用时机。
3. 好处:知识库与编排解耦——换知识库不影响 Agent 流程;还获得 LangGraph 的 Checkpointer / 熔断 / 可观测等生产能力。
4. 适用判断:若是纯"文档问答"可直接 LlamaIndex 的 QueryEngine;一旦有循环 / 多步 / 需要人工干预,就应包成工具交给 LangGraph 编排。
- **加分亮点 / 深度追问**:主动提"工具描述(docstring)决定了模型何时触发检索"这个要点;追问常是"何时不需要 LangGraph"——答:单轮固定检索问答直接 QueryEngine 即可,避免过度设计。