跳到正文

LlamaIndex 与检索框架 ​

一句话:LlamaIndex 把“文档 → 带关系的 Node → 专用索引 → 可组装的查询引擎”做成一条检索流水线;它换来的是检索策略的可替换与可评测,代价是索引与 Node 模型比“切块 + 向量库”更重,简单场景反而多一层抽象。

适用版本:LlamaIndex 0.12+(2026 年)。包结构与 API 迭代较快,类名以官方文档为准。

为什么需要 ​

用 LangChain 搭 RAG,常见写法是:加载文档、切块、写向量库、similarity_search、拼 prompt。链路短,但一旦要回答下面这些问题,代码就开始分叉:

  • 这段文本属于哪一章、前后文是什么?切块之后关系丢了,只能靠 metadata 手工补。
  • 用户问“对比 A 和 B”,单个 chunk 装不下,召回哪一块都不完整。
  • 同一个问题换个说法就召回不到,要不要先改写查询?
  • 检索回来 20 条,哪些该合并、哪些该丢、哪些该扩上下文?
  • 换一个索引或重排器,业务代码要不要重写?

LlamaIndex 的答案不是“更好的向量库”,而是把检索拆成四个可替换的位置:数据模型(Node)、索引(Index)、检索器(Retriever)、响应合成(Synthesizer)。你可以只换其中一段,其余不动;也正因为拆得细,理解成本比一条 similarity_search 高。

核心机制 ​

Node:检索的最小单位不是 chunk ​

Node 是 LlamaIndex 的核心数据结构,至少包含四类信息:

字段作用工程意义
text / text_resource节点正文送去 embedding 或直接进 prompt 的内容
metadata标题、章节、来源、时间、权限过滤、引用、审计的依据
relationships父节点、子节点、前后节点、来源文档召回小块、补全大块;支持层级检索
excluded_embed_metadata_keys / excluded_llm_metadata_keys哪些元数据不参与向量化 / 不进 prompt避免作者、修改时间等噪声占据向量与 token

它和“文本块”最大的区别是关系是一等字段。切分时同时记录 parent/child/prev/next,查询时才可能做到“命中一句、返回整节”。代价是 Node 比 chunk 大,存储与序列化成本更高;关系维护错了,比没有关系更难排查。

IngestionPipeline:转换链 + 缓存 ​

摄取不是一次 split_text,而是一条转换链:解析 → 清洗 → 切分 → 抽元数据 → 去重 → embedding。IngestionPipeline 把每一步做成可插拔的 transformation,并用 docstore 缓存“这段输入已经处理过”。

缓存的关键是 Node hash:内容或元数据变了,hash 变,才重新处理。这里有两个生产坑:

  • 静默丢数据:批处理中途失败,已写入的部分没有回滚,后续统计却按“全部成功”算。要分批提交、记录 checkpoint,并用“输入文档数 = 产出 Node 数 + 明确跳过数”对账。
  • 缓存命中错误:hash 只算正文、不算 embedding 模型版本,换模型后仍命中旧向量。把模型名与版本纳入缓存键。

五类索引:查询模式决定索引结构 ​

索引适合的查询代价
VectorStoreIndex语义相似、开放问答精确匹配弱;需要 embedding 成本
SummaryIndex / ListIndex“把全文总结一下”、穷举全量扫描,长文档会撑爆上下文
KeywordTableIndex术语、实体、精确关键词依赖关键词抽取质量,同义词弱
TreeIndex层级归纳、从粗到细构建成本高,树结构错了难修
KnowledgeGraphIndex多跳关系、实体关联抽取与消歧成本最高,噪声会沿边传播

没有一种索引能同时覆盖语义、精确与关系。生产上常用组合:向量召回 + 关键词召回,再用融合或重排合并;而不是指望把 chunk 调小就能解决所有问题。

QueryEngine 是组装出来的 ​

一个 QueryEngine 由三段组成:

