从名字开始:LlamaIndex 的重点是 Index
只看 Agent Course 中的示例,很容易把 LlamaIndex 理解成另一个 Agent 框架:它有 Tool、Agent、Multi-Agent,也有 Workflow。然而这些能力并不独特,LangChain、LangGraph 和 smolagents 都能提供类似机制。
LlamaIndex 更核心的问题其实是:
外部数据怎样被加载、切分、索引和检索,最终成为 LLM 可以使用的上下文?
因此,LlamaIndex 更接近 LLM 应用中的“数据访问与知识检索层”。它的核心路径可以概括为:
Agent 与 Workflow 是建立在这条数据链路之上的应用和编排能力。理解 LlamaIndex,首先要把 RAG 主链路理顺。
RAG 第一阶段:Loading 不等于 Embedding
课程使用 Hugging Face 上的 persona 数据集:
from datasets import load_dataset
dataset = load_dataset(
path="dvilasuero/finepersonas-v0.1-tiny",
split="train",
)这里的 load_dataset() 只负责把远程数据集加载成 Hugging Face Dataset。它还没有创建 LlamaIndex 的 Document,也没有调用 LlamaIndex 的 Embedding 模型。
数据集本身实际上已经包含 embedding 字段,并记录了生成该向量的模型:
model_name_embeddings = Alibaba-NLP/gte-large-en-v1.5
embedding = 1024 维向量但课程随后只取出 persona 文本并写入文件:
from pathlib import Path
Path("data").mkdir(parents=True, exist_ok=True)
for i, persona in enumerate(dataset):
with open(Path("data") / f"persona_{i}.txt", "w") as f:
f.write(persona["persona"])这一操作丢弃了数据集已有的 embedding、最近邻和聚类字段。也就是说,后面的 LlamaIndex Pipeline 并没有复用原始向量,而是从纯文本重新开始。
接下来:
from llama_index.core import SimpleDirectoryReader
reader = SimpleDirectoryReader(input_dir="data")
documents = reader.load_data()SimpleDirectoryReader 会读取目录中的文件,并把正文和文件元数据包装为:
list[Document]概念上相当于:
documents = []
for file in directory:
documents.append(
Document(
text=read_text(file),
metadata={"file_name": file.name},
)
)因此,Loading 阶段的结果不是向量,而是适合 LlamaIndex 后续处理的 Document 对象列表。
Document.example() 与 [Document.example()]
课程还出现过:
nodes = await pipeline.arun(
documents=[Document.example()]
)Document.example() 创建一个示例 Document,外层的 [] 则把单个对象包装成列表,因为 documents 参数接收多个文档。
对应 Java 的思路接近:
List<Document> documents = List.of(Document.example());这不是特殊语法,只是“创建对象后立即放入 List”的 Python 写法。
RAG 第二阶段:IngestionPipeline 完成分块和 Embedding
加载完成后,课程使用:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.ingestion import IngestionPipeline
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_overlap=0),
HuggingFaceEmbedding(
model_name="BAAI/bge-small-en-v1.5"
),
]
)
nodes = await pipeline.arun(documents=documents)transformations 会按照声明顺序执行:
Document
↓ SentenceSplitter
Node
↓ HuggingFaceEmbedding
带 embedding 的 NodeNode 不等于 Embedding
Node 是数据结构,不是“向量化结果”的同义词。刚经过 SentenceSplitter 的 Node 可以只有文本:
Node
├── text
├── metadata
├── relationships
└── embedding = None经过 HuggingFaceEmbedding 后,embedding 属性才会被填充。对于 BAAI/bge-small-en-v1.5,向量通常是 384 维:
print(nodes[0].embedding is not None)
print(len(nodes[0].embedding))需要注意,后续从 Chroma 检索回来的 TextNode 可能再次显示 embedding=None。这不代表数据库没有向量,而是检索结果通常不会把整段向量重新传回应用层;相似度已经由 Chroma 使用存储的向量计算完成。
为什么不直接使用数据集已有的 embedding?
课程重新 Embedding 有两个重要原因。
第一,原数据集的向量对应整条 persona,而 SentenceSplitter 可能把一条文本切成多个 Node。每个新 Node 都需要自己的向量。
第二,文档向量和查询向量必须来自同一个 Embedding 模型。原数据集使用 Alibaba-NLP/gte-large-en-v1.5,课程查询阶段使用 BAAI/bge-small-en-v1.5。不同模型产生的向量不在同一个语义空间中,不能直接进行有意义的相似度比较。
RAG 第三阶段:把 Node 自动写入 Chroma
为 IngestionPipeline 增加 vector_store:
import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
db = chromadb.PersistentClient(path="./alfred_chroma_db")
chroma_collection = db.get_or_create_collection("alfred")
vector_store = ChromaVectorStore(
chroma_collection=chroma_collection
)
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=25, chunk_overlap=0),
HuggingFaceEmbedding(
model_name="BAAI/bge-small-en-v1.5"
),
],
vector_store=vector_store,
)此时,真正触发处理和写入的仍然是:
nodes = await pipeline.arun(documents=documents)完整数据流变成:
IngestionPipeline(...) 只是配置对象;只有执行 run() 或 arun() 才会真正分块、Embedding 和保存。
可以通过以下代码确认写入数量:
print(chroma_collection.count())Document 数量不一定等于 Chroma 记录数,因为一个 Document 可以被拆成多个 Node。
Index 到底是什么
数据写入 Chroma 后,课程创建:
from llama_index.core import VectorStoreIndex
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-en-v1.5"
)
index = VectorStoreIndex.from_vector_store(
vector_store,
embed_model=embed_model,
)这一步不会重新写入或重新 Embedding 数据。它是在已有 Vector Store 外建立 LlamaIndex 的检索抽象,告诉系统:
数据存储在哪里
查询文本用什么模型转为向量
如何把检索结果还原为 Node
如何接入后续 Retriever 和 QueryEngine因此,Index 不是简单标签,也不等于数据库。更合适的理解是:
Index 是面向查询的数据访问和检索协调层。
它与 Spring Boot 的 Mapper/Repository 有些相似,但还包含查询向量生成、Node 恢复以及 LlamaIndex StorageContext 等能力。
从 Index 派生三种查询接口
同一个 Index 可以提供三个层次的接口。
as_retriever():只找资料
retriever = index.as_retriever(similarity_top_k=3)
nodes = retriever.retrieve("Who is interested in AI?")返回值通常是:
list[NodeWithScore]Retriever 只负责:
问题文本 → 查询向量 → 相似度搜索 → NodeWithScore它通常不调用生成式 LLM,适合检查 RAG 到底找到了什么。
as_query_engine():检索后生成一次答案
query_engine = index.as_query_engine(
llm=llm,
similarity_top_k=3,
)
response = query_engine.query(
"Who is interested in AI?"
)QueryEngine 可以理解为:
Retriever
+ Prompt
+ Response Synthesizer
+ LLM它返回的不只是字符串,还包括最终答案、来源 Node 和元数据。
as_chat_engine():在 RAG 上增加对话历史
ChatEngine 适合处理:
第一轮:找一个对 AI 感兴趣的人。
第二轮:他还有什么其他兴趣?第二轮中的“他”依赖上一轮对话。ChatEngine 会结合聊天历史理解或改写问题,再调用 Retriever。
三者可以简单记成:
Retriever = 找资料
QueryEngine = 根据资料回答一次问题
ChatEngine = 根据资料进行连续对话Response Processing:检索相同,答案合成方式不同
response_mode 通常不改变 Retriever 找到哪些 Node,而是决定如何把这些 Node 交给 LLM。
refine
refine 按顺序读取 Node,并不断修改已有答案:
问题 + Node A → 初始答案
初始答案 + Node B → 修订答案
修订答案 + Node C → 最终答案优点是每个 Node 都有机会补充细节,缺点是 LLM 调用次数多、速度慢,并且结果可能受 Node 顺序影响。
compact
compact 会尽量把多个 Node 合并进一个上下文窗口,再生成答案。如果内容放不下,才把它们打包成较大的块并执行类似 refine 的处理。
这是普通 RAG 问答中常用的默认选择:调用次数少,延迟和成本较低。
tree_summarize
tree_summarize 会先分组生成中间摘要,再递归合并:
它适合大量资料的整体总结,但中间摘要也可能损失细节。
三者的关系是:
| 模式 | 主要方式 | LLM 调用 | 适合场景 |
|---|---|---|---|
refine | 逐段修订 | 较多 | 多段细节逐步补充 |
compact | 尽量合并 | 通常最少 | 普通 RAG 问答 |
tree_summarize | 树状归纳 | 中等或较多 | 大量文档整体总结 |
Evaluation:不是一个“总分”,而是多个评价维度
课程介绍的 Evaluator 本质上属于 LLM-as-a-judge:再调用一个 LLM 作为裁判,但每个 Evaluator 看到的输入不同。
FaithfulnessEvaluator
比较:
最终回答 vs 检索到的 Context它判断回答是否被 source_nodes 支持,主要用于发现幻觉。不需要标准答案。
需要注意:Faithfulness 只表示“忠于 Context”,不表示 Context 本身符合现实。如果资料本身错误,而回答忠实复述错误资料,Faithfulness 仍可能通过。
AnswerRelevancyEvaluator
比较:
用户问题 vs 最终回答它判断回答是否切题,不直接使用检索 Context。一个答案可以非常切题,却没有任何检索依据。
CorrectnessEvaluator
比较:
最终回答 vs Reference Answer它通常需要人工准备的标准答案,用于判断端到端结果是否正确。
三个指标可能产生不同结果:
| 情况 | Faithfulness | Relevancy | Correctness |
|---|---|---|---|
| 忠实复述错误资料 | 高 | 高 | 低 |
| 答非所问但内容有依据 | 高 | 低 | 低 |
| 使用外部常识答对但 Context 无依据 | 低 | 高 | 高 |
它们主要评价最终 Response 的不同侧面。若要直接评价 Retriever,还需要 Hit Rate、MRR、Precision@K、Recall@K 或 Context Relevancy 等指标。
QueryEngine 为什么还要包装成 Tool
课程将 QueryEngine 包装为:
from llama_index.core.tools import QueryEngineTool
query_engine_tool = QueryEngineTool.from_defaults(
query_engine=query_engine,
name="name",
description="a specific description",
return_direct=False,
)QueryEngineTool 没有增加新的检索能力。它只是给 QueryEngine 增加 Agent 可识别的统一接口:
- 名称
- 描述
- 输入 Schema
call()/acall()ToolOutput
可以类比为把已有 Service 包装成统一 Tool Adapter。
进一步创建 Agent:
query_engine_agent = AgentWorkflow.from_tools_or_functions(
[query_engine_tool],
llm=llm,
system_prompt=(
"You are a helpful assistant that has access "
"to a database containing persona descriptions."
),
)此时调用链变成:
Agent
↓ 决定是否调用
QueryEngineTool
↓ 适配调用
QueryEngine
↓
Retriever
↓
ChromaDB它有点像 Spring Boot 中的 Controller → Service → Mapper → Database。一次请求最终可能只查询一次数据库,各层主要用于分离职责。
但 Agent 与普通 Controller 存在一个重要差别:Controller 的调用路径由代码写死,Agent 的调用路径由 LLM 在运行时决定。Agent 可以直接回答、调用一个 Tool、连续调用多个 Tool,或者根据观察结果改变计划。
只有一个 Tool 时,Agent 是否多余?
如果所有问题都必须查询同一个知识库,那么直接调用 QueryEngine 更简单:
response = await query_engine.aquery(question)只有一个 Tool 且必定调用时,Agent 层通常会增加额外 LLM 请求、延迟和成本。课程这样写主要是为了展示“上一节的 Tool 如何接入 Agent”。
Agentic RAG 真正有价值的情况通常是:
Agent
├── Persona Query Tool
├── SQL Tool
├── Web Search Tool
├── Calculator Tool
└── Weather ToolAgent 需要根据任务动态选择工具,甚至多次检索、改写问题并综合多个结果。
ToolSpec:相关工具的适配器集合
ToolSpec 可以理解为某个服务或业务域的一组 Tool 定义。例如:
from llama_index.tools.google import GmailToolSpec
tool_spec = GmailToolSpec()
tool_spec_list = tool_spec.to_tool_list()GmailToolSpec 本身不是 Agent 最终看到的一个“大 Tool”。to_tool_list() 会把相关方法分别转换成多个独立 Tool,例如搜索邮件、读取邮件、创建草稿和发送邮件。
更准确的关系是:
ToolSpec = 一组相关能力的定义和适配器
to_tool_list = 展开为多个标准 Tool
Agent = 根据名称、描述和参数选择具体 ToolWorkflow:通过 Event 类型连接 Step
LlamaIndex Workflow 使用事件驱动模型:
from llama_index.core.workflow import (
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
class ProcessingEvent(Event):
intermediate_result: str
class MultiStepWorkflow(Workflow):
@step
async def step_one(
self,
ev: StartEvent,
) -> ProcessingEvent:
return ProcessingEvent(
intermediate_result="Step 1 complete"
)
@step
async def step_two(
self,
ev: ProcessingEvent,
) -> StopEvent:
final_result = (
f"Finished processing: "
f"{ev.intermediate_result}"
)
return StopEvent(result=final_result)
w = MultiStepWorkflow(timeout=10, verbose=False)
result = await w.run()执行顺序不是由函数名或代码书写位置决定的,而是由类型注解决定:
step_one 接收 StartEvent,返回 ProcessingEvent
step_two 接收 ProcessingEvent,返回 StopEvent因此 Workflow 自动形成:
await w.run() 会创建 StartEvent、启动事件调度并等待 StopEvent。返回 StopEvent 后,Workflow 结束并将其中的 result 交给调用者。
它类似 Spring 的事件发布与 @EventListener,但 Workflow 会围绕 Event 和 Step 构建一个可执行流程,而不只是广播普通应用事件。
LlamaIndex、LangGraph 与 smolagents 的边界
三个框架都能构建 Agent,但核心关注点不同:
| 框架 | 核心关键词 | 主要解决的问题 |
|---|---|---|
| LlamaIndex | Index / Data / RAG | 如何让 LLM 使用外部与私有数据 |
| LangGraph | Graph / State / Workflow | 如何编排复杂、有状态、可恢复的流程 |
| smolagents | Small / Agent Loop / CodeAgent | 如何用较少抽象运行轻量 Agent |
LlamaIndex
优势在复杂数据和 RAG:数据连接、Document/Node、Ingestion、Index、Retriever、Reranker、Response Synthesis 与 Evaluation。Workflow 是有用扩展,但不是最独特的贡献。
LangGraph
重点是状态化流程运行:State、Node、Edge、分支、循环、持久化、暂停恢复和 Human-in-the-loop。它更像 Agent 系统的 Control Plane。
smolagents
强调更薄、更容易理解的 Agent Loop,并提供 CodeAgent 和 ToolCallingAgent。它适合快速实验和代码型 Agent,但不会把复杂 RAG 数据层作为中心。
一个职责清晰的组合是:
LangGraph:决定流程怎样走
LlamaIndex:决定从数据中检索什么即 LangGraph 作为控制层,LlamaIndex 作为数据与检索层。简单项目则没有必要同时引入多个框架。
本地 Notebook 运行的几个经验
Kernel 重启不会删除安装包
Kernel 重启只会清空内存中的变量、对象和正在执行的任务。通过 pip 安装到 Conda 环境的包、下载到本地缓存的模型、.env 文件以及 Chroma 持久化数据都不会消失。
Notebook 导出的 .py 不一定能直接执行
Colab 导出的 Python 文件可能包含:
!pip install ...
nodes = await pipeline.arun(...)其中 !pip 和顶层 await 都依赖 IPython/Jupyter 运行环境。学习课程时保留 .ipynb 更合适;需要进入正式项目时,再将验证后的单元格整理成普通模块和入口脚本。
本地有 GPU,不代表 PyTorch 使用 GPU
即使机器安装了 NVIDIA GPU,如果环境中的 PyTorch 是 CPU Build:
torch.version.cuda = None
torch.cuda.is_available() = FalseEmbedding 仍会全部在 CPU 上执行。性能排查应同时检查硬件、驱动和 PyTorch Build,而不是只看任务管理器中是否出现显卡。
小规模课程没有必要处理全部数据
如果后续只使用:
documents[:10]就没有必要先创建并读取 5000 个小文件。学习阶段可以在数据集加载后立即限制数量,以减少下载、磁盘 I/O 和 Embedding 时间。
总结
理解 LlamaIndex 的关键不是记住多少类名,而是分清各层职责:
Reader 把外部数据变成 Document
SentenceSplitter 把 Document 变成 Node
Embedding 把 Node 文本变成向量
IngestionPipeline 串联转换并写入 Vector Store
VectorStoreIndex 提供面向查询的索引抽象
Retriever 找到相关 Node
ResponseSynthesizer 决定如何让 LLM 使用这些 Node
QueryEngine 完成一次 RAG 问答
QueryEngineTool 把 QueryEngine 适配给 Agent
Agent 动态选择和编排 Tool
Workflow 使用 Event 控制确定性的流程如果应用只是单知识库问答,直接使用 QueryEngine 往往已经足够。只有在需要多种工具、动态路由、多次检索或复杂状态管理时,Agent 与 Workflow 才真正体现价值。
LlamaIndex 最重要的贡献可以归结为一句话:
它为“LLM 如何访问、检索和回答私有数据”建立了一套完整且可组合的领域模型。