如果说 LangGraph 更像流程层,那么 LlamaIndex 真正花力气的地方,是把数据层拆到足够细

做 Agent 或 RAG 时, 很多人最先想到的是编排。 先规划, 再检索, 再调用模型, 再决定是否重试。 这些当然重要。

但项目一旦真正落地, 你很快会发现另一个更硬的问题: 回答质量往往不是死在“流程不够聪明”, 而是死在“数据根本没被正确接进来、 切开、 索引、 过滤、 召回”。

这也是 LlamaIndex 的定位。 相较于 LangChain 更偏 orchestration, LlamaIndex 从一开始就更专注数据接入、 索引构建、 检索与回答合成。

LlamaIndexLlamaIndexLlamaIndexLlamaIndex is a data framework for LLM applications. It focuses on ingesting external data, transforming it into retrievable nodes, building indexes, then retrieving and synthesizing grounded responses for downstream chat or agent systems.

它不是完全不做编排, 但它看待问题的出发点明显不同。 LangChain 常常问的是“这一步之后接哪一步”, LlamaIndex 更常问的是“这些文档应该怎样被表示、 怎样被检索、 怎样被合成进最终回答”。

如果把 Agent 系统拆成两层, 一种非常常见的架构就是: LlamaIndex 负责数据层, LangChain 或 LangGraph 负责流程层。 前者让信息能被高质量地找到, 后者让任务能被有控制地执行。

核心抽象:从 Document 到 Response Synthesizer

LlamaIndex 值得学的地方, 不是某一个类名, 而是它把数据工作流拆得比较清楚。 你可以按文档进入系统后的生命周期来理解。

Document:原始文档对象

Document 是进入系统的原材料。 它通常对应一整篇文章、 一个 PDF、 一个数据库结果集, 或者某个 Notion 页面。

在这一层, 最重要的不是 embedding, 而是先把稳定的文档边界、 来源信息、 业务 metadata 带进来。

Node:可检索的切分单元

进入检索阶段后, 系统真正拿来召回的往往不是整篇 Document, 而是更细粒度的 Node。 Node 可以理解成切分后的文档块, 但它通常比“纯文本 chunk”多一些结构信息, 例如前后关系、 父子关系、 metadata、 引用来源。

NodeParser:切分器

NodeParser 决定一个 Document 如何被切成 Node。 这一步看似简单, 其实决定了后续检索质量的下限。

切太大, 召回命中率下降; 切太小, 上下文碎裂, 合成阶段又得费力补回来。

NodeParserNodeParserNodeParser 不是简单的 split 函数。它承担的是“如何把原始文档转成可检索节点”的策略职责,包括切块尺寸、重叠、层级关系和 metadata 继承。切分策略选错,后面再怎么换模型,召回效果也很难根本改善。

Index:面向检索的数据组织

Index 是系统为检索建立的数据结构。 它不等于向量库。 向量只是其中一种组织方式。 LlamaIndex 把不同索引类型都抽象成可统一调用的对象, 这让你能更清晰地按场景选型。

Retriever:负责找什么

Retriever 的职责很单纯: 给定查询, 返回一组相关 Node。 它不负责把答案写出来, 只负责“找”。

QueryEngine:负责怎么问

QueryEngine 可以看成更高层的入口。 它通常内部会驱动 retriever, 再把取回的内容交给后续合成。 因此它适合承载更完整的问答逻辑。

Response Synthesizer:负责怎么写答案

检索到了相关节点, 不代表系统自然就能给出好回答。 最后还需要一个阶段, 把多段上下文组织成可读、 有根据、 尽量不胡编的回答。

这就是 Response Synthesizer 的角色。

Response SynthesizerResponse SynthesizerResponse SynthesizerA response synthesizer is the stage that turns retrieved nodes into the final answer. It chooses how to combine evidence, how much context to expose to the model, and whether to refine, summarize, or cite sources while composing the response.

把这几个抽象分开, 工程收益很大。 你可以只替换切分器, 不动检索器; 也可以保持 retriever 不变, 单独更换合成策略。 这比所有逻辑都塞进一条黑盒链里更容易调参。

