拆解 LlamaIndex:数据 Ingestion -> Index -> Query 的管道设计
🦞 一只用 AI Agent 搭副业产线的程序员
上篇文章拆完 LangChain,有读者问我:“虾哥,我做 RAG 应用,到底用 LangChain 还是 LlamaIndex?”
这个问题问得好。LangChain 是通用框架,LlamaIndex 是专用框架——LangChain 什么都能干,LlamaIndex 只干一件事,但干到了极致:把非结构化数据变成 LLM 能检索的索引。
但我想挖的更深。不是"用哪个",而是"它们分别是怎样设计的"。这篇文章,我们拆开 LlamaIndex 的源码,看它那套 Ingestion -> Index -> Query 的数据管道。
项目简介
LlamaIndex(GitHub 36k+ Stars)是专门为 RAG(检索增强生成)场景设计的框架。它的核心工作流分三步:Ingestion(把各种格式的文档吃进去)-> Index(构建可检索的索引)-> Query(根据用户问题检索并生成答案)。听起来简单,但每一层都有精心设计的抽象。
架构全景
┌─────────────────────────────────────────────────────────────┐
│ Query Engine 层 │
│ "用户问了一个问题,我从索引里找到相关内容,拼给 LLM 回答" │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Retriever │ → │ Postprocessor │ → │ Response Synthesizer│ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Index 层 │
│ "把 Node 组织成可高效检索的数据结构" │
│ VectorStoreIndex · SummaryIndex · KeywordTableIndex · ... │
├─────────────────────────────────────────────────────────────┤
│ Ingestion 管道 │
│ "文档 → Node → Embedding → 写入索引" │
│ Reader → [Transformations] → Embedding → VectorStore │
├─────────────────────────────────────────────────────────────┤
│ 底层存储 │
│ Chroma · Pinecone · Weaviate · Qdrant · Milvus · ... │
└─────────────────────────────────────────────────────────────┘
关键设计一:Node——比 Chunk 多走了一步
做 RAG 的人都知道"切块"(chunking)很重要。绝大多数框架的做法是:文档 → 切块 → 存向量。LangChain 就是这个模型。
LlamaIndex 多了一个抽象:Node。
# llama_index/core/schema.py —— 核心数据结构
from dataclasses import dataclass, field
from typing import Dict, Any, List, Optional
@dataclass
class BaseNode:
"""所有 Node 类型的基类"""
id_: str # 全局唯一 ID
metadata: Dict[str, Any] # 元数据(来源、页数、作者...)
excluded_embed_metadata_keys: List[str] # 哪些元数据不参与 Embedding
excluded_llm_metadata_keys: List[str] # 哪些元数据不传给 LLM
relationships: Dict[str, "RelatedNodeInfo"] # 与其他 Node 的关系
text: str # 文本内容
embedding: Optional[List[float]] # 向量
@dataclass
class TextNode(BaseNode):
"""纯文本 Node"""
text_template: str = "{metadata_str}\n\n{content}"
@dataclass
class ImageNode(BaseNode):
"""图片 Node——多模态支持"""
image: Optional[str] = None
image_path: Optional[str] = None
Node 比普通 Chunk 多出来的东西:
- relationships:Node A 是 Node B 的"上一段",Node C 是 Node B 的"来源文档"。检索到 Node B 后,框架能沿着关系图找到邻居 Node,做上下文扩展。
- 元数据控制:
excluded_embed_metadata_keys让某些元数据只用于过滤、不参与向量相似度计算。比如"文档创建日期"对语义匹配没帮助,但对过滤有帮助。 - 多模态支持:
TextNode和ImageNode继承同一个BaseNode,索引不区分文本和图片——统一检索,按需返回。
设计洞察:Chunk 是"一段文本",Node 是"一段有上下文关系的文本"。多出来的关系信息,让检索结果的质量提升了一个档次——检索到一段相关文本后,能顺着
PREVIOUS/NEXT关系把前后文也拉出来。
关键设计二:IngestionPipeline——可组合的转换链
拿到文档后,怎么把它变成可以入库的 Node?LlamaIndex 的设计是 IngestionPipeline——一个由 Transformations 组成的管道。
# llama_index/core/ingestion/pipeline.py —— 核心管道
class IngestionPipeline:
"""文档摄取管道:Reader → Transformations → VectorStore"""
transformations: List[TransformComponent]
def run(
self,
documents: List[Document],
in_place: bool = True,
) -> List[BaseNode]:
nodes = documents # 起始:原始 Document
for transform in self.transformations:
nodes = transform(nodes) # 每个 Transform 处理并返回
return nodes
每一个 Transformation 都是一个可替换的组件:
# llama_index/core/ingestion/transformations.py —— 转换组件基类
class TransformComponent:
def __call__(self, nodes: List[BaseNode], **kwargs) -> List[BaseNode]:
"""同步调用"""
...
async def acall(self, nodes: List[BaseNode], **kwargs) -> List[BaseNode]:
"""异步调用"""
...
实际使用时,管道是这样组装的:
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.openai import OpenAIEmbedding
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=1024, chunk_overlap=200), # 切块
OpenAIEmbedding(model="text-embedding-3-small"), # 向量化
]
)
nodes = pipeline.run(documents=docs)
这个管道的精妙之处:每个 Transformation 只管自己的事,不关心上下文。你可以把 SentenceSplitter 换成 SemanticSplitter(按语义切块),把 OpenAIEmbedding 换成 HuggingFaceEmbedding——管道代码不动。跟 LangChain 的 __or__ 管道异曲同工,但 LlamaIndex 的管道是专门为数据摄取优化的,支持缓存、并行处理、断点续传。
设计洞察:管道的本质是"把复杂的串行流程拆成可独立替换的步骤"。LangChain 和 LlamaIndex 独立得出了这个结论——这不是巧合,是必然。
关键设计三:QueryEngine 的组装器模式
Index 建好了,怎么查?LlamaIndex 没有像 LangChain 那样用管道——它用了一个更灵活的"组装器"模式。
# llama_index/core/query_engine/ —— QueryEngine 的组装逻辑
from llama_index.core import VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.postprocessor import SimilarityPostprocessor
from llama_index.core.response_synthesizers import CompactAndRefine
# 方式 1:一行搞定(默认组合)
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()
# 方式 2:手动组装每个组件(灵活控制)
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=10, # 粗筛 10 条
)
postprocessor = SimilarityPostprocessor(
similarity_cutoff=0.75, # 精筛:相似度 > 0.75 才保留
)
synthesizer = CompactAndRefine(
llm=llm,
streaming=True,
)
query_engine = RetrieverQueryEngine(
retriever=retriever,
node_postprocessors=[postprocessor],
response_synthesizer=synthesizer,
)
response = query_engine.query("什么是 PagedAttention?")
这里的关键抽象是 QueryEngine 的三个插件化组件:
| 组件 | 职责 | 类比 |
|---|---|---|
| Retriever | 从索引里粗筛候选 Node | 搜索引擎的召回阶段 |
| NodePostprocessor | 对候选 Node 精筛/重排 | 搜索引擎的排序阶段 |
| ResponseSynthesizer | 把 Node 拼成 prompt 调用 LLM | 生成摘要 |
每个组件都是一个可插拔的接口。你可以把 VectorIndexRetriever 换成混合检索(向量 + BM25),把 SimilarityPostprocessor 换成 Reranker(用 Cross-Encoder 重排序),完全不影响其他部分。
核心代码拆解:from_documents 到底做了什么
新手最爱的一行代码是 VectorStoreIndex.from_documents(docs)。拆开看它背后发生了什么:
# llama_index/core/indices/vector_store/base.py —— 简化版
class VectorStoreIndex:
@classmethod
def from_documents(
cls,
documents: List[Document],
embed_model: Optional[BaseEmbedding] = None,
transformations: Optional[List[TransformComponent]] = None,
**kwargs,
) -> "VectorStoreIndex":
# Step 1: 运行 IngestionPipeline
pipeline = IngestionPipeline(
transformations=transformations or [
SentenceSplitter(),
embed_model or OpenAIEmbedding(),
]
)
nodes = pipeline.run(documents=documents)
# Step 2: 把 Node 的 embedding 写入向量库
index = cls(
nodes=nodes,
embed_model=embed_model,
**kwargs,
)
# Step 3: 构建 Node → Vector 的映射
# 本质上就是调 vector_store.add(embeddings, nodes)
return index
每一步都可以被替换。from_documents 是"一键默认",而不是"唯一路径"。当你需要自定义时,回到三步走的底层 API 重新组装就好。
你可以抄的作业
1. Node 的关系建模
如果你在做搜索结果展示(不只是 RAG),给每个"结果项"加上关系字段:上一项、下一项、来源、同类推荐。检索到一条结果后,沿着关系图扩展上下文,用户体验天差地别。
2. 管道优于一体化
LlamaIndex 的 IngestionPipeline 告诉你:任何超过 3 步的数据处理流程,都值得拆成一个可替换的管道。每步只做一件事,测试和优化都变得简单。
3. 组装器模式比继承树好
QueryEngine 的三组件不是通过继承(SummaryQueryEngine、RefineQueryEngine…)实现的,而是通过组合(把 Retriever + Postprocessor + Synthesizer 拼在一起)。当你有多种变体需要支持时,组合优于继承——这不是口号,这是 LlamaIndex 用代码证明的。
4. 默认要好,但要有逃生舱
from_documents 是一行 API,足够好用。但底层每步都能单独调。做框架最怕的就是"隐藏得太好,用户想改的时候改不了"。
最后
LlamaIndex 和 LangChain 的对比是个经典话题。我的看法:LangChain 赢了"通用编排",LlamaIndex 赢了"数据处理深度"。如果你只做 RAG,用 LlamaIndex;如果你要做 Agent + 多工具编排,用 LangChain。如果你两个都要——学它们的设计思路,自己写。
下一讲拆 Ollama。一行 ollama run llama3 背后,到底发生了什么?
本文拆解的 LlamaIndex 版本:v0.11.x。源码地址:github.com/run-llama/llama_index
🦞 一只用 AI Agent 搭副业产线的程序员
全平台同名:虾哥不加班 | 源码:GitHub - lobster-bujiaban
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)