text
QueryTransform → Retriever → NodePostprocessor → ResponseSynthesizer
  • QueryTransform:HyDE(先让模型写假设答案再检索)、Multi-Query(一个问题生成多个变体)、Step-back(先问更上位的问题)。它们解决的是“查询与文档表述不一致”,代价是每次查询多一次或多次模型调用。
  • Retriever:向量、关键词、树、知识图谱、递归检索各自封装;RouterQueryEngine 先判断问题该去哪个索引。
  • NodePostprocessor:相似度阈值、去重、重排、元数据替换、上下文扩展。召回宽、精排严的两级漏斗就落在这里。
  • ResponseSynthesizer:refine 逐条迭代、compact 压缩后迭代、tree_summarize 分层归纳、simple_summarize 一次性塞入。默认常用 compact:在 token 与完整性之间取折中。

一个问题进来,在哪一段被改写、在哪一段被收敛、在哪一段决定 token 成本?

读图顺序是:先在 T 段判断“问题要不要改写”,再由 R 段决定召回范围,P 段把候选收敛到能进 prompt 的数量,S 段决定 token 与完整性的折中。线上排查也按这四段定位:召回为空看 R,召回相关但答案错看 P 与 S。

“组装”而不是“继承”的价值在于:换重排器不影响索引,换索引不影响合成策略。代价是默认参数分散在四层,线上排查必须知道问题出在哪一段。

三种高级检索,各解一类结构问题 ​

模式解决什么做法边界
Sentence Window小块召回准、但上下文不够用句子建索引,命中后用 MetadataReplacementPostProcessor 换成前后窗口窗口太大 token 暴涨;跨段指代仍可能丢
Auto-Merging层级文档召回粒度不对同时建大/中/小三档块,命中子块达到阈值就合并到父块阈值与块大小要按文档结构调;层级错了合并也错
Recursive Retrieval多数据源先路由再精查为每个数据源建摘要节点与子引擎,先选源再下钻源很多时路由本身成为瓶颈;摘要质量决定上限

三者不是替代关系:窗口解决“上下文”,合并解决“粒度”,递归解决“范围”。选错模式,调参只会让错误更稳定。

组合部署:LlamaIndex 做检索、LangGraph 做编排 ​

痛点:一条 similarity_search 写进业务代码后,每次换策略(加路由、上层级索引、改重排)都要动编排侧代码——检索与编排耦合的账会在第三次需求变更时到期。四段拆分的价值在此兑现:

检索侧的四段(改写→召回→精排→合成)全部封在工具里,编排侧只看到 invoke(query) -> list[Document]——两边的版本可以独立演进,这是"组装而不是继承"在跨框架场景下的第二次兑现。

实战场景 ​

场景一:LlamaIndex 检索器接进 LangChain Agent(转换层十行,坑在字段映射) ​

痛点:两套框架对接最常见三种死法——业务代码调了 query() 导致两次生成(LlamaIndex 合成一次、主 Agent 再合成一次,费用翻倍且引用断在中间);Node 对象直接塞进消息历史,checkpoint 表随会话指数膨胀;跨框架引用只存在 metadata 里,主 Agent 生成时根本带不走。封装层就是要一次写对、处处复用:

python
@tool
def search_manual(query: str, runtime) -> str:
    """查询产品手册与操作指南。政策与退款类问题用 search_policy,不要用本工具。"""
    nodes = manual_engine.retrieve(query)                    # 只检索,不合成——合成留给主 Agent
    docs = [(n.node.text, n.node.metadata) for n in nodes[:6]]
    return "\n\n".join(f"[{m['doc_id']}#{m['page']}] {t[:800]}" for t, m in docs)

四个实战决定:retrieve 而不是 query(LlamaIndex 的 ResponseSynthesizer 再合成一次,等于两次生成,成本翻倍且引用断裂);返回截断后的纯文本(Node 对象直接进消息历史会让 checkpoint 膨胀,见 05 篇);doc_id#page 引用锚写进返回文本而不是 metadata(主 Agent 生成时才带得走);docstring 写清与另一个检索工具的分工边界——两个都"能答"的问题会随机路由。

场景二:Auto-Merging 修"对比类问题答不全" ​

现象:用户问"V2 和 V3 的审批流程差别",向量检索只命中某一节,答案各说一半。改造:手册按 章/节/段 三档同时入库,flat_param 相似度阈值调参的实战口径是先看命中率再看 token——阈值调松合并到章级,一次召回三五万 token,生成延迟从 2s 到 9s;评测集里放 20 条对比类问题,Recall@6 与平均输入 token 一起报,不许只报其一。

排障:trace 打开再归因(抽象层掩盖成本的真实形态) ​