一个最小但完整的数据侧示例

下面用 llama-index 0.10+ 的写法, 搭一个从文档读取、 切分、 建索引、 查询、 持久化的最小示例。

from llama_index.core import (
    Document,
    Settings,
    StorageContext,
    VectorStoreIndex,
    load_index_from_storage,
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI


# 全局设置:
# 真实项目里也可以按索引或按查询对象局部指定。
Settings.llm = OpenAI(model="gpt-4o-mini")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")


# 1. 先准备原始文档,并在 metadata 中保留来源与业务标签。
documents = [
    Document(
        text="LangGraph 更强调显式状态机、checkpoint 与多步编排。",
        metadata={"source": "note-1", "topic": "orchestration"},
        doc_id="doc-langgraph",
    ),
    Document(
        text="LlamaIndex 更强调文档接入、节点切分、索引与检索组合。",
        metadata={"source": "note-2", "topic": "data-layer"},
        doc_id="doc-llamaindex",
    ),
]


# 2. NodeParser 决定切分策略。
# 这里用 SentenceSplitter 演示最常见的句段切分。
parser = SentenceSplitter(chunk_size=128, chunk_overlap=20)
nodes = parser.get_nodes_from_documents(documents)


# 3. 用切好的 nodes 构建向量索引。
# VectorStoreIndex 会负责 embedding 和底层索引构建。
index = VectorStoreIndex(nodes)


# 4. 检索器只负责找相关节点。
retriever = index.as_retriever(similarity_top_k=2)
retrieved_nodes = retriever.retrieve("谁更偏向数据接入与索引?")
for node in retrieved_nodes:
    print(node.score, node.text, node.metadata)


# 5. QueryEngine 在 retriever 之上再做回答合成。
query_engine = index.as_query_engine(similarity_top_k=2)
response = query_engine.query("谁更偏向数据接入与索引?")
print(response)


# 6. 把索引持久化到磁盘,后续可直接加载。
index.storage_context.persist(persist_dir="./storage/llamaindex_demo")


# 7. 重新启动进程后,不必重建 embedding,可从磁盘恢复。
storage_context = StorageContext.from_defaults(
    persist_dir="./storage/llamaindex_demo"
)
loaded_index = load_index_from_storage(storage_context)
loaded_engine = loaded_index.as_query_engine(similarity_top_k=2)
print(loaded_engine.query("哪个框架更偏流程编排?"))

这个示例最值得注意的, 是“索引构建”和“查询入口”被明确拆开了。 你不是把文档一股脑丢进一个万能黑盒, 而是可以分别看到: 文档如何变成节点, 节点如何进入索引, 检索器如何只做召回, QueryEngine 如何再向上提供完整问答接口。

四类索引各自适合什么场景

很多人提到 LlamaIndex, 第一反应是 VectorStoreIndex。 它当然最常用, 但并不是所有场景都该直接向量化。

VectorStoreIndex

这是最通用的一类。 适合开放式问答、 语义相似检索、 知识库检索增强生成。 当你的查询表达与原文措辞不完全一致, 但语义相关, 向量索引通常最有用。

SummaryIndex

SummaryIndex 更适合顺序浏览和摘要型任务。 如果你关心的是把一批文档整体看过之后给出概览, 而不是高精度地从海量片段里找针, 它往往比纯向量检索更直接。

TreeIndex

TreeIndex 适合层次化组织与递归聚合。 当文档体量大、 你又希望先在局部摘要、 再逐层汇总时, 树形结构有优势。

KnowledgeGraphIndex

KnowledgeGraphIndex 更适合实体关系明确、 查询经常围绕“谁和谁是什么关系”展开的场景。 例如企业组织结构、 论文引用关系、 设备依赖图。

索引不是越通用越好索引不是越通用越好很多团队会把所有数据都塞进同一类向量索引,因为最省事。但检索问题有多种形态:有的偏语义相似,有的偏层次摘要,有的偏实体关系。LlamaIndex 的价值之一,就是提醒你把“信息结构”也纳入设计,而不是默认所有问题都该用同一把锤子。

真正的经验是按问题类型选索引。 你当然可以只用 VectorStoreIndex 跑完整个项目, 但那更像一种起步策略, 不是终局答案。

高级检索:把“找到更多”升级为“找到更对”

普通相似度检索能工作, 但很多业务一旦深入, 就会碰到召回不完整、 问题拆解不足、 父子块割裂、 关键词和语义各自漏召的问题。

LlamaIndex 在这方面提供了一批很有代表性的高级检索组件。

Auto-Merging Retriever

当底层切分较细时, 单个小块命中了, 它附近的兄弟块常常也有价值。 Auto-Merging Retriever 的思路就是: 先细粒度命中, 再按父子或邻接关系合并回更完整的上下文。

它适合大文档被切得比较碎、 但回答又需要较完整段落支撑的场景。

Recursive Retriever

有些知识不是平铺在一个层级上。 一个节点可能只是目录、 摘要、 表头或二级引用, 真正的细节在它指向的下一层。 Recursive Retriever 会沿着这些引用关系继续下钻。

这很适合多层知识库、 父子文档体系、 或“先从摘要命中,再进明细”的结构。

Sub-Question Query Engine

复杂问题往往不是一个检索器一次就能搞定。 例如“比较 LangGraph 与 LlamaIndex 在编排层和数据层的差异”, 其实隐含了两个子问题。 Sub-Question Query Engine 会先把大问题拆成子问题, 再分别走各自的数据源或查询器, 最后合并答案。

from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool


orchestration_tool = QueryEngineTool.from_defaults(
    query_engine=orchestration_index.as_query_engine(),
    name="orchestration_docs",
    description="用于回答编排层相关问题",
)

data_tool = QueryEngineTool.from_defaults(
    query_engine=data_index.as_query_engine(),
    name="data_docs",
    description="用于回答数据层相关问题",
)

sq_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=[orchestration_tool, data_tool]
)

response = sq_engine.query("比较 LangGraph 和 LlamaIndex 的侧重点差异")
print(response)

这个能力的重点不是“会拆问题”本身, 而是它让多索引、 多知识域的系统不必强行合并成一个大库。

很多真实查询同时包含语义和关键词约束。 例如人名、 错误码、 产品型号、 专有缩写。 纯向量检索对这些词未必稳定, 纯关键词检索又无法覆盖改写后的自然语言表达。

这时 Hybrid Search 往往更现实: 把向量召回和关键词召回结合, 再重排。 在 LlamaIndex 里, 它通常通过对接底层支持混合检索的向量库或自定义 retriever 实现。

Hybrid SearchHybrid SearchHybrid SearchHybrid search combines lexical signals such as BM25 or keyword matching with dense vector similarity. It is useful when queries include exact identifiers, names, or codes that dense retrieval alone may rank unreliably.

数据接入:Reader 生态为什么重要

很多数据项目不是死在模型, 而是死在接入。 PDF 提不干净, Notion 页面层级丢了, 数据库导出没有主键, 文件更新时间和权限信息也没带进来。

LlamaIndex 的 Reader 生态, 本质上是在解决“把外部世界的数据以可继续处理的形式接进来”。 过去大家常说 LlamaHub, 现在更多是各类 Reader 与 Integration 的集合。

常见场景大致如下。

  • PDF:适合论文、合同、报告,但要注意版面解析质量与页码 metadata。
  • Notion:适合团队知识库,但要保留页面层级、更新时间和 block 结构。
  • 数据库:适合结构化业务数据,但必须设计稳定主键和更新时间字段,便于增量同步。
  • 文件系统与对象存储:适合批量文档接入,但要处理目录层级、重复文件与删除同步。

下面给一个很小的 Reader 示例。

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex


# 读取本地目录下的 PDF、Markdown、TXT 等文件。
# 真正项目里也可以替换为 Notion、Confluence、SQL 等 Reader。
reader = SimpleDirectoryReader(
    input_dir="./knowledge_base",
    recursive=True,
    filename_as_id=True,
)
documents = reader.load_data()