一次 query() P95 从 1.8s 涨到 6s,逐段拆:QueryTransform(Multi-Query) 3 次改写 ≈ 0.9s,重排 cross-encoder 批量退化 ≈ 2.4s,refine 合成随候选数线性 ≈ 1.7s。三段各自的修法完全不同(改写缓存、重排降 k、合成换 compact)——不开 trace 就只能整体换方案,而换方案往往只解决其中一段。这就是"抽象层会掩盖成本"边界的实操含义:监控必须按四段分位数建,不是按端到端一个数。

最小上手 ​

python
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter

documents = SimpleDirectoryReader("./docs", recursive=True).load_data()
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=64)
index = VectorStoreIndex.from_documents(documents, transformations=[splitter])

engine = index.as_query_engine(similarity_top_k=6)
print(engine.query("退款流程里哪些步骤需要人工审批?"))

这段代码只覆盖“能跑”。生产还要补:元数据过滤(租户、权限、时间)、混合检索、重排、引用返回、评测集。检索链路的评测与失败归因见 RAG 检索基础 与 RAG 进阶;LangChain 侧的组件抽象见 RAG 与文档检索。

生产动作 ​

  • 先定评测集,再换索引:至少 100 条“问题 + 应命中文档 ID”,同时看 Recall@k 与最终答案正确率;只看向量相似度会把“召回错但看起来像”当成成功。
  • 元数据即权限:租户、密级、有效期必须在检索前过滤,不能召回后再删;Node 的 metadata 要能追溯到源文档与版本。
  • 缓存要带版本:embedding 模型、切分参数、抽取 prompt 任一变化都必须让缓存失效,否则会出现“改完没效果”的假象。
  • 监控四段而非一端:摄取成功率、索引大小与去重率、检索延迟 P95、合成 token 成本。出现问题时先定位是哪一段退化,再调参。
  • 控制并发与重试:向量库与模型 API 的限流按租户隔离;重试只覆盖 429/超时/5xx,并带指数退避与幂等键。LlamaIndex 的内置重试不能替代业务级重试策略。

边界 ​

  • 它不是 Agent 框架:LlamaIndex 也有 Agent/Workflow 能力,但核心优势在检索与索引;需要复杂状态图、暂停恢复、多 Agent 协作时,LangGraph 一类图运行时更合适。常见组合是“LlamaIndex 做检索工具,LangGraph 做编排”。
  • 抽象层会掩盖成本:一次 query() 背后可能是查询改写、向量化、重排、多次合成;不打开 trace,很容易把延迟和费用归错因。
  • Node 关系不是免费的:关系越多,摄取越慢、存储越大、迁移越难;文档结构稳定、查询模式明确时才值得建层级。
  • 简单场景不必上:单文档、固定问答、几万条以内的知识库,切块 + 向量库 + 阈值过滤往往更直接;先证明检索是瓶颈,再引入框架。
  • 选型看查询模式,不看功能清单:语义为主选向量,术语为主加关键词,多跳关系才考虑知识图谱;把“支持”当成“适合”是选型最常见的错误。

讲清楚 ​

LlamaIndex 的核心不是向量库封装,而是把检索拆成 Node、Index、Retriever、Synthesizer 四个可替换的位置。Node 除了正文还带元数据和父子/前后关系,所以能“命中一句、返回整节”;摄取是一条带缓存的转换链,缓存键必须包含模型与切分版本,否则会静默命中旧结果。

索引按查询模式选:语义用向量,穷举用列表,术语用关键词,层级归纳用树,多跳关系才上知识图谱;生产上更常见的是向量 + 关键词混合,再用重排收敛。QueryEngine 是组装出来的:查询改写解决表述差异,检索器决定召回,后处理做阈值/重排/扩上下文,合成器决定 token 与完整性的折中。Sentence Window、Auto-Merging、Recursive Retrieval 分别解决上下文、粒度、范围三类问题。

它的价值在“换一段不影响其余”,代价是四层默认参数分散、排查要开 trace。简单场景直接用切块 + 向量库更省;需要复杂编排时,把 LlamaIndex 当检索工具接进 LangGraph,而不是让它同时承担编排职责。

适用:LlamaIndex 0.12+ / 2026 年检索框架实践 | 初稿 2026-10

把 AI 工程讲到既懂也会用