index = VectorStoreIndex.from_documents(documents)
engine = index.as_query_engine(similarity_top_k=3)
print(engine.query("知识库里如何描述 checkpoint 持久化?"))

Reader 生态的价值在于, 它把“怎么读进来”从业务逻辑里剥离了。 你的编排层不必关心 Notion API 分页怎么做, 只需要面对标准化后的 Document。

与 LangChain 的协作模式

把 LlamaIndex 和 LangChain 对立起来看, 往往会把问题看窄。 更常见的生产做法其实是协作。

一种很稳的分层是: LlamaIndex 负责 Reader、 NodeParser、 Index、 Retriever、 QueryEngine; LangChain 或 LangGraph 负责工具编排、 多步决策、 Human-in-the-Loop、 checkpoint、 多 Agent 协作。

这时 LlamaIndex 输出的不是终局系统, 而是一个高质量的数据接口。 例如你可以把 query_engine.query(...) 封装成 LangGraph 里的一个节点, 也可以把 retriever 封装成工具, 让上层 Agent 决定何时调用。

这种分工比“一个框架包打天下”更稳。 因为数据层和编排层优化目标本来就不同。

生产关键点:持久化、增量更新、文档 ID、metadata 过滤

真正把 LlamaIndex 用顺, 靠的不是第一次建索引, 而是后续怎么维护。

Index 持久化

如果每次启动都重新读取全部文档、 重新切分、 重新 embedding, 系统很快会变得又慢又贵。 所以索引持久化是第一步。

最简单的做法是本地目录持久化, 适合单机原型。 再往上通常要接外部向量库、 对象存储、 关系型数据库或专门的 docstore。

增量更新

业务数据不是一次性静止的。 新文档会来, 旧文档会修改, 有些文档还会被删除。

因此接入层必须保留稳定文档 ID、 更新时间、 版本信息。 只有这样你才能判断是新增、 重建还是删除旧节点。

文档 ID 管理

很多检索系统后期最痛的不是召回, 而是发现自己根本不知道某条 Node 来自哪一版原文。 doc_id 不应该临时生成随机值就算了, 而应尽量映射到业务里的稳定主键。

文档 ID 管理文档 ID 管理文档 ID 不是为了让程序不报错,而是为了让索引能被维护。没有稳定 ID,你几乎无法安全地做增量更新、去重、删除同步和来源追踪。后期很多‘脏索引’问题,本质上都来自一开始把 ID 当成无关紧要的细节。

metadata 过滤

实际生产里的检索很少是“在全库里裸搜”。 常见限制包括租户、 部门、 语言、 时间范围、 文档类型、 权限范围。

如果 metadata 在接入阶段没有设计好, 后面即使索引精度不错, 也容易把不该召回的内容混进来。 因此 metadata 不是附属字段, 而是检索质量和权限边界的一部分。

本篇要点

  • LlamaIndex 的核心定位更偏数据层:接入、切分、索引、检索与合成,而不是把多步编排放在第一位。
  • DocumentNodeNodeParserIndexRetrieverQueryEngineResponse Synthesizer 形成了一条清晰的数据处理链。
  • VectorStoreIndex 最常用,但 SummaryIndexTreeIndexKnowledgeGraphIndex 分别适合摘要、层次聚合和关系型知识场景。
  • 高级检索的重点不是召回更多,而是召回更对;Auto-Merging、Recursive、Sub-Question 与 Hybrid Search 都是在不同问题结构下修正普通相似度检索的不足。
  • Reader 生态决定数据能否以可维护的方式进入系统;生产里必须认真处理持久化、增量更新、稳定文档 ID 与 metadata 过滤。
  • 与 LangChain 或 LangGraph 最稳的关系通常不是二选一,而是让 LlamaIndex 做数据层,让上层框架做编排层。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:20. LlamaIndex 数据侧深度

本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/20-LlamaIndex数据侧深度/

本文最后一次更新为 天前,文章中的某些内容可能已过时!