第一部分 · 理解与 RAG
第1章 RAG 五步流程
🔥高频
RAG 全景与加载
Building an LLM application | Developer Documentation Introduction to RAG | Developer Documentation globalQ什么是 RAG?它在整体上是如何工作的?为什么需要它?深挖·拓展🔥高频
答 RAG(Retrieval-Augmented Generation,检索增强生成)要解决的核心问题是:LLM 虽然在海量数据上训练,但并没有在你自己的数据上训练过,因此无法回答关于你私有数据的问题。RAG 的机制是把你的数据"加"到 LLM 已经能访问的数据上——先把你的数据加载并"索引化"(indexed)以便查询;用户查询作用在索引上,索引把你的数据过滤到最相关的上下文(context);这段上下文连同用户查询和一个 prompt 一起送给 LLM,LLM 据此生成回答。这样做的权衡在于:相比重新训练/微调模型,RAG 通过外部检索注入知识,成本更低、更新更快,且能引用最新或私有数据;代价是引入了检索质量这一新变量——检索到的上下文是否相关直接决定回答质量。文档特别强调,即便你构建的是 chatbot 或 agent,也需要掌握 RAG 技术来把数据喂给应用,因为 query engine、chat engine 和 agent 常常用 RAG 来完成任务。
术语
RAG(检索增强生成,把私有数据加到 LLM 可访问的数据上); index(索引,把数据结构化以支持查询); context(上下文,被过滤出的最相关数据片段); prompt(提示词,与 context 和 query 一起送入 LLM)📖 "LLMs are trained on enormous bodies of data but they aren’t trained on your data. Retrieval-Augmented Generation (RAG) solves this problem by adding your data to the data LLMs already have access to." — Introduction to RAG | Developer Documentation
📖 "In RAG, your data is loaded and prepared for queries or “indexed”. User queries act on the index, which filters your data down to the most relevant context. This context and your query then go to the LLM along with a prompt, and the LLM provides a response." — Introduction to RAG | Developer Documentation
🧪 实例 RAG 的基本数据流可以用下图概括:
flowchart LR A[你的数据] -->|Loading| B[Index 索引] Q[用户 Query] --> B B -->|过滤出最相关 context| C[Context + Query + Prompt] C --> D[LLM] D --> E[Response 回答]
🔍 追问 RAG 和直接微调(fine-tuning)模型相比,主要优势是什么? → 根据文档,RAG 是"把你的数据加到 LLM 已有数据上"的方式,无需重新训练即可让模型使用你的私有/最新数据;query engine、chat engine、agent 都常用 RAG 来完成任务。
📚 拓展阅读
- Introduction to RAG — RAG 概念与五阶段总览的原始页面
- Building an LLM application — 把 RAG 放进整个 agentic 应用构建流程的上层视角
- LlamaHub — 数百个数据连接器,是 RAG"把数据喂进来"的入口
QRAG 的五个关键阶段是什么?每个阶段各做什么?深挖·拓展🔥高频
答 文档明确指出 RAG 内部有五个关键阶段(five key stages),它们同时也是你构建的更大应用的组成部分:Loading(加载)——把数据从它所在的地方(文本文件、PDF、网站、数据库或 API)拿进你的 workflow,LlamaHub 提供数百个连接器可选;Indexing(索引)——创建一种能支持查询的数据结构,对 LLM 而言几乎总是意味着创建
vector embeddings(数据含义的数值表示)以及其它元数据策略,以便准确找到语境相关的数据;Storing(存储)——数据被索引后几乎总要把索引和其它元数据存下来,以避免重复索引;Querying(查询)——对任一索引策略都有多种利用 LLM 和 LlamaIndex 数据结构去查询的方式,包括 sub-queries、multi-step queries 和 hybrid strategies;Evaluation(评估)——任何流程的关键一步,用来客观衡量回答的准确性、忠实度和速度,并在你做改动或对比不同策略时提供客观度量。这五步的设计逻辑是把"数据进来 → 结构化 → 持久化 → 取出来用 → 度量效果"拆成可独立优化的环节,每个阶段的取舍(如索引策略、检索策略)都直接影响最终质量与成本。术语
Loading(加载,取数据进 workflow); Indexing(索引,建可查询结构,常为 vector embeddings); Storing(存储,持久化索引避免重复索引); Querying(查询,含 sub/multi-step/hybrid 策略); Evaluation(评估,度量准确/忠实/速度)📖 "There are five key stages within RAG, which in turn will be a part of most larger applications you build." — Introduction to RAG | Developer Documentation
📖 "Evaluation: a critical step in any flow is checking how effective it is relative to other strategies, or when you make changes. Evaluation provides objective measures of how accurate, faithful and fast your responses to queries are." — Introduction to RAG | Developer Documentation
🧪 实例 五阶段的先后关系:
flowchart LR L[Loading] --> I[Indexing] --> S[Storing] --> Q[Querying] --> E[Evaluation]
🔍 追问 为什么 Storing 阶段几乎总是必要的? → 因为数据被索引后重新索引代价高,文档指出你几乎总会想把索引及其它元数据存下来,"to avoid having to re-index it"。
📚 拓展阅读
- Stages within RAG — 五阶段定义的原文
- Indexing and Embedding — Indexing 阶段的深入教程
- Storing — Storing 阶段的深入教程
- Querying — Querying 阶段的深入教程
QLlamaIndex 中 Document 和 Node 有什么区别和联系?深挖·拓展🔥高频
答 在 LlamaIndex 的数据模型里,
Document 是围绕任意数据源的一个"容器"——比如一个 PDF、一段 API 输出、或从数据库检索的数据;而 Node 是 LlamaIndex 中数据的原子单位,代表源 Document 的一个"chunk"(块)。二者的联系是层级与派生关系:Node 带有元数据,把自己关联到所属的 Document 以及其它 Node;在加载文档后,转换(transformation)步骤会把 Document 切分成一个个 Node,这些 Node 与其父 Document 保持关系。文档还给出一个关键实现细节:Document 其实是 Node 的子类(a Document is a subclass of a Node),转换的输入/输出都是 Node 对象。这样设计的意义在于:Document 负责"承载完整来源+来源级元数据",Node 负责"可被检索、可喂给 LLM 的 bite-sized 片段",检索和向量化都发生在 Node 粒度上,从而在保留来源可追溯性的同时,让相关性过滤更精细。术语
Document(文档,任意数据源的容器,是 Node 的子类); Node(节点,数据的原子单位,Document 的一个 chunk); metadata(元数据,把 Node 关联回 Document 及其它 Node); chunk(数据块,便于检索和喂给 LLM 的小片段)📖 "ADocumentis a container around any data source - for instance, a PDF, an API output, or retrieve data from a database. ANodeis the atomic unit of data in LlamaIndex and represents a “chunk” of a sourceDocument. Nodes have metadata that relate them to the document they are in and to other nodes." — Introduction to RAG | Developer Documentation
📖 "Transformation input/outputs areNodeobjects (aDocumentis a subclass of aNode)." — global
🧪 实例 你也可以绕过 loader,直接手动构造 Node 并传给索引器:
python
from llama_index.core.schema import TextNode
node1 = TextNode(text="<text_chunk>", id_="<node_id>")
node2 = TextNode(text="<text_chunk>", id_="<node_id>")
index = VectorStoreIndex([node1, node2])🔍 追问 Node 上的 metadata 有什么作用? → Node 的 metadata 把它关联到所属 Document 以及其它 Node("relate them to the document they are in and to other nodes"),维系了 chunk 与来源、chunk 之间的关系,支撑可追溯和更准确的检索。
📚 拓展阅读
- Nodes and Documents — Document 与 Node 数据模型模块指南
- how to customize Nodes — 自定义 Node 的用法指南
- how to customize Documents — 自定义 Document 的用法指南
Q在 LlamaIndex 中把数据加载进来有哪几种方式?各自适用什么场景?深挖·拓展🔥高频
答 LlamaIndex 加载数据的核心机制是 data connectors(也叫
Reader):它从不同数据源、不同格式摄取数据,并把数据格式化成 Document 对象。实践上主要有三种方式。第一种、最简单的是 SimpleDirectoryReader,它内建于 LlamaIndex,会把给定目录里的每个文件都变成 documents,支持 Markdown、PDF、Word、PowerPoint、图像、音频、视频等多种格式,适合本地目录快速上手。第二种是从 LlamaHub 下载 Reader:因为数据来源太多不可能全部内建,你从连接器注册表 LlamaHub 下载所需连接器,例如 DatabaseReader 会对 SQL 数据库运行查询、把结果的每一行作为一个 Document 返回,适合数据库、API 等外部/专用数据源。第三种是直接创建 Document:不经过 loader,用 Document(text="text") 直接构造,适合你已经手里有文本、只想快速塞进流程的场景。取舍在于:SimpleDirectoryReader 零配置但通用;LlamaHub Readers 覆盖面广(数百个连接器)但需按需下载;直接构造最灵活但要你自己准备好数据。术语
data connector/Reader(数据连接器,摄取数据并格式化为 Document); SimpleDirectoryReader(目录读取器,把目录内每个文件变成 documents); DatabaseReader(数据库连接器,把 SQL 查询每行作为一个 Document); LlamaHub(数据连接器注册表,数百个连接器)📖 "The easiest reader to use is our SimpleDirectoryReader, which creates documents out of every file in a given directory. It is built in to LlamaIndex and can read a variety of formats including Markdown, PDFs, Word documents, PowerPoint decks, images, audio and video." — global
📖 "Data connectors ingest data from different data sources and format the data intoDocumentobjects. ADocumentis a collection of data (currently text, and in future, images and audio) and metadata about that data." — global
🧪 实例 最简单的目录加载,一行搞定:
python
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("./data").load_data()🔍 追问 为什么大量连接器不直接内建,而要放在 LlamaHub 让用户下载? → 因为可以取数据的地方实在太多("there are so many possible places to get data, they are not all built-in"),内建不现实,所以做成可按需下载的注册表 LlamaHub。
📚 拓展阅读
- Loading Data (Ingestion) — 加载与 ingestion 的原始教程页
- DatabaseReader — 对 SQL 库查询并按行返回 Document 的连接器
- LlamaHub — 数百个数据连接器的注册表
- Connectors — data connector / Reader 的模块指南
Q数据加载进来之后为什么还要 Transformation?它包含哪些步骤?深挖·拓展中频
答 加载只是第一步。整个 ingestion pipeline 通常由三个主要阶段组成:1) 加载数据、2) 转换数据、3) 索引并存储数据。数据加载后、放入存储系统之前,必须先处理和转换,这些 transformations 包括分块(chunking)、抽取元数据(extracting metadata)和为每个 chunk 生成 embedding。文档把它类比为 ML 世界里的数据清洗/特征工程,或传统数据领域的 ETL 流水线——目的是确保数据能被检索、并被 LLM 最优地使用。其中最关键的一步是把 documents 切成"chunks"/Node 对象:核心思想是把数据处理成 bite-sized(小口)的片段,以便检索并喂给 LLM。这背后的权衡是 chunk 粒度:块太大检索不精准、超出上下文;块太小又可能割裂语义,所以要通过 text splitter(如
SentenceSplitter、TokenTextSplitter)控制 chunk_size 和 chunk_overlap。此外 transformations 的输入输出都是 Node 对象,可以堆叠(stacked)和重排序(reordered),给了流水线很大的组合灵活性。术语
ingestion pipeline(摄取流水线,加载→转换→索引存储三阶段); transformation(转换,含 chunking、抽取元数据、embedding); chunking/text splitter(分块/文本切分器); chunk_size/chunk_overlap(块大小/块重叠,控制切分粒度)📖 "After the data is loaded, you then need to process and transform your data before putting it into a storage system. These transformations include chunking, extracting metadata, and embedding each chunk. This is necessary to make sure that the data can be retrieved, and used optimally by the LLM." — global
📖 "A key step to process your documents is to split them into “chunks”/Node objects. The key idea is to process your data into bite-sized pieces that can be retrieved / fed to the LLM." — global
🧪 实例 用自定义 text splitter 控制切分粒度:
python
from llama_index.core.node_parser import SentenceSplitter
text_splitter = SentenceSplitter(chunk_size=512, chunk_overlap=10)🔍 追问 transformation 除了 chunking 还有什么?它们的输入输出是什么类型? → 还包括抽取元数据和为每个 chunk 生成 embedding;transformation 的输入/输出都是
Node 对象,且可以堆叠与重排序。📚 拓展阅读
- Transformations — 转换步骤的原文说明
- Transformation Pipeline interface — 声明式 ingestion pipeline 接口
- text splitters — 各类文本切分器模块
- automatic metadata extractors — 自动元数据抽取器指南
QLlamaIndex 转换文档时的高层 API 和低层 API 有什么区别?怎么选?深挖·拓展中频
答 LlamaIndex 为文档转换提供了高层(high-level)和低层(lower-level)两套 API。高层 API 指索引的
.from_documents() 方法:它接受一个 Document 对象数组,会正确地解析并切块——底层就是把 Document 拆成 Node 对象(Node 与 Document 类似,都含 text 和 metadata,但与父 Document 有关系)。它最省事,适合默认流程;当你想要对切分方式有更大控制时,可以给它传入自定义的 transformations 列表,或应用到全局 Settings。低层 API 则让你显式定义这些步骤:你可以把转换模块(text splitters、metadata extractors 等)当作独立组件单独使用,或用声明式的 Transformation Pipeline 接口(IngestionPipeline)把它们组合起来。选择的权衡在于控制粒度 vs 便捷度:高层 API 一行 from_documents 覆盖大多数场景;当你需要精确掌控切分器、元数据抽取顺序或想复用一条可声明的流水线时,就下沉到低层 API。术语
.from_documents()(高层 API,接受 Document 数组自动解析切块); transformations 列表(可传入以自定义切分); Settings(全局配置,可设 text_splitter); IngestionPipeline(低层声明式转换流水线接口)📖 "Indexes have a .from_documents() method which accepts an array of Document objects and will correctly parse and chunk them up. However, sometimes you will want greater control over how your documents are split up." — global📖 "You can do this by either using our transformation modules (text splitters, metadata extractors, etc.) as standalone components, or compose them in our declarative Transformation Pipeline interface." — global
🧪 实例 低层 API 用
IngestionPipeline 显式编排转换:python
from llama_index.core import SimpleDirectoryReader
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import TokenTextSplitter
documents = SimpleDirectoryReader("./data").load_data()
pipeline = IngestionPipeline(transformations=[TokenTextSplitter(), ...])
nodes = pipeline.run(documents=documents)🔍 追问 用高层 API 时如何全局改掉默认的 text splitter? → 把自定义 splitter 赋给全局
Settings.text_splitter,或在 from_documents 中通过 transformations=[text_splitter] 按索引传入。📚 拓展阅读
- High-Level Transformation API —
.from_documents()高层转换 API 原文 - Transformation Pipeline interface — 声明式 IngestionPipeline 接口指南
- used on their own or as part of an ingestion pipeline — node parser 独立使用或入流水线的指南
Q什么是 embedding?VectorStoreIndex 如何利用它实现语义检索?深挖·拓展🔥高频
答 在 LlamaIndex 中,
VectorStoreIndex 是最常见的索引类型:它把 Documents 切成 Nodes,再为每个 node 的文本生成 vector embeddings。embedding 本质上是文本语义(meaning)的数值表示——两段意思相近的文本会得到数学上相似的 embedding,哪怕字面完全不同,这正是"语义检索"区别于关键词匹配的根本机制。检索时,用户的 query 本身也会被转成一个 vector embedding,然后 VectorStoreIndex 做一次数学运算,按语义相似度对所有 embedding 排序。这里的权衡在于:embedding 的生成依赖对 LLM 的 API 调用,文本量大时会有大量往返调用,既慢又(在托管 LLM 上)贵,这也是为什么建议先把 embedding 存下来。默认 embedding 是 OpenAI 的 text-embedding-ada-002,但不同 embedding 在效率、效果、算力成本上各有差异,换 LLM 时通常也要换 embedding。术语
vector embedding(文本语义的数值表示); semantic search(基于含义而非关键词的检索); VectorStoreIndex(最常用的向量索引); text-embedding-ada-002(默认 embedding 模型)📖 "A vector embedding, often just called an embedding, is a numerical representation of the semantics, or meaning of your text. Two pieces of text with similar meanings will have mathematically similar embeddings, even if the actual text is quite different." — Indexing | Developer Documentation📖 "When you want to search your embeddings, your query is itself turned into a vector embedding, and then a mathematical operation is carried out by VectorStoreIndex to rank all the embeddings by how semantically similar they are to your query." — Indexing | Developer Documentation
🧪 实例 用一行代码从 Documents 构建向量索引:
python
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)🔍 追问 为什么大文本量时构建索引会慢? → 因为"embeds your text"意味着通过 LLM 的 API 把所有文本转成 embedding,文本多时涉及大量往返 API 调用(round-trip API calls),所以耗时。
📚 拓展阅读
- Embeddings 模块指南 — 了解多种 embedding 类型及其效率/效果/成本差异
- Storing — 把 embedding 存下来以省时省钱
- Querying — 学习比 top-k 更复杂的检索策略
Q什么是 top-k 语义检索?top_k 参数控制什么?深挖·拓展🔥高频
答 当 VectorStoreIndex 把所有 embedding 按与 query 的语义相似度排序后,会把最相似的若干个 embedding 以其对应的文本块(chunks)形式返回。返回的数量记作
k,控制返回多少个 embedding 的参数叫 top_k,因此这种检索被称为"top-k semantic retrieval"。这是查询向量索引最简单的形式——它直接取相似度最高的固定数量结果,实现简单、可预期,但也因此缺乏更精细的控制(例如没有相似度阈值、不做重排)。在 querying 阶段可以调大/调小 similarity_top_k,并叠加 postprocessing 步骤(如设置相似度下限)来改善相关性,这就是为什么文档说 top-k 只是"最简单"的一种,还有许多更复杂微妙的策略。术语
top_k(控制返回 embedding 数量的参数); k(返回的 embedding 个数); top-k semantic retrieval(最简单的向量查询形式); similarity_top_k(retriever 中的对应配置)📖 "The number of embeddings it returns is known ask, so the parameter controlling how many embeddings to return is known astop_k. This whole type of search is often referred to as “top-k semantic retrieval” for this reason." — Indexing | Developer Documentation
📖 "Top-k retrieval is the simplest form of querying a vector index; you will learn about more complex and subtler strategies when you read the querying section." — Indexing | Developer Documentation
🧪 实例 在 querying 阶段自定义 retriever 的 top_k:
python
from llama_index.core.retrievers import VectorIndexRetriever
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=10,
)🔍 追问 只调大 top_k 一定更好吗? → 不一定。配合
SimilarityPostprocessor 设相似度下限时,"有相关结果时会给你很多数据,但没有相关结果时可能一条都没有",需要在召回量和相关性之间权衡。📚 拓展阅读
- Retriever 模块指南 — 了解种类繁多的 retriever
- Querying — top-k 之外更复杂的检索与后处理
Qquerying 分为哪三个阶段?各自做什么?深挖·拓展🔥高频
答 querying 看似只是一次对 LLM 的 prompt 调用,但实际上由三个明确的阶段组成:Retrieval(检索)是从
Index 中找到并返回与 query 最相关的文档,最常见的就是 top-k 语义检索,但还有很多其他策略;Postprocessing(后处理)是对检索到的 Node 做可选的重排(rerank)、变换或过滤,例如要求它们带有特定 metadata(如关键词);Response synthesis(响应合成)是把你的 query、最相关数据和 prompt 组合起来发给 LLM 得到回答。这三段式设计的价值在于给了你一个低层组合 API(low-level composition API)来精细控制查询:你可以替换 retriever、插入 postprocessor、切换合成模式,从而在成本(LLM 调用次数)、延迟和回答质量之间做权衡,而不是被 as_query_engine() 的默认行为锁死。术语
Retrieval(检索最相关文档); Postprocessing(对 Node 重排/变换/过滤); Response synthesis(合成最终回答); QueryEngine(一切查询的基础)📖 "Retrieval is when you find and return the most relevant documents for your query from your Index." — build index📖 "Response synthesis is when your query, your most-relevant data and your prompt are combined and sent to your LLM to return a response." — build index
🧪 实例
低层组合方式手动装配三段:
flowchart LR
Q[Query] --> R[Retrieval
top-k semantic]
R --> P[Postprocessing
rerank/filter]
P --> S[Response synthesis
send to LLM]
S --> A[Response]低层组合方式手动装配三段:
python
from llama_index.core import get_response_synthesizer
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SimilarityPostprocessor
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=response_synthesizer,
node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)],
)🔍 追问 postprocessing 除了过滤还能干什么? → 可以做重排(reranked)、变换(transformed)或过滤,例如要求 Node 带有特定关键词 metadata;也能增强上下文(如
PrevNextNodePostprocessor 基于 Node 关系补充相邻内容)。📚 拓展阅读
- Node Postprocessor 参考 — 完整的后处理器列表
- Reference docs — 已实现组件与支持的配置
- Workflow Guide — 组合自定义复杂查询流
QLlamaIndex 支持哪些 response synthesis 模式?如何选择?深挖·拓展中频
答 在 retriever 取回相关 nodes 后,
BaseSynthesizer 通过组合信息合成最终回答,可用 response_mode 配置。default 是"create and refine":按顺序逐个遍历 Node,每个 Node 单独一次 LLM 调用,适合更详细的答案但调用最多;compact 会在每次 LLM 调用时尽量把能塞进最大 prompt 的 Node 文本块塞进去,塞不下才多次"create and refine",因此更省调用;tree_summarize 递归构建一棵树并返回根节点,适合摘要场景;no_text 只跑 retriever 取回本该发给 LLM 的 nodes 而不真正发送,可通过 response.source_nodes 检查;accumulate 对每个 Node 文本块分别应用 query 并把结果累积成数组,返回拼接字符串,适合需要对每个文本块单独跑同一 query 的情况。选择的核心权衡是 LLM 调用次数/成本/延迟与答案详尽度、以及任务是问答还是摘要。术语
response_mode(合成模式配置); default(create and refine,逐 Node 一次调用); compact(塞满 prompt 减少调用); tree_summarize(递归树摘要); accumulate(逐块累积结果)📖 "compact: “compact” the prompt during each LLM call by stuffing as manyNodetext chunks that can fit within the maximum prompt size. If there are too many chunks to stuff in one prompt, “create and refine” an answer by going through multiple prompts." — build index
📖 "tree_summarize: Given a set ofNodeobjects and the query, recursively construct a tree and return the root node as the response. Good for summarization purposes." — build index
🧪 实例 通过
from_args 指定 response_mode:python
query_engine = RetrieverQueryEngine.from_args(
retriever, response_mode=response_mode
)🔍 追问 想只看会被送进 LLM 的 nodes 而不真正调用 LLM 怎么办? → 用
no_text 模式,它只运行 retriever 取回 nodes 而不发送,再通过 response.source_nodes 检查。📚 拓展阅读
- Querying — response synthesis 各模式说明
- Structured Outputs 指南 — 让输出结构化
Q为什么要 store 索引数据?如何持久化与使用 vector store?深挖·拓展🔥高频
答 默认情况下,索引后的数据只存在内存里,因此为避免重新索引的时间和金钱成本,通常需要把它存下来。最简单的方式是每个 Index 都自带的
.persist() 方法,把数据写到指定磁盘位置,适用于任何类型的索引;之后用 StorageContext.from_defaults(persist_dir=...) 重建 storage context 再 load_index_from_storage 即可,注意如果初始化时用了自定义 transformations、embed_model 等,加载时必须传入相同选项或设为全局 settings。更进一步,因为 VectorStoreIndex 里创建 embeddings 的 API 调用在时间和金钱上都可能很贵,所以会想把它们存进 vector store 以避免反复重新索引。LlamaIndex 支持数量庞大、在架构/复杂度/成本上各异的 vector store;文档以开源的 Chroma 为例,流程是:初始化 Chroma client、创建 Collection、把 Chroma 作为 StorageContext 的 vector_store、再用该 StorageContext 初始化 VectorStoreIndex。若 embeddings 已存好,可用 from_vector_store 直接加载,无需再加载文档或重建索引。术语
.persist()(把索引写到磁盘); StorageContext(存储上下文,挂载 vector_store); vector_store(向量存储后端); Chroma(示例用的开源向量库); from_vector_store(直接从已存向量加载)📖 "The simplest way to store your indexed data is to use the built-in .persist() method of every Index, which writes all the data to disk at the location specified. This works for any type of index." — rebuild storage context📖 "The API calls to create the embeddings in a VectorStoreIndex can be expensive in terms of time and money, so you will want to store them to avoid having to constantly re-index things." — rebuild storage context
🧪 实例 持久化到磁盘再加载:
用 Chroma 作为 vector store:
python
from llama_index.core import StorageContext, load_index_from_storage
# rebuild storage context
storage_context = StorageContext.from_defaults(persist_dir="<persist_dir>")
# load index
index = load_index_from_storage(storage_context)用 Chroma 作为 vector store:
python
import chromadb
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext
# initialize client, setting path to save data
db = chromadb.PersistentClient(path="./chroma_db")
# create collection
chroma_collection = db.get_or_create_collection("quickstart")
# assign chroma as the vector_store to the context
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)🔍 追问 加载持久化索引时最容易踩的坑是什么? → 若初始化索引时用了自定义
transformations、embed_model 等,load_index_from_storage 时必须传入相同选项,或已设为全局 settings,否则会不一致。📚 拓展阅读
- 支持的 vector stores 列表 — 架构/复杂度/成本各异的众多选择
- Chroma 深入示例 — 更完整的 Chroma 使用例
- 全局 settings — 统一配置 embed_model/transformations
QVectorStoreIndex 和 Summary Index 有什么区别?如何向已有索引插入文档?深挖·拓展中频
答
Index 在 LlamaIndex 中是由 Document 对象构成、专为让 LLM 查询而设计的数据结构,并且应与你的查询策略互补。VectorStoreIndex 是最常见的类型,把文本转成 embedding 支持 top-k 语义检索;而 Summary Index 是更简单的形式,最适合"生成文档摘要"这类查询——它只是存下所有 Document,并在查询时把它们全部返回给 query engine。二者的权衡很清晰:向量索引擅长在大量内容中按语义精准定位少数相关片段,而 Summary Index 不做相似度筛选、全量返回,适合需要通盘覆盖全文的摘要任务但不适合精确检索。若已经建好索引,可用 insert 方法把新文档加进去,无需重建整个索引,这对增量更新场景很关键。术语
Index(供 LLM 查询的数据结构); VectorStoreIndex(向量语义检索索引); Summary Index(全量返回、适合摘要); insert(向已有索引增量添加文档)📖 "In LlamaIndex terms, anIndexis a data structure composed ofDocumentobjects, designed to enable querying by an LLM. Your Index is designed to be complementary to your querying strategy." — Indexing | Developer Documentation
📖 "A Summary Index is a simpler form of Index best suited to queries where, as the name suggests, you are trying to generate a summary of the text in your Documents. It simply stores all of the Documents and returns all of them to your query engine." — Indexing | Developer Documentation
🧪 实例 向已有索引逐个插入文档:
python
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex([])
for doc in documents:
index.insert(doc)🔍 追问 如果数据是一组相互关联的概念(图结构)该用什么索引? → 可考虑 knowledge graph index,文档在 Further Reading 中给出了对应的示例链接。
📚 拓展阅读
- Knowledge graph index 示例 — 面向"图"结构化互联概念的数据
- Document management how-to — 管理文档的更多细节与示例 notebook
第2章 Agent·抽取·整合
🔥高频
构建 Agent 与结构化抽取
Building an agent | Developer Documentation Introduction to Structured Data Extraction | Developer DocumentationQLlamaIndex 里的 agent 是什么?它的执行循环是怎么运转的?深挖·拓展🔥高频
答 在 LlamaIndex 中,agent 是一段由 LLM 驱动的半自主软件:给它一个任务,它会朝着解决任务的方向执行一系列步骤。它被赋予一组 tools(可以是任意函数,也可以是完整的 LlamaIndex query engine),在每一步中它自己挑选最合适的 tool 来完成该步。关键在于这是一个"判断—循环"的机制:每完成一步,agent 会判断任务是否已经完成——若完成就把结果返回给用户,若未完成就带着新的中间结果回到起点再走一步。这种循环让 agent 能处理需要多步推理与工具调用的任务,而不是一次性生成答案;代价是它高度依赖模型的推理能力,因此原文明确提醒较小的模型可能不够可靠。构建方式上你既可以从零搭建 agentic workflow,也可以用预置的
FunctionAgent(简单的函数/工具调用 agent)或 AgentWorkflow(可管理多个 agent)。flowchart LR
A[接收任务] --> B[选择最合适的 tool]
B --> C[执行该步]
C --> D{任务完成?}
D -- 是 --> E[返回结果给用户]
D -- 否 --> B术语
agent(由 LLM 驱动、能自主执行多步的半自主软件); tools(agent 可调用的能力,从任意函数到完整 query engine); FunctionAgent(预置的函数/工具调用 agent); AgentWorkflow(可管理多个 agent 的编排)📖 "In LlamaIndex, an agent is a semi-autonomous piece of software powered by an LLM that is given a task and executes a series of steps towards solving that task." — Building an agent | Developer Documentation
📖 "When each step is completed, the agent judges whether the task is now complete, in which case it returns a result to the user, or whether it needs to take another step, in which case it loops back to the start." — Building an agent | Developer Documentation
🧪 实例 用
FunctionAgent 给两个工具,让它解 20+(2*4),模型会先调 multiply 再调 add:python
workflow = FunctionAgent(
tools=[multiply, add],
llm=llm,
system_prompt="You are an agent that can perform basic mathematical operations using tools.",
)
response = await workflow.run(user_msg="What is 20+(2*4)?")
print(response)
# The result of (20 + (2 times 4)) is 28.🔍 追问 为什么这个例子里要在 system prompt 里强调"用工具"? → 因为 GPT-4o-mini 本身就能算这种简单数学,不强制它就不会去调用 tool,原文说它"smart enough to not need tools to do such simple math"。
📚 拓展阅读
- Building an agent 教程 — 本卡片的官方出处,完整走通最基础 agent。
- python-agents-tutorial 代码仓库 — 教程配套的全部可运行代码。
- 1_basic_agent.py 最终代码 — 基础 agent 的完整参考实现。
Qagent 是怎么决定该用哪个 tool 的?工具函数要怎么写才靠谱?深挖·拓展🔥高频
答 agent 选工具靠的不是魔法,而是工具函数暴露给它的元信息:名字、参数、docstring 决定了 agent 判断"这个工具是做什么的、适不适合当前这一步",而 type hints 则告诉它期望的参数类型和返回类型。因此工具本质上就是普通 Python 函数,但 docstring 必须写得描述性强、有帮助——因为这段自然语言是模型选择工具的主要依据。这里的权衡很直接:docstring 写得含糊,agent 就会选错工具或传错参数;写得清晰准确,选择才可靠。这也解释了为什么"函数即工具"这种设计既低门槛又要求纪律——你不用写额外的 schema,但你写函数签名和文档的质量直接决定了 agent 的行为质量。
术语
docstring(函数文档字符串,agent 判断工具用途的主要依据); type hints(类型注解,用于推断参数与返回类型); tool(可被 agent 调用的普通 Python 函数)📖 "When deciding what tool to use, your agent will use the tool’s name, parameters, and docstring to determine what the tool does and whether it’s appropriate for the task at hand." — Building an agent | Developer Documentation
📖 "It will also use the type hints to determine the expected parameters and return type." — Building an agent | Developer Documentation
🧪 实例 名字、类型注解、docstring 三者齐备,agent 才能正确识别这是"两数相乘"的工具:
python
def multiply(a: float, b: float) -> float:
"""Multiply two numbers and returns the product"""
return a * b🔍 追问 如果两个工具 docstring 写得很像会怎样? → agent 依赖 docstring 区分用途,描述雷同会增加选错工具的风险,所以原文强调"it’s important to make sure the docstrings are descriptive and helpful"。
📚 拓展阅读
- Building an agent 教程 — 工具定义与 docstring 说明所在章节。
- python-agents-tutorial 代码仓库 — 查看更多工具写法示例。
QLlamaIndex 有哪些 agent 类型?FunctionAgent、ReActAgent、CodeActAgent、AgentWorkflow 有什么区别?深挖·拓展低频
答 LlamaIndex 提供了几档不同的 agent 抽象。最基础的是
FunctionAgent,一个简单的函数/工具调用 agent;AgentWorkflow 则是能管理多个 agent 的编排,用于多智能体系统。在 FunctionAgent 之外,还有 ReActAgent 和 CodeActAgent,它们与 FunctionAgent 的差别在于使用了不同的 prompting 策略来执行工具——也就是说,它们解决的是同一类"选工具、调工具"的问题,但引导 LLM 的方式不同(例如 ReAct 的推理—行动交替提示)。选择的权衡在于:FunctionAgent 上手最简单,适合支持原生函数调用的模型;不同 prompting 策略的 agent 则在不同模型能力或可解释性需求下更合适。此外你也可以完全不用预置类,从零搭建自己的 agentic workflow。术语
FunctionAgent(简单函数/工具调用 agent); AgentWorkflow(可管理多个 agent 的编排); ReActAgent(用不同 prompting 策略调工具的 agent); CodeActAgent(另一种 prompting 策略的 agent)📖 "you can use our pre-built agentic workflows likeFunctionAgent(a simple function/tool calling agent) orAgentWorkflow(an agent capable of managing multiple agents)." — Building an agent | Developer Documentation
📖 "which use different prompting strategies to execute tools." — Building an agent | Developer Documentation
🧪 实例 切换 agent 类型时,工具与 LLM 的准备是一致的,改变的只是 agent 类;下面是最简的
FunctionAgent 初始化骨架:python
from llama_index.llms.openai import OpenAI
from llama_index.core.agent.workflow import FunctionAgent
llm = OpenAI(model="gpt-4o-mini")
workflow = FunctionAgent(tools=[multiply, add], llm=llm, system_prompt="...")🔍 追问 为什么"agent 需要有能力的模型"这一点对选型很重要? → 因为 agent 要自主推理和调工具,原文指出"Agents require capable models, so smaller models may be less reliable.",较弱的模型在任何 agent 类型下都可能不稳定。
📚 拓展阅读
- Building an agent 教程 — 列出
ReActAgent、CodeActAgent等其他 agent 的入口。 - OpenAI API Key — 教程默认用 OpenAI 模型驱动 agent,需要此 key。
QLlamaIndex 的结构化数据抽取是什么?为什么核心是 Pydantic?深挖·拓展🔥高频
答 结构化抽取要解决的问题是:LLM 擅长理解"非结构化数据"(自然语言),我们希望把它转成计算机程序能消费的、固定而规整的格式,也就是"结构化数据";因为转换过程中往往会丢掉大量冗余信息,所以这个过程叫"extraction"。LlamaIndex 实现这件事的核心是 Pydantic 类:你用 Pydantic 定义一个数据结构,LlamaIndex 与 Pydantic 协作,把 LLM 的输出强制"coerce"成那个结构。之所以选 Pydantic,是因为它是被广泛使用的数据校验与转换库,重度依赖 Python 类型声明——这意味着你用来定义结构的同一份类型声明,既能约束程序内部的数据,又能被转成给 LLM 的指令。权衡在于:你必须先把想要的输出结构显式建模成 Pydantic 类,换来的是输出可校验、可预期、能直接被下游程序使用。
术语
unstructured data(非结构化数据,即自然语言); structured data(结构化的、程序可消费的输出); extraction(抽取,转换中丢弃冗余信息的过程); Pydantic(数据校验与转换库,抽取的核心); BaseModel(定义 Pydantic 数据结构的基类)📖 "you define a data structure in Pydantic and LlamaIndex works with Pydantic to coerce the output of the LLM into that structure." — Introduction to Structured Data Extraction | Developer Documentation
📖 "Pydantic is a widely-used data validation and conversion library. It relies heavily on Python type declarations." — Introduction to Structured Data Extraction | Developer Documentation
🧪 实例 继承
这里
BaseModel 定义一个带默认值的最简结构:python
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "Jane Doe"这里
id 是整数、name 是默认为 Jane Doe 的字符串。🔍 追问 抽取为什么叫"extraction"而不是"conversion"? → 因为转换过程中通常会忽略掉大量多余数据,只保留期望字段,原文因此把它称作 extraction。
📚 拓展阅读
- 结构化抽取导论 — 本卡片官方出处。
- Pydantic 官方文档 — 抽取所依赖的数据校验库。
- Pydantic Models 指南 — 深入理解如何定义模型。
QPydantic 类如何变成 JSON schema?为什么要给字段加 Field 描述?深挖·拓展🔥高频
答 Pydantic 能把类序列化成符合流行标准的 JSON schema 对象,而这一点之所以"crucial",是因为这些 JSON 格式的 schema 常常被直接传给 LLM,LLM 反过来把它们当作"该如何返回数据"的指令。也就是说,schema 同时扮演了两个角色:对程序是数据结构定义,对模型是输出规范。既然 LLM 是拿 schema 当指令用,光有字段名和类型还不够——为了帮助模型、提高返回数据的准确度,应该用自然语言描述对象和字段各自的用途。Pydantic 通过 docstring(描述类)和
Field(描述字段,如 Field(description=...))支持这件事。权衡很清楚:多写这些自然语言注解会让类定义更啰嗦,但它直接转化为 schema 里的 description,成为模型理解每个字段该填什么的依据,从而降低抽取出错的概率。术语
JSON schema(Pydantic 序列化出的、可传给 LLM 的结构描述); Field(为字段附加 description 等元信息的 Pydantic 工具); annotations(用 docstring 和 Field 添加的自然语言说明); description(schema 中指导 LLM 填值的字段说明)📖 "This property is crucial: these JSON-formatted schemas are often passed to LLMs and the LLMs in turn use them as instructions on how to return data." — Introduction to Structured Data Extraction | Developer Documentation
📖 "To assist them and improve the accuracy of your returned data, it’s helpful to include natural-language descriptions of objects and fields and what they’re used for." — Introduction to Structured Data Extraction | Developer Documentation
🧪 实例
User 类序列化后的 JSON schema,id 被标为必填:json
{
"properties": {
"id": { "title": "Id", "type": "integer" },
"name": { "default": "Jane Doe", "title": "Name", "type": "string" }
},
"required": ["id"],
"title": "User",
"type": "object"
}🔍 追问 为什么
name 不在 required 里而 id 在? → 因为 name 有默认值 Jane Doe,id 没有默认值,schema 因此只把 id 列为 required。📚 拓展阅读
- Pydantic JSON schema 概念 — Pydantic 如何按流行标准生成 schema。
- Pydantic Fields 文档 — 给字段加 description 等元信息。
- Python docstrings 说明 — 用 docstring 描述类的用途。
Q怎么用 Pydantic 表达嵌套/列表这类复杂结构?以发票抽取为例。深挖·拓展中频
答 复杂结构在 Pydantic 里通过"模型嵌套模型"来表达:一个模型的字段类型可以是另一个模型,也可以是
List[另一个模型],还能用 Optional[...] 标记可选字段。文档用 Spam 举例——Spam 同时拥有一个 foo(类型是 Foo)和一个 bars(类型是 List[Bar]),其中 Foo 有一个 count 和一个可选的 size,bars 里每个对象都有 apple 和 banana。这种可组合性正是结构化抽取能建模真实文档的关键:像发票这种既有顶层字段、又含"多行明细"的对象,就用 Invoice 内嵌 list[LineItem] 来表达。它展开后会得到一个复杂得多的 JSON schema,用 $defs 定义子模型、用 $ref 引用它——但对你而言只是把嵌套的类型声明清楚,剩下的 schema 生成交给 Pydantic。术语
nesting(模型嵌套,字段类型为另一个模型); List[Bar](元素为某模型的列表字段); Optional[float](可为空的可选字段); $defs/$ref(JSON schema 中定义与引用子模型的机制)📖 "You can create more complex structures by nesting these models:" — Introduction to Structured Data Extraction | Developer Documentation
📖 "NowSpamhas afooand abars." — Introduction to Structured Data Extraction | Developer Documentation
🧪 实例 发票模型把多行明细建模成
list[LineItem],每个字段都用 Field(description=...) 给出说明:python
from datetime import datetime
class LineItem(BaseModel):
"""A line item in an invoice."""
item_name: str = Field(description="The name of this item")
price: float = Field(description="The price of this item")
class Invoice(BaseModel):
"""A representation of information from an invoice."""
invoice_id: str = Field(description="A unique identifier for this invoice, often a number")
date: datetime = Field(description="The date this invoice was created")
line_items: list[LineItem] = Field(description="A list of all the items in this invoice")🔍 追问
Invoice 的 schema 里 line_items 为什么会出现 $ref? → 因为 LineItem 被抽取为 $defs 中的共享定义,line_items 数组的 items 用 {"$ref": "#/$defs/LineItem"} 引用它,避免重复展开。📚 拓展阅读
- 结构化抽取导论 — 嵌套模型与 Invoice 示例出处。
- Pydantic Models 指南 — 嵌套模型的更完整说明。
QLlamaIndex 中不同 index 的成本结构如何划分?哪些 index 在构建阶段完全不产生 LLM 调用?深挖·拓展🔥高频
答 LlamaIndex 的整体成本由四个因素决定:所用 LLM 的类型、所用数据结构(index)的类型、构建时的参数、以及查询时的参数;因为每次 LLM 调用都要按 token 计费(原文以 gpt-3.5-turbo 为例,$0.002 / 1k tokens),所以成本高低本质上取决于「触发了多少次 LLM 调用」。据此可把 index 分成两类:构建阶段零成本的一类不需要任何 LLM 调用,包括
SummaryIndex、SimpleKeywordTableIndex(用正则关键词抽取器)和 RAKEKeywordTableIndex(用 RAKE 关键词抽取器);另一类在 build 阶段就要调用 LLM,包括 TreeIndex(用 LLM 层次化地对文本做摘要来建树)和 KeywordTableIndex(用 LLM 从每篇文档抽取关键词)。这里的核心权衡是:构建期省下的 LLM 调用往往会转移到查询期(例如 SummaryIndex 建索引免费,但查询时代价高),所以选型时要看构建频率与查询频率的比例——数据一次性入库而查询频繁,则倾向于把成本前移到构建期。术语
SummaryIndex(顺序汇总型索引,建索引零成本); SimpleKeywordTableIndex(正则关键词表索引); RAKEKeywordTableIndex(RAKE 关键词表索引); TreeIndex(树形摘要索引,建树需 LLM); KeywordTableIndex(LLM 抽词的关键词表索引)📖 "Each call to an LLM will cost some amount of money - for instance, OpenAI’s gpt-3.5-turbo costs $0.002 / 1k tokens." — use a mock llm globally
📖 "The following indices don’t require LLM calls at all during building (0 cost):" — use a mock llm globally
🧪 实例 需要把大量文档一次性入库、且预期查询量很大时,若选择用 LLM 抽词的
KeywordTableIndex,构建期每篇文档都要一次 LLM 调用;若改用 SimpleKeywordTableIndex,则改由正则抽词,构建期 0 成本:python
# 构建期零 LLM 成本的关键词表索引(用正则抽词)
from llama_index.core import SimpleKeywordTableIndex
index = SimpleKeywordTableIndex.from_documents(documents)🔍 追问 为什么
SummaryIndex 建索引免费却不代表整体便宜? → 因为它把成本推迟到了查询期——默认查询时会对每个 node 各调用一次 LLM,查询频繁时总成本反而更高。📚 拓展阅读
- Cost Analysis 完整用法 — 详解构建/查询期的 token 预估用法
- TokenCountingHandler 示例 — 统计真实 token 消耗的 callback 示例
Q各类 index 在查询阶段的 LLM 调用次数分别是多少?这些复杂度差异如何影响成本?深挖·拓展🔥高频
答 查询期一定至少有一次 LLM 调用用于合成最终答案,但不同 index 的调用规模差别很大,这正是成本建模的关键。
SummaryIndex 在没有过滤或 embedding 检索的情况下,默认要对 N 个 node 各调用一次 LLM,即 O(N),这也是它「建索引免费但查询贵」的原因;TreeIndex 默认是 log(N) 次调用(N 为叶节点数),而把 child_branch_factor 从默认的 1 提到 2 会让复杂度从对数级变成多项式级、明显更贵,因为每个父节点要遍历 2 个孩子而非 1 个;KeywordTableIndex 默认要一次 LLM 调用来抽取查询关键词,但可以用 retriever_mode="simple" 或 "rake" 改用正则/RAKE 抽词以省掉这次调用;VectorStoreIndex 默认每次查询一次 LLM 调用,但一旦调大 similarity_top_k、chunk_size 或改动 response_mode,调用次数就会上升。理解这些复杂度让你能在检索质量与成本之间做量化取舍。术语
similarity_top_k(向量检索返回的近邻数,越大越贵); child_branch_factor(树遍历时每个父节点访问的孩子数); response_mode(答案合成模式,影响 LLM 调用次数); retriever_mode(检索模式,可切换为免 LLM 的关键词抽取)📖 "There will always be >= 1 LLM call during query time, in order to synthesize the final answer." — use a mock llm globally
📖 "SummaryIndex: by default requires {math}NLLM calls, where N is the number of nodes." — use a mock llm globally
📖 "Settingchild_branch_factor=2will be more expensive than the defaultchild_branch_factor=1(polynomial vs logarithmic), because we traverse 2 children instead of just 1 for each parent node." — use a mock llm globally
🧪 实例 各 index 查询期默认 LLM 调用规模对比:
flowchart LR
Q[查询] --> S[SummaryIndex: N 次]
Q --> T[TreeIndex: log N 次]
Q --> K[KeywordTableIndex: 1 次抽词]
Q --> V[VectorStoreIndex: 1 次/查询]
V -.调大 top_k/chunk_size.-> More[调用次数上升]🔍 追问 用
VectorStoreIndex 时想提高召回但控制成本,该注意什么? → similarity_top_k 越大送进合成阶段的上下文越多、LLM 调用/token 消耗也越高,需要在召回率与成本间权衡,并可配合 response_mode 调整合成策略。📚 拓展阅读
- Cost Analysis 概念页 — 各 index 构建/查询期成本注解
- Cost Analysis 完整用法 — 结合预测器估算查询成本
Q如何在真正发起 LLM 调用之前预估 token 用量和成本?MockLLM 与 MockEmbedding 各解决什么问题?深挖·拓展中频
答 LlamaIndex 提供 token predictors,用来在任何实际 LLM 调用发生之前,预测 index 构建和 index 查询两个阶段的 token 用量,从而提前估算成本。机制上,token 由
TokenCountingHandler 这个 callback 负责统计;而对 LLM 调用的预测靠 MockLLM——它的 max_tokens 参数被当作「最坏情况」预测,即假设每次 LLM 响应恰好包含这么多 token,如果不指定 max_tokens,它就直接把 prompt 原样预测回来(即只计入输入侧)。对 embedding 调用的预测则用 MockEmbedding,通过 embed_dim 指定维度(如 1536)。二者通常以全局方式挂到 Settings.llm 和 Settings.embed_model 上,这样构建和查询流程都会走 mock,不产生真实费用却能得到 token 估算。这样做的价值在于:在把 pipeline 推到生产前就能对成本上界有量化把握,尤其配合 Q2 里各 index 的调用复杂度,可以估出不同参数组合下的费用。术语
MockLLM(模拟 LLM,用于预测 token 而不真正调用); max_tokens(最坏情况下每次响应的 token 数); MockEmbedding(模拟 embedding 模型); embed_dim(embedding 维度,如 1536); TokenCountingHandler(统计 token 的 callback); Settings(全局配置对象)📖 "LlamaIndex offers token predictors to predict token usage of LLM and embedding calls." — use a mock llm globally
📖 "Themax_tokensparameter is used as a “worst case” prediction, where each LLM response will contain exactly that number of tokens. Ifmax_tokensis not specified, then it will simply predict back the prompt." — use a mock llm globally
🧪 实例 全局挂上 MockLLM 与 MockEmbedding,之后照常 build/query 即可拿到 token 估算:
python
from llama_index.core.llms import MockLLM
from llama_index.core import MockEmbedding
from llama_index.core import Settings
# use a mock llm globally
Settings.llm = MockLLM(max_tokens=256)
# use a mock embedding globally
Settings.embed_model = MockEmbedding(embed_dim=1536)🔍 追问 不指定
max_tokens 时预测结果会怎样? → 它不再假设固定长度的响应,而是直接把 prompt 原样预测回来,相当于只估算输入侧 token。📚 拓展阅读
- TokenCountingHandler 示例 — token 统计 callback 的完整 setup
- Cost Analysis 完整用法 — 在构建与查询期使用预测器的详细模式
Q数据加载、索引、存储、查询都跑通后,如何把它「整合」成一个可上生产的应用?LlamaIndex 提供哪些整合路径?深挖·拓展中频
答 「Putting It All Together」这一章的定位是:当你已经完成加载数据、建索引、存储、查询之后,把这些环节组装成可发布的产品。它给出多条进阶整合路径:用 Q&A Patterns 构建超出基础功能的 query engine(含 terms definition 这类逐步教程、跨多个 index 的统一查询框架、以及对 SQL 等结构化数据的查询);构建 chatbot;在 LlamaIndex 里构建 agents;用 property graphs 做索引与检索;以及搭一个全栈 Web 应用。为降低搭建成本,LlamaIndex 还提供工具/项目模板:
create-llama 可以直接为你生成一个全栈脚手架,llamaindex-cli rag 则把上述若干概念组合成一个能在终端里跟文件对话的易用工具。整合的核心思路是「不重复造轮子」——用现成模板和 CLI 把检索、合成、对话、Agent 等能力拼装起来,把精力放在业务逻辑而非基础设施上。术语
create-llama(生成全栈脚手架的项目模板); llamaindex-cli rag(把多概念组合成的终端 RAG 工具); query engine(查询引擎); property graphs(属性图,用于索引与检索); agents(在 LlamaIndex 中构建的智能体)📖 "LlamaIndex also provides some tools / project templates to help you build a full-stack template." — Putting It All Together | Developer Documentation
📖 "We also have the llamaindex-cli rag CLI tool that combines some of the above concepts into an easy to use tool for chatting with files from your terminal!" — Putting It All Together | Developer Documentation🧪 实例 用官方脚手架一键生成全栈项目骨架:
bash
npx create-llama@latest🔍 追问 想跨多个 index 一起查询该看哪条路径? → 参考「creating a unified query framework over your indexes」指南,它展示如何在多个 index 上统一运行查询(reciprocal rerank fusion)。
📚 拓展阅读
- Q&A Patterns — 超越基础的 query engine 构建方式
- Building agents in LlamaIndex — 在 LlamaIndex 中构建 agent
- create-llama 源码 — 全栈脚手架生成器
- 全栈 Web 应用指南 — 用 LlamaIndex 搭全栈应用
Q面向生产整合时,还有哪些结构化数据、chatbot、property graph 等专项能力可用?深挖·拓展低频
答 在把 pipeline 推向生产的整合阶段,LlamaIndex 除了基础的 Q&A,还覆盖几类常见的进阶场景:一是对结构化数据(如 SQL)的查询,让 query engine 不只面向非结构化文本;二是构建 chatbot 的完整指南,把检索能力包装成多轮对话;三是 property graphs(属性图)用于索引和检索的完整指南,适合实体/关系型知识;四是跨多个 index 的统一查询框架(通过 reciprocal rerank fusion 融合多路检索结果)。这些能力都是从「已跑通单一 index 查询」向「真实产品」演进时会用到的组件,配合 Q4 里的
create-llama 脚手架和 llamaindex-cli rag,构成从原型到生产的完整整合链路。选择哪条路径取决于数据形态(结构化 vs 文本 vs 图)和交互形态(一次问答 vs 多轮对话)。术语 structured data / SQL(结构化数据查询); chatbot(多轮对话应用); property graphs(属性图索引与检索); unified query framework(跨多 index 的统一查询); reciprocal rerank fusion(倒数重排融合)
📖 "We have a guide on how to build a chatbot" — Putting It All Together | Developer Documentation
📖 "We have a complete guide to using property graphs for indexing and retrieval" — Putting It All Together | Developer Documentation
🧪 实例 从数据形态出发选择整合路径:
flowchart TD
D{数据/交互形态} --> Text[非结构化文本] --> QA[Q&A / VectorStoreIndex]
D --> SQL[结构化 SQL] --> ST[Structured Data 指南]
D --> Graph[实体关系] --> PG[Property Graph 指南]
D --> Chat[多轮对话] --> CB[Chatbot 指南]🔍 追问 数据是关系型/实体型知识,更适合哪条路径? → 用 property graphs 做索引与检索的完整指南,它针对实体与关系结构而非纯文本切分。
📚 拓展阅读
- 结构化数据 (SQL) 查询 — 对 SQL 等结构化数据构建查询
- 构建 chatbot 指南 — 把检索包装成多轮对话
- Property Graph 索引指南 — 属性图索引与检索完整指南
- 统一查询框架 — 跨多个 index 运行统一查询
第二部分 · 组件模块指南
第1章 加载与摄取
🔥高频
加载总览·Documents/Nodes
Component Guides | Developer Documentation Loading Data | Developer Documentation build indexQLlamaIndex 里的 Document 和 Node 分别是什么?两者是什么关系?深挖·拓展🔥高频
答 Document 和 Node 是 LlamaIndex 数据层的两个核心抽象。Document 是对任意数据源的通用封装——一个 PDF、一次 API 输出、或从数据库检索到的数据都可以装进同一个 Document;它默认存文本,外加
metadata(注解字典)和 relationships(与其他 Document/Node 的关系)等属性。Node 则代表源 Document 的一个"chunk"(切片),可以是文本片段、图像或其他形态,同样带有 metadata 和与其它 node 的关系信息。二者的关系是容器与切片:先有 Document 作为原始载体,再把它切分成一个个 Node 供索引与检索使用。之所以这样分层,是因为检索粒度往往远小于原始文档——直接对整篇文档做向量化会稀释语义,而 Node 提供了可控的最小检索单元,同时通过 relationships 保留切片间的上下文脉络。flowchart LR
A[数据源: PDF / API / DB] --> B[Document 通用容器]
B -->|NodeParser 切分| C[Node chunk]
C --> D[VectorStoreIndex]术语
Document(对任意数据源的通用容器); Node(源 Document 的一个 chunk/切片); metadata(可附加到文本上的注解字典); relationships(指向其他 Document/Node 的关系字典)📖 "Document and Node objects are core abstractions within LlamaIndex." — build index
📖 "is a generic container around any data source - for instance, a PDF, an API output, or retrieved data from a database." — build index
📖 "represents a “chunk” of a source Document, whether that is a text chunk, an image, or other." — build index
🧪 实例 手动从文本列表构造 Document,再直接建索引:
python
from llama_index.core import Document, VectorStoreIndex
text_list = [text1, text2, ...]
documents = [Document(text=t) for t in text_list]
# build index
index = VectorStoreIndex.from_documents(documents)🔍 追问 Document 能不能存图像? → 可以,但文档标注为 beta——官方 "have beta support for allowing Documents to store images",多模态能力仍在积极完善中。
📚 拓展阅读
- Using Documents — Document 对象的进阶自定义用法
- Using Nodes — 直接定义与操作 Node 的深入指南
QNode 是怎么从 Document 产生的?元数据会自动继承吗?深挖·拓展🔥高频
答 Node 是 LlamaIndex 的"一等公民",有两条产生路径:你可以直接定义 Node 及其全部属性,也可以通过
NodeParser 类把源 Document"解析(parse)"成 Node。实际工程中后者是主流——用 SentenceSplitter 之类的 parser 调用 get_nodes_from_documents(documents) 就能把文档批量切成 Node。关键机制在于元数据传播:默认情况下,每个从 Document 派生出来的 Node 都会继承该 Document 相同的 metadata(例如 Document 上的 file_name 字段会被传播到它衍生的每一个 Node)。这样设计的权衡是:切片虽然打散了原文,但通过继承的 metadata 仍能追溯来源、支持按文件过滤,避免切分后丢失出处信息;代价是若不加控制,冗余的 metadata 也会随每个 Node 复制并进入 embedding/LLM 上下文,因此进阶场景需要自定义哪些 metadata 参与嵌入或提示。术语
NodeParser(把 Document 解析为 Node 的类); SentenceSplitter(一种基于句子的 node parser); get_nodes_from_documents(从文档批量生成 Node 的方法); file_name(会从 Document 传播到每个 Node 的元数据字段示例)📖 "Nodes are a first-class citizen in LlamaIndex. You can choose to define Nodes and all its attributes directly." — build index
📖 "You may also choose to “parse” source Documents into Nodes through our NodeParser classes." — build index📖 "By default every Node derived from a Document will inherit the same metadata from that Document" — build index
🧪 实例 用 SentenceSplitter 把文档解析成 Node 再建索引:
python
from llama_index.core.node_parser import SentenceSplitter
# load documents
...
# parse nodes
parser = SentenceSplitter()
nodes = parser.get_nodes_from_documents(documents)
# build index
index = VectorStoreIndex(nodes)🔍 追问 两种建索引调用有何不同? →
VectorStoreIndex.from_documents(documents) 接收 Document 并在内部自动切分;而 VectorStoreIndex(nodes) 直接接收你已经解析/定义好的 Node 列表,给你对切分过程的完全控制权。📚 拓展阅读
- Node Parser Usage Pattern — node parser 的使用方式
- Node Parser Modules — sentence/token/HTML/JSON 等各类文本切分器模块
QLlamaIndex 的数据摄取(ingestion)整体流程是怎样的?深挖·拓展🔥高频
答 官方把数据摄取的核心概括为两件事:loading(加载)和 transformations(转换)。流程是先加载得到 Documents,然后通过 transformations 处理它们并输出 Nodes——也就是"加载 Document → 转换 → 产出 Node"这条主线。transformations 包含像切分文本这样的常见操作。之所以把摄取拆成"加载"和"转换"两个阶段,是为了解耦"数据从哪来"和"数据怎么被加工":加载层负责对接各种来源(本地目录、PDF、上百种外部连接器),转换层负责统一的切分/清洗/富化逻辑。为把整条链路做成可复用、可缓存的过程,LlamaIndex 提供了 ingestion pipeline,让你搭建一个"可重复、缓存优化"的数据加载流程,从而在数据量大或反复迭代时避免重复计算。
flowchart LR
A[Sources] -->|loading| B[Documents]
B -->|transformations 如切分| C[Nodes]
C --> D[Index]术语
loading(把数据源加载成 Document); transformations(对 Document 的处理操作,如切分文本); ingestion pipeline(可重复、缓存优化的端到端加载流程)📖 "The key to data ingestion in LlamaIndex is loading and transformations. Once you have loaded Documents, you can process them via transformations and output Nodes." — Loading Data | Developer Documentation
📖 "The ingestion pipeline](/python/framework/module_guides/loading/ingestion_pipeline/index.md) which allows you to set up a repeatable, cache-optimized process for loading data." — Loading Data | Developer Documentation
🧪 实例 从组件导航看,Loading 模块自成一组:Introduction to Loading、Documents and Nodes、SimpleDirectoryReader、Data Connectors、Node Parsers / Text Splitters、Ingestion Pipeline——恰好对应"加载→抽象→转换→端到端管线"的分层。
🔍 追问 transformations 具体包含什么? → 文档明确它 "includes common operations like splitting text",即切分文本这类通用操作是转换阶段的典型代表。
📚 拓展阅读
- The ingestion pipeline — 搭建可重复、缓存优化的加载流程
- Introduction to Loading — 数据加载能力总览
QDocument 默认存哪些属性?metadata 和 relationships 各起什么作用?深挖·拓展中频
答 默认情况下,一个 Document 存储文本,再加上若干其他属性。文档列出的两个典型属性是:
metadata——一个可以附加到文本上的注解字典;以及 relationships——一个保存与其他 Document/Node 关系的字典。metadata 的价值在于给原始文本挂上结构化标签(来源文件名、页码、时间等),这些标签后续会随 Node 继承并可用于过滤与富化;relationships 则维护对象之间的引用关系,让切分后的内容仍能表达"谁属于谁、谁挨着谁"的脉络。可以说 text 承载内容、metadata 承载"关于内容的信息"、relationships 承载"内容之间的连接",三者共同构成一个既能被检索又能被追溯的数据单元。术语
metadata(可附加到文本上的注解字典); relationships(包含与其他 Documents/Nodes 关系的字典); text(Document 默认存储的主体内容)📖 "By default, a Document stores text along with some other attributes." — build index
📖 "metadata - a dictionary of annotations that can be appended to the text." — build index📖 "relationships - a dictionary containing relationships to other Documents/Nodes." — build index🧪 实例 Node 与 Document 在这一点上高度一致——文档说明 Node 也 "contain metadata and relationship information with other nodes",所以 metadata/relationships 这套设计是跨 Document 与 Node 复用的统一模型。
🔍 追问 Node 上的 metadata/relationship 和 Document 的一样吗? → 结构一致:Node "Similar to Documents, they contain metadata and relationship information with other nodes",且默认继承源 Document 的 metadata。
📚 拓展阅读
- Using Documents — 如何为更高级场景自定义 Document
- Documents and Nodes — 核心数据结构总览
Q定义 Node 有几种方式?什么时候直接定义、什么时候用 parser?深挖·拓展中频
答 有两种方式。其一是直接定义 Node 及其所有属性——因为 Node 是 LlamaIndex 的一等公民,你可以完全手工构造它、精确控制文本、metadata 与 relationships;其二是通过
NodeParser 类把源 Document"解析"成 Node,由框架按既定策略自动切分。选择取决于对切分的控制需求:当你已经有结构化好的切片(例如按业务字段拆好的记录、外部系统已分段的内容),直接定义 Node 能保证切分边界与语义单元完全对齐;当你面对的是整篇非结构化文档,用 parser 自动切分更省力,也能利用现成的 sentence/token 等切分器。二者可以互补——先用 parser 得到 Node,再对个别 Node 手动调整属性。术语
first-class citizen(Node 在框架中是一等公民,可被直接定义); NodeParser(把 Document 解析成 Node 的类); define directly(手动定义 Node 的全部属性)📖 "You can choose to define Nodes and all its attributes directly." — build index
📖 "You may also choose to “parse” source Documents into Nodes through our NodeParser classes." — build index🧪 实例 parser 路径的最小写法就是实例化一个 parser 再调用
get_nodes_from_documents:python
parser = SentenceSplitter()
nodes = parser.get_nodes_from_documents(documents)🔍 追问 直接定义的 Node 也会自动继承 Document 的 metadata 吗? → 继承规则针对"从 Document 派生"的 Node("every Node derived from a Document will inherit the same metadata");完全手动、不经由某个 Document 派生的 Node,其 metadata 由你自己设置。
📚 拓展阅读
- Using Nodes — 直接定义与操作 Node 的细节
- Node Parser Modules — 可选用的各类切分器模块
Q除了手动构造,LlamaIndex 提供哪些加载数据的工具?各自定位是什么?深挖·拓展低频
答 加载层提供了三类工具,覆盖从"本地简单"到"复杂 PDF"再到"任意来源"的谱系。SimpleDirectoryReader 是内置加载器,用于从本地目录加载各种文件类型,是上手最快的默认选项。LlamaParse 是 LlamaIndex 的官方 PDF 解析工具,以托管 API(managed API)形式提供,专门解决复杂 PDF 的解析难题。LlamaHub 则是一个包含数百个数据加载库的注册表(registry),用于从任意来源摄取数据。这样的分层是一种"渐进复杂度"设计:大多数本地场景 SimpleDirectoryReader 足矣;当文档是难啃的 PDF 时升级到 LlamaParse;当来源五花八门(SaaS、数据库、API)时则到 LlamaHub 找现成连接器,避免为每种来源手写加载逻辑。三者最终都产出统一的 Document,再进入相同的转换与索引链路。
术语
SimpleDirectoryReader(从本地目录加载多种文件类型的内置加载器); LlamaParse(官方 PDF 解析工具,以 managed API 提供); LlamaHub(数百个数据加载库的注册表)📖 "SimpleDirectoryReader](/python/framework/module_guides/loading/simpledirectoryreader/index.md), our built-in loader for loading all sorts of file types from a local directory" — Loading Data | Developer Documentation
📖 "LlamaParse](/python/framework/module_guides/loading/connector/llama_parse/index.md), LlamaIndex’s official tool for PDF parsing, available as a managed API." — Loading Data | Developer Documentation
📖 "LlamaHub](/python/framework/module_guides/loading/connector/index.md), our registry of hundreds of data loading libraries to ingest data from any source" — Loading Data | Developer Documentation
🧪 实例 选型直觉——本地一堆混合格式文件用 SimpleDirectoryReader;一份排版复杂的 PDF 用 LlamaParse;要接 Notion/Slack/数据库等外部源就去 LlamaHub 挑连接器(即组件导航里的 Data Connectors)。
🔍 追问 想系统了解 loading 从何看起? → 官方建议先读 Understanding 部分的 loading 基础("learned about the basics of loading data"),再进入这些 module guide 深入。
📚 拓展阅读
- SimpleDirectoryReader — 内置的本地目录加载器
- Data Connectors — LlamaHub 连接器,接入外部数据源
- LlamaParse — 官方 PDF 解析托管 API
🔥高频
连接器·节点解析·摄取管道
Data Connectors (LlamaHub) | Developer Documentation global create the pipeline with transformationsQIngestionPipeline 是什么?它用 Transformations 做了什么,为什么每一步都要缓存?深挖·拓展🔥高频
答
IngestionPipeline 的核心抽象是 Transformations——一组按顺序作用于输入数据的转换步骤(如 SentenceSplitter、TitleExtractor、OpenAIEmbedding)。数据流过这条流水线后,产出的 nodes 要么被返回,要么在你传入了 vector store 时被直接写入向量库。机制上最关键的设计是「node + transformation 对」级别的哈希缓存:每一个节点与它经过的某个转换的组合都会被单独哈希并缓存,因此当后续运行(且缓存被持久化)遇到相同的 node+transformation 组合时,就能命中缓存、跳过重复计算而省时。这带来的权衡是:缓存能显著加速重复摄取(尤其是 embedding 这类昂贵步骤),但缓存本身会增长,因此框架提供了 persist/load/clear 以及 Redis、MongoDB、Firestore 等远程后端来管理它。要注意如果流水线连接了向量库,embedding 必须作为流水线的一个阶段存在,否则之后从向量库实例化 index 会失败。flowchart LR D[Documents] --> S[SentenceSplitter] S --> T[TitleExtractor] T --> E[OpenAIEmbedding] E --> N[Nodes] N --> V[(Vector Store)] S -.hash+cache.-> C[(IngestionCache)] T -.hash+cache.-> C E -.hash+cache.-> C
术语
Transformations(作用于输入数据的有序转换步骤); IngestionCache(缓存 node+transformation 组合结果的对象); vector_store(可选,令 nodes 直接写入远程向量库)📖 "AnIngestionPipelineuses a concept ofTransformationsthat are applied to input data. TheseTransformationsare applied to your input data, and the resulting nodes are either returned or inserted into a vector database (if given). Each node+transformation pair is cached, so that subsequent runs (if the cache is persisted) with the same node+transformation combination can use the cached result and save you time." — create the pipeline with transformations
📖 "If you are connecting your pipeline to a vector store, embeddings must be a stage of your pipeline or your later instantiation of the index will fail." — create the pipeline with transformations
🧪 实例
python
from llama_index.core import Document
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.extractors import TitleExtractor
from llama_index.core.ingestion import IngestionPipeline, IngestionCache
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=25, chunk_overlap=0),
TitleExtractor(),
OpenAIEmbedding(),
]
)
nodes = pipeline.run(documents=[Document.example()])🔍 追问 如果不接向量库,还能省略 embedding 吗? → 可以;文档明确说不连接向量库、只想产出一批 nodes 时可以从流水线中省略 embeddings。
📚 拓展阅读
- Transformations Guide — 深入讲解可用的转换步骤
- Advanced Ingestion Pipeline — 进阶摄取流水线示例
- RAG CLI — IngestionPipeline 的交互式实例
QNode parser 是什么?它如何把 Document 切成 Node,切分时元数据怎么处理?深挖·拓展🔥高频
答 Node parser 是一个简单抽象:接收一组 documents,把它们切成
Node 对象,每个 node 是父文档的一个具体 chunk。切分的一个关键语义是继承——当一个 document 被拆成多个 node 时,它的所有属性(如 metadata、text 与 metadata 模板等)都会被继承给子 node,这保证了检索到某个 chunk 时仍能携带原文档的上下文与来源信息。使用方式有三种层次:可以独立使用(直接 get_nodes_from_documents);可以作为 transformations 放进 ingestion pipeline;也可以放进 index 的 transformations 或全局 Settings,在用 .from_documents() 构建 index 时自动生效。像 SentenceSplitter 这样的切分器通过 chunk_size 和 chunk_overlap 控制粒度与重叠,权衡在于 chunk 越大上下文越完整但检索精度和成本变差,overlap 能缓解边界处语义被切断的问题。术语
Node(父文档的一个具体 chunk); get_nodes_from_documents(独立调用切分的方法); chunk_overlap(相邻 chunk 的重叠量,缓解边界截断); Settings.text_splitter(全局默认切分器)📖 "Node parsers are a simple abstraction that take a list of documents, and chunk them intoNodeobjects, such that each node is a specific chunk of the parent document. When a document is broken into nodes, all of it’s attributes are inherited to the children nodes (i.e.metadata, text and metadata templates, etc.)." — global
🧪 实例
python
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
node_parser = SentenceSplitter(chunk_size=1024, chunk_overlap=20)
nodes = node_parser.get_nodes_from_documents(
[Document(text="long text")], show_progress=False
)🔍 追问 想让全库默认都用同一切分器,最省事的做法是什么? → 设置全局
Settings.text_splitter = SentenceSplitter(...),index 构建时会自动使用。📚 拓展阅读
- Node Parser Modules — 全部可用 node parser 模块清单
- Documents and Nodes — Node 与 Document 属性详解
QData connector(Reader)在 LlamaIndex 加载链路里扮演什么角色?深挖·拓展🔥高频
答 Data connector(又名
Reader)负责把不同数据源、不同格式的数据摄取成统一的 Document 表示(文本加简单元数据),是整条加载链路的入口。它把「异构数据源」这一复杂度收敛到一个统一抽象后面,下游的 node parsing、indexing、query 都只需面对 Document,这是分层解耦的关键。这些 connector 通过开源的 LlamaHub 分发,你可以即插即用地接入任意 LlamaIndex 应用。典型模块包括本地目录 SimpleDirectoryReader(支持 .pdf、.jpg、.png、.docx 等多种文件类型)、NotionPageReader、GoogleDocsReader、SlackReader、DiscordReader 以及可爬网抓取的 ApifyActor。摄取完成后,你就能在其上构建 Index、用 Query Engine 提问、用 Chat Engine 对话。术语
Reader(data connector 的别名); Document(文本 + 简单元数据的统一表示); LlamaHub(开源 data loader 仓库); SimpleDirectoryReader(本地目录读取器)📖 "A data connector (akaReader) ingest data from different data sources and data formats into a simpleDocumentrepresentation (text and simple metadata)." — Data Connectors (LlamaHub) | Developer Documentation
📖 "LlamaHub is an open-source repository containing data loaders that you can easily plug and play into any LlamaIndex application." — Data Connectors (LlamaHub) | Developer Documentation
🧪 实例
python
from llama_index.core import download_loader
from llama_index.readers.google import GoogleDocsReader
loader = GoogleDocsReader()
documents = loader.load_data(document_ids=[...])🔍 追问 一个 reader 能覆盖很多种文件格式吗? → 能,例如
SimpleDirectoryReader 支持解析 .pdf、.jpg、.png、.docx 等多种文件类型。📚 拓展阅读
- LlamaHub — 官方 data loader 仓库
- Connector Modules Guide — 更多 connector 模块
- Usage Pattern Guide — connector 完整用法
QIngestionPipeline 的缓存怎么管理?本地和远程分别怎么做?深挖·拓展中频
答 在
IngestionPipeline 里,每个 node + transformation 组合都会被哈希并缓存,从而让后续用相同数据的运行省去重复计算。本地管理上,用 pipeline.persist("./pipeline_storage") 保存缓存,用 new_pipeline.load(...) 恢复状态;恢复后再对相同 documents 运行会「因缓存而瞬间完成」;缓存太大时可 cache.clear() 清空。远程管理上,框架支持多种远程存储后端——RedisCache、MongoDBCache、FirestoreCache;使用远程缓存时无需 persist 步骤,因为一切都在指定的远程 collection 中随处理进度即时缓存。权衡在于:本地缓存简单、无外部依赖,适合单机与开发;远程缓存适合分布式/多进程或需要跨运行共享缓存的生产场景,代价是引入外部服务。术语
persist/load(本地缓存的保存与恢复); cache.clear()(清空缓存); RedisCache / MongoDBCache / FirestoreCache(三种远程缓存后端); collection(远程缓存的命名集合)📖 "In an IngestionPipeline, each node + transformation combination is hashed and cached. This saves time on subsequent runs that use the same data." — create the pipeline with transformations📖 "Here, no persist step is needed, since everything is cached as you go in the specified remote collection." — create the pipeline with transformations
🧪 实例
python
# save
pipeline.persist("./pipeline_storage")
# load and restore state
new_pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=25, chunk_overlap=0),
TitleExtractor(),
],
)
new_pipeline.load("./pipeline_storage")
# will run instantly due to the cache
nodes = pipeline.run(documents=[Document.example()])🔍 追问 用 Redis 远程缓存时还要手动 persist 吗? → 不需要,处理过程中一切都即时缓存进指定的远程 collection。
📚 拓展阅读
- Redis Ingestion Pipeline — 用 Redis 作为整个摄取栈
- Async Ingestion Pipeline — 异步摄取示例
Q摄取流水线的 Document Management(去重)机制是怎么工作的?深挖·拓展中频
答 给 ingestion pipeline 附加一个
docstore 即可启用文档管理。它以 document.doc_id 或 node.ref_doc_id 为锚点主动查找重复文档,内部维护一张 doc_id -> document_hash 的映射。行为分两种情形:如果附加了 vector store,当检测到重复 doc_id 且 hash 已变化时,文档会被重新处理并 upsert;若 doc_id 重复但 hash 未变,则跳过该 node。如果没有附加 vector store,则对每个 node 检查所有已存在的 hash,命中重复就跳过,否则照常处理。关键权衡与限制是:不附加 vector store 时,只能检查并移除重复的输入,而无法做增量 upsert。这套机制让重复摄取既能避免冗余计算,又能在源文档更新时正确刷新向量库内容。术语
docstore(启用文档管理的存储); doc_id -> document_hash(去重用的映射); ref_doc_id(node 指向父文档的 id); upsert(hash 变化时重处理并写入)📖 "Using thedocument.doc_idornode.ref_doc_idas a grounding point, the ingestion pipeline will actively look for duplicate documents." — create the pipeline with transformations
📖 "NOTE: If we do not attach a vector store, we can only check for and remove duplicate inputs." — create the pipeline with transformations
🧪 实例
python
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.storage.docstore import SimpleDocumentStore
pipeline = IngestionPipeline(
transformations=[...], docstore=SimpleDocumentStore()
)🔍 追问 源文档内容改了、但 doc_id 不变,会发生什么? → 若附加了 vector store,检测到重复 doc_id 且 hash 变化时会重新处理并 upsert。
📚 拓展阅读
- Document Management Pipeline — 完整去重演练 notebook
- Google Drive Ingestion Pipeline — 结合 Google Drive 的摄取示例
QIngestionPipeline 如何做并行与异步执行?深挖·拓展低频
答
IngestionPipeline 的 run 方法支持并行执行,底层通过 multiprocessing.Pool 把一批批 nodes 分发到多个处理器上完成。要开启并行,只需把 num_workers 设为你希望使用的进程数,例如 pipeline.run(documents=[...], num_workers=4)。这适合 CPU 密集、文档量大的摄取场景,用多进程摊薄处理时间,代价是进程创建与数据分发的开销、以及更高的内存占用。除并行外,流水线还提供异步支持:用 await pipeline.arun(documents=documents) 以协程方式运行,更适合 I/O 密集(如调用远程 embedding/提取服务)时并发等待。两者面向不同瓶颈——num_workers 解决 CPU 并行,arun 解决 I/O 并发。术语
num_workers(并行使用的进程数); multiprocessing.Pool(分发批次的底层机制); arun(异步运行方法)📖 "Therunmethod ofIngestionPipelinecan be executed with parallel processes. It does so by making use ofmultiprocessing.Pooldistributing batches of nodes to across processors." — create the pipeline with transformations
📖 "The IngestionPipeline also has support for async operation" — create the pipeline with transformations🧪 实例
python
from llama_index.core.ingestion import IngestionPipeline
pipeline = IngestionPipeline(
transformations=[...],
)
pipeline.run(documents=[...], num_workers=4)🔍 追问 异步运行用哪个方法? → 用
await pipeline.arun(documents=documents)。📚 拓展阅读
- Parallel Execution Pipeline — 并行执行示例
- Async Ingestion Pipeline — 异步摄取示例
第2章 索引与存储
Q在 LlamaIndex 中,Index 到底是什么?它在 RAG 里承担什么角色?深挖·拓展🔥高频
答
Index 是一个数据结构,作用是让我们能针对用户查询快速取回相关上下文,它是 LlamaIndex 做检索增强生成(RAG)的核心基础。机制上,Index 从 Documents 构建而来,底层把数据存进 Node 对象(每个 Node 代表原始文档被切分后的一个 chunk),再对外暴露一个 Retriever 接口,这个接口支持额外的配置与自动化。之所以要引入 Node 这一层,是因为原始文档往往太长、不利于检索粒度控制,切成 chunk 后既能提升召回精度,又能在检索时只把相关片段喂给 LLM,从而控制上下文窗口成本。构建好的 Index 进一步被用来构建 Query Engines 和 Chat Engines,从而实现对数据的问答与对话。权衡点在于:不同 Index 有不同的存取与检索策略,其中 VectorStoreIndex 是最常见的一种,也是绝大多数 RAG 场景的默认起点。术语
Index(可快速取回查询相关上下文的数据结构); Node(原始文档切分后的 chunk 对象); Retriever(检索接口,支持配置与自动化); RAG(检索增强生成)📖 "An Index is a data structure that allows us to quickly retrieve relevant context for a user query. For LlamaIndex, it’s the core foundation for retrieval-augmented generation (RAG) use-cases." — Indexing | Developer Documentation📖 "Under the hood,Indexesstore data inNodeobjects (which represent chunks of the original documents), and expose a Retriever interface that supports additional configuration and automation." — Indexing | Developer Documentation
🧪 实例 从文档到可检索索引的最小心智模型:
flowchart LR
D[Documents] --> N[Node 对象/chunks]
N --> I[Index]
I --> R[Retriever 接口]
R --> QE[Query / Chat Engine]🔍 追问 为什么
Index 不直接存整篇 Document,而要拆成 Node? → 因为底层是以 Node(即文档 chunk)为单位存储和检索的,chunk 粒度让检索既能精准命中相关片段,又便于 Retriever 做配置与自动化。📚 拓展阅读
- VectorStoreIndex usage guide — 最常见索引的上手指南
- how each index works — 帮助你判断哪种索引匹配你的用例
- Retriever — Index 对外暴露的检索接口
QLlamaIndex 的可插拔存储组件有哪几类?它们各存什么?深挖·拓展🔥高频
答 在底层,LlamaIndex 支持可替换的存储组件(swappable storage components),允许你分别自定义几类存储:Document stores 存放被摄入的文档(即
Node 对象);Index stores 存放索引的元数据;Vector stores 存放 embedding 向量;Property Graph stores 存放知识图谱(供 PropertyGraphIndex 使用);Chat Stores 存放并组织聊天消息。之所以把存储拆成这么多类,是为了把"数据本体、索引元数据、向量、图、对话"这些职责解耦,从而可以针对不同后端各自优化和替换。其中 Document stores 与 Index stores 都建立在一个共同的 Key-Value store 抽象之上——也就是说它们本质上是 KV 存储的上层封装,这带来的好处是切换底层 KV 后端时,docstore 和 index store 能一并复用同一套抽象。术语
Document stores(存 Node 对象); Index stores(存索引元数据); Vector stores(存 embedding 向量); Property Graph stores(存知识图谱); Chat Stores(存聊天消息); Key-Value store(docstore/index store 共用的底层抽象)📖 "Under the hood, LlamaIndex also supports swappable storage components that allows you to customize:" — construct vector store and customize storage context
📖 "The Document/Index stores rely on a common Key-Value store abstraction, which is also detailed below." — construct vector store and customize storage context
🧪 实例 各存储组件与其职责一览:
text
Document stores -> ingested documents (Node objects)
Index stores -> index metadata
Vector stores -> embedding vectors
Property Graph -> knowledge graphs (PropertyGraphIndex)
Chat Stores -> chat messages
Docstore / Index store 都构建在共用的 Key-Value store 抽象之上🔍 追问 Document store 和 Index store 有什么共同的底层实现? → 两者都依赖一个共同的 Key-Value store 抽象,因此可以共享同一套 KV 后端。
📚 拓展阅读
- Vector Stores — 向量存储的深入指南
- Docstores — 文档存储组件
- Index Stores — 索引元数据存储
- Key-Val Stores — docstore/index store 共用的 KV 抽象
Q用向量库时为什么常常不需要单独的 docstore/index store,也不用手动持久化?深挖·拓展🔥高频
答 大多数向量库(FAISS 除外)会同时存储数据本身以及索引(embeddings),这意味着你不需要再用一个单独的 document store 或 index store,也意味着你不需要显式地持久化这些数据——持久化会自动发生。这背后的权衡是:向量库把"原始数据 + 向量索引"打包托管,简化了存储栈和运维;但 FAISS 是个例外,它只管向量索引、不托管数据本体,所以用 FAISS 时你仍需自己安排 docstore/index store 与持久化。使用上,构建新索引时把
vector_store 交给 StorageContext.from_defaults(...) 再传给 VectorStoreIndex.from_documents(...);重载已有索引则用 VectorStoreIndex.from_vector_store(vector_store=vector_store)。若要更一般地使用存储抽象,则需要显式定义一个 StorageContext 对象,把 docstore、vector_store、index_store 组合进去。术语
StorageContext(聚合各存储组件的上下文对象); from_vector_store(从已有向量库重载索引); FAISS(只存向量索引、不存数据本体的例外); 自动持久化(向量库场景下无需显式 persist)📖 "Many vector stores (except FAISS) will store both the data as well as the index (embeddings). This means that you will not need to use a separate document store or index store. This *also* means that you will not need to explicitly persist this data - this happens automatically." — construct vector store and customize storage context
📖 "Note that in general to use storage abstractions, you need to define a StorageContext object:" — construct vector store and customize storage context🧪 实例 构建新索引与重载已有索引:
python
## build a new index
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.deeplake import DeepLakeVectorStore
# construct vector store and customize storage context
vector_store = DeepLakeVectorStore(dataset_path="<dataset_path>")
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# Load documents and build index
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
## reload an existing one
index = VectorStoreIndex.from_vector_store(vector_store=vector_store)🔍 追问 为什么 FAISS 被单独排除在"数据和索引一起存"之外? → 因为文档明确 FAISS 是 except 的情况,它不像其他向量库那样同时托管数据本体,因此用它时仍要单独准备 docstore/index store 并手动持久化。
📚 拓展阅读
- Vector Store Module Guide — 向量存储更多细节
- Save/Load — 显式保存与加载
- Customization — 存储自定义与持久化
Q如何用默认存储手动组装一个 StorageContext?各默认实现是什么?深挖·拓展中频
答 当你不走"向量库自动托管"的捷径、而要一般性地使用存储抽象时,需要显式定义一个
StorageContext 对象。默认组装方式是调用 StorageContext.from_defaults(...),分别传入三类默认 store:SimpleDocumentStore() 作为 docstore、SimpleVectorStore() 作为 vector_store、SimpleIndexStore() 作为 index_store。这种"三件套"结构直接对应了前面讲的存储组件分层(文档、向量、索引元数据),好处是把三种职责显式暴露出来,任何一层都可以被替换成生产级后端而不动其它层;代价是相比向量库一把梭,你要自己管理各 store 的生命周期与持久化。更多关于自定义与持久化的细节见 Customization 与 Save/Load 指南。术语
StorageContext.from_defaults(用默认 store 组装存储上下文); SimpleDocumentStore(默认文档存储); SimpleVectorStore(默认向量存储); SimpleIndexStore(默认索引元数据存储)📖 "Note that in general to use storage abstractions, you need to define a StorageContext object:" — construct vector store and customize storage context🧪 实例 用默认 store 显式构建 StorageContext:
python
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.core.storage.index_store import SimpleIndexStore
from llama_index.core.vector_stores import SimpleVectorStore
from llama_index.core import StorageContext
# create storage context using default stores
storage_context = StorageContext.from_defaults(
docstore=SimpleDocumentStore(),
vector_store=SimpleVectorStore(),
index_store=SimpleIndexStore(),
)🔍 追问
StorageContext.from_defaults 里的三个参数分别对应哪三类存储组件? → docstore 对应 Document stores、vector_store 对应 Vector stores、index_store 对应 Index stores。📚 拓展阅读
- Customization — 存储组件的自定义
- Save/Load — 保存与重新加载
- Docstores — SimpleDocumentStore 所属分类
QLlamaIndex 支持持久化到哪些存储后端?底层靠什么实现?深挖·拓展中频
答 LlamaIndex 支持把数据持久化到任何被 fsspec 支持的存储后端,也就是说它没有把持久化绑死在某一种文件系统上,而是复用 fsspec 这个统一的文件系统抽象层来对接各种后端。官方已确认支持的后端包括:本地文件系统(Local filesystem)、AWS S3、Cloudflare R2。这样设计的价值在于:开发时可以用本地文件系统快速迭代,上生产时无需改动存储代码即可切换到 S3 或 R2 等对象存储;而通过 fsspec 抽象,未来接入更多后端也不需要 LlamaIndex 逐一适配。需要注意"已确认支持"是官方明确列出的清单,并不排斥 fsspec 生态里的其它后端。
术语
fsspec(统一文件系统抽象,持久化后端的对接层); Local filesystem(本地文件系统后端); AWS S3(对象存储后端); Cloudflare R2(对象存储后端)📖 "LlamaIndex supports persisting data to any storage backend supported by fsspec. We have confirmed support for the following storage backends:" — construct vector store and customize storage context
🧪 实例 官方已确认支持的持久化后端清单:
text
- Local filesystem
- AWS S3
- Cloudflare R2
(凡 fsspec 支持的后端,理论上都可作为持久化目标)🔍 追问 换持久化后端(比如从本地换到 S3)为什么代价小? → 因为持久化统一走 fsspec 抽象层,只要目标后端被 fsspec 支持,上层代码基本不用改就能切换。
📚 拓展阅读
- fsspec — LlamaIndex 依赖的统一文件系统抽象
- Save/Load — 数据的保存与加载流程
- Customization — 存储持久化的自定义
第3章 查询流水线
Q在 LlamaIndex 中,Retriever(检索器)的职责是什么?它在 RAG 流程里处于什么位置?深挖·拓展🔥高频
答 Retriever 的核心职责是:给定一个用户查询(或聊天消息),负责取回最相关的上下文(context),把它作为后续生成的素材。它并不是孤立存在的——它既可以构建在 index 之上(利用索引已经组织好的数据结构与向量),也可以脱离 index 独立定义,这给了工程上很大的灵活度:当你已有一套索引时直接复用,当你的检索逻辑特殊(比如混合检索、外部服务)时可以自己实现。之所以它是 RAG 的关键构件,是因为它是 query engine 和 chat engine 内部"取上下文"这一步的承担者;检索质量直接决定了喂给 LLM 的证据质量,进而决定最终答案质量,所以检索器往往是 RAG 系统里最值得调优的一环。
术语
Retriever(检索器,负责取回相关上下文); context(上下文,喂给 LLM 的证据片段); index(索引,可作为检索器的底座); query engine(查询引擎,检索器是其关键构件)📖 "Retrievers are responsible for fetching the most relevant context given a user query (or chat message)." — Retriever | Developer Documentation
📖 "It can be built on top of indexes, but can also be defined independently. It is used as a key building block in query engines (and Chat Engines) for retrieving relevant context." — Retriever | Developer Documentation
🧪 实例 最小检索用法——先从 index 拿到一个 retriever,再对问题做检索:
返回的
python
retriever = index.as_retriever()
nodes = retriever.retrieve("Who is Paul Graham?")返回的
nodes 就是被判定为最相关的上下文节点,可交给下游的 response synthesizer 组装答案。🔍 追问 检索器一定要建立在 index 上吗? → 不一定;它可以构建在 index 之上,但也可以独立定义(built on top of indexes, but can also be defined independently)。
📚 拓展阅读
- Retrievers — 检索器模块的官方总览页
- High-Level Concepts — 理清 retriever 在 RAG 工作流中的位置
- Indexing — 检索器可构建其上的索引层
Q如何从一个 index 拿到检索器?High-Level API 里怎么选择和配置具体的检索器?深挖·拓展🔥高频
答 高层用法的入口是
index.as_retriever(),它会为该索引返回一个默认检索器。若要指定索引特有的检索器类,通过 retriever_mode 参数来选——例如在 SummaryIndex 上传入 retriever_mode="llm",就会在该 summary index 之上创建一个 SummaryIndexLLMRetriever。不同索引类型支持的 mode 与其映射到的检索器类各不相同,需要查 Retriever Modes 表。选定 mode 之后,可以用同样的方式继续传 kwargs 来配置这个被选中的检索器,比如 choice_batch_size=5;至于哪些 kwargs 合法,要去看对应检索器类构造函数的 API 参考。这种"mode 选类 + kwargs 调参"的两段式设计,好处是让常见场景保持一行代码即可用,同时又保留了细粒度可配置性,权衡上把简单默认和可调性都照顾到了。术语
as_retriever()(从索引获取检索器的入口方法); retriever_mode(选择索引特有检索器类的参数); SummaryIndexLLMRetriever(summary index 上 llm 模式对应的检索器类); kwargs(配置选中检索器的关键字参数); choice_batch_size(llm 模式下的一个配置项)📖 "You can select the index-specific retriever class via retriever_mode." — Retriever | Developer Documentation📖 "In the same way, you can pass kwargs to configure the selected retriever." — Retriever | Developer Documentation
🧪 实例 选择 llm 模式并配置 batch size:
其中
python
retriever = summary_index.as_retriever(
retriever_mode="llm",
choice_batch_size=5,
)其中
retriever_mode="llm" 决定了用哪一个检索器类,choice_batch_size=5 则是传给它的配置 kwarg。🔍 追问 怎么知道某个检索器支持哪些 kwargs? → 查该检索器类构造函数的 API 参考(take a look at the API reference for the selected retriever class' constructor parameters)。
📚 拓展阅读
- Retriever Modes — 各索引的 retriever mode 与对应检索器类全表
- SummaryIndexLLMRetriever — llm 模式生成的检索器类 API 参考
- Retrievers Guide — 更多检索器示例
QHigh-Level API 和 Low-Level Composition API 有什么区别?什么时候该用低层 API?深挖·拓展低频
答 两者能达到相同的检索结果,区别在于抽象层次与控制粒度。高层 API 走
index.as_retriever(retriever_mode=..., **kwargs),由框架根据 mode 帮你选好检索器类并实例化,写起来简洁。低层组合式 API 则是当你需要更细粒度控制时使用:你直接从 llama_index.core.retrievers 导入目标检索器类并亲手构造它,把 index、choice_batch_size 等参数显式传进构造函数。权衡在于:高层 API 隐藏了实例化细节、适合快速上手和常规场景;低层 API 暴露了构造过程,便于你精确掌控用哪个类、怎样组装,适合需要定制或组合多个组件的高级场景。文档明确指出这两条路可以得到"same outcome",所以选择更多是关于你想要多少控制权,而非功能差异。术语
Low-Level Composition API(低层组合式 API,提供更细粒度控制); granular control(细粒度控制); llama_index.core.retrievers(直接导入检索器类的模块路径)📖 "You can use the low-level composition API if you need more granular control." — Retriever | Developer Documentation
🧪 实例 用低层 API 直接构造,等价于前面高层写法的结果:
这里没有经过
python
from llama_index.core.retrievers import SummaryIndexLLMRetriever
retriever = SummaryIndexLLMRetriever(
index=summary_index,
choice_batch_size=5,
)这里没有经过
retriever_mode 的间接选择,而是显式点名 SummaryIndexLLMRetriever 并手动传入 index 和 choice_batch_size。🔍 追问 低层写法和高层
retriever_mode="llm" 结果一样吗? → 是的,文档说低层 API 是"To achieve the same outcome as above",两者产出相同结果,只是控制粒度不同。📚 拓展阅读
- SummaryIndexLLMRetriever — 低层直接构造的检索器类 API
- Retriever Modes — 高层 mode 与检索器类的映射关系
- Retrievers Guide — 更多组合与用法示例
QLlamaIndex 的"查询"环节由哪些查询模块组成?它们各自负责什么?深挖·拓展中频
答 LlamaIndex 把查询拆成一组可以独立使用的 query modules,官方文档列出的包括:Query Engines、Chat Engines、Agents、Retrievers、Response Synthesizers、Routers、Node Postprocessors、Structured Outputs。它们构成了一条从"取上下文"到"给出结构化答案"的流水线:Retriever 负责取回相关上下文,Node Postprocessor 对取回的节点做重排/过滤,Router 负责在多条查询路径间做选择,Response Synthesizer 把上下文合成为答案,Structured Outputs 约束输出格式,而 Query Engine / Chat Engine / Agent 则是把这些底层组件编排起来、面向部署的高层封装。文档强调这些模块可以"作为独立组件"使用,这种模块化设计的价值在于:你既能一键使用高层引擎,也能只挑其中某个组件按需组装,灵活性和可组合性都得到保证。查询之所以被反复强调,是因为它是 LLM 应用中最重要的部分。
术语
Query Engines(查询引擎); Chat Engines(聊天引擎); Agents(智能体); Response Synthesizers(响应合成器); Routers(路由器); Node Postprocessors(节点后处理器); Structured Outputs(结构化输出)📖 "Querying is the most important part of your LLM application." — Querying | Developer Documentation
📖 "Otherwise check out how to use our query modules as standalone components 👇." — Querying | Developer Documentation
🧪 实例 查询流水线中各模块的大致数据流:
flowchart LR
Q[User Query] --> R[Retriever]
R --> NP[Node Postprocessors]
NP --> RS[Response Synthesizers]
RS --> SO[Structured Outputs]
RT[Routers] -.选择路径.-> R
QE[Query / Chat Engines / Agents] -.编排.-> R🔍 追问 这些查询模块必须一起用吗? → 不必;文档说明它们可以作为 standalone components(独立组件)单独使用。
📚 拓展阅读
- Query Engines — 面向部署的查询引擎
- Response Synthesizers — 把上下文合成为最终答案
- Routers — 在多条查询路径间做选择
- Node Postprocessors — 对检索节点做重排与过滤
Q如果不用现成的查询模块,LlamaIndex 还提供了什么方式来编排查询?深挖·拓展低频
答 除了直接使用 Query Engines、Retrievers 等现成模块,LlamaIndex 还提供了基于事件驱动(event-driven)的
Workflow 接口,让你可以自行为"查询"编排流程。它的定位是:当你想把检索、后处理、合成等步骤按自己的逻辑串联、加入条件分支或多步推理时,用 Workflow 来组织比硬编码调用更清晰。文档把它和"直接把查询模块当独立组件用"并列作为两条路径——前者偏向自定义编排,后者偏向拿来即用。之所以采用事件驱动模型,是因为查询过程天然存在分支与多步骤流转,事件驱动能更自然地表达这种控制流。对于复杂的检索增强流程(例如多轮检索、路由后再检索),Workflow 提供了比单个 engine 更强的组合与可控性。术语
Workflow(事件驱动的查询编排接口); event-driven(事件驱动,天然表达分支与多步流转); standalone components(独立组件,与 Workflow 并列的另一条使用路径)📖 "You can create workflows for querying with ease, using our event-driven Workflow interface." — Querying | Developer Documentation🧪 实例 一个查询 Workflow 的概念流转(检索→后处理→合成,由事件驱动):
具体如何定义步骤与事件,参见官方 workflow guide。
flowchart TD
Start[Query Event] --> Retrieve[Retrieve Nodes]
Retrieve --> Post[Postprocess]
Post --> Synth[Synthesize Response]
Synth --> End[Result Event]具体如何定义步骤与事件,参见官方 workflow guide。
🔍 追问 Workflow 用的是什么范式来驱动查询流程? → 事件驱动(event-driven)的
Workflow 接口。📚 拓展阅读
- Workflow Guide — 事件驱动 Workflow 的完整说明
- Agents — 结合高级推理与工具使用的编排方式
- Chat Engines — 面向多轮对话的查询封装
🔥高频
后处理与响应合成
similarity postprocessor: filter nodes below 0.75 similarity score NOTE: we add an extra tone_name variable hereQLlamaIndex 的 response_mode 有哪些?refine、compact、tree_summarize、accumulate 各自的机制与取舍是什么?深挖·拓展🔥高频
答 Response Synthesizer 通过
response_mode 参数决定如何把检索到的多个 text chunk 合成为一个答案,核心权衡是「LLM 调用次数 / token 成本」与「答案细节保真度」之间的取舍。refine 逐个 chunk 顺序处理:第一个 chunk 用 text_qa_template 生成初答,之后每个 chunk 连同已有答案和原问题一起用 refine_template 再查询一次,因此每个 Node 都会产生一次单独的 LLM 调用,答案最细但最贵;当某个 chunk 太大放不进窗口时会用 TokenTextSplitter 切分(允许 overlap)后当作新 chunk 继续 refine。compact(默认)先把 chunk 尽量拼接塞满上下文窗口再送入 refine 流程,本质是「调用更少的 refine」,在成本和质量间取平衡。tree_summarize 把 chunk 拼接后各自用 summary_template 查询(没有 refine 步骤),得到的多个答案再递归当作 chunk 继续 tree_summarize,直到只剩一个最终答案,适合摘要。simple_summarize 直接把所有 chunk 截断进单个 prompt,快但可能因截断丢细节。accumulate 对每个 chunk 分别跑同一个 query 并把结果累加成数组,适合需要对每块单独提问的场景;compact_accumulate 是其压缩版。此外 no_text 只跑检索不调用 LLM(用于查看 response.source_nodes),context_only 直接返回拼接后的 chunk 文本。术语
refine(逐块创建并精炼答案); compact(默认模式,先拼接再 refine,减少调用); tree_summarize(递归树形摘要); accumulate(对每块独立提问再累加); text_qa_template / refine_template / summary_template(各阶段使用的 prompt 模板); TokenTextSplitter(超窗时的切分器)📖 "refine: *create and refine* an answer by sequentially going through each retrieved text chunk. This makes a separate LLM call per Node/retrieved chunk." — NOTE: we add an extra tone_name variable here📖 "compact(default): similar torefinebut *compact* (concatenate) the chunks beforehand, resulting in less LLM calls." — NOTE: we add an extra tone_name variable here
📖 "simple_summarize: Truncates all text chunks to fit into a single LLM prompt. Good for quick summarization purposes, but may lose detail due to truncation." — NOTE: we add an extra tone_name variable here🧪 实例 用
response_mode 显式配置一个偏摘要场景的合成器:python
from llama_index.core import get_response_synthesizer
# 摘要类问题优先用 tree_summarize;追求低成本可换成 "compact"
response_synthesizer = get_response_synthesizer(response_mode="tree_summarize")
query_engine = index.as_query_engine(response_synthesizer=response_synthesizer)
response = query_engine.query("query_text")🔍 追问 为什么
compact 通常比 refine 便宜? → 因为 compact 会先把多个 chunk 拼接/打包塞进上下文窗口再走 refine 流程,等价于「chunk 更少的 refine」,从而显著减少 LLM 调用次数。📚 拓展阅读
- Response Synthesizer 模块指南 — 各合成器与模式的完整模块列表
- Indexing — 了解 query engine 依赖的 index 如何构建
- High-level concepts — response synthesizer 在 RAG 工作流中的位置
Q什么是 Node Postprocessor?它在 RAG 查询流水线中处于哪个位置?深挖·拓展🔥高频
答 Node postprocessor 是一组模块,接收一批 node 后对其做某种「转换」或「过滤」再返回,典型用途包括按相似度阈值过滤、按时间加权、以及用训练好的 rerank 模型重排序。它在流水线中的位置很关键:最常见是嵌在 query engine 内部,在 retriever 完成 node 检索之后、response synthesis 之前运行;也就是说它作用于「已经召回、但还没送进 LLM 合成」的候选节点,因此是控制送入 LLM 的上下文质量与数量的关键闸门。postprocessor 的输入输出都是
NodeWithScore(Node 加 score 的包装类),既可以像 SimilarityPostprocessor 那样基于 score 直接过滤,也可以像 CohereRerank 那样结合 query 重排。它既能作为 query engine 的 node_postprocessors 列表在每次 query 时自动应用,也能作为独立对象手动对 retriever 返回的节点调用 postprocess_nodes 使用;注意 postprocess_nodes 接受 query_str 或 query_bundle 二选一,不能同时传。术语
node postprocessor(节点后处理器,检索后合成前的转换/过滤模块); NodeWithScore(带分数的节点包装类); SimilarityPostprocessor(按相似度阈值过滤); CohereRerank(基于模型的重排序); postprocess_nodes(执行后处理的方法)📖 "Node postprocessors are a set of modules that take a set of nodes, and apply some kind of transformation or filtering before returning them." — similarity postprocessor: filter nodes below 0.75 similarity score
📖 "In LlamaIndex, node postprocessors are most commonly applied within a query engine, after the node retrieval step and before the response synthesis step." — similarity postprocessor: filter nodes below 0.75 similarity score
🧪 实例 后处理在流水线中的位置:
把相似度过滤挂到 query engine:
flowchart LR Q[User Query] --> R[Retriever 召回 nodes] R --> P[Node Postprocessor 过滤/重排] P --> S[Response Synthesis 合成答案] S --> A[Response]
把相似度过滤挂到 query engine:
python
from llama_index.core.postprocessor import SimilarityPostprocessor
nodes = index.as_retriever().retrieve("test query str")
# filter nodes below 0.75 similarity score
processor = SimilarityPostprocessor(similarity_cutoff=0.75)
filtered_nodes = processor.postprocess_nodes(nodes)🔍 追问
postprocess_nodes 能同时传 query_str 和 query_bundle 吗? → 不能,它接受二者之一但不能两者都传。📚 拓展阅读
- Node Postprocessor 模块列表 — 内置各类 postprocessor 详情
- High-level concepts — 后处理在 RAG 工作流中的定位
- Response Synthesizer — 后处理之后的下一步合成环节
QResponse Synthesizer 是什么?如何独立使用或挂到 query engine 上?深挖·拓展🔥高频
答 Response Synthesizer 负责用「用户 query + 一组 text chunk」调用 LLM 生成回答,输出是一个
Response 对象;它把「跨数据生成回答」这件事抽象化,实现方式可以简单到遍历 chunk,也可以复杂到构建一棵摘要树。在 query engine 中,它运行在 retriever 检索出 node 之后、并且在所有 node-postprocessor 跑完之后——这明确了它是流水线的最后合成环节。使用上有两种方式:一是通过 get_response_synthesizer(response_mode=...) 拿到合成器后直接调用 synthesize("query text", nodes=[...]) 独立使用,nodes 可以是裸 Node 也可以是 NodeWithScore;二是更常见地把它作为 response_synthesizer= 传给 index.as_query_engine(...),之后 query_engine.query(...) 就会用它合成答案。这种解耦让你可以先独立调好合成逻辑再插入任意 query engine,或复用同一合成器于不同 index。术语
Response Synthesizer(响应合成器); Response(合成输出对象); get_response_synthesizer(工厂函数,按 response_mode 构造); synthesize(独立合成入口); as_query_engine(把合成器挂进 query engine)📖 "AResponse Synthesizeris what generates a response from an LLM, using a user query and a given set of text chunks. The output of a response synthesizer is aResponseobject." — NOTE: we add an extra tone_name variable here
📖 "When used in a query engine, the response synthesizer is used after nodes are retrieved from a retriever, and after any node-postprocessors are ran." — NOTE: we add an extra tone_name variable here
🧪 实例 独立使用合成器:
python
from llama_index.core.data_structs import Node
from llama_index.core.response_synthesizers import ResponseMode
from llama_index.core import get_response_synthesizer
response_synthesizer = get_response_synthesizer(
response_mode=ResponseMode.COMPACT
)
response = response_synthesizer.synthesize(
"query text", nodes=[Node(text="text"), ...]
)🔍 追问 合成器和 postprocessor 的执行先后是怎样的? → 先 retriever 检索,再跑 node-postprocessor,最后才是 response synthesizer 合成。
📚 拓展阅读
- Response Synthesizer 模块指南 — 所有合成器与模式详解
- Node Postprocessor — 合成前的后处理环节
- Indexing — 构建 index 以创建 query engine
Q如何实现一个自定义的 Node Postprocessor?需要实现哪个接口?深挖·拓展中频
答 自定义 postprocessor 只需继承基类
BaseNodePostprocessor 并实现一个抽象方法 _postprocess_nodes,其接口极简:入参是 List[NodeWithScore] 和一个可选的 QueryBundle,返回处理后的 List[NodeWithScore]。因为 postprocessor 拿到的是 NodeWithScore(Node + score 的包装类),你可以在方法里自由地改写 score、按条件过滤、或结合 query_bundle 做重排,整个自定义逻辑往往只有几行代码。这种设计的价值在于:过滤/重排逻辑与检索、合成解耦,任何新的评分或过滤策略都能以插件式的方式接入既有 query engine,而不必改动检索或合成层。术语
BaseNodePostprocessor(自定义后处理器的基类); _postprocess_nodes(必须实现的抽象方法); QueryBundle(封装 query 的可选入参); NodeWithScore(带分数节点包装)📖 "The base class is BaseNodePostprocessor, and the API interface is very simple:" — similarity postprocessor: filter nodes below 0.75 similarity score📖 "the postprocessors takeNodeWithScoreobjects as inputs, which is just a wrapper class with aNodeand ascorevalue." — similarity postprocessor: filter nodes below 0.75 similarity score
🧪 实例 几行代码实现一个「把每个 score 减 1」的示例:
python
from llama_index.core import QueryBundle
from llama_index.core.postprocessor.types import BaseNodePostprocessor
from llama_index.core.schema import NodeWithScore
class DummyNodePostprocessor(BaseNodePostprocessor):
def _postprocess_nodes(
self, nodes: List[NodeWithScore], query_bundle: Optional[QueryBundle]
) -> List[NodeWithScore]:
# subtracts 1 from the score
for n in nodes:
n.score -= 1
return nodes🔍 追问 自定义 postprocessor 能拿到 query 信息吗? → 能,
_postprocess_nodes 的第二个参数是可选的 QueryBundle,可用于做与 query 相关的重排或过滤。📚 拓展阅读
- Node Postprocessor 模块列表 — 内置 postprocessor 供参考
- High-level concepts — 后处理在整体架构中的角色
Qstructured_answer_filtering 是什么?什么场景下开启它,对 LLM 有什么要求?深挖·拓展中频
答
structured_answer_filtering 是在 refine 或 compact 合成模式下可选开启的选项,设为 True 后 refine 模块能够过滤掉那些与所问问题不相关的输入 node。它对 RAG 式问答尤其有用——这类系统会从外部向量库为一个 query 召回若干文本块,其中难免混入不相关内容,结构化过滤能在合成阶段把这些噪声剔除,提升答案质量。代价与限制在于:该特性依赖 LLM 产出结构化响应,因此在支持 function calling 的 OpenAI 模型上尤其可靠;而其他不具备原生 function calling 能力的提供商或模型,在生成这种结构化响应时可能不够可靠。开启方式很简单,在 get_response_synthesizer(structured_answer_filtering=True) 传入即可。术语
structured_answer_filtering(结构化答案过滤选项); refine / compact(支持该选项的两种模式); function calling(结构化输出所依赖的模型能力)📖 "Withstructured_answer_filteringset toTrue, our refine module is able to filter out any input nodes that are not relevant to the question being asked." — NOTE: we add an extra tone_name variable here
📖 "Other LLM providers or models that don’t have native function calling support may be less reliable in producing the structured response this feature relies on." — NOTE: we add an extra tone_name variable here
🧪 实例 开启结构化过滤:
python
from llama_index.core import get_response_synthesizer
response_synthesizer = get_response_synthesizer(structured_answer_filtering=True)🔍 追问 为什么在非 function-calling 模型上要谨慎使用该选项? → 因为该特性依赖模型稳定产出结构化响应,缺乏原生 function calling 的模型在这方面可能不可靠,过滤效果会打折扣。
📚 拓展阅读
- OpenAI function calling 说明 — 该特性所依赖的模型能力背景
- Response Synthesizer 模块指南 — refine/compact 等模式详情
Q如何自定义 response synthesizer 的 prompt 模板并在查询时注入额外变量?深挖·拓展低频
答 你可以自定义合成器所用的 prompt,并在 query 阶段追加额外变量。做法是先用
PromptTemplate 定义一个带自定义占位符(如 {tone_name} 之外的标准 {context_str}、{query_str})的模板,把它作为 summary_template 传给合成器(例如 TreeSummarize),然后在调用 get_response 时通过 **kwargs 传入这些额外变量的实际值。机制上,合成器渲染 prompt 时会把 get_response 收到的关键字参数填入模板对应占位符,因此可以在不改动合成器内部逻辑的前提下,动态地为每次查询注入语气、角色等定制信息。若要更深度定制(比如控制 tree_summarize 每一步用哪个模板,或实现全新的响应生成算法),可继承 BaseSynthesizer 并实现 get_response / aget_response 两个抽象方法。术语
PromptTemplate(prompt 模板类); summary_template(传给合成器的模板参数); get_response(合成入口,**kwargs 注入额外变量); TreeSummarize(可自定义模板的合成器); BaseSynthesizer(自定义合成器基类)📖 "You may want to customize the prompts used in our response synthesizer, and also add additional variables during query-time." — NOTE: we add an extra tone_name variable here
📖 "You can specify these additional variables in the**kwargsforget_response." — NOTE: we add an extra tone_name variable here
🧪 实例 给 TreeSummarize 注入一个
tone_name 变量:python
from llama_index.core import PromptTemplate
from llama_index.core.response_synthesizers import TreeSummarize
# NOTE: we add an extra tone_name variable here
qa_prompt_tmpl = (
"Context information is below.\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"Given the context information and not prior knowledge, "
"answer the query.\n"
"Please also write the answer in the tone of {tone_name}.\n"
"Query: {query_str}\n"
"Answer: "
)
qa_prompt = PromptTemplate(qa_prompt_tmpl)
# initialize response synthesizer
summarizer = TreeSummarize(verbose=True, summary_template=qa_prompt)
# get response
response = summarizer.get_response(
"who is Paul Graham?", [text], tone_name="a Shakespeare play"
)🔍 追问 想完全替换合成算法而不只是换模板该怎么办? → 继承
BaseSynthesizer,实现 get_response 与 aget_response 两个抽象方法(处理 query 和 text chunks 并返回字符串或字符串生成器),即可插入任意 query engine 或独立使用。📚 拓展阅读
- Response Synthesizer 模块指南 — 自定义合成器的完整说明
- Response Synthesizer 概念页 — BaseSynthesizer 抽象方法与用法
中频
路由与结构化输出
pydantic selectors feed in pydantic objects to a function calling API Structured Outputs | Developer DocumentationQ什么是 LlamaIndex 的 Router(路由器)?它解决什么问题?深挖·拓展🔥高频
答 Router 是接收用户 query 和一组由 metadata 定义的 "choices",然后返回一个或多个被选中 choice 的模块。它本质上是用 LLM 的决策能力来做路由:既可以作为独立的 "selector module" 使用,也可以叠在其他 query engine / retriever 之上作为 query engine 或 retriever 使用。典型用例包括在多样化数据源中挑选正确的数据源、决定该走 summarization(如 summary index query engine)还是 semantic search(如 vector index query engine),以及借助 multi-routing 能力一次性尝试多个 choice 并把结果合并。它的权衡在于:模块本身简单,但决策质量完全依赖 LLM,以及你为每个 choice 写的 description 是否清晰——因为 LLM 就是靠这些 description 来判断该路由到哪里的。
术语
Router(路由器,基于 metadata 选择 choice 的模块); choices(候选项,由 metadata 定义); selector module(选择器模块,可独立使用的核心); multi-routing(多路由,一次选多个并合并结果)📖 "Routers are modules that take in a user query and a set of “choices” (defined by metadata), and returns one or more selected choices." — pydantic selectors feed in pydantic objects to a function calling API
📖 "They are simple but powerful modules that use LLMs for decision making capabilities." — pydantic selectors feed in pydantic objects to a function calling API
🧪 实例 一个 summary/vector 双引擎路由:summarization 类问题走 list_tool,检索具体上下文走 vector_tool。
python
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import PydanticSingleSelector
from llama_index.core.tools import QueryEngineTool
list_tool = QueryEngineTool.from_defaults(
query_engine=list_query_engine,
description="Useful for summarization questions related to the data source",
)
vector_tool = QueryEngineTool.from_defaults(
query_engine=vector_query_engine,
description="Useful for retrieving specific context related to the data source",
)
query_engine = RouterQueryEngine(
selector=PydanticSingleSelector.from_defaults(),
query_engine_tools=[list_tool, vector_tool],
)
query_engine.query("<query>")🔍 追问 Router 只能当 query engine 用吗? → 不是,它还能作为 retriever(
RouterRetriever)使用,或作为独立 selector module 直接调用 select()。📚 拓展阅读
- Router Query Engine — RouterQueryEngine 的完整示例
- SQL Router Query Engine — 在 SQL 与其他源之间路由的示例
QLLM selector 和 Pydantic selector 有什么区别?怎么选?深挖·拓展🔥高频
答 定义 "selector" 是定义 router 的核心。核心 router 模块有两种形态:LLM selectors 把 choices 作为一段文本 dump 塞进 prompt,用 LLM 的 text completion endpoint 来做决策;Pydantic selectors 则把 choices 作为 Pydantic schema 传进 function calling endpoint,并返回 Pydantic 对象。两者又各有 single 和 multi 变体,于是有四种:
LLMSingleSelector、LLMMultiSelector、PydanticSingleSelector、PydanticMultiSelector。权衡在于:Pydantic selector 依赖底层模型支持 function calling,结构化程度更高、返回的是强类型对象,更可靠;而 LLM selector 只需要文本补全能力,兼容性更广,但需要靠 output parsing 从文本里提取选择。single 表示只选一个 choice,multi 表示可选多个(对应 multi-routing 场景)。术语
LLM selector(把 choices 当文本喂给 text completion 端点); Pydantic selector(把 choices 当 schema 喂给 function calling 端点并返回 Pydantic 对象); single/multi selector(单选/多选变体)📖 "LLM selectors put the choices as a text dump into a prompt and use LLM text completion endpoint to make decisions" — pydantic selectors feed in pydantic objects to a function calling API
📖 "Pydantic selectors pass choices as Pydantic schemas into a function calling endpoint, and return Pydantic objects" — pydantic selectors feed in pydantic objects to a function calling API
🧪 实例 四种 selector 的构造方式:
python
from llama_index.core.selectors import LLMSingleSelector, LLMMultiSelector
from llama_index.core.selectors import (
PydanticMultiSelector,
PydanticSingleSelector,
)
# pydantic selectors feed in pydantic objects to a function calling API
selector = PydanticSingleSelector.from_defaults()
selector = PydanticMultiSelector.from_defaults()
# LLM selectors use text completion endpoints
selector = LLMSingleSelector.from_defaults()
selector = LLMMultiSelector.from_defaults()🔍 追问 什么时候必须用 LLM selector 而不是 Pydantic selector? → 当底层 LLM 不支持 function calling API 时,只能走 text completion 的 LLM selector。
📚 拓展阅读
- Router Retriever — 用 selector 驱动 RouterRetriever 的示例
- Retriever Router Query Engine — 检索式路由示例
Q如何把 selector 当作独立模块使用?choices 怎么定义?深挖·拓展中频
答 除了作为 query engine / retriever,selector 还能作为独立模块直接用。此时你需要自己定义 choices,有两种写法:一是
ToolMetadata 对象的列表(每个带 name 和 description),二是字符串列表(把描述直接写进字符串)。定义好 selector(如 LLMSingleSelector.from_defaults())后,调用 select(choices, query=...) 得到 selector_result,通过 .selections 拿到选择结果。这种用法的价值在于:当你不想把整个 RouterQueryEngine 搭起来、只需要"给定一堆选项让 LLM 挑"这一决策能力时,可以把它作为轻量的决策原语嵌进自己的流水线。术语
ToolMetadata(工具元数据,含 name 与 description); select()(执行选择的方法); SelectorResult.selections(选择结果列表)📖 "You can use the selectors as standalone modules. Define choices as either a list of ToolMetadata or as a list of strings." — pydantic selectors feed in pydantic objects to a function calling API🧪 实例 独立调用 selector 做一次选择:
python
from llama_index.core.tools import ToolMetadata
from llama_index.core.selectors import LLMSingleSelector
# choices as a list of tool metadata
choices = [
ToolMetadata(description="description for choice 1", name="choice_1"),
ToolMetadata(description="description for choice 2", name="choice_2"),
]
# choices as a list of strings
choices = [
"choice 1 - description for choice 1",
"choice 2: description for choice 2",
]
selector = LLMSingleSelector.from_defaults()
selector_result = selector.select(
choices, query="What's revenue growth for IBM in 2007?"
)
print(selector_result.selections)🔍 追问 当 choices 数量非常大时怎么办? → 用
ToolRetrieverRouterQueryEngine 做 retrieval-augmented routing,把 choices 本身索引起来(注意这是 beta feature)。📚 拓展阅读
- Router Query Engine — 结合 selector 的完整路由示例
- Router Retriever — selector 驱动检索的示例
QLlamaIndex 里 structured output 有哪几类模块?各自定位是什么?深挖·拓展中频
答 LLM 产出结构化输出对于依赖可靠解析输出值的下游应用很重要,LlamaIndex 自身也在两处依赖它:Document retrieval(许多数据结构依赖带特定 schema 的 LLM 调用,比如 tree index 期望 LLM 返回 "ANSWER: (number)" 格式)和 Response synthesis(用户可能期望最终响应带一定结构,如 JSON 或格式化 SQL)。默认情况下结构化输出内建于 LLM 类中,此外还提供三类更底层的模块:Pydantic Programs(通用模块,把输入 prompt 映射为 Pydantic 对象表示的结构化输出,可用 function calling API 或 text completion API + output parser,也能与 query engine 集成)、Pre-defined Pydantic Program(把输入映射到特定输出类型如 dataframe 的预定义程序)、Output Parsers(在 LLM text completion 端点前后运作的模块,不用于 function calling 端点,因为后者本身就自带结构化输出)。
术语
Pydantic Program(把 prompt 映射为 Pydantic 对象的通用模块); Output Parser(在文本补全端点前后运作的解析器); Response synthesis(响应合成)📖 "The ability of LLMs to produce structured outputs are important for downstream applications that rely on reliably parsing output values." — Structured Outputs | Developer Documentation
📖 "Output Parsers: These are modules that operate before and after an LLM text completion endpoint. They are not used with LLM function calling endpoints (since those contain structured outputs out of the box)." — Structured Outputs | Developer Documentation
🧪 实例 一个 tree index 期望的结构化返回格式即
ANSWER: (number),LlamaIndex 靠它在检索时定位节点。🔍 追问 为什么 Output Parser 不用于 function calling 端点? → 因为 function calling 端点的输出本身就是结构化的(structured outputs out of the box),不需要再解析文本。
📚 拓展阅读
- Pydantic Programs — Pydantic Program 详解
- Output Parsers — output parser 详解
- Structured data extraction tutorial — 结构化抽取入门教程
Qtext completion API 和 function calling API 在结构化输出流水线上有何不同?深挖·拓展中频
答 结构化输出函数的流水线取决于你用的是 generic LLM text completion API 还是 LLM function calling API。用 generic completion API 时,输入和输出都由文本 prompt 承载,output parser 在 LLM 调用前后各起一次作用:调用之前,output parser 可以把 format instructions 追加到 prompt 上;调用之后,output parser 可以按指定 instructions 把输出解析出来。用 function calling API 时,输出天生就是结构化格式,输入可以接受目标对象的 signature,结构化输出只需要被 cast 成正确的对象格式(如 Pydantic)。所以本质区别是:completion 路线靠"提示 + 事后解析"来逼近结构,可靠性依赖 parser;function calling 路线把结构约束下沉到 API 层,更省心也更稳。
术语
format instructions(格式说明,附加到 prompt 前); function calling API(输出天然结构化的调用接口); cast(把结构化输出转成目标对象格式)📖 "With generic completion APIs, the inputs and outputs are handled by text prompts. The output parser plays a role before and after the LLM call in ensuring structured outputs." — Structured Outputs | Developer Documentation
📖 "With function calling APIs, the output is inherently in a structured format, and the input can take in the signature of the desired object." — Structured Outputs | Developer Documentation
🧪 实例 两条路线的对比流程:
flowchart TD
Q[输入 prompt] --> B{哪种 API?}
B -->|completion| P1[output parser 追加 format instructions]
P1 --> L1[LLM text completion]
L1 --> P2[output parser 解析输出]
P2 --> O1[结构化结果]
B -->|function calling| L2[LLM function calling]
L2 --> C[cast 成 Pydantic 对象]
C --> O2[结构化结果]🔍 追问 completion 路线里 output parser 具体在何时介入? → 两次:LLM 调用之前追加 format instructions,调用之后解析输出。
📚 拓展阅读
- Structured Outputs + Query Engines — 结构化输出与 query engine 集成
- Examples of Structured Outputs — 结构化输出示例集
第4章 模型与提示
Q在 LlamaIndex 中,prompt 到底承担什么角色?为什么说它贯穿了整个索引与查询流程?深挖·拓展🔥高频
答 prompt 是驱动 LLM 表达能力的根本输入,而 LlamaIndex 并不是只在"最后生成答案"这一步才用到它,而是把 prompt 贯穿到了整个数据管线的四个环节:构建索引(build the index)、执行插入(insertion)、查询时的遍历(traversal during querying),以及最终答案的合成(synthesize the final answer)。这样设计的原因在于,RAG 与 agentic workflow 的每一步本质上都是一次对 LLM 的调用,而每次调用的行为都由背后的 prompt 决定;因此把 prompt 抽象成可管理、可复用的模板,就能对框架从底层索引到顶层答案的每个阶段行为进行统一控制。权衡在于:框架自带一套开箱即用(work well out of the box)的默认模板以降低上手成本,但也把这些模板暴露出来,允许用户在构建 agentic workflow 时接管 prompt 的构建与管理,因为这本身就是开发过程的关键一环。
术语
prompt(提示,给 LLM 的根本输入); index(索引,LlamaIndex 组织数据的结构); traversal(遍历,查询时在索引中检索的过程); synthesize(合成,把检索到的上下文汇总成最终答案); agentic workflow(智能体工作流)📖 "Prompting is the fundamental input that gives LLMs their expressive power. LlamaIndex uses prompts to build the index, do insertion, perform traversal during querying, and to synthesize the final answer." — you can create text prompt (for completion API)
📖 "When building agentic workflows, building and managing prompts is a key part of the development process. LlamaIndex provides a flexible and powerful way to manage prompts, and to use them in a variety of ways." — you can create text prompt (for completion API)
🧪 实例 一个典型 RAG 问答里,同一个 QA prompt 模板会在查询阶段被填充检索到的上下文与用户问题:
python
from llama_index.core.prompts import RichPromptTemplate
template_str = """We have provided context information below.
---------------------
{{ context_str }}
---------------------
Given this information, please answer the question: {{ query_str }}
"""
qa_template = RichPromptTemplate(template_str)🔍 追问 prompt 只在生成答案时才起作用吗? → 不是,它同时用于 build the index、insertion、traversal during querying 和 synthesize the final answer 四个阶段。
📚 拓展阅读
- Prompts 用法模式指南 — 深入讲解如何充分利用各类 prompt 模板
- default_prompts.py 默认模板源码 — 框架开箱即用的默认 prompt 集合
QLlamaIndex 提供了哪几种 prompt template?它们分别适合什么场景?深挖·拓展🔥高频
答 LlamaIndex 目前提供三种 prompt template,区分点在于"模板语法"和"面向 completion 还是 chat"。
RichPromptTemplate 是最新风格(latest-style),用 jinja 语法来构建带变量和逻辑的 prompt,因此能表达条件、循环等更复杂的动态结构;PromptTemplate 是较老风格(older-style)的简单模板,仅用单个 f-string 构建 prompt,适合无逻辑的直接文本填充;ChatPromptTemplate 同样是较老风格的简单模板,但面向 chat API,用一组 messages 加 f-string 来构建对话式 prompt。选择上的权衡是:如果只是把几个变量拼进一段文本,老式的 f-string 模板足够轻量;一旦 prompt 里需要条件分支、循环或更强的可维护性,就应优先用支持 jinja 逻辑的 RichPromptTemplate;而当目标模型是 chat 模型、需要区分 system/user/assistant 等角色消息时,则天然对应 ChatPromptTemplate 或用 RichPromptTemplate 的 message 输出。术语
RichPromptTemplate(jinja 风格、支持变量与逻辑的最新模板); PromptTemplate(单 f-string 的老式简单模板); ChatPromptTemplate(基于 messages 与 f-string 的老式 chat 模板); jinja(一种支持变量与逻辑的模板语法); f-string(Python 的字符串插值语法)📖 "RichPromptTemplate - latest-style for building jinja-style prompts with variables and logic" — you can create text prompt (for completion API)📖 "PromptTemplate - older-style simple templating for building prompts with a single f-string" — you can create text prompt (for completion API)📖 "ChatPromptTemplate - older-style simple templating for building chat prompts with messages and f-strings" — you can create text prompt (for completion API)🧪 实例 三者的定位可以用一张图区分:
flowchart TD
A[需要构建 prompt] --> B{是否需要变量+逻辑?}
B -->|需要 jinja 逻辑| C[RichPromptTemplate]
B -->|简单文本插值| D{面向哪种 API?}
D -->|completion 单串文本| E[PromptTemplate f-string]
D -->|chat 多角色消息| F[ChatPromptTemplate messages]🔍 追问
PromptTemplate 和 RichPromptTemplate 最本质的语法差别是什么? → 前者用单个 f-string,后者用 jinja 风格、支持 variables and logic。📚 拓展阅读
- Advanced Prompts — 进阶 prompt 工程示例
- RichPromptTemplate Features — RichPromptTemplate 的功能演示
- Chat prompts 自定义示例 — chat 场景下的 prompt 定制
QRichPromptTemplate 怎么用?同一个模板如何既服务 completion API 又服务 chat API?深挖·拓展中频
答
RichPromptTemplate 用 jinja 语法定义模板字符串,变量以 {{ 变量名 }} 的双花括号占位,实例化后通过两个方法把同一个模板落地到两种不同的调用方式:format(...) 把变量填进去生成一段纯文本 prompt,用于 completion API;format_messages(...) 则把同一个模板转换成 message 列表,用于 chat API。这样设计的价值在于:开发者只需维护一份模板定义,就能在面向 completion 的单串文本接口和面向 chat 的多消息接口之间自由切换,而不必为两种 API 各写一份 prompt,降低了重复与不一致的风险。官方强调"用 prompt 很简单",核心就是"定义模板字符串 → 实例化 → 用 format 或 format_messages 填值"这条固定路径。术语
format(生成纯文本 prompt,供 completion API); format_messages(生成 message 列表,供 chat API); template_str(jinja 模板字符串); context_str / query_str(模板中被填充的变量占位)📖 "Using prompts is simple. Below is an example of how to use the RichPromptTemplate to build a jinja-style prompt template:" — you can create text prompt (for completion API)🧪 实例 同一个
qa_template 走两条路:python
# you can create text prompt (for completion API)
prompt = qa_template.format(context_str=..., query_str=...)
# or easily convert to message prompts (for chat API)
messages = qa_template.format_messages(context_str=..., query_str=...)🔍 追问 想把模板用在 chat 模型上,应该调用哪个方法? → 调用
format_messages(...),它会把模板转换成 chat API 所需的 messages。📚 拓展阅读
- Prompts 用法模式指南 — 讲解如何充分利用 RichPromptTemplate 及其他模板的细节
- Completion prompts 自定义示例 — completion 场景的 prompt 定制
QLlamaIndex 自带默认 prompt,想自定义时官方推荐的最佳做法是什么?深挖·拓展中频
答 LlamaIndex 内置了一套默认 prompt 模板(default prompt templates),它们"开箱即用、效果不错",另外还有一批专门为
gpt-3.5-turbo 这类 chat 模型写的 prompt。用户当然可以提供自己的模板来进一步定制框架行为,但官方给出的最佳定制方法不是从零手写,而是先把默认 prompt 从源码链接里复制下来,再以它为基底(base)做修改。这个建议背后的权衡很实际:默认模板已经在框架的索引、检索、合成各阶段被验证过、能稳定工作,直接从零写很容易漏掉关键结构或占位变量导致行为异常;以官方模板为起点改动,既保留了被验证过的骨架,又能精准注入你需要的差异,把定制风险降到最低。术语
default prompt templates(开箱即用的默认模板); gpt-3.5-turbo(一类 chat 模型,框架为其专门写了 prompt); base(定制时作为起点的默认模板骨架); customize(在默认模板基础上做定制)📖 "Users may also provide their own prompt templates to further customize the behavior of the framework. The best method for customizing is copying the default prompt from the link above, and using that as the base for any modifications." — you can create text prompt (for completion API)
📖 "In addition, there are some prompts written and used specifically for chat models like gpt-3.5-turbo here." — you can create text prompt (for completion API)🧪 实例 推荐的定制流程:
bash
# 1) 打开默认模板源码,复制目标 prompt
# https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/prompts/default_prompts.py
# 2) 以复制内容为 base,只改需要变动的部分
# 3) 用 RichPromptTemplate / PromptTemplate 重新实例化后传入框架🔍 追问 为什么不建议从零写自定义 prompt? → 官方推荐 copy the default prompt 作为 base 改,能复用已验证有效的模板结构,避免遗漏关键占位或结构导致行为异常。
📚 拓展阅读
- default_prompts.py 默认模板源码 — 复制并作为定制基底的默认 prompt
- chat_prompts.py chat 模型专用 prompt — 为 gpt-3.5-turbo 等 chat 模型编写的 prompt
- Prompt Mixin — 在模块中查看与替换 prompt 的机制
Q在 LlamaIndex 里与"模型"打交道有哪几种主要方式?深挖·拓展中频
答 LlamaIndex 把"与模型交互"归纳为几种主要方式:LLMs 和 Embeddings 是两条核心路径,而 MultiModal(多模态)则被标注为实验性(Experimental)。这种划分对应了 RAG 系统里模型的两类基本职责:Embeddings 负责把文本转成向量用于建索引和相似度检索,LLMs 负责在检索到上下文后做理解、推理与答案生成;两者协作构成检索增强生成的骨架。把 MultiModal 单列并标为实验性,则是在告诉使用者:图文等多模态能力已经可用但尚不稳定、接口可能变动,生产选型时需要权衡其成熟度。理解这个分层有助于在设计应用时清楚地知道每种模型该接到管线的哪个位置。
术语
LLMs(大语言模型,负责理解与生成); Embeddings(嵌入模型,把文本转向量用于检索); MultiModal(多模态,处理图文等多种模态,实验性); Experimental(实验性,尚不稳定的能力标注)📖 "There are a few primary ways you interact with models in LlamaIndex:" — Models | Developer Documentation
🧪 实例 三种模型在 RAG 管线中的分工:
flowchart LR
D[原始文档] -->|Embeddings 向量化| I[索引/向量库]
Q[用户问题] -->|Embeddings 向量化| R[检索]
I --> R
R -->|上下文| L[LLMs 生成答案]
M[MultiModal 实验性] -.图文输入.-> L🔍 追问 MultiModal 和 LLMs、Embeddings 在文档里的地位一样吗? → 不一样,LLMs 与 Embeddings 是主要交互方式,而 MultiModal 被明确标为 Experimental。
📚 拓展阅读
- LLMs 模块指南 — LLM 在 LlamaIndex 中的使用方式
- Embeddings 模块指南 — 嵌入模型的使用方式
- MultiModal 模块指南 — 实验性的多模态能力
第5章 部署引擎
🔥高频
Query/Chat/Agent 引擎
Query Engine | Developer Documentation Chat Engine | Developer Documentation Define a simple calculator toolQLlamaIndex 里的 Query Engine 是什么?它和 Index、Retriever 是什么关系?深挖·拓展🔥高频
答 Query Engine 是一个"提问-回答"式的通用接口:接收一段自然语言 query,返回一段富文本 response。它本身不是一个孤立组件,而是绝大多数情况下建立在一个或多个 index 之上、通过 retriever 去检索数据的编排层——检索出相关内容后再交给 LLM 合成答案。设计上它是无状态的单轮问答(single question & answer),不记录上下文,这也是它和 Chat Engine 的根本分界:需要多轮对话就用 Chat Engine,只要独立的一次性提问就用 Query Engine。它的可组合性是关键卖点——你可以把多个 query engine 组合起来实现更高级的能力(例如子问题分解、路由到不同索引),所以在 RAG 系统中它常常作为"检索+合成"的最小可复用单元,甚至可被包成
QueryEngineTool 供 agent 调用。最简用法就是 index.as_query_engine(),并可通过 streaming=True 开启流式返回。术语
Query Engine(查询引擎,单轮问答接口); index(索引,数据的组织结构); retriever(检索器,从索引取相关内容); streaming(流式返回)📖 "Query engine is a generic interface that allows you to ask question over your data." — Query Engine | Developer Documentation
📖 "A query engine takes in a natural language query, and returns a rich response. It is most often (but not always) built on one or many indexes via retrievers. You can compose multiple query engines to achieve more advanced capability." — Query Engine | Developer Documentation
🧪 实例 从索引直接构造一个 query engine 并提问,或开启流式输出:
python
query_engine = index.as_query_engine(streaming=True)
streaming_response = query_engine.query("Who is Paul Graham.")
streaming_response.print_response_stream()🔍 追问 为什么 query engine 不适合做客服机器人式的连续对话? → 因为它是无状态的单轮接口,不追踪 conversation history,追问会丢失上文语境,这类场景应改用有状态的 Chat Engine。
📚 拓展阅读
- Indexing 模块指南 — query engine 底层依赖的 index 组织方式
- Retriever 模块指南 — 决定检索质量的检索器
- Query Engine usage pattern — 完整用法细节
- Query Engine modules — 各类 query engine 模块清单
QChat Engine 与 Query Engine 有何本质区别?为什么说它是 Query Engine 的"有状态类比"?深挖·拓展🔥高频
答 Chat Engine 是一个用于"和你的数据对话"的高层接口,支持多轮来回(multiple back-and-forth),可以类比成 ChatGPT,但被你自己的知识库增强过。它与 Query Engine 的核心差异在于状态:官方明确把它定义为 Query Engine 的一个 stateful(有状态)类比——它通过持续追踪 conversation history,让当前问题能带着过去的上下文来回答。这一区别决定了选型权衡:如果只需要对数据做一次独立提问、不关心历史,应该用 Query Engine,避免维护状态的开销;如果需要连续对话、指代消解、追问延续,才用 Chat Engine。用法上同样极简,
index.as_chat_engine() 即可创建,调用 chat() 做一轮对话,或用 stream_chat() 拿到 response_gen 逐 token 流式输出。可以把 Chat Engine 理解为在 Query Engine 的检索能力之上,叠加了一层对话记忆管理。术语
Chat Engine(对话引擎,多轮有状态接口); stateful(有状态); conversation history(对话历史); stream_chat(流式对话方法)📖 "Chat engine is a high-level interface for having a conversation with your data (multiple back-and-forth instead of a single question & answer). Think ChatGPT, but augmented with your knowledge base." — Chat Engine | Developer Documentation
📖 "Conceptually, it is a stateful analogy of a Query Engine. By keeping track of the conversation history, it can answer questions with past context in mind." — Chat Engine | Developer Documentation
🧪 实例 用流式方式进行一轮对话,逐 token 打印:
python
chat_engine = index.as_chat_engine()
streaming_response = chat_engine.stream_chat("Tell me a joke.")
for token in streaming_response.response_gen:
print(token, end="")🔍 追问 "有状态"具体状态存在哪里? → 存在被追踪的 conversation history 中;正是靠保留这段历史,引擎才能结合过去上下文回答后续问题。
📚 拓展阅读
- Query Engine 模块指南 — Chat Engine 的无状态对照物
- Chat Engine usage pattern — 完整用法指南
- Chat Engine modules — 可用的各类 chat engine 教程
QLlamaIndex 如何定义"agent"?FunctionAgent 的执行循环是怎样的?深挖·拓展🔥高频
答 在 LlamaIndex 里,"agent"被定义为一个使用 LLM、memory 和 tools 来处理外部用户输入的具体系统;它与更宽泛的"agentic"要区分开——后者泛指任何在流程中带有 LLM 决策的系统,是 agent 的超类。创建 agent 只需几行代码(如
FunctionAgent(tools=[...], llm=..., system_prompt=...))。调用后会触发一个明确的动作循环:agent 拿到最新消息+chat history,把 tool schemas 和 chat history 发给 API,随后 LLM 要么直接给出回答,要么返回一组 tool calls;每个 tool call 被执行,结果被追加进 chat history,然后带着更新后的历史再次调用 agent,如此往复直到给出最终答案。FunctionAgent 依赖 LLM 提供商原生的 function/tool calling 能力来执行工具;而 ReActAgent、CodeActAgent 等其它类型则用不同的 prompting 策略执行工具——这就是权衡点:function calling 需要模型原生支持、通常更可靠简洁,ReAct 类则靠提示词约定,兼容性更广但更依赖解析。流式默认开启,若模型不支持可设 streaming=False。术语
agent(智能体,LLM+memory+tools 的系统); agentic(更宽泛的含 LLM 决策的超类); FunctionAgent(基于原生 function calling 的 agent); tool schemas(工具的接口描述); chat history(对话历史)📖 "In LlamaIndex, we define an “agent” as a specific system that uses an LLM, memory, and tools, to handle inputs from outside users. Contrast this with the term “agentic”, which generally refers to a superclass of agents, which is any system with LLM decision making in the process." — Define a simple calculator tool
📖 "TheFunctionAgentis a type of agent that uses an LLM provider’s function/tool calling capabilities to execute tools. Other types of agents, such asReActAgentandCodeActAgent, use different prompting strategies to execute tools." — Define a simple calculator tool
🧪 实例 agent 每次被调用时的循环可视化如下:
flowchart TD
A[获取最新消息 + chat history] --> B[将 tool schemas 和 history 发给 API]
B --> C{LLM 直接回答
还是返回 tool calls?}
C -->|直接回答| D[返回最终 response]
C -->|tool calls| E[执行每个 tool call]
E --> F[结果追加进 chat history]
F --> A🔍 追问 FunctionAgent 和 ReActAgent 执行工具的机制差在哪? → FunctionAgent 用 LLM 提供商的原生 function/tool calling 能力,ReActAgent、CodeActAgent 则用不同的 prompting 策略来触发工具执行。
📚 拓展阅读
- Agents guide(understanding) — 更深入了解 agent 及其能力
- ReActAgent 示例 — 基于提示策略的 agent
- CodeActAgent 示例 — 另一种 prompting 策略的 agent
- Agents 模块指南 — 更完整的示例与模块清单
QLlamaIndex agent 的 memory 默认是什么?如何自定义?深挖·拓展中频
答 memory 是构建 agent 的核心组件——因为 agent 循环每一步都要把 chat history 发给 LLM,memory 决定了历史如何被保存与截断。默认情况下,所有 LlamaIndex agent 都使用
ChatMemoryBuffer 作为 memory。若要自定义,做法是在 agent 之外先声明 memory 再传入 run(),例如用 ChatMemoryBuffer.from_defaults(token_limit=40000) 设定一个 token 上限。这里的权衡在于 token_limit:设太小会过早丢弃早期上下文导致"失忆",设太大则每轮请求携带的历史更长、消耗更多 token 与延迟,还可能逼近模型上下文窗口上限。把 memory 显式声明在外部还有一个好处——它可以在多次 run() 调用之间被复用,从而在不同调用间延续对话状态。术语
memory(记忆,保存对话历史的组件); ChatMemoryBuffer(默认记忆缓冲区实现); token_limit(历史保留的 token 上限); from_defaults(以默认配置构造)📖 "Memory is a core-component when building agents. By default, all LlamaIndex agents are using a ChatMemoryBuffer for memory." — Define a simple calculator tool
🧪 实例 在 agent 外声明带 token 上限的 memory 并注入:
python
from llama_index.core.memory import ChatMemoryBuffer
memory = ChatMemoryBuffer.from_defaults(token_limit=40000)
response = await agent.run(..., memory=memory)🔍 追问 为什么要把 memory 声明在 agent 外面再 pass in? → 这样能显式控制配置(如 token_limit)并在多次
run() 调用间复用同一份记忆,从而跨调用保留对话状态。📚 拓展阅读
- Memory guide — 配置 memory 的完整说明
- Agents guide(understanding) — memory 在 agent 系统中的角色
QLlamaIndex agent 的 tools 可以怎样定义?有哪些内置封装?深挖·拓展中频
答 工具是 agent 能"动手"的关键。LlamaIndex 给了从简到繁的多档定义方式:最简单可以直接把一个 python 函数当作 tool(函数的 docstring 会作为工具用途描述供 LLM 判断何时调用);需要更多定制时,可用
FunctionTool、QueryEngineTool 这类类来包装——后者尤其重要,它把一个 Query Engine 封装成工具,从而让 agent 在推理过程中按需触发 RAG 检索。此外,针对常见 API,LlamaIndex 还提供成套的预定义工具,叫 Tool Specs,省去手工封装第三方服务的成本。这条从"裸函数"到"类封装"再到"Tool Specs"的谱系,本质是在开发便捷性与可控性之间给出不同档位:快速原型用裸函数,需要接检索或复杂逻辑用类,接标准 API 直接用 Tool Specs。术语
tools(工具,agent 可调用的能力单元); FunctionTool(函数工具封装类); QueryEngineTool(把 query engine 包成工具); Tool Specs(常见 API 的预定义工具集)📖 "Tools can be defined simply as python functions, or further customized using classes likeFunctionToolandQueryEngineTool. LlamaIndex also provides sets of pre-defined tools for common APIs using something calledTool Specs." — Define a simple calculator tool
🧪 实例 最简单地把一个 python 函数直接作为工具交给 agent:
python
def multiply(a: float, b: float) -> float:
"""Useful for multiplying two numbers."""
return a * b
agent = FunctionAgent(
tools=[multiply],
llm=OpenAI(model="gpt-4o-mini"),
system_prompt="You are a helpful assistant that can multiply two numbers.",
)🔍 追问 想让 agent 具备"查资料"的能力应该用哪种工具? → 用
QueryEngineTool 把一个 query engine 封装成工具传给 agent,这样 agent 在推理时可主动触发检索。📚 拓展阅读
- Tools guide — 配置工具的完整说明
- Agents 模块指南 — 工具在完整 agent 示例中的用法
Q什么是 multi-agent 系统?LlamaIndex 用什么机制协调多个 agent?深挖·拓展低频
答 多智能体系统是把多个 agent 组合起来,让每个 agent 都能把控制权移交(hand off)给另一个 agent,从而在完成任务时相互协调。LlamaIndex 提供
AgentWorkflow 作为其中一种实现方式,用 AgentWorkflow(agents=[FunctionAgent(...), FunctionAgent(...)]) 就能把若干 agent 编排在一起再 run 一个 query。设计上强调这只是构建多智能体系统的"一种方式"(only one way),意味着 handoff 式编排并非唯一范式。这种拆分的价值在于分工与专精:每个 agent 可以聚焦一类子任务、配自己的工具和提示,靠交接把复杂流程分解开,从而在可维护性和能力边界上优于把所有职责堆进单个巨型 agent;代价则是协调复杂度和交接可靠性需要额外设计。术语
multi-agent system(多智能体系统); hand off(控制权交接); AgentWorkflow(编排多个 agent 的工作流类); coordinate(协作完成任务)📖 "You can combine agents into a multi-agent system, where each agent is able to hand off control to another agent to coordinate while completing tasks." — Define a simple calculator tool
🧪 实例 把多个 FunctionAgent 组合成一个工作流:
python
from llama_index.core.agent.workflow import AgentWorkflow
multi_agent = AgentWorkflow(agents=[FunctionAgent(...), FunctionAgent(...)])
resp = await agent.run("query")🔍 追问
AgentWorkflow 是构建多智能体的唯一办法吗? → 不是,官方说明它只是其中一种方式(only one way),handoff 式编排之外还有其它构建多智能体系统的途径。📚 拓展阅读
- Multi-agent systems — 多智能体系统的更多构建方式
- Agents guide(understanding) — 单 agent 能力基础
第6章 评估·可观测·协议
中频
评估·可观测·回调·MCP
Evaluating | Developer Documentation general usage Callbacks | Developer Documentation Model Context Protocol (MCP) | Developer DocumentationQLlamaIndex 的评估分成哪两大类?它们各自回答什么问题?深挖·拓展🔥高频
答 LlamaIndex 把评估拆成两条相互独立的线:Response Evaluation(生成质量)与 Retrieval Evaluation(检索质量)。前者关心最终生成的回答是否站得住——是否忠于检索到的上下文、是否切题、是否符合参考答案或既定 guidelines;后者只关心检索器召回的 sources 与 query 是否相关,可用 MRR、hit-rate、precision 等排序指标衡量,前提是有一份带 ground-truth 排名的问题数据集。之所以要拆开,是因为 RAG/agent 的错误可能出在两个完全不同的环节:检索没召回对的证据,或召回对了但生成阶段跑偏;把两条线分开评估才能定位到底是 retriever 还是 generator 的问题。评估生成质量本身很难,因为不像传统机器学习那样预测结果是单个数字,很难定义量化指标,所以 LlamaIndex 走的是 LLM-based 路线而非纯统计指标。
术语
Response Evaluation(响应/生成评估); Retrieval Evaluation(检索评估); MRR(mean-reciprocal rank,平均倒数排名); hit-rate(命中率); ground-truth rankings(真值排名)📖 "Does the response match the retrieved context? Does it also match the query? Does it match the reference answer or guidelines?" — Evaluating | Developer Documentation
📖 "Are the retrieved sources relevant to the query?" — Evaluating | Developer Documentation
📖 "given a dataset of questions and ground-truth rankings, we can evaluate retrievers using ranking metrics like mean-reciprocal rank (MRR), hit-rate, precision, and more." — Evaluating | Developer Documentation
🧪 实例 检索评估的两步核心流程——先从语料合成数据集,再用排序指标打分:
flowchart LR
A[unstructured text corpus] --> B[Dataset generation
合成 question,context 对]
B --> C[Retrieval Evaluation
用 MRR/hit-rate 打分]
C --> D[定位: retriever 问题?]🔍 追问 检索评估的数据集从哪来? → 由 "Dataset generation" 步骤给定非结构化文本语料后,合成 (question, context) 对,再喂给 retriever 评估。
📚 拓展阅读
- Query Eval Usage Pattern — 生成结果评估的完整用法
- Retrieval Eval Usage Pattern — 检索评估的完整用法
- Evaluating with LabelledRagDataset — 用评估数据集评估整套 RAG
QLlamaIndex 的 LLM-based 评估模块有哪些?为什么很多不需要 ground-truth 标签?深挖·拓展🔥高频
答 LlamaIndex 用一个"gold" LLM(如 GPT-4)充当裁判,从多个维度判断预测答案是否正确,提供的模块包括 Correctness(与参考答案是否匹配,需标签)、Semantic Similarity(与参考答案语义是否相似,需标签)、Faithfulness(答案是否忠于检索上下文,即是否有幻觉)、Context Relevancy(上下文与 query 是否相关)、Answer Relevancy(答案与 query 是否相关)、Guideline Adherence(是否遵循特定 guidelines)。关键权衡在于:其中许多模块不需要 ground-truth 标签,因为评估可以只用 query、context、response 三者的某种组合再配合 LLM 调用完成——这大幅降低了搭建评估集的成本,尤其 Faithfulness/Answer Relevancy 这类 referenceless 指标可直接在缺乏标准答案的生产数据上跑;代价是评估质量取决于裁判 LLM 的能力,且没有绝对量化的对错基线。此外 LlamaIndex 还能用你的数据自动生成问题(Question Generation),从而自动构造评估流。
术语
gold LLM(充当裁判的高能力模型); Faithfulness(忠实度/幻觉检测); Correctness(正确性,需标签); Guideline Adherence(指南遵循度); Question Generation(用数据自动生成待评估问题)📖 "This uses a “gold” LLM (e.g. GPT-4) to decide whether the predicted answer is correct in a variety of ways." — Evaluating | Developer Documentation
📖 "Evaluation can be done with some combination of the query, context, response, and combine these with LLM calls." — Evaluating | Developer Documentation
📖 "Evaluates if the answer is faithful to the retrieved contexts (in other words, whether if there’s hallucination)." — Evaluating | Developer Documentation
🧪 实例 哪些模块要标签、哪些不要,一眼区分:
text
需要 labels: Correctness, Semantic Similarity
不需要 labels: Faithfulness, Context Relevancy, Answer Relevancy, Guideline Adherence🔍 追问 想在没有参考答案的生产环境检测幻觉,应该用哪个模块? → 用 Faithfulness,它评估答案是否忠于检索到的 contexts,不依赖 ground-truth 标签。
📚 拓展阅读
- Ragas 集成 — 社区评估工具 Ragas 与 LlamaIndex 结合
- DeepEval — 开源 LLM 评估框架,含 faithfulness/answer relevancy 等指标
- RAGChecker — 亚马逊出品的 RAG 评估工具
QLlamaIndex 的 "one-click observability" 怎么工作?新旧机制有什么区别?深挖·拓展🔥高频
答 LlamaIndex 提供"一键可观测",让你在生产环境构建规范的 LLM 应用:只需配置一次变量,就能查看 LLM/prompt 的输入输出、确认各组件(LLMs、embeddings)表现符合预期、并查看 indexing 与 querying 的调用链路(call traces)。触发方式极简——
from llama_index.core import set_global_handler 后调用 set_global_handler("<handler_name>", kwargs),所有 kwargs 都会透传给底层的 callback handler,之后执行会被无缝管道化到下游服务。机制上有一个重要迁移:从 v0.10.20 起,可观测性改由 instrumentation 模块处理;文档明确标注了页面里不少工具与集成仍使用legacy** 的 CallbackManager 或不走 set_global_handler,这些被单独标记为 legacy。这个分层的意义在于:instrumentation 提供更细粒度的 span/event 机制(如 OpenTelemetry 集成能把所有事件按 OTel 格式导出),而 legacy 的 global handler 更简单但能力受限,选型时要看合作方集成走的是哪条路。术语
set_global_handler(一键注册全局可观测 handler); instrumentation 模块(v0.10.20+ 的新可观测机制); CallbackManager(legacy 回调机制); call traces(调用链路追踪); OpenTelemetry(可导出 span 的开放追踪标准)📖 "LlamaIndex provides one-click observability 🔭 to allow you to build principled LLM applications in a production setting." — general usage
📖 "Note that allkwargstoset_global_handlerare passed to the underlying callback handler." — general usage
📖 "A lot of the tooling and integrations mentioned in this page use our legacyCallbackManageror don’t useset_global_handler." — general usage
🧪 实例 最简单的 "simple" handler,把每个 LLM 输入输出对打印到终端,用于快速开 debug:
python
import llama_index.core
llama_index.core.set_global_handler("simple")🔍 追问 从哪个版本起可观测性改由 instrumentation 模块处理? → v0.10.20 及之后。
📚 拓展阅读
- instrumentation 模块 — v0.10.20+ 的新可观测机制
- OpenTelemetry — 被广泛使用的开源追踪与可观测标准
- agents-observability-demo — 官方演示:追踪 agentic workflow 并写入 Postgres
QLlamaIndex 的 Callbacks 机制能追踪哪些东西?有哪些事件类型?深挖·拓展中频
答 Callbacks 用来 debug、track、trace 库的内部运作:通过 callback manager 可以按需添加任意多个 callback。除了记录事件相关的数据,它还能追踪每个事件的持续时间和发生次数;此外还会记录一张事件的 trace map(追踪映射图),callback 可以随意使用这份数据——例如
LlamaDebugHandler 默认会在大多数操作后打印事件 trace。可追踪的事件类型是一组固定枚举:CHUNKING(文本切分前后)、NODE_PARSING(文档解析成 nodes)、EMBEDDING(embedding 文本数量)、LLM(LLM 调用的模板与响应)、QUERY(每次 query 的起止)、RETRIEVE(为 query 检索到的 nodes)、SYNTHESIZE(synthesize 结果)、TREE(生成的摘要与层级)、SUB_QUESTION(生成的子问题及答案)。注意每个 callback 不一定用到全部事件类型——设计上这是一套"发布可用事件、由各 handler 按需消费"的机制,好处是既能做通用的 token 计数(TokenCountingHandler)也能做专用的调试打印或第三方上报,同一套事件源支撑多种用途。术语
callback manager(回调管理器,可挂多个 callback); trace map(事件追踪映射图); LlamaDebugHandler(默认打印事件 trace 的调试 handler); TokenCountingHandler(灵活统计 prompt/completion/embedding token); SUB_QUESTION(子问题事件)📖 "LlamaIndex provides callbacks to help debug, track, and trace the inner workings of the library. Using the callback manager, as many callbacks as needed can be added." — Callbacks | Developer Documentation
📖 "In addition to logging data related to events, you can also track the duration and number of occurrences of each event." — Callbacks | Developer Documentation
📖 "For example, the LlamaDebugHandler will, by default, print the trace of events after most operations." — Callbacks | Developer Documentation🧪 实例 一次 query 会依序触发的部分事件类型:
text
QUERY(start) -> RETRIEVE(召回 nodes) -> SYNTHESIZE -> LLM(模板+响应) -> QUERY(end)🔍 追问 想统计 prompt、completion、embedding 各自的 token 用量,该用哪个 handler? → 用
TokenCountingHandler,它专门做灵活的 token 计数。📚 拓展阅读
- TokenCountingHandler — prompt/completion/embedding 的 token 计数
- LlamaDebugHandler 示例 — 基础事件追踪与 trace 打印
- OpenAIFineTuningHandler — 记录 LLM 输入输出并导出为微调格式
Q什么是 MCP?它的架构和核心能力是什么?深挖·拓展中频
答 Model Context Protocol(MCP)是一个开源标准协议,让大语言模型(LLMs)通过结构化 API 调用与外部工具和数据源交互;它充当 AI 应用与外部服务(工具、数据库、预定义模板)之间的标准化层,官方比喻为 AI 应用的"USB-C 接口"——提供一种标准方式,让各种工具、平台和数据源连接到 AI 模型。架构上采用 client-server 模式,三个角色各司其职:MCP Hosts 是希望通过 MCP 访问数据的应用(如 Claude Desktop、IDE 或 AI 工具);MCP Clients 是与 MCP servers 保持 1:1 连接的协议客户端;MCP Servers 是轻量服务,通过标准协议暴露能力(tools、resources、prompts)。核心能力有三类:Tools(可用结构化输入调用的函数)、Resources(可读的数据源,如文件、数据库)、Prompts(带参数的可复用 prompt 模板)。这套设计的价值在于用一层标准化协议替代 N×M 的定制集成——任何遵循 MCP 的 server 都能被任何 MCP client 消费,client 与 server 保持 1:1 连接保证了连接的清晰隔离。
术语
MCP Hosts(发起数据访问的宿主应用); MCP Clients(与 server 保持 1:1 连接的协议客户端); MCP Servers(暴露 tools/resources/prompts 的轻量服务); Tools/Resources/Prompts(三类核心能力); USB-C port(标准化连接的官方比喻)📖 "Model Context Protocol (MCP) is an open-source standard protocol that allows Large Language Models (LLMs) to interact with external tools and data sources through structured API calls." — Model Context Protocol (MCP) | Developer Documentation
📖 "Think of MCP as a “USB-C port” for AI applications - it provides a standardized way for various tools, platforms, and data sources to connect to AI models." — Model Context Protocol (MCP) | Developer Documentation
📖 "Lightweight services that expose capabilities (tools, resources, prompts) via the standardized protocol" — Model Context Protocol (MCP) | Developer Documentation
🧪 实例 MCP 的 client-server 架构:
flowchart LR
H[MCP Host
Claude Desktop / IDE] --> C[MCP Client
1:1 连接]
C -->|structured API calls| S[MCP Server
暴露 tools/resources/prompts]
S --> R[外部工具 / 数据库 / 模板]🔍 追问 MCP Client 和 MCP Server 之间是什么样的连接关系? → 1:1 连接,每个协议客户端与一个 MCP server 维持一对一连接。
📚 拓展阅读
- Using MCP Tools with LlamaIndex — 在 LlamaIndex 里使用 MCP 工具
- Convert existing workflow/tool to MCP — 把已有 workflow/tool 转成 MCP Tool/Server
- LlamaCloud APIs as MCP Tools/Servers — 把 LlamaCloud API 当作 MCP 工具/服务
Q在 LlamaIndex 中使用 MCP 有哪几种方式?深挖·拓展低频
答 LlamaIndex 提供三种使用 MCP servers 的方式,把额外的资源和功能引入 agentic workflows。第一种是消费方:在 LlamaIndex workflows 中使用已有 MCP servers 暴露的工具,从通过现有 MCP servers 提供服务的外部资源获取数据。第二种是提供方:把你自己的自定义 LlamaIndex workflows 转换成 MCP servers,从而让别的 MCP client 反过来调用你的 workflow。第三种是跨生态复用:运行 LlamaIndex 提供的 MCP servers(Python 和 TypeScript 都有),在任何能与 MCP servers 通信的应用(包括 LlamaIndex workflows 自身)里,使用 LlamaCloud 的功能如 LlamaExtract 或 LlamaParse。这三种方式对应 MCP 双向能力的完整闭环——既能当 client 拉取外部能力,也能当 server 输出自身能力,还能把 LlamaCloud 的托管服务通过 MCP 桥接进任意生态;权衡点在于:消费方式接入快但依赖第三方 server 的稳定性与安全,提供方式让你的 workflow 可被复用但需自行维护 server。
术语
agentic workflows(智能体工作流); MCP servers(可被消费或自建的能力服务); LlamaExtract(LlamaCloud 的抽取服务); LlamaParse(LlamaCloud 的解析服务); Python/TypeScript servers(双语言实现的 MCP 服务)📖 "Get data from external resources that are served via existing MCP servers." — Model Context Protocol (MCP) | Developer Documentation
📖 "You can convert your own custom LlamaIndex workflows to MCP servers." — Model Context Protocol (MCP) | Developer Documentation
🧪 实例 三种方式的角色定位:
text
1. 消费已有 MCP server 的 tools -> LlamaIndex 作为 client
2. 把自己的 workflow 变成 MCP server -> LlamaIndex 作为 server
3. 跑 LlamaCloud MCP server (Py/TS) -> 桥接 LlamaExtract/LlamaParse 到任意 MCP 应用🔍 追问 想让别的 MCP 应用直接调用你在 LlamaIndex 写的 workflow,该走哪种方式? → 走"提供方"方式,把自定义 LlamaIndex workflows 转换成 MCP servers。
📚 拓展阅读
- Use any existing LlamaIndex workflow/tool as an MCP Tool/Server — 把 workflow/tool 变成 MCP server 的做法
- Use LlamaCloud APIs as MCP Tools/Servers — 用 LlamaExtract/LlamaParse 等 LlamaCloud 服务
- Using MCP Tools with LlamaIndex — 消费已有 MCP 工具的入门
第三部分 · Workflows
第1章 事件驱动 Workflows
Q什么是 LlamaIndex 的 Workflow?它的核心执行模型是怎样的?深挖·拓展低频
答 Workflow 是一种事件驱动、以 step 为单位来控制应用执行流程的方式。它把应用切分成一个个 step,每个 step 接收一个 event、做一些工作、再返回另一个 event;返回的 event 会触发下一个「其类型注解能接受该 event」的 step,如此串联下去——这就是全部的模型。step 内部可以调用 LLM、执行检索、请求人类输入、更新共享状态或分发一批工作,机制上是「event 类型描述 workflow 的边(edge),而普通 Python 描述每条边内部的逻辑」。这种设计的权衡在于:把控制流交给 event 类型的静态注解,框架就能在运行前推断每个 step 的输入输出并校验图是否有效;而 step 内部保持纯 Python,开发者写循环、分支不需要学新的图 DSL。
术语
step(把应用切分出的执行单元,接收 event 返回 event); Event(用户自定义的 Pydantic 对象,充当 step 之间的边); @step(装饰器,用来推断 step 的输入输出类型并做校验)📖 "A workflow is an event-driven, step-based way to control the execution flow of an application." — pip install llama-index-llms-openai if you don't already have it📖 "A step receives an event, does some work, and returns another event. That returned event triggers the next step whose type annotation accepts it." — pip install llama-index-llms-openai if you don't already have it🧪 实例 最小可用形态是「生成 → 传递给下一个 step → 以结果停止」。
flowchart LR
Start[StartEvent] --> G[generate_joke]
G --> J[JokeEvent]
J --> C[critique_joke]
C --> Stop[StopEvent]🔍 追问 step 之间是怎么知道谁接谁的? → 靠
@step 装饰器从类型注解推断输入输出类型,返回的 event 会触发那个类型注解能接受它的 step,不需要显式声明连线。📚 拓展阅读
- Common Workflow Patterns — 用简单 workflow 走一遍循环与状态管理等常见用法,是很好的起点。
- Managing State (Context) — 介绍 workflow 中的
Context对象。
Q相比 DAG(有向无环图),Workflows 解决了哪些问题?深挖·拓展🔥高频
答 随着生成式 AI 应用变复杂,数据流和执行控制越来越难管理;LlamaIndex 自己和其他框架此前都用 DAG 来解决,但 DAG 有几个 Workflows 没有的局限:循环和分支这类逻辑必须编码进图的「边」里,导致图难读难懂;在 DAG 节点间传数据会带来关于可选值、默认值、该传哪些参数的复杂度;而且 DAG 对想开发复杂、带循环、带分支 AI 应用的开发者来说并不自然。Workflows 用「基于 event + 纯 Python」的模式化解了这些问题:分支就是返回不同 event 类型的普通
if 语句,循环就是返回一个「由更早的 step 处理的 event」的 step,并发就是一个返回 list[Event] 的 step 搭配另一个接受 list[Event] 的 step;当流程需要变得动态时,可以直接从 Context 发送 event。权衡上,类型优先的写法更易校验和可视化,而 Context 这类 API 更灵活但要你自己承担更多簿记工作。术语
DAG(有向无环图,把循环分支编码进边导致难读); Context(运行期上下文,可动态发送 event); list[Event](表达并发的返回/接收类型)📖 "Logic like loops and branches needed to be encoded into the edges of graphs, which made them hard to read and understand." — pip install llama-index-llms-openai if you don't already have it📖 "Branches are ordinaryifstatements that return different event types. Loops are steps that return an event handled by an earlier step. Concurrent work is a step that returnslist[Event], paired with another step that acceptslist[Event]." —pip install llama-index-llms-openaiif you don't already have it
🧪 实例 循环即「返回一个被更早 step 处理的 event」,例如反射式改写直到结构化输出合格:
python
@step
async def reflect(self, ev: DraftEvent) -> DraftEvent | StopEvent:
# 校验不通过就返回 DraftEvent,被更早的生成 step 再次处理,形成循环
...🔍 追问 为什么说 DAG 传数据会带来复杂度? → 因为在 DAG 节点间传数据会围绕可选值/默认值/该传哪些参数产生复杂度,而 Workflows 的 event 是用户自定义的 Pydantic 对象,字段由你控制。
📚 拓展阅读
- Reliable Structured Generation — 展示如何在 workflow 中实现循环,用反射改进结构化输出。
- Utilizing Concurrency — 讲解如何管理 workflow 中 step 的并行执行。
QStartEvent 和 StopEvent 是什么?workflow 的入口和出口怎么定义?深挖·拓展中频
答 大多数 event 都是用户自定义的,但框架内置了两个特例 event:
StartEvent 和 StopEvent。StartEvent 标记初始输入送往哪里,它比较特殊,可以持有任意属性——你可以用 ev.topic 直接取(不存在会报错),或用 ev.get("topic") 在属性可能缺失时不报错地处理;为了更强的类型安全也可以继承 StartEvent。传给 run() 的关键字参数会变成这个自动发出的 StartEvent 的字段。出口方面,当 workflow 遇到某个 step 返回 StopEvent 时会立即停止,并返回 result 参数里传入的任何东西——可以是字符串、字典、列表或任意对象,同样可以继承 StopEvent。.run() 会启动 workflow 并返回一个 WorkflowHandler,该 handler 是可 await 的,所以 await w.run(...) 会等到最终结果;如果需要运行期的流式 event,就保留 handler,用 handler.stream_events() 异步迭代,最后再 await handler 拿结果。术语
StartEvent(内置入口 event,可持有任意属性,run 的 kwargs 变为其字段); StopEvent(内置出口 event,返回即停止并回传 result); WorkflowHandler(run 返回的可 await 句柄,支持 stream_events)📖 "When the workflow encounters a returnedStopEvent, it immediately stops the workflow and returns whatever we passed in theresultparameter." —pip install llama-index-llms-openaiif you don't already have it
📖 "The.run()method starts the workflow and returns aWorkflowHandler. The handler is awaitable, soawait w.run(...)waits for the final result." —pip install llama-index-llms-openaiif you don't already have it
🧪 实例 保留 handler 以获取流式 event:
python
handler = w.run(topic="pirates")
async for ev in handler.stream_events():
...
result = await handler🔍 追问
ev.topic 和 ev.get("topic") 有什么区别? → ev.topic 在属性不存在时会抛错,ev.get("topic") 用于属性可能缺失时不报错地处理。📚 拓展阅读
- Query Planning with Workflows — 演示如何从 workflow 流式 event、并行执行 step 并循环直到满足条件。
- Writing Durable Workflows — 展示如何 checkpoint context 并在重启后恢复运行。
QWorkflows 在运行前做哪些校验(validation)?常见失败原因是什么?深挖·拓展中频
答 在 workflow 运行之前,Workflows 会校验由 step 签名描述出来的 event 图:检查是否存在 start 和 stop event、被产出的 event 是否有消费者、被消费的 event 是否有生产者、以及图里是否含有意外的死路(dead ends)。大多数校验失败其实是有用的设计反馈,通常意味着三种情况之一:某个 step 消费了一个从未被产出的 event(往往是漏了返回注解,或该 event 只通过
ctx.send_event 动态发送)、某个 step 产出了没人消费的 event(下一个 step 事件类型写错,或分支还没写完)、或 workflow 没有终止 event(没有可达的 step 返回 StopEvent 或其子类)。对于有意做成动态的 workflow,应尽量把信息留在类型注解里、只用 ctx.send_event 来控制时机;如果某个 step 是有意让静态分析不可达的,可以在该 step 上用 @step(skip_graph_checks=["reachability"]) 跳过这一项检查。你也可以在测试或启动代码里直接调 workflow.validate();resource 配置文件默认会被校验,而 resource 工厂只有在调用 validate(validate_resources=True) 时才会被解析。术语
validate()(可直接调用的校验方法); skip_graph_checks(在 step 上跳过指定图检查,如 reachability); dead ends(图中意外的死路,校验会检查)📖 "Before a workflow runs, Workflows validates the event graph described by your step signatures. It checks that start and stop events are present, produced events have consumers, consumed events have producers, and the graph does not contain accidental dead ends." — pip install llama-index-llms-openai if you don't already have it🧪 实例 对有意静态不可达的 step 跳过可达性检查:
python
@step(skip_graph_checks=["reachability"])
async def receive_webhook(self, ev: WebhookEvent) -> StopEvent:
...🔍 追问 「某 step 消费一个从未被产出的 event」通常是什么原因? → 一个返回注解漏写了,或该 event 只通过
ctx.send_event 动态发送,所以静态分析看不到它的生产者。📚 拓展阅读
- Writing Durable Workflows — checkpoint 与恢复运行,配合校验理解 workflow 生命周期。
- Common Workflow Patterns — 常见模式帮助理解图结构与状态管理。
Q什么时候用类型优先的 API,什么时候用 Context 上的 API(如 send_event、store)?深挖·拓展中频
答 大多数 workflow 代码应使用「带类型的 step 输入」和「带类型的返回」,这样能得到校验、示意图和可读代码。只有当工作的形态确实需要时才用其他 API:返回
A | B 用于分支到不同 event 类型;返回 list[A] 用于一个 step 有有限的一批工作、能在下游 worker 开始前把所有工作项都产出;接受 list[A] 用于一个 step 需要拿到整批结果后再继续;ctx.send_event(...) 用于需要增量发出 event、发出数量未知的 event、或在 workflow 运行时从外部发送 event;ctx.collect_events(...) 用于你用了 ctx.send_event 后需要手动等待一组已知的 event;ctx.store 用于 step 之间需要每次运行的共享状态;Resource(...) 用于 step 需要 client、index、模型、配置等「不该存进序列化状态」的依赖。核心权衡是:类型优先的 API 更易校验和可视化,而 context API 更灵活但让你自己承担更多簿记工作。术语
ctx.send_event(增量/未知数量/外部发送 event); ctx.collect_events(手动等待一组已知 event); ctx.store(每次运行的共享状态); Resource(不进序列化状态的依赖,如 client/index/模型)📖 "Most workflow code should use typed step inputs and typed returns. That gives validation, diagrams, and readable code" — pip install llama-index-llms-openai if you don't already have it📖 "The type-first APIs are easier to validate and visualize. The context APIs are more flexible, but they make you own more of the bookkeeping." — pip install llama-index-llms-openai if you don't already have it🧪 实例 何时用哪种 API(源自官方对照表):
text
Return `A | B` -> 分支到不同 event 类型
Return `list[A]` -> 有限一批工作,先全部产出再交给下游
Accept `list[A]` -> 需要整批结果后再继续
ctx.send_event(...) -> 增量/未知数量/外部发送 event
ctx.collect_events(...) -> 手动等待一组已知 event
ctx.store -> step 间每次运行的共享状态
Resource(...) -> 不该进序列化状态的依赖🔍 追问 为什么 client、模型这类依赖要用
Resource(...) 而不是放进 ctx.store? → 因为它们是不该存进序列化状态(serialized state)的依赖,Resource(...) 专门用于承载这类 client、index、模型、配置。📚 拓展阅读
- Managing State (Context) — 深入
Context对象与共享状态。 - Utilizing Concurrency — 并发场景下
list[Event]与收集事件的用法。
第四部分 · LlamaCloud / LlamaParse
第1章 云平台与解析
低频
LlamaCloud·LlamaParse·Extract
LlamaCloud | Developer Documentation Upload and parse a document Overview of Parse | Developer Documentation Overview of Extract | Developer DocumentationQLlamaCloud 是什么?它和 LlamaParse 平台上的六个产品是什么关系?深挖·拓展🔥高频
答 LlamaCloud 是一个托管平台(managed platform),把"从原始文档到可供 LLM 使用的高质量数据"这条链路的各环节——解析(parsing)、摄取(ingestion)、检索(retrieval)、结构化抽取(structured data extraction)等——统一收口,目标是为生产级 LLM 应用产出 production-quality data。在这个平台之上,LlamaParse 被定位为"把文档变成生产 AI 流水线"的企业级入口:核心设计是 一把 API key、一套 SDK、六个可组合(composable)的产品——Parse(agentic OCR)、Extract(结构化数据)、Classify、Split、Sheets、Index。这样设计的权衡在于:文档处理往往不是单点动作,而是"先分类/切分 → 再解析 → 再抽取 → 再建索引"的组合流水线,把六个产品放在同一凭据与 SDK 下,可以让它们互为上下游(例如 Classify/Split 作为 Parse、Extract、Index 的预处理)而不必反复接入。新用户被建议从 Parse 起步,因为它是大多数流水线的基础(the foundation most pipelines build on)。
术语
managed platform(托管平台,官方托管而非自建); composable products(可组合产品,六个能力可串成流水线); one API key, one SDK(单一凭据与 SDK 统一接入); agentic OCR(基于 agent 的 OCR,Parse 的定位)📖 "LlamaCloud is a managed platform for data parsing, ingestion, retrieval, structured data extraction, and more. LlamaCloud enables you to get production-quality data for your production LLM application." — LlamaCloud | Developer Documentation
📖 "LlamaParse is the enterprise platform for turning documents into production AI pipelines. One API key, one SDK, and six composable products: Parse (agentic OCR), Extract (structured data), Classify, Split, Sheets, and Index." — Upload and parse a document
🧪 实例 用同一个 key 装好 SDK 后即可调用平台,安装与鉴权都基于同一环境变量:
bash
pip install llama-cloud>=2.1
export LLAMA_CLOUD_API_KEY=llx-...python
from llama_cloud import LlamaCloud
client = LlamaCloud() # Uses LLAMA_CLOUD_API_KEY env var
# Upload and parse a document
file = client.files.create(file="document.pdf", purpose="parse")
result = client.parsing.parse(
file_id=file.id,
tier="agentic",
version="latest",
expand=["markdown"],
)🔍 追问 六个产品里官方建议新手从哪个开始,为什么? → 从 Parse 开始,因为它是大多数流水线赖以构建的基础(the foundation most pipelines build on),先把文档变成干净文本,后续 Extract/Index 才有可靠输入。
📚 拓展阅读
- LlamaCloud documentation — LlamaCloud 官方完整文档站
- Sign up for LlamaCloud — 注册即每月获得 10,000 免费 credits
- Learn more about LlamaCloud — 企业版能力介绍页
- Python SDK (llama-cloud-py) — 平台 Python SDK 源码仓库
QParse 为什么强调自己是"agentic OCR",它和传统 OCR/PDF-to-text 有何本质区别?深挖·拓展🔥高频
答 传统解析器的做法是"在 OCR 引擎上再螺栓式地拼一层 bounding-box 检测器"——先框出版面区块,再靠几十条脆弱的启发式规则把文字、表格、图表拼回一份文档,任何一步框错或规则失效,复杂版面就会崩。Parse 的机制不同:它用生成式模型做端到端文档理解(end-to-end document understanding),让版面、结构、表格、图表和文字在同一次推理(same reasoning pass)中一起产出,而不是事后缝合,因此在多栏排版、合并单元格、以图片形式嵌入的图表、手写批注、倾斜扫描页等真实场景下更稳。权衡的另一面是"要不要解析":对单份短文档,直接把原始 PDF 喂给多模态模型往往够用;但规模化时问题暴露——表格丢结构、扫描页产生噪声 OCR、且 token 成本随原始页面内容线性增长(无论这些内容是否携带有用信号)。Parse 的价值主张就是把结构化工作在上游一次做完(does the structural work once, upstream),让下游模型只看到干净、版面感知的输入,从而在规模上更省 token、更准。
术语
bounding-box detectors(边框检测器,传统解析范式); end-to-end document understanding(端到端文档理解); same reasoning pass(同一次推理产出全部结构); layout-aware(版面感知,输出保留版面结构)📖 "Most parsers are bounding-box detectors bolted onto an OCR engine. Parse is different. It uses generative models for end-to-end document understanding, so layout, structure, tables, charts, and text all come out of the same reasoning pass—instead of getting stitched back together from dozens of fragile heuristics. That’s why it handles complex real-world documents where traditional parsers break down." — Overview of Parse | Developer Documentation
📖 "For a single short document, passing the raw PDF to a multimodal model often works fine. At scale it breaks down: tables lose structure, scanned pages produce noisy OCR, and token cost scales linearly with raw page content regardless of whether it carried useful signal. Parse does the structural work once, upstream, so downstream models only see clean, layout-aware input." — Overview of Parse | Developer Documentation
🧪 实例 五行即可把 PDF 解析为干净 markdown:
python
from llama_cloud import LlamaCloud
client = LlamaCloud() # reads LLAMA_CLOUD_API_KEY
file = client.files.create(file="doc.pdf", purpose="parse")
result = client.parsing.parse(file_id=file.id, tier="agentic", version="latest")
print(result.markdown.pages[0].markdown)flowchart LR
A[原始文档 PDF/扫描/图表] --> B{解析范式}
B -->|传统: OCR + bbox + 启发式缝合| C[版面/表格易崩]
B -->|Parse: 生成式端到端一次推理| D[干净 layout-aware markdown/text/JSON]
D --> E[下游 LLM 只看干净输入]🔍 追问 既然多模态模型能直接读 PDF,为什么还要 Parse? → 单份短文档直喂常够用;但规模化时表格丢结构、扫描页产生噪声 OCR,且 token 成本随原始页面内容线性增长,Parse 把结构化工作在上游一次做完,规模上更省更准。
📚 拓展阅读
- Overview of Parse — Parse 定位、能力与工作流总览
- Learn more about LlamaParse — 产品营销页
- LlamaParse Agent Skills (GitHub Releases) — 让 coding agent 内嵌解析能力的技能包
QParse 提供哪些 tier?Cost Optimizer 是怎么在成本与精度之间做取舍的?深挖·拓展中频
答 Parse 用分层(tiers)让你为每个任务挑成本-精度折中:Agentic Plus 用最强模型冲最难文档的最高精度;Agentic 面向复杂、视觉丰富的文件;Cost Effective 是日常工作负载的平衡档;Fast 是最低延迟、最低成本档,面向高并发的纯文本文档,且只返回 text 和 spatial text(没有 markdown)。单档定价的问题是:一份文档里既有简单纯文本页,也有带表格/图表/多栏的复杂页,整篇用高档太贵、整篇用低档又不准。Cost Optimizer 的机制正是解决这个错配——它按页自动路由:简单页走更便宜的
cost_effective tier,复杂页(表格、图表、多栏版面、图示)留在你的高档 tier,并且两者并行执行,所以没有延迟惩罚(no latency penalty)。核心权衡就是"只在真正需要的页上付高价(pay premium prices only on the pages that need it)",在不牺牲整体延迟的前提下把成本压到与内容复杂度匹配。术语
tier(分层,成本-精度档位); Fast tier(最快最便宜档,仅返回 text/spatial text,无 markdown); Cost Optimizer(按页路由的成本优化器); no latency penalty(简单页与复杂页并行,无额外延迟)📖 "Fast — lowest-latency, lowest-cost tier for plain-text documents at high volume; returns text and spatial text only (no markdown)." — Overview of Parse | Developer Documentation
📖 "Cost Optimizer — pay premium prices only on the pages that need it. Parse routes each page in your document to the right tier automatically: simple pages go to the cheaper cost_effective tier, complex pages (tables, charts, multi-column layouts, diagrams) stay on your premium tier, and both run in parallel so there’s no latency penalty." — Overview of Parse | Developer Documentation🧪 实例 一份 100 页年报里 80 页是纯文字说明、20 页是财务表格与图表。开启 Cost Optimizer 后,80 页走
cost_effective、20 页留在 agentic,两组并行,账单接近"80×低价 + 20×高价"而非"100×高价",且总耗时由最慢的那组决定,而非串行相加。🔍 追问 Fast tier 有什么输出限制需要注意? → 它只返回 text 和 spatial text,不产出 markdown,所以依赖 markdown 结构(如表格)的下游流水线不能直接用 Fast。
📚 拓展阅读
- Overview of Parse — Cost Optimizer、tiers 与核心特性说明
- Parse in 60 seconds — 一分钟看懂 Parse 的官方短片
- LlamaCloud documentation — 平台完整文档,含各 tier 与定价细节
QLlamaExtract 解决什么问题?它靠什么保证抽取结果"可靠可用"?深挖·拓展中频
答 LlamaExtract 提供一个简单的 API,从 PDF、文本文件、图片等非结构化文档中抽取结构化数据,并以 web UI、Python SDK、REST API 三种形态交付。它的核心价值不是"抽出点东西",而是契约保证:你给一个 schema,LlamaExtract 要么保证输出符合该 schema,要么在不符合时给出有帮助的错误信息(helpful error messages)——这正是"下游可用"的关键,因为抽取结果往往要直接进数据库、喂仪表盘或训练模型,字段类型不受控就会污染下游。为此它主打几个能力:well-typed 数据以服务下游任务、用 best in class LLM 模型保证抽取准确度、以及可迭代的 schema 开发(iterative schema development)——你能在样本文档上快速试 schema 并拿到反馈(某字段是否要多给示例、是否该设为 optional)。权衡在于:它不是"随便问文档一个问题"的通用问答,而是围绕 schema 的强类型抽取,先设计好 schema 才能发挥它的类型保证。
术语
structured data extraction(结构化抽取,从非结构化文档产出结构化字段); schema(抽取的目标结构与类型契约); iterative schema development(schema 迭代开发,在样本上快速试错); well-typed data(强类型数据,可直接进下游)📖 "LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files, and images." — Overview of Extract | Developer Documentation
📖 "LlamaExtract guarantees that your data complies with the provided schema or provides helpful error messages when it doesn’t." — Overview of Extract | Developer Documentation
🧪 实例 用 Pydantic 定义 schema 并抽取简历结构化字段:
python
from pydantic import BaseModel, Field
from llama_cloud import LlamaCloud
# Define your schema
class Resume(BaseModel):
name: str = Field(description="Full name of candidate")
email: str = Field(description="Email address")
skills: list[str] = Field(description="Technical skills")
client = LlamaCloud()
# Upload and extract
file = client.files.create(file="resume.pdf", purpose="extract")
job = client.extract.create(
document_input_value=file.id,
config={
"extract_options": {
"data_schema": Resume.model_json_schema(),
"tier": "agentic",
},
},
)
print(job.extract_result)🔍 追问 如果某字段在部分文档里根本不存在,该怎么迭代 schema? → 官方把这类调整列为 iterative schema development 的一部分——给字段多补几个示例,或把该字段设为 optional,在样本文档上快速验证效果。
📚 拓展阅读
- Overview of Extract — LlamaExtract 定位与适用判断
- Learn more about LlamaExtract — 产品页
- Sign up for LlamaCloud — 获取 API key 开始 SDK 抽取
QExtract 的 v2 与 v1 有何区别?version 该如何 pin 才能保证生产可复现?深挖·拓展低频
答 LlamaExtract 现在默认跑在 v2 API 上,UI 里给三个 tier:
Cost Effective(更低成本、更高吞吐,适合简单抽取)、Agentic(推荐默认,质量/速度/成本平衡)、Agentic Plus(coming soon,高保真、面向极复杂或高风险抽取)。v2 相对 v1 的一个关键架构变化是:直接用 SDK/REST 时,V2 把 parse 与 extract 两个 tier 解耦(decouples parse and extract tiers)——即抽取算法档与底层解析档可分别配置,官方给出了 V2 组合到 V1 extraction_mode 的映射表(如 cost_effective+fast≈FAST、agentic+agentic≈MULTIMODAL、agentic+agentic_plus≈PREMIUM)。可复现的关键在 configuration.version:生产工作流应把 version pin 到你创建/更新配置的日期(YYYY-MM-DD,如 2026-03-31),日期 pin 会解析到"该日期或之前、所选 tier 下最新可用的 extract 版本",从而让后续发布不会悄悄改变既有行为;而 latest 则让任务自动追新版本。权衡很清楚:latest 拿新能力但行为可能漂移,日期 pin 换取稳定与可复现——生产优先后者。若确需旧体验,可通过 workspace 设置或 llama-cloud-services 包回到 legacy v1,但官方建议新项目迁移到 v2。术语
v2 (default)(当前默认 API 版本); configuration.version(控制 extract 算法版本的字段); date pin (YYYY-MM-DD)(按日期固定版本,防行为漂移); decouples parse and extract tiers(v2 解耦解析档与抽取档)📖 "For production workflows, pinversionto the date you create or update the configuration inYYYY-MM-DDformat, for example2026-03-31. A date pin resolves to the most recent available extract version for the selected tier at or before that date, so later releases do not silently change existing behavior. Uselatestwhen you want jobs to automatically pick up the newest extract version." — Overview of Extract | Developer Documentation
📖 "When using the SDK or REST API directly, V2 decouples parse and extract tiers." — Overview of Extract | Developer Documentation
🧪 实例 V2 tier 组合到 V1
生产配置里把
extraction_mode 的对应关系(源自官方映射表):text
V2 extract tier V2 parse tier V1 equivalent (extraction_mode)
cost_effective fast FAST
agentic agentic MULTIMODAL
agentic agentic_plus PREMIUM生产配置里把
configuration.version 写成一个具体日期(如 2026-03-31)而非 latest,即可锁定行为。🔍 追问 想临时回退到 legacy Extract v1 有哪些入口? → Web UI 在 Settings → General 打开 workspace 的 Extract v1 开关;Python SDK 用
llama-cloud-services 包;REST 用 v1 legacy 端点;官方另提供 v1→v2 迁移指南。📚 拓展阅读
- Overview of Extract — tiers、版本 pin 与 v1/v2 映射的原文出处
- LlamaCloud documentation — 平台整体文档,含 Extract 深入配置
- Learn more about LlamaExtract — 产品能力概览
第五部分 · 源码讲解
第1章 索引与检索源码
🔥高频
索引构建源码(VectorStoreIndex)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/base.pyQVectorStoreIndex.from_documents 到底做了哪些事?整个索引构建链路是怎样的?深挖·拓展🔥高频
答
from_documents 是 BaseIndex 上的类方法,是构建索引的入口。它先拿到 storage_context(没传就用 StorageContext.from_defaults())、docstore、callback_manager、transformations,然后在 callback_manager.as_trace("index_construction") 追踪上下文里做两件关键事:一是对每个 document 调用 docstore.set_document_hash(doc.id_, doc.hash) 记录文档哈希(为后续 refresh_ref_docs 的增量更新做基础),二是调用 run_transformations 把 Document 切分/转换成 Sequence[BaseNode]。之后它并不是直接存,而是把生成的 nodes 交给构造函数 cls(nodes=nodes, ...)。构造函数 __init__ 里再在同名的 index_construction trace 中,当 index_struct is None 时调用 build_index_from_nodes(nodes + objects) 真正建立索引结构,并把结果 index_struct 通过 index_store.add_index_struct 持久化。所以设计上把"文档 → 节点"(transformations)和"节点 → 索引结构"(build_index_from_nodes)两个阶段解耦,from_documents 只负责前半段并委托构造函数完成后半段;之所以强制走 from_documents 而不能直接把 Document 塞进构造函数,是因为构造函数显式检查了 isinstance(nodes[0], Document) 并抛错提示改用 from_documents,以此保证"构造函数只吃 Node"的新式 UX。术语
from_documents(以文档构建索引的类方法); run_transformations(把 Document 转换/切分为 Node); set_document_hash(记录文档哈希用于增量刷新); index_construction(索引构建的 callback trace 名); add_index_struct(把索引结构写入 index store)📖 "nodes = run_transformations(" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/base.py
📖 "The constructor now takes in a list of Node objects. " — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/base.py
🧪 实例
flowchart TD
A[Document 列表] -->|set_document_hash| B[记录文档哈希]
A -->|run_transformations| C[Sequence of BaseNode]
C --> D["cls(nodes=nodes, ...)"]
D -->|__init__| E{"index_struct is None?"}
E -->|是| F["build_index_from_nodes(nodes + objects)"]
F --> G[IndexDict]
G --> H["index_store.add_index_struct"]🔍 追问 如果我已经手上有切好的 Node,不想再走一遍 transformations,怎么办? → 直接用构造函数
VectorStoreIndex(nodes=nodes, ...),构造函数会跳过 transformations 直接 build_index_from_nodes;但注意传入的必须是 BaseNode 而非 Document,否则会被显式报错拦截。📚 拓展阅读
- BaseIndex 源码 —
from_documents与构造函数中index_constructiontrace 的完整逻辑 - VectorStoreIndex 源码 — 覆写版
build_index_from_nodes的实现细节
QVectorStoreIndex 构建时是怎么给节点算 embedding 并写入向量库的?为什么要分批?深挖·拓展🔥高频
答 核心在
_add_nodes_to_index。它用 iter_batch(nodes, self._insert_batch_size) 把节点切成批,insert_batch_size 默认 2048;对每一批先调 self._get_node_with_embedding(nodes_batch, show_progress) 生成带 embedding 的节点,该方法内部用 embed_nodes(nodes, self._embed_model, ...) 得到 id_to_embed_map,然后对每个 node 做 node.model_copy() 拷贝一份、把 result.embedding = embedding 赋上去再返回(注意是拷贝而非原地改),避免污染原始节点。拿到带向量的批后调用 self._vector_store.add(nodes_batch, **insert_kwargs),返回向量库分配的 new_ids。分批的意义在于:embedding 调用本身是"in batches"批量发给 embedding 模型的,批量能摊薄网络/模型调用开销并配合进度条显示;同时向量库的写入也按批提交,控制单次内存占用和请求体大小。异步路径 _async_add_nodes_to_index 结构完全对称,只是把 _get_node_with_embedding/add 换成 _aget_node_with_embedding(内部 async_embed_nodes)和 async_add,以便高并发地打 embedding 请求。术语
_add_nodes_to_index(把节点批量嵌入并写入向量库的核心方法); insert_batch_size(默认 2048 的插入批大小); embed_nodes(批量计算节点嵌入,返回 id→embedding 映射); model_copy(拷贝节点后再挂 embedding,避免改动原节点); iter_batch(按批大小切分序列的工具)📖 "for nodes_batch in iter_batch(nodes, self._insert_batch_size):" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "Embeddings are called in batches." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "new_ids = self._vector_store.add(nodes_batch, **insert_kwargs)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
🧪 实例
python
# 构造时可自定义批大小,大批量导入时调大可减少往返
index = VectorStoreIndex(
nodes=nodes,
insert_batch_size=2048, # 默认值
show_progress=True,
)
# 内部等价于对每个 batch:
# nodes_batch = self._get_node_with_embedding(nodes_batch, show_progress)
# new_ids = self._vector_store.add(nodes_batch, **insert_kwargs)🔍 追问 为什么
_get_node_with_embedding 要先 model_copy() 再赋 embedding,而不是直接改传入的 node? → 直接改会污染调用方持有的原始节点对象;拷贝后挂 embedding 返回,保证原节点保持无向量的干净状态,也和后面"remove embedding to avoid duplication"的思路一致。📚 拓展阅读
- VectorStoreIndex 源码 —
_get_node_with_embedding与_add_nodes_to_index的批处理实现 - BaseIndex 源码 —
insert_nodes/_insert的抽象契约
Q什么时候节点会被写进 docstore/index_struct,什么时候不会?stores_text 和 store_nodes_override 起什么作用?深挖·拓展🔥高频
答 关键分支是
if not self._vector_store.stores_text or self._store_nodes_override:。当向量库本身不存文本(stores_text == False),或用户显式把 store_nodes_override=True 时,索引会对每个节点做 node.model_copy() 并把 embedding 置 None(注释明确写 "remove embedding from node to avoid duplication",因为向量已经在向量库里了,再存一份到 docstore 是冗余),然后 index_struct.add_node(node_without_embedding, text_id=new_id) 记进索引结构、self._docstore.add_documents([...], allow_update=True) 存进文档库。反之,如果向量库已经保存文本(常见的托管向量库),就只需要对 isinstance(node, (ImageNode, IndexNode)) 的特殊节点补存到 index_struct 和 docstore,普通文本节点不再重复落 docstore。这样设计的权衡是:文本已在向量库时,避免把节点和向量在本地 docstore 里再冗余一份、节省存储;但代价是 ref_doc_info 属性在这种情况下会直接 raise NotImplementedError,提示"store text in the vector store are not supported by ref_doc_info yet"。store_nodes_override=True 正是给那些"即使向量库存文本也想在本地强制保留 Node"的场景开的后门,好处是能恢复 ref_doc_info、按 ref_doc 删除等依赖本地 docstore 的能力。术语
stores_text(向量库是否自身存储文本的标志); store_nodes_override(强制在本地 index/docstore 存 Node 的开关); node_without_embedding(去掉 embedding 的节点拷贝,避免向量重复存储); index_struct.add_node(把 node 及其 text_id 记入索引结构); ref_doc_info(文档→节点映射,依赖本地 docstore)📖 "if not self._vector_store.stores_text or self._store_nodes_override:" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "# NOTE: remove embedding from node to avoid duplication" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "Vector store integrations that store text in the vector store are " — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
🧪 实例
python
# 向量库存文本时,仍想保留本地 Node(以便 ref_doc_info / 删除)
index = VectorStoreIndex(
nodes=nodes,
store_nodes_override=True, # 强制写 index_struct + docstore
)
# 否则 index.ref_doc_info 在 stores_text=True 时会 raise NotImplementedError🔍 追问
set to True to always store Node objects 这个开关不打开,直接访问 ref_doc_info 会怎样? → 若 stores_text=True 且未开 override,ref_doc_info 直接抛 NotImplementedError;只有 not stores_text or store_nodes_override 成立时才会从本地 docstore 汇总 RefDocInfo。📚 拓展阅读
- VectorStoreIndex 源码 —
_add_nodes_to_index分支与ref_doc_info属性 - BaseIndex 源码 — 抽象
ref_doc_info的定义位置
Quse_async=True 在构建索引时改变了什么?同步和异步路径有何区别?深挖·拓展中频
答 分叉点在
_build_index_from_nodes:它先 index_struct = self.index_struct_cls() 建一个空的 IndexDict,然后根据 self._use_async 二选一。若为真,构造一个 self._async_add_nodes_to_index(index_struct, nodes, show_progress=..., **insert_kwargs) 协程放进 tasks 列表,交给 run_async_tasks(tasks) 驱动执行;否则直接同步调用 self._add_nodes_to_index(...)。两条路径最终都返回同一个 index_struct。异步版 _async_add_nodes_to_index 与同步版逻辑一一对应,差别只在把嵌入和写入换成 await 形式:_aget_node_with_embedding(内部 async_embed_nodes)、self._vector_store.async_add(...)、以及 self._docstore.async_add_documents(...)。使用异步的收益在于 embedding 计算和向量库写入通常是 I/O 密集(打远程 API),协程可以让一批内/多批间的请求并发飞出去、显著缩短总墙钟时间;权衡是需要事件循环环境,use_async 默认为 False 以保证最朴素的同步调用在任何上下文都能跑通。注意 build_index_from_nodes(公开覆写版)在委派给 _build_index_from_nodes 之前,还会先过滤掉 get_content(metadata_mode=MetadataMode.EMBED) 为空的节点。术语
_build_index_from_nodes(根据 use_async 选择同步/异步落库的私有方法); use_async(是否使用异步调用,默认 False); run_async_tasks(驱动协程任务列表执行); async_add(向量库的异步写入接口); IndexDict(向量索引使用的 index_struct 类型)📖 "if self._use_async:" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "run_async_tasks(tasks)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "Whether to use asynchronous calls. Defaults to False." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
🧪 实例
python
# 大规模导入时开启异步,让 embedding / 写入并发
index = VectorStoreIndex(
nodes=nodes,
use_async=True, # 走 _async_add_nodes_to_index + run_async_tasks
)🔍 追问
use_async=True 时构建过程是彻底不阻塞当前线程吗? → 不是。_build_index_from_nodes 用 run_async_tasks(tasks) 同步地把协程跑完再返回 index_struct,它只是让内部的 embedding/写入并发,构建调用本身仍然等到全部完成才继续。📚 拓展阅读
- VectorStoreIndex 源码 —
_build_index_from_nodes与_async_add_nodes_to_index - BaseIndex 源码 —
_build_index_from_nodes抽象声明
Qfrom_vector_store 是干什么的?为什么它要求向量库必须 stores_text?深挖·拓展中频
答
from_vector_store 是给"向量库里已经有数据、想直接挂一个 index 来检索"的场景准备的类方法。它开头就校验 if not vector_store.stores_text: 并抛 ValueError("Cannot initialize from a vector store that does not store text.")——原因是从既有向量库初始化时,LlamaIndex 本地并没有 docstore 里的节点文本,只能依赖向量库自身返回文本,所以向量库必须 stores_text=True 才有意义。接着它 kwargs.pop("storage_context", None) 丢掉外部可能传入的 storage_context,改用 StorageContext.from_defaults(vector_store=vector_store) 以该向量库新建存储上下文,最后 return cls(nodes=[], embed_model=embed_model, storage_context=storage_context, **kwargs)。注意这里传的是 nodes=[] 空列表——构造函数会走 build_index_from_nodes([]),由于没有新节点要嵌入,等于只是把 index 结构接到既有向量库上而不重新灌数据。这样的设计让"复用已存在向量库"和"从零构建"共用同一套构造逻辑。术语
from_vector_store(从既有向量库初始化索引的类方法); stores_text(向量库须存文本,否则无法初始化); StorageContext.from_defaults(以给定 vector_store 构建存储上下文); nodes=[](空节点,表示不再灌入新数据)📖 "Cannot initialize from a vector store that does not store text." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "storage_context = StorageContext.from_defaults(vector_store=vector_store)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
🧪 实例
python
# 向量库里已有 embedding + 文本,直接挂载检索
index = VectorStoreIndex.from_vector_store(
vector_store=my_vector_store, # 必须 stores_text=True
embed_model=my_embed_model,
)
retriever = index.as_retriever()🔍 追问
from_vector_store 里为什么要 kwargs.pop("storage_context", None)? → 因为它要强制用传入的 vector_store 新建 StorageContext,若外部又传了 storage_context 会产生冲突,直接 pop 掉以该向量库为准。📚 拓展阅读
- VectorStoreIndex 源码 —
from_vector_store与as_retriever实现 - BaseIndex 源码 — 构造函数如何处理
nodes=[]
Q构建时空内容的节点会被怎么处理?更新(update/refresh)机制的底层是删+插吗?深挖·拓展低频
答 两点。其一,VectorStoreIndex 覆写的
build_index_from_nodes 会先过滤:content_nodes = [node for node in nodes if node.get_content(metadata_mode=MetadataMode.EMBED) != ""],把嵌入视角下内容为空的节点剔除,并在数量不一致时 print("Some nodes are missing content, skipping them...") 提示;这是因为对空文本算 embedding 没有意义,提前跳过可避免脏向量。其二,更新语义在 BaseIndex 上定义:update_ref_doc 的 docstring 明确写 "This is equivalent to deleting the document and then inserting it again.",实现就是先 delete_ref_doc(document.id_, delete_from_docstore=True, ...) 再 insert(document, ...)。refresh_ref_docs 则基于文档哈希做增量:遍历文档,existing_doc_hash = self._docstore.get_document_hash(document.id_),若为 None 说明是新文档就 insert 并标记 True,若哈希与当前 document.hash 不同就 update_ref_doc 并标记 True,哈希相同则跳过——这样只对真正变化的文档重算 embedding/LLM,节省调用成本。注意 delete()、update()、refresh() 都已被标记 deprecated,应改用带 ref_doc 的新方法。术语
MetadataMode.EMBED(嵌入视角下取节点内容的模式); update_ref_doc(先删后插等价的文档更新); refresh_ref_docs(按文档哈希增量刷新); get_document_hash(取已存文档哈希用于变更检测); delete_ref_doc(按 ref_doc_id 删除文档及其节点)📖 "print("Some nodes are missing content, skipping them...")" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/base.py
📖 "This is equivalent to deleting the document and then inserting it again." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/base.py
📖 "existing_doc_hash = self._docstore.get_document_hash(document.id_)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/base.py
🧪 实例
python
# refresh 只对哈希变化的文档重建,返回每个文档是否被刷新
refreshed = index.refresh_ref_docs(documents)
# refreshed[i] == True 表示第 i 个文档是新插入或因内容变化被 update🔍 追问 为什么
refresh_ref_docs 能"省下 LLM 和 Embedding 调用"? → 它先比对 get_document_hash 与 document.hash,哈希一致的文档直接跳过,不再走 transformations/embedding,只有新增或内容变更的文档才 insert/update,从而避免对未变文档重复计算。📚 拓展阅读
- BaseIndex 源码 —
update_ref_doc/refresh_ref_docs/deprecated 警告 - VectorStoreIndex 源码 —
build_index_from_nodes的空内容过滤
🔥高频
检索源码(BaseRetriever)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/base_retriever.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/retrievers/retriever.pyQBaseRetriever.retrieve 的整体执行流程是怎样的?为什么把 retrieve 和 _retrieve 拆成两层?深挖·拓展🔥高频
答
retrieve 是一个典型的模板方法(Template Method):它固化了检索的通用骨架,把真正的检索算法下沉给子类实现的 _retrieve 抽象方法。流程上,retrieve 先调用 _check_callback_manager 保证回调管理器存在,再通过 dispatcher.event(RetrievalStartEvent(...)) 发出 instrumentation 事件;然后把入参标准化——如果 str_or_query_bundle 是 str 就包成 QueryBundle(str_or_query_bundle),否则直接当作 QueryBundle 使用,这样子类只需面对统一的 QueryBundle。核心检索发生在 self.callback_manager.as_trace("query") 与 CBEventType.RETRIEVE 事件的双层上下文里:先 nodes = self._retrieve(query_bundle) 拿到子类原始结果,再 nodes = self._handle_recursive_retrieval(query_bundle, nodes) 做递归展开,最后 retrieve_event.on_end(payload={EventPayload.NODES: nodes}) 上报节点并发出 RetrievalEndEvent。这种拆分的权衡在于:通用的可观测性(callback + dispatcher)、入参归一化、递归检索这些横切逻辑只写一遍并对所有子类生效,子类只专注"给定 query 怎么召回节点"这一件事;代价是子类必须遵守约定实现 _retrieve(它被 @abstractmethod 标注),不能直接覆写 retrieve。术语
retrieve(对外入口,固化流程骨架); _retrieve(子类实现的抽象检索方法); QueryBundle(查询的标准化封装); callback_manager(回调/追踪管理器); dispatcher(instrumentation 事件分发器)📖 " if isinstance(str_or_query_bundle, str):
📖 " nodes = self._retrieve(query_bundle)
🧪 实例 无论传字符串还是 QueryBundle,子类看到的都是统一对象:
python
# 两种调用最终在 retrieve 内部都归一化为 QueryBundle
retriever.retrieve("什么是向量检索?")
retriever.retrieve(QueryBundle(query_str="什么是向量检索?"))flowchart TD
A["retrieve(str_or_query_bundle)"] --> B["_check_callback_manager"]
B --> C["RetrievalStartEvent"]
C --> D{"是 str?"}
D -->|是| E["QueryBundle(str)"]
D -->|否| F["直接用 QueryBundle"]
E --> G["_retrieve(query_bundle)"]
F --> G
G --> H["_handle_recursive_retrieval"]
H --> I["RetrievalEndEvent + 返回 nodes"]🔍 追问 子类应该覆写
retrieve 还是 _retrieve? → 覆写 _retrieve;_retrieve 被 @abstractmethod 标注,是子类应实现的钩子,而 retrieve 负责统一的回调、事件、归一化和递归检索,不应被绕过。📚 拓展阅读
- base_retriever.py 源码 — BaseRetriever 完整实现,含 retrieve/_retrieve 定义
- VectorIndexRetriever 源码 — 一个具体子类如何实现
_retrieve
Q_handle_recursive_retrieval 做了什么?递归检索是如何被触发和去重的?深挖·拓展🔥高频
答
_handle_recursive_retrieval 是在子类 _retrieve 之后统一执行的后处理:它遍历初步召回的每个 NodeWithScore,取出底层 node 和 score = n.score or 1.0(没有分数时兜底为 1.0)。关键判断是——如果该 node 是 IndexNode(即一个指向"其他可检索对象"的间接节点),就尝试解析它引用的对象:obj = node.obj or self.object_map.get(node.index_id, None),即优先用节点自带的 obj,否则回退到检索器构造时传入的 object_map 用 index_id 查找。若解析出的 obj 不为空,就调用 _retrieve_from_object 进入该对象继续检索并把结果 extend 进来;若对象为空或节点根本不是 IndexNode,则原样保留该节点。这实现了"检索出的节点本身又能引导到下一层检索"的递归能力,常用于文档摘要节点指向原文、或节点指向另一个 query engine/retriever 的场景。最后为避免多个路径召回同一节点,用一个 seen 集合基于 node_id 做去重:遍历中 n.node.node_id in seen or seen.add(...),已见过的就过滤掉。权衡在于:递归带来了强大的组合检索能力,但它是同步顺序执行的(异步版本 _ahandle_recursive_retrieval 里还留有 # TODO: Add concurrent execution via run_jobs() ? 的注释),深层递归会串行放大延迟。术语
IndexNode(指向其他对象的间接节点); object_map(index_id → 对象的映射表); _retrieve_from_object(进入被引用对象继续检索); node_id 去重(用 seen 集合过滤重复节点)📖 " obj = node.obj or self.object_map.get(node.index_id, None)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/base_retriever.py
📖 " # remove any duplicates based on node_id
🧪 实例 构造检索器时通过
之后若某召回节点是
objects 建立 object_map,__init__ 里会执行:python
if objects is not None:
object_map = {obj.index_id: obj.obj for obj in objects}之后若某召回节点是
IndexNode 且其 index_id 命中该 map,_handle_recursive_retrieval 就会跳进对应对象再检索一层。🔍 追问 如果
IndexNode 既没有 obj 也不在 object_map 中会怎样? → 走 else 分支 retrieved_nodes.append(n),即把这个 IndexNode 原样返回,不做递归展开,不会报错。📚 拓展阅读
- base_retriever.py 源码 —
_handle_recursive_retrieval与异步版对照 - VectorIndexRetriever 源码 — 具体检索器产出的节点会进入该递归逻辑
Q_retrieve_from_object 支持递归进入哪些类型的对象?各自如何转成 NodeWithScore?深挖·拓展中频
答
_retrieve_from_object 是递归检索的分发核心,按 obj 的运行时类型走不同分支:如果 obj 已是 NodeWithScore,直接 return [obj];如果是裸的 BaseNode,包成 [NodeWithScore(node=obj, score=score)],分数沿用上层传入的 score;如果是 BaseQueryEngine,则调用 response = obj.query(query_bundle) 真正执行一次子查询,再把回答文本包成 TextNode——node=TextNode(text=str(response), metadata=response.metadata or {})——并附上 score;如果是另一个 BaseRetriever,就直接 return obj.retrieve(query_bundle) 委托给它完整跑一遍检索(注意这里调用的是公开的 retrieve,因此被引用检索器自身的 callback/递归逻辑也会生效);其余任何类型都 raise ValueError(f"Object {obj} is not retrievable.")。这种基于 isinstance 的多态分发让"节点、子索引、子查询引擎、子检索器"能在同一棵检索树里自由组合。权衡是:进入 QueryEngine 分支意味着触发一次完整的 LLM 生成(query),开销显著高于纯向量召回;而进入 BaseRetriever 分支则是递归调用公开 retrieve,可能层层叠加 callback 与再一次递归展开。同步版还带 _verbose 打印(Retrieving from object ...)便于调试,异步版 _aretrieve_from_object 逻辑对应但用 await obj.aquery/await obj.aretrieve。术语
BaseQueryEngine(可被查询、返回带 metadata 响应的引擎); BaseNode(底层节点基类); NodeWithScore(节点+相似度分数); TextNode(把查询响应包成文本节点)📖 " elif isinstance(obj, BaseQueryEngine):
📖 " raise ValueError(f"Object {obj} is not retrievable.")" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/base_retriever.py
🧪 实例 一个 IndexNode 指向另一个检索器时的委托关系:
python
# _retrieve_from_object 内部
elif isinstance(obj, BaseRetriever):
return obj.retrieve(query_bundle) # 递归进入子检索器的完整 retrieve 流程🔍 追问 为什么进入子
BaseRetriever 调用的是 retrieve 而不是 _retrieve? → 因为要复用子检索器自身的 callback、事件与递归展开逻辑;若调 _retrieve 会绕过这些横切处理,失去一致的可观测性和递归能力。📚 拓展阅读
- base_retriever.py 源码 —
_retrieve_from_object/_aretrieve_from_object分支实现 - VectorIndexRetriever 源码 — 作为可被递归进入的 BaseRetriever 子类
QVectorIndexRetriever 的 _retrieve 如何决定是否需要生成查询向量?_needs_embedding 的逻辑是什么?深挖·拓展🔥高频
答
VectorIndexRetriever._retrieve 的第一步是判断本次查询是否需要嵌入向量:调用 self._needs_embedding(),只有当 self._vector_store.is_embedding_query 为真、且当前 _vector_store_query_mode 不属于 TEXT_SEARCH 或 SPARSE 这两种模式时才需要 embedding。这样设计是因为纯文本检索或稀疏检索(如 BM25/关键词)本身不依赖稠密向量,强行生成 embedding 既浪费一次模型调用又无意义。当确实需要 embedding 且 query_bundle.embedding is None and len(query_bundle.embedding_strs) > 0 时,才用 self._embed_model.get_agg_embedding_from_queries(query_bundle.embedding_strs) 把查询文本聚合成一个向量并写回 query_bundle.embedding——这里的"聚合"允许一个查询由多个字符串组成、取其聚合嵌入。拿到(或跳过)embedding 后统一交给 self._get_nodes_with_embeddings(query_bundle) 完成实际的向量库查询。异步版 _aretrieve 逻辑对称,但用 await ... aget_agg_embedding_from_queries 并新建一个只带 query_str 和 embedding 的 QueryBundle 传下去。这个设计的关键权衡是:把"是否嵌入"与具体 query_mode 解耦,使同一个检索器类能覆盖 dense、sparse、text、hybrid 多种模式,而不必为每种模式写一个子类。术语
_needs_embedding(判断当前模式是否需要稠密向量); is_embedding_query(向量库是否以向量为查询输入); VectorStoreQueryMode(检索模式枚举:TEXT_SEARCH/SPARSE 等); get_agg_embedding_from_queries(把多个查询串聚合为单一嵌入)📖 " return (
📖 " if query_bundle.embedding is None and len(query_bundle.embedding_strs) > 0:" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/retrievers/retriever.py
🧪 实例 若用 SPARSE 模式,则跳过 embedding 生成,直接查询稀疏索引:
python
retriever = index.as_retriever(
vector_store_query_mode="sparse", # _needs_embedding() 返回 False
similarity_top_k=5,
)🔍 追问 如果调用方已经在
QueryBundle 里预置了 embedding 会发生什么? → 由于判断条件是 query_bundle.embedding is None,预置了 embedding 就不会再触发 get_agg_embedding_from_queries,直接复用已有向量,省掉一次嵌入模型调用。📚 拓展阅读
- VectorIndexRetriever 源码 —
_retrieve/_needs_embedding完整实现 - base_retriever.py 源码 — 父类
_retrieve抽象方法契约
Q向量库查询后为什么还要回到 docstore 取节点?_determine_nodes_to_fetch 的判定规则是什么?深挖·拓展中频
答 向量库查询返回的
VectorStoreQueryResult 不一定包含完整的节点内容,因此 _get_nodes_with_embeddings 在拿到 query_result 后调用 _determine_nodes_to_fetch 判断哪些节点需要回到 docstore 补全。规则有两条分支:如果向量库本身存了节点(query_result.nodes 非空),说明大部分内容已在结果里,只需为其中"非文本类型"的节点(node.as_related_node_info().node_type != ObjectType.TEXT)去 docstore 取回完整对象——文本节点已带文本无需再取;如果向量库没有存节点、只返回了 id(query_result.ids 非空),则需要把每个 id 通过 self._index.index_struct.nodes_dict[idx] 映射成 node_id 并全部从 docstore 拉取。两者都为空则返回空列表。真正的取数在 _get_nodes_with_embeddings 里:若 nodes_to_fetch 非空,用 self._docstore.get_nodes(node_ids=nodes_to_fetch, raise_error=False) 拉取,再由 _insert_fetched_nodes_into_query_result 把取回的节点按 id 替换/合并进结果,最后 _convert_nodes_to_scored_nodes 把节点与 similarities 配对成 NodeWithScore。这一设计的权衡是:向量库可以只存向量+id(节省存储、也支持不落文本的后端),把节点全文的权威副本放在 docstore,检索时按需回补,既省向量库空间又保证返回内容完整。术语
docstore(节点全文的权威存储); VectorStoreQueryResult(向量库查询结果,含 nodes/ids/similarities); ObjectType.TEXT(文本节点类型,无需回 docstore); nodes_dict(index_struct 中 id→node_id 的映射)📖 " If the vector store does not store text, we need to fetch every node from the docstore." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/indices/vector_store/retrievers/retriever.py
📖 " fetched_nodes: List[BaseNode] = self._docstore.get_nodes(
🧪 实例 只存向量不存文本的后端,检索链路会走"id → docstore 全量回补":
python
# _get_nodes_with_embeddings 内
nodes_to_fetch = self._determine_nodes_to_fetch(query_result)
if nodes_to_fetch:
fetched_nodes = self._docstore.get_nodes(node_ids=nodes_to_fetch, raise_error=False)
query_result.nodes = self._insert_fetched_nodes_into_query_result(query_result, fetched_nodes)🔍 追问 向量库既没返回
nodes 也没返回 ids 时会怎样? → _determine_nodes_to_fetch 返回 [],但后续 _insert_fetched_nodes_into_query_result 在 ids is None and nodes is None 时会 raise ValueError,提示结果至少要包含 nodes 或 ids 之一。📚 拓展阅读
- VectorIndexRetriever 源码 —
_determine_nodes_to_fetch/_insert_fetched_nodes_into_query_result实现 - base_retriever.py 源码 — 检索结果统一封装为 NodeWithScore 的上游契约
QBaseRetriever 的异步接口 _aretrieve 默认行为是什么?为什么它不是抽象方法?深挖·拓展低频
答 与被
@abstractmethod 强制要求实现的同步 _retrieve 不同,异步 _aretrieve 在父类里提供了一个默认实现:直接 return self._retrieve(query_bundle),即在未覆写异步版时退化为同步检索。源码里对应位置留有 # TODO: make this abstract 和被注释掉的 # @abstractmethod,说明设计者的意图是最终把它也变成抽象方法,但当前为了向后兼容仍保留降级实现——这样存量子类哪怕只写了 _retrieve,aretrieve 也能工作,不会因为缺少异步实现而报错。对外的 aretrieve 与同步 retrieve 结构对称:同样先 _check_callback_manager、发 RetrievalStartEvent、归一化 QueryBundle,在 callback 上下文里 await self._aretrieve(...) 后再 await self._ahandle_recursive_retrieval(...),最后发 RetrievalEndEvent。权衡在于:默认降级让迁移无痛,但如果子类只实现了同步 _retrieve,其 aretrieve 实际上是在事件循环里跑同步阻塞代码,拿不到真正的并发收益;像 VectorIndexRetriever 就显式覆写了 _aretrieve/_aget_nodes_with_embeddings(用 await self._vector_store.aquery)来获得真正的异步 I/O。术语
_aretrieve(异步检索钩子,父类给默认降级实现); aretrieve(对外异步入口,结构对称于 retrieve); _ahandle_recursive_retrieval(异步递归展开); @abstractmethod(同步版强制实现,异步版被注释保留 TODO)📖 " # TODO: make this abstract
📖 " return self._retrieve(query_bundle)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/base_retriever.py
🧪 实例 VectorIndexRetriever 覆写异步版以获得真正的非阻塞查询:
python
async def _aget_nodes_with_embeddings(self, query_bundle_with_embeddings):
query = self._build_vector_store_query(query_bundle_with_embeddings)
query_result = await self._vector_store.aquery(query, **self._kwargs)
...🔍 追问 一个子类只实现了
_retrieve,调用它的 aretrieve 有性能问题吗? → 有;此时 _aretrieve 走默认实现同步调用 _retrieve,会在事件循环中阻塞,无法真正并发,若追求异步吞吐应显式覆写 _aretrieve。📚 拓展阅读
- base_retriever.py 源码 —
aretrieve/_aretrieve默认降级实现与 TODO - VectorIndexRetriever 源码 — 真正覆写异步路径
_aretrieve/aquery的范例
第2章 查询与合成源码
🔥高频
查询引擎源码(RetrieverQueryEngine)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/query_engine/retriever_query_engine.pyQRetrieverQueryEngine 的 _query 一次查询到底经历了哪几步?深挖·拓展🔥高频
答
_query 是整个 RAG 查询的编排入口,它把"检索 + 后处理 + 合成"这三件事串成一条流水线,并整体包裹在一个 callback 事件里做可观测性追踪。具体机制是:先用 with self.callback_manager.event(CBEventType.QUERY, ...) 开一个 QUERY 事件并把 query_str 作为 payload 记录;然后调用 self.retrieve(query_bundle) 拿到 List[NodeWithScore](注意这里的 retrieve 已经内含 node_postprocessors 的后处理,不是裸检索);再把 query 和 nodes 一起交给 self._response_synthesizer.synthesize 生成最终 response;最后 query_event.on_end 把 response 写回事件收尾。这样设计的权衡在于:QueryEngine 本身只负责"编排 + 埋点",把"怎么检索"委托给 retriever、"怎么用 LLM 合成答案"委托给 synthesizer,职责单一、可替换性强;并且方法上标注了 @dispatcher.span,让每次查询能被 instrumentation 体系记录为一个 span,便于做 trace 和性能分析。flowchart LR Q[query_bundle] --> E[callback QUERY event] E --> R["self.retrieve()
检索+后处理"] R --> S["response_synthesizer.synthesize()
LLM 合成答案"] S --> O[query_event.on_end] O --> Resp[RESPONSE_TYPE]
术语
_query(同步查询编排入口); QueryBundle(封装 query 字符串及其嵌入的查询包); NodeWithScore(带相关度分数的检索节点); callback_manager.event(把一次查询包成可追踪事件); @dispatcher.span(instrumentation 埋点装饰器)📖 " with self.callback_manager.event(
🧪 实例 一次典型调用
engine.query("What is X?") 内部会展开为如下伪流程:python
nodes = self.retrieve(query_bundle) # 检索并跑完所有 node_postprocessors
response = self._response_synthesizer.synthesize(query=query_bundle, nodes=nodes)
# 整个过程被 CBEventType.QUERY 事件包裹,记录 query_str 与 response🔍 追问
_query 里调用的是 self.retrieve 还是 self._retriever.retrieve? → 是 self.retrieve,它在裸检索之后额外跑了 _apply_node_postprocessors,所以后处理不会被绕过。📚 拓展阅读
- retriever_query_engine.py 源码 — RetrieverQueryEngine 的完整实现与查询编排逻辑
Qretrieve 方法和 node_postprocessors 是什么关系?后处理在什么时机执行?深挖·拓展🔥高频
答
RetrieverQueryEngine.retrieve 并不是简单转发给底层 retriever,而是"裸检索 + 节点后处理"两步。它先 nodes = self._retriever.retrieve(query_bundle) 拿到原始节点,再 return self._apply_node_postprocessors(nodes, query_bundle=query_bundle)。而 _apply_node_postprocessors 内部是一个 for 循环,按 self._node_postprocessors 列表顺序依次调用每个 postprocessor 的 postprocess_nodes,把上一个的输出作为下一个的输入,形成链式管道。这个设计的关键权衡是:后处理(如 reranking、相似度截断 SimilarityPostprocessor、时间加权、去重等)被统一收拢在 QueryEngine 层而不是散落在 retriever 里,从而任何 retriever 都能复用同一套后处理链;顺序性也很重要——postprocessor 的排列顺序直接决定处理效果(比如先 rerank 再截断,和先截断再 rerank 结果不同)。异步路径有对称实现 _async_apply_node_postprocessors,改用 await node_postprocessor.apostprocess_nodes,保证 async 查询下后处理同样不被跳过。默认情况下若不传 postprocessor,self._node_postprocessors 会是空列表(node_postprocessors or []),链路等价于直通。术语
_apply_node_postprocessors(顺序执行后处理链); postprocess_nodes(单个后处理器的同步接口); apostprocess_nodes(异步接口); node_postprocessors or [](缺省为空表,即无后处理)📖 " def retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
📖 " for node_postprocessor in self._node_postprocessors:
🧪 实例 构造时挂两个后处理器,它们会按列表顺序对同一批 nodes 依次加工:
python
engine = RetrieverQueryEngine(
retriever=retriever,
node_postprocessors=[reranker, SimilarityPostprocessor(similarity_cutoff=0.7)],
)
# retrieve 内部: nodes = retriever.retrieve(...) -> reranker -> similarity_cutoff🔍 追问 构造时也会把 callback_manager 注入每个 postprocessor,为什么? →
__init__ 里有 for node_postprocessor in self._node_postprocessors: node_postprocessor.callback_manager = callback_manager,让后处理器共享同一 callback,埋点/追踪能贯穿全链路。📚 拓展阅读
- retriever_query_engine.py 源码 — retrieve 与 _apply_node_postprocessors 的实现
Qfrom_args 工厂方法解决了什么问题?response_mode 的默认值是什么?深挖·拓展🔥高频
答
RetrieverQueryEngine.__init__ 只接收已经构造好的 response_synthesizer 等高层对象,直接用它会很啰嗦。from_args 是一个 classmethod 工厂,把大量"合成器相关的散装参数"(各种 prompt 模板、response_mode、output_cls、use_async、streaming、verbose、multimodal 等)一次性接进来,内部调用 get_response_synthesizer(...) 组装出 synthesizer,再回调 cls(...) 完成构造。这是典型的"便利构造器"模式:让用户不必先手工 new 一个 synthesizer 就能配好查询引擎,同时把 LLM 缺省逻辑收敛为 llm = llm or Settings.llm、callback 缺省为 callback_manager or Settings.callback_manager,统一走全局 Settings。关键面试点是默认合成模式:签名里 response_mode: ResponseMode = ResponseMode.COMPACT,即默认用 COMPACT 模式——它会把尽量多的检索文本塞进单次 prompt 以减少 LLM 调用次数,是在"答案质量 / 上下文完整"与"调用成本 / 延迟"之间的默认折中。若显式传入了 response_synthesizer,则 response_synthesizer or get_response_synthesizer(...) 会直接复用它,跳过这些散装参数。术语
from_args(便利工厂 classmethod); get_response_synthesizer(根据参数组装合成器); ResponseMode.COMPACT(默认合成模式,压缩上下文减少调用); Settings.llm(全局默认 LLM 兜底)📖 " response_mode: ResponseMode = ResponseMode.COMPACT," — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/query_engine/retriever_query_engine.py
📖 " response_synthesizer = response_synthesizer or get_response_synthesizer(
🧪 实例 用
from_args 一行开启流式并覆盖合成模式:python
engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.REFINE, # 覆盖默认的 COMPACT
streaming=True,
verbose=True,
)🔍 追问
from_args 里 llm 没传时用什么? → llm = llm or Settings.llm,回退到全局 Settings 里配置的 LLM。📚 拓展阅读
- retriever_query_engine.py 源码 — from_args 的完整参数列表与默认值
Q同步 _query 和异步 _aquery 的实现有何异同?为什么两条路径都要保留?深挖·拓展中频
答
_query 与 _aquery 是完全对称的两条路径,骨架一模一样——都用 CBEventType.QUERY 事件包裹、都记录 query_str、都是"检索 → 合成 → on_end"三段式——区别只在于异步版把每个 I/O 环节换成了可 await 的对应方法:检索由 self.retrieve 变为 await self.aretrieve,合成由 self._response_synthesizer.synthesize 变为 await self._response_synthesizer.asynthesize。aretrieve 内部同样调用异步后处理 _async_apply_node_postprocessors。保留两条路径的原因是:检索(向量库/网络)和 LLM 合成都是重 I/O 操作,异步版本能在等待时释放事件循环,支撑高并发场景(如一个服务同时处理多个查询),而同步版本则用于脚本、notebook 等简单场景,避免强制引入 event loop 的复杂度。两者都带 @dispatcher.span,所以无论走哪条路都能被 instrumentation 追踪。术语
_aquery(异步查询编排入口); aretrieve(异步检索+异步后处理); asynthesize(异步合成); @dispatcher.span(同步/异步都埋点)📖 " nodes = await self.aretrieve(query_bundle)
📖 " async def aretrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
🧪 实例 高并发服务里应走异步入口以复用事件循环:
python
response = await engine.aquery("What is X?") # 触发 _aquery -> aretrieve -> asynthesize🔍 追问 异步路径下 node_postprocessors 走哪个方法? → 走
_async_apply_node_postprocessors,内部 await node_postprocessor.apostprocess_nodes,而非同步的 postprocess_nodes。📚 拓展阅读
- retriever_query_engine.py 源码 — _query / _aquery 与同步/异步后处理的对称实现
Qwith_retriever 和独立的 synthesize 方法体现了怎样的解耦设计?深挖·拓展低频
答 RetrieverQueryEngine 刻意把"检索器"和"合成器"做成可独立替换、可独立调用的两个部件。
with_retriever 返回一个新的 RetrieverQueryEngine,只替换 retriever,而 response_synthesizer、callback_manager、node_postprocessors 全部沿用当前实例——这是不可变式(immutable-style)的复用:想换检索源(比如从向量检索换成混合检索)时,不必重建整套合成配置,一行就能派生一个新引擎。另一方面,synthesize / asynthesize 把"合成"这一步单独暴露出来,允许调用方传入自己已经准备好的 nodes 以及 additional_source_nodes,直接调用底层 _response_synthesizer.synthesize 而跳过检索。这在需要"自定义检索/外部召回结果再交给同一个 LLM 合成"的场景很有用,把检索与合成彻底解耦,让引擎既能一站式 query,也能拆开当"纯合成器"用。此外类还暴露了 retriever 只读属性,直接返回内部 self._retriever,方便外部读取当前检索器。术语
with_retriever(派生只换检索器的新引擎); synthesize(可单独调用的合成步骤); additional_source_nodes(额外附加的来源节点); retriever property(只读暴露内部检索器)📖 " def with_retriever(self, retriever: BaseRetriever) -> "RetrieverQueryEngine":
📖 " def synthesize(
🧪 实例 复用同一套合成配置、只切换检索器:
python
new_engine = engine.with_retriever(hybrid_retriever) # synthesizer / postprocessors 全部沿用
# 或者拿外部节点直接合成,跳过检索:
resp = engine.synthesize(query_bundle, nodes=my_prefetched_nodes)🔍 追问
with_retriever 会保留 node_postprocessors 吗? → 会,新实例显式传入了 node_postprocessors=self._node_postprocessors,后处理链原样保留。📚 拓展阅读
- retriever_query_engine.py 源码 — with_retriever、synthesize 与 retriever 属性的实现
🔥高频
响应合成源码(Refine/TreeSummarize)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/compact_and_refine.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/factory.pyQRefine 响应合成器是如何跨多个文本块逐步「精炼」出答案的?深挖·拓展🔥高频
答
Refine 的核心是 _run_refine_loop:它把所有 chunk 用 prompt_helper.repack 拆装进一个 deque,然后串行遍历。第一块 chunk 时 response is None,走 text_qa_template(QA 模板,只填 context_str 与 query_str)生成初始答案;从第二块起,改用 refine_template,并把上一轮的答案作为 existing_answer 连同新 chunk 一起喂给 LLM,让模型在已有答案基础上决定是否用新上下文改写。因为 existing_answer 会随每轮更新而变长,重新格式化后的 prompt 可能装不下当前 chunk,所以每轮都会对已 partial_format 的模板再 repack 一次;如果一个 chunk 拆出多段(len(repacked) > 1),就把它们压回 deque 队首继续处理,保证不超上下文窗口。这种设计的权衡:它天然支持任意数量的 chunk 且每次只需装下「一份答案+一块上下文」,但代价是严格串行——有 N 个 chunk 就要 N 次 LLM 调用,无法并发,延迟随 chunk 数线性增长。flowchart LR
A[chunk1] -->|text_qa_template| B[answer_v1]
B -->|refine_template + existing_answer| C[answer_v2]
C -->|refine_template + existing_answer| D[answer_v3]
D --> E[最终答案]术语
refine loop(逐块精炼循环); text_qa_template(首块用的问答模板); refine_template(后续块用的精炼模板); existing_answer(上一轮累积的答案); repack(按上下文窗口重新打包文本块)📖 """Refine a response to a query across text chunks.""" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py📖 query_str=query_str, existing_answer=response — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py📖 # If chunk is too big to be packed into a single chunk, push new chunks into the front of the deque — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py🧪 实例 3 个检索到的文档块过 Refine,会发生 3 次串行 LLM 调用:
python
from llama_index.core import get_response_synthesizer
synth = get_response_synthesizer(response_mode="refine")
# chunk0 -> QA 得到初稿; chunk1、chunk2 分别带着 existing_answer 走 refine
answer = synth.get_response(query_str="LlamaIndex 是什么?", text_chunks=[c0, c1, c2])🔍 追问 为什么第一块用 QA 模板而后续块用 refine 模板? → 因为第一块没有
existing_answer 可供精炼(response is None),只能先做普通问答产出初稿;后续块的任务是「在已有答案上决定是否吸收新上下文」,所以需要 refine 模板携带 existing_answer。📚 拓展阅读
- compact_and_refine.py — Refine 的子类,通过压缩 chunk 减少循环次数
- factory.py —
ResponseMode.REFINE如何构造Refine实例
QCompactAndRefine 相比 Refine 做了什么优化?为什么它是默认模式?深挖·拓展🔥高频
答
CompactAndRefine 直接继承自 Refine,唯一的差别是在进入精炼循环之前先调用 _make_compact_text_chunks 把零散的 text_chunks「压实」。它取 QA 模板和 refine 模板中体积更大的那个(get_biggest_prompt)作为约束,再用 prompt_helper.repack 把尽可能多的原始小块合并进同一个上下文窗口大小的大块里,padding 用 _response_padding_size 预留答案空间。这样原本可能是十几个小 chunk,压实后只剩两三个大 chunk,而 Refine 的调用次数正比于 chunk 数,所以压实直接把 LLM 调用次数(和延迟、成本)大幅降下来,同时又不会超出上下文限制。这也是为什么 factory.py 里把 ResponseMode.COMPACT 设为默认——它在「尽量少调用」和「不超窗口」之间取得平衡,是纯 Refine 的严格更优版本。除文本外它还对称地实现了 _make_compact_message_chunks,对 chat message 走 _chat_prompt_helper 做同样的压实。术语
CompactAndRefine(压实后精炼); _make_compact_text_chunks(压实文本块); get_biggest_prompt(取更大的模板作约束); _response_padding_size(为答案预留的 padding); ResponseMode.COMPACT(默认响应模式)📖 """Refine responses across compact text chunks.""" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/compact_and_refine.py📖 # use prompt helper to fix compact text_chunks under the prompt limitation — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/compact_and_refine.py📖 response_mode: ResponseMode = ResponseMode.COMPACT, — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/factory.py🧪 实例 同样 3 个小块,若能压进 1 个窗口,
CompactAndRefine 可能只调用 1 次 LLM,而 Refine 调用 3 次:python
from llama_index.core import get_response_synthesizer
# 不传 response_mode 时,factory 默认走 ResponseMode.COMPACT => CompactAndRefine
synth = get_response_synthesizer() # 等价于 response_mode="compact"
answer = synth.get_response(query_str="...", text_chunks=[c0, c1, c2])🔍 追问 压实用的是 QA 模板还是 refine 模板的大小? → 用两者中更大的那个,
_make_compact_text_chunks 里对两个模板分别 partial_format(query_str=...) 后取 get_biggest_prompt([text_qa_template, refine_template]),以保证无论后续走哪个模板都不超窗口。📚 拓展阅读
- refine.py — 父类 Refine 的循环实现,压实后仍复用它
- factory.py — COMPACT 分支如何构造 CompactAndRefine
QTreeSummarize 的合成机制是什么?它和 Refine 的根本区别在哪?深挖·拓展🔥高频
答
TreeSummarize 采用自底向上的递归树式归并。每一层递归都做三件事:先用 summary_template(已 partial_format 填入 query_str)对 chunk 做 repack,让每个 chunk 尽量填满上下文窗口;若 repack 后只剩一个 chunk,就直接对它出最终答案(支持 stream/predict/structured_predict);否则对每个 chunk 各自生成一份摘要,再把这批摘要作为新的 text_chunks 递归调用 get_response,直到收敛成一块为止。它与 Refine 的根本区别在于依赖结构:Refine 是串行链式,每步都依赖上一步的 existing_answer,无法并行;而 TreeSummarize 同一层内各 chunk 的摘要彼此独立,可以并发(异步 aget_response 用 asyncio.gather,同步下若 use_async=True 用 run_async_tasks),因此对大量 chunk 延迟更低。权衡上,TreeSummarize 每层都要对所有 chunk 各调一次 LLM,总调用次数可能更多、也会多次总结「摘要的摘要」而丢失细节,更适合「汇总/概述」类查询;Refine 则更适合逐步累积、需要保留原文细节的精确问答。flowchart BT
c1[chunk1] --> s1[摘要1]
c2[chunk2] --> s2[摘要2]
c3[chunk3] --> s3[摘要3]
s1 --> R[递归汇总]
s2 --> R
s3 --> R
R --> F[最终响应]术语
TreeSummarize(树式归并总结); summary_template(总结模板); repack(填满上下文窗口的重打包); recursively summarize(对摘要再递归总结); asyncio.gather(并发生成各块摘要)📖 in a bottom-up fashion (i.e. building a tree from leaves to root). — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py📖 # give final response if there is only one chunk — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py📖 # recursively summarize the summaries — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py🧪 实例 6 个 chunk 若 repack 成 3 块,则第一层并发出 3 份摘要,第二层再把 3 份摘要归并成 1 份最终答案:
python
from llama_index.core import get_response_synthesizer
synth = get_response_synthesizer(response_mode="tree_summarize", use_async=True)
summary = synth.get_response(query_str="总结这些文档的要点", text_chunks=chunks)🔍 追问 递归的终止条件是什么? →
repack 后 len(text_chunks) == 1,即所有内容能装进一个上下文窗口时给出最终响应,否则继续对摘要递归。📚 拓展阅读
- refine.py — 对比串行链式的 Refine 机制
- factory.py —
ResponseMode.TREE_SUMMARIZE分支构造与use_async传递
QRefine 里的 structured_answer_filtering / query_satisfied 是干什么用的?深挖·拓展中频
答 这是 Refine 的「答案过滤」机制,用来跳过无关上下文。
StructuredRefineResponse 是一个 pydantic 模型,含 query_satisfied(布尔)和 answer 两个字段。当 structured_answer_filtering=True 时,_default_program_factory 会通过 get_program_for_llm 让 LLM 以结构化方式输出这两个字段;_update_response 读取 query_satisfied:若为 False,说明该 chunk 不足以回答查询,就返回 None、不更新 response,从而把无关块过滤掉。注意 query_satisfied 在模型里被特意放在第一个字段——因为 LLM 通常按字段声明顺序流式输出 JSON,把它放前面能在头十几个 token 就判断出该源是否相关,从而在流式场景下尽早放弃无关源。反之,当不开启过滤时用的是 DefaultRefineProgram,它「always returns the answer with query_satisfied=True」,等于不做任何过滤。此外 program_factory 只有在开启结构化过滤时才允许传入,否则直接抛错。权衡:开启过滤能提升答案质量、避免被噪声块带偏,但每块都要走结构化预测,成本更高、也依赖模型能可靠遵循 schema。术语
structured_answer_filtering(结构化答案过滤开关); StructuredRefineResponse(含 query_satisfied 与 answer 的结构化响应); query_satisfied(该上下文是否足以回答查询); DefaultRefineProgram(不过滤的默认 program); program_factory(自定义 program 工厂)📖 description="True if there was enough context given to provide an answer " — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py📖 query_satisfied=True. In effect, doesn't do any answer filtering. — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py📖 "Program factory not supported without structured answer filtering." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/refine.py🧪 实例 开启过滤后,与查询无关的 chunk 会被判
query_satisfied=False 而跳过:python
from llama_index.core import get_response_synthesizer
synth = get_response_synthesizer(
response_mode="refine",
structured_answer_filtering=True, # 无关块会被 query_satisfied=False 过滤
)🔍 追问 为什么
query_satisfied 要被放在 pydantic 模型的第一个字段? → 因为 LLM 几乎总是按字段声明顺序流式序列化 JSON,把它放最前面能在最初的十几个 token 内拿到判断,一旦为 False 就无需等待剩余响应,可立刻转向下一个源(源码注释:if it is False, we do not need to wait for the rest of the response, we can simply move on to the next source.)。📚 拓展阅读
- refine.py —
_update_response中对query_satisfied的判定逻辑 - factory.py —
structured_answer_filtering与program_factory如何透传给 Refine
Qget_response_synthesizer 工厂是怎么根据 response_mode 选出合成器的?深挖·拓展中频
答
get_response_synthesizer 是响应合成器的统一入口。它先把各类模板与 prompt_helper/chat_prompt_helper/llm/callback_manager 补齐默认值(未传时从 Settings 或 DEFAULT_*_PROMPT_SEL 取),然后用一长串 if/elif 按 response_mode 分派:REFINE 返回 Refine,COMPACT(默认)返回 CompactAndRefine,TREE_SUMMARIZE 返回 TreeSummarize,还有 SIMPLE_SUMMARIZE、GENERATION、ACCUMULATE、COMPACT_ACCUMULATE、NO_TEXT、CONTEXT_ONLY 等模式,遇到未知模式则抛 ValueError。值得注意的是不同模式接收的参数并不一样:只有 Refine 系(REFINE/COMPACT)接收 structured_answer_filtering 和 program_factory,而 use_async 只对 TreeSummarize/Accumulate 这类可并发的模式有意义。这个工厂把「模式选择」与「各合成器的构造细节」集中收敛到一处,调用方只需给一个字符串/枚举即可切换合成策略,是典型的工厂模式解耦。术语
get_response_synthesizer(合成器工厂函数); ResponseMode(响应模式枚举); DEFAULT_TEXT_QA_PROMPT_SEL(默认 QA 模板选择器); Settings(全局默认 llm/prompt_helper 来源); PromptHelper.from_llm_metadata(按 LLM 元数据构造 prompt helper)📖 response_mode: ResponseMode = ResponseMode.COMPACT, — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/factory.py📖 raise ValueError(f"Unknown mode: {response_mode}") — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/factory.py🧪 实例 通过字符串切换底层合成器实现:
python
from llama_index.core import get_response_synthesizer
r = get_response_synthesizer(response_mode="refine") # -> Refine
c = get_response_synthesizer() # -> CompactAndRefine(默认)
t = get_response_synthesizer(response_mode="tree_summarize") # -> TreeSummarize🔍 追问 不传
llm 和 prompt_helper 时它们从哪来? → llm 回退到 Settings.llm,prompt_helper 回退到 Settings._prompt_helper,再不行就用 PromptHelper.from_llm_metadata(llm.metadata) 按当前 LLM 的元数据现造一个。📚 拓展阅读
- refine.py — REFINE 模式对应的类
- tree_summarize.py — TREE_SUMMARIZE 模式对应的类
QTreeSummarize 的 use_async 参数如何影响同层多块摘要的执行?深挖·拓展中频
答 在 TreeSummarize 中,一旦某层
repack 后仍有多个 chunk,就需要对每个 chunk 各生成一份摘要,use_async 决定这批摘要是并发还是串行执行。异步入口 aget_response 恒定用 asyncio.gather(*str_tasks) 并发跑所有 chunk 的 apredict/astructured_predict。同步入口 get_response 则看 use_async:为 True 时把每个 chunk 的任务收集成 list 交给 run_async_tasks 一起跑,借事件循环实现并发;为 False 时退化成一个普通列表推导逐块串行调用 predict。因为同一层各 chunk 的摘要相互独立、无数据依赖,这种并发是安全的,能把该层延迟从「N 次调用之和」压到「约一次调用」;这正是 TreeSummarize 相对串行 Refine 的核心吞吐优势。若指定了 output_cls,并发得到的结构化对象会先 model_dump_json() 转成字符串,再作为下一层递归的 text_chunks。术语
use_async(同步入口下是否并发的开关); run_async_tasks(把协程任务集合一起跑的工具); asyncio.gather(异步入口的并发原语); output_cls(结构化输出类型); model_dump_json(结构化对象转 JSON 字符串)📖 # summarize each chunk — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py📖 summary_responses = run_async_tasks(tasks) — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py📖 summaries = await asyncio.gather(*str_tasks) — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/tree_summarize.py🧪 实例 同步调用下用
use_async=True 让同层摘要并发,显著降低多块场景延迟:python
from llama_index.core import get_response_synthesizer
synth = get_response_synthesizer(response_mode="tree_summarize", use_async=True)
# 内部对每层多个 chunk 的摘要走 run_async_tasks 并发,而非逐块串行
resp = synth.get_response(query_str="概述全部文档", text_chunks=many_chunks)🔍 追问 异步入口
aget_response 还需要看 use_async 吗? → 不需要。aget_response 本身就在协程里,直接用 asyncio.gather 并发,use_async 仅用于同步的 get_response 决定是否借 run_async_tasks 并发。📚 拓展阅读
- tree_summarize.py —
get_response/aget_response中的并发分支 - factory.py —
use_async如何被传入 TreeSummarize
第3章 分块·摄取·配置源码
中频
分块源码(SentenceSplitter)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/node_parser/text/sentence.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/node_parser/interface.pyQSentenceSplitter 的多级切分是按什么顺序进行的?为什么要设计成"递归降级"?深挖·拓展🔥高频
答 SentenceSplitter 的核心目标是"尽量保持句子和段落完整",所以它不是一刀切按 token 数硬切,而是先用最粗粒度的边界尝试切分,只有当某个 split 仍然超过 chunk_size 时才递归地用更细的分隔符继续切。
_split 里先做短路判断:如果整段文本的 token_size 已经不大于 chunk_size,就直接把它作为一个 is_sentence=True 的完整 split 返回。否则调用 _get_splits_by_fns 按优先级列表尝试:第一梯队 _split_fns 是段落分隔符(默认 "\n\n\n")和句子级 tokenizer(默认 nltk),只要能切出多于一个片段就返回并标记 is_sentence=True;若都切不动,则退到第二梯队 _sub_sentence_split_fns——次级正则(默认 CHUNKING_REGEX,即按逗号句号分号等标点切)、按 word 分隔符切、最后按字符切,这些切出来的片段 is_sentence=False。对每个切出的片段再判断 token_size,超限就对该片段递归 _split。这样设计的权衡在于:粗粒度优先保证语义完整、减少"半截句子"挂在 chunk 末尾;细粒度兜底保证任何超长文本最终都能被切到不超过 chunk_size,同时通过 is_sentence 标记把"是否是完整句子"的信息传给下游 _merge,让合并阶段可以优先在句子边界断开。flowchart TD
A[text] --> B{token_size <= chunk_size?}
B -- yes --> C[return 单个 _Split is_sentence=True]
B -- no --> D[_split_fns: 段落分隔符 / 句子 tokenizer]
D -- 切出 >1 片 --> E[is_sentence=True]
D -- 切不动 --> F[_sub_sentence_split_fns: 次级正则 / word / char]
F --> G[is_sentence=False]
E --> H{片段仍超 chunk_size?}
G --> H
H -- yes --> D
H -- no --> I[加入 text_splits]术语
_split(把文本递归拆成小于 chunk_size 的 split 列表); _get_splits_by_fns(按优先级尝试切分函数并返回是否句子级); is_sentence(标记该 split 是否为完整句子); _split_fns / _sub_sentence_split_fns(一级 / 二级切分函数列表)📖 "In general, this class tries to keep sentences and paragraphs together. Therefore
📖 "The order of splitting is:
🧪 实例
_get_splits_by_fns 的短路逻辑:一级函数只要有一个切出多片就立即返回 True,二级函数则是"能切动就 break"。python
def _get_splits_by_fns(self, text: str) -> Tuple[List[str], bool]:
for split_fn in self._split_fns:
splits = split_fn(text)
if len(splits) > 1:
return splits, True
for split_fn in self._sub_sentence_split_fns:
splits = split_fn(text)
if len(splits) > 1:
break
return splits, False🔍 追问 为什么
secondary_chunking_regex 为空时二级函数列表会少一个元素? → 构造函数里有分支:有 regex 时 _sub_sentence_split_fns 包含 split_by_regex、split_by_sep、split_by_char 三个;没有时只保留 split_by_sep(separator) 和 split_by_char(),即跳过标点正则这一层。📚 拓展阅读
- sentence.py 源码 — SentenceSplitter 的完整切分/合并实现
- node_parser interface.py — TextSplitter/MetadataAwareTextSplitter 基类定义
Q_merge 是如何把 splits 合并成 chunk 的?overlap 是在什么时机、以什么方式加入的?深挖·拓展🔥高频
答
_merge 用一个贪心的滑动累加过程把细碎 split 拼成尽量接近 chunk_size 的 chunk。它维护 cur_chunk(当前 chunk 的 (text, length) 列表)、cur_chunk_len(累计 token 数)和 new_chunk 标志。遍历每个 split 时,先做硬校验:若单个 split 的 token_size 已经大于 chunk_size,直接抛 "Single token exceeded chunk size"——这对应 _split 里那个无法再切分的不可分单元。正常情况下,如果把当前 split 加进去会超过 chunk_size 且当前不是新 chunk,就 close_chunk() 收尾;否则把 split 加入当前 chunk。关键的 overlap 逻辑在 close_chunk 内:收尾时把 cur_chunk 存为 last_chunk,然后从 last_chunk 的末尾往前回填,只要累计 overlap 长度不超过 self.chunk_overlap,就把这些尾部片段 insert(0, ...) 到新的 cur_chunk 开头,作为下一个 chunk 的前缀重叠。这样 overlap 是"从上一个 chunk 的尾部借文本"实现的,保证相邻 chunk 语义连续。合并时还有一个偏好:只有当 cur_split.is_sentence 为真、或加进去不超限、或是新 chunk 必须至少放一个 split 时才加入,否则宁可提前 close_chunk,以此优先在句子边界断开而不是硬塞。最后所有 chunk 会经 _postprocess_chunks 去掉纯空白 chunk 并 strip 首尾空白。术语
_merge(把 split 贪心合并为带 overlap 的 chunk); close_chunk(收尾当前 chunk 并从上一 chunk 尾部回填 overlap); chunk_overlap(相邻 chunk 之间重叠的 token 数,默认 200); new_chunk(标记当前 chunk 是否刚开始,保证至少放入一个 split)📖 "if cur_split.token_size > chunk_size:
📖 " # add overlap to the next chunk using the last one first
🧪 实例 合并阶段的偏好条件——非句子片段若会超限则宁可先收尾:
python
if (
cur_split.is_sentence
or cur_chunk_len + cur_split.token_size <= chunk_size
or new_chunk # new chunk, always add at least one split
):
# add split to chunk
cur_chunk_len += cur_split.token_size
cur_chunk.append((cur_split.text, cur_split.token_size))
split_idx += 1
new_chunk = False
else:
# close out chunk
close_chunk()🔍 追问 新开的 chunk 已经带了 overlap 前缀,但加入下一个 split 又会超 chunk_size 怎么办? → 源码里有专门分支:当
new_chunk 且加入 split 会超限时,从 cur_chunk 开头逐个 pop(0) 移除 overlap 片段直到 split 放得下,即"remove overlap to make room"。📚 拓展阅读
- sentence.py 源码 —
_merge与close_chunk的完整实现 - interface.py 源码 — split 结果如何经 build_nodes_from_splits 变成节点
Qsplit_text_metadata_aware 为什么要从 chunk_size 里扣掉 metadata 的长度?深挖·拓展中频
答 在 RAG 里,节点做 embedding 或喂给 LLM 时,metadata(如标题、来源)往往会被拼接进文本一起编码,所以实际留给正文的 token 预算要小于名义 chunk_size。
split_text_metadata_aware 先用 tokenizer 算出 metadata 字符串的 token 数 metadata_len,再计算 effective_chunk_size = self.chunk_size - metadata_len,然后用这个有效尺寸去切正文,保证"正文 + metadata"合起来不超过 chunk_size。这里有两道防护:若 effective_chunk_size <= 0(metadata 比整个 chunk_size 还长)直接抛 ValueError,提示要么加大 chunk_size 要么精简 metadata;若有效尺寸小于 50,虽不报错但会 print 警告,因为切出的 chunk 会小到不足 50 token、信息密度太低。谁来提供 metadata_str?在基类 MetadataAwareTextSplitter._parse_nodes 里,通过 _get_metadata_str 取节点的 EMBED 和 LLM 两种模式 metadata,取更长的那个用于切分,从而按最坏情况预留空间。这套设计的取舍是:牺牲一点正文容量,换取 embedding/检索阶段不会因 metadata 挤占而截断正文。术语
split_text_metadata_aware(在切分时预留 metadata token 的方法); effective_chunk_size(扣除 metadata 后正文可用的 token 数); _get_metadata_str(取 EMBED/LLM 两模式中更长的 metadata 串); MetadataMode(控制 metadata 以何种模式参与 embed/llm/none)📖 " metadata_len = len(self._tokenizer(metadata_str))
📖 " # use the longest metadata str for splitting
🧪 实例 有效尺寸过小时的警告(不报错,只提醒):
python
elif effective_chunk_size < 50:
print(
f"Metadata length ({metadata_len}) is close to chunk size "
f"({self.chunk_size}). Resulting chunks are less than 50 tokens. "
"Consider increasing the chunk size or decreasing the size of "
"your metadata to avoid this.",
flush=True,
)🔍 追问 不带 metadata 的普通
split_text 和它有何区别? → split_text 直接用 self.chunk_size 调 _split_text,不扣除 metadata;而 split_text_metadata_aware 用 effective_chunk_size 调同一个 _split_text,两者共用底层切分逻辑,只是预算不同。📚 拓展阅读
- interface.py 源码 — MetadataAwareTextSplitter 与
_get_metadata_str - sentence.py 源码 —
split_text_metadata_aware的完整校验逻辑
QSentenceSplitter 对 chunk_overlap 和 chunk_size 有哪些约束?默认值是多少?深挖·拓展中频
答 SentenceSplitter 在
__init__ 里对参数做前置校验:如果 chunk_overlap > chunk_size 直接抛 ValueError,因为重叠不可能大于 chunk 本身——若允许,overlap 回填逻辑会把上一个 chunk 几乎整段塞进下一个,失去分块意义。字段层面用 pydantic Field 约束:chunk_size 用 gt=0(必须大于 0),chunk_overlap 用 ge=0(允许为 0,即不重叠)。默认值方面,chunk_overlap 默认取模块常量 SENTENCE_CHUNK_OVERLAP = 200,chunk_size 取 DEFAULT_CHUNK_SIZE(从 constants 引入),separator 默认单空格 " ",paragraph_separator 默认 DEFAULT_PARAGRAPH_SEP = "\n\n\n",次级正则 secondary_chunking_regex 默认 CHUNKING_REGEX = "[^,.;。?!]+[,.;。?!]?|[,.;。?!]"——注意这个正则同时覆盖英文和中文标点(句号问号感叹号)。这些默认值加校验共同构成了"开箱即用又不会配出非法组合"的设计:overlap 提供上下文连续性,gt/ge 约束防止 0 或负值破坏切分,句子级正则兼顾中英文标点边界。术语
SENTENCE_CHUNK_OVERLAP(默认 chunk_overlap = 200); CHUNKING_REGEX(默认次级切分正则,含中英文标点); DEFAULT_PARAGRAPH_SEP(默认段落分隔符 "\n\n\n"); gt=0 / ge=0(pydantic 对 chunk_size/overlap 的取值约束)📖 " if chunk_overlap > chunk_size:
📖 "SENTENCE_CHUNK_OVERLAP = 200
🧪 实例 字段级约束(pydantic Field):
python
chunk_size: int = Field(
default=DEFAULT_CHUNK_SIZE,
description="The token chunk size for each chunk.",
gt=0,
)
chunk_overlap: int = Field(
default=SENTENCE_CHUNK_OVERLAP,
description="The token overlap of each chunk when splitting.",
ge=0,
)🔍 追问 若某个 split 大到无论如何都放不进一个 chunk,会发生什么? → 在
_merge 里会命中 if cur_split.token_size > chunk_size: raise ValueError("Single token exceeded chunk size");_split 对无法再切分的单一不可分单元(如小 chunk_size 下的多 token CJK/emoji 字符)会保留为超大 split,交由 _merge 抛出这个明确错误,而不是无限递归崩溃。📚 拓展阅读
- sentence.py 源码 — 常量定义与
__init__参数校验 - interface.py 源码 — NodeParser 的 include_metadata / include_prev_next_rel 等公共字段
QNodeParser 把切好的文本变成节点后,_postprocess_parsed_nodes 还做了哪些收尾工作?深挖·拓展低频
答
split_text 只产出字符串列表,真正把它们变成带关系、带定位、带 metadata 的 BaseNode 是在 NodeParser 基类里完成的。get_nodes_from_documents 先调 _parse_nodes(TextSplitter 的实现里对每个 node 调 split_text 再 build_nodes_from_splits),再调 _postprocess_parsed_nodes 做三件事:一是回填字符定位——用 parent_doc.text.find(node_content, search_start) 在原文里定位每个 chunk 的起止 char idx,并且按文档维护 doc_search_positions 记录搜索起点,关键是它记录的是 START 位置(start_char_idx + 1)而不是 END,这样重叠的 chunk 也能被正确定位而不会因为 overlap 导致 find 跳过。二是合并 metadata——若 include_metadata,把父文档/父节点的 metadata 合进节点,且以节点自身的值优先。三是建立前后关系——若 include_prev_next_rel,当相邻两个节点共享同一 source_node 时,互相设置 PREVIOUS / NEXT 关系。这套后处理让下游检索既能回溯到原文位置、又能沿 prev/next 做上下文扩展,是 chunk 之外的"节点图"能力来源。术语
_postprocess_parsed_nodes(切分后回填 char_idx、合并 metadata、建前后关系); doc_search_positions(按文档记录搜索起点,支持重叠 chunk 定位); NodeRelationship(PREVIOUS/NEXT/SOURCE 等节点关系); build_nodes_from_splits(把字符串 split 构造成 BaseNode)📖 " # Update search position to start from next character after this node's START
📖 " if self.include_prev_next_rel:
🧪 实例 用起点+1 支持重叠 chunk 的定位:
python
node_content = node.get_content(metadata_mode=MetadataMode.NONE)
start_char_idx = parent_doc.text.find(node_content, search_start)
# update start/end char idx
if start_char_idx >= 0 and isinstance(node, TextNode):
node.start_char_idx = start_char_idx
node.end_char_idx = start_char_idx + len(node_content)🔍 追问 为什么
_postprocess_parsed_nodes 假设 nodes 是文档顺序的? → 注释明确写了 "Nodes are assumed to be in document order from _parse_nodes",因为它按顺序推进每个文档的搜索位置,只有顺序正确才能保证 find 从上一个 chunk 之后开始、正确处理重复文本。📚 拓展阅读
- interface.py 源码 —
_postprocess_parsed_nodes与get_nodes_from_documents - sentence.py 源码 — SentenceSplitter 作为 MetadataAwareTextSplitter 子类的切分入口
中频
摄取管道源码(IngestionPipeline)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.pyQIngestionPipeline 的 run() 从输入到落库经历了哪几个阶段?深挖·拓展🔥高频
答
IngestionPipeline 是一条把原始数据变成可检索节点的流水线。run() 首先调用 _prepare_inputs 汇聚输入——它会把方法参数里的 documents/nodes、构造时挂在实例上的 self.documents,以及 self.readers 逐个 reader.read() 读出的节点全部拼进 input_nodes,所以既能一次性传数据也能靠 reader 惰性加载。接着进入去重决策:只有当 docstore 存在时才做 dedup;若同时挂了 vector_store 就走 _handle_upserts 或 _handle_duplicates,只挂 docstore 则退化为纯去重,两者都没有则原样放行。去重之后是真正的转换阶段——把 nodes_to_run 交给 run_transformations 顺序跑完 self.transformations(默认是 SentenceSplitter() 加 Settings.embed_model),或者在 num_workers > 1 时切批并行。转换产出节点后,只有带 embedding 的节点会被 self.vector_store.add(...);最后如果有 docstore 再 _update_docstore 写回哈希与文本。这种"输入汇聚→去重→转换→写向量库→写文档库"的固定次序,让缓存命中和增量更新可以在同一次调用里协同工作。术语
IngestionPipeline(摄取流水线,继承 BaseModel); _prepare_inputs(把 documents/nodes/readers 汇聚成待处理节点); transformations(按顺序应用的转换组件列表); run_transformations(逐个执行转换的核心函数)📖 "An ingestion pipeline that can be applied to data." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
📖 "If a vector store is provided, nodes with embeddings will be added to the vector store." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
🧪 实例
python
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=512, chunk_overlap=20),
OpenAIEmbedding(),
],
)
nodes = pipeline.run(documents=documents)🔍 追问 不显式传
transformations 会怎样? → 构造函数里 if transformations is None 时调用 _get_default_transformations(),返回 [SentenceSplitter(), Settings.embed_model]。📚 拓展阅读
- IngestionPipeline 源码 — 类定义、
__init__默认参数与run()主流程 - _prepare_inputs 实现 — documents/nodes/readers 的汇聚逻辑
Q摄取流水线的转换缓存是怎么工作的?为什么能跳过重复计算?深挖·拓展🔥高频
答 缓存的核心是"内容 + 转换配置"两者共同决定的哈希键。
get_transformation_hash 把当前一批节点用 MetadataMode.ALL 拼成 nodes_str,再把转换组件序列化后的字典字符串(先经 remove_unstable_values 去掉像内存地址那样每次都变的不稳定值)拼上,一起做 sha256 得到十六进制摘要。run_transformations 逐个 transform 执行时,如果传入了 cache,就先用这个 hash 去 cache.get(...):命中就直接把缓存里的节点当作本步结果,未命中才真正 transform(nodes) 并 cache.put(...) 写回。因为哈希同时绑定了输入内容和转换参数,所以只要文档或某一步的配置(如 chunk_size)有任何变化,哈希就变、缓存自然失效并重算;反之内容和配置都没变时就能整步跳过——这对昂贵的 embedding 步骤尤其省钱省时。之所以要 remove_unstable_values,是因为不去掉对象地址这类值,哈希每次都不同,缓存永远无法命中。disable_cache=True 时 run 会传 None 给 cache,直接走无缓存分支。术语
get_transformation_hash(内容+转换配置的 sha256 键); remove_unstable_values(剔除内存地址等不稳定串); IngestionCache(键值缓存,可 persist/load); cache_collection(缓存分区命名空间)📖 "return sha256((nodes_str + transform_string).encode("utf-8")).hexdigest()" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
📖 " cached_nodes = cache.get(hash, collection=cache_collection)
🧪 实例
python
# 第一次 run 会计算并缓存每一步;不改文档与配置再次 run,transformation 直接命中缓存
pipeline.run(documents=documents) # 计算 + 写缓存
pipeline.run(documents=documents) # 命中缓存,跳过重复转换
pipeline.persist("./pipeline_storage") # 把 cache(与 docstore)落盘🔍 追问 不稳定值不去掉会有什么后果? → 转换字典里含
<function test_fn at 0x7fb9f37a8900> 这类地址,每次序列化都不同,哈希永不相等,缓存彻底失效。📚 拓展阅读
- run_transformations 与缓存分支 — 命中/未命中两条路径
- get_transformation_hash / remove_unstable_values — 哈希键的构成与净化
QDocstoreStrategy 的三种去重策略分别怎么判重、有什么区别?深挖·拓展🔥高频
答 去重全靠比较存在 docstore 里的哈希或 id,所以必须挂一个能跨多次运行持久化的 document store。
DUPLICATES_ONLY 最简单:_handle_duplicates 拉出所有已存在哈希,遍历节点,只有 node.hash 既不在已存在哈希也不在本批已见哈希里才写入并加入待运行列表——它只拦重复,不处理内容更新。UPSERTS 走 _handle_upserts:按 ref_doc_id(没有就用 id_)查已存哈希,不存在则新增;存在但哈希变了说明文档被改,先 delete_ref_doc 并从 vector store 删掉旧节点再重跑;哈希一致就跳过——这实现了"新增+更新"的 upsert 语义。UPSERTS_AND_DELETE 在 upsert 基础上额外算出"docstore 里有、但本批输入里已没有"的文档 id,把它们从 docstore 和 vector store 一并删除,从而让索引与最新输入完全对齐。三者的权衡是:DUPLICATES_ONLY 开销最小但会漏掉更新;UPSERTS 能反映修改但保留历史删除文档;UPSERTS_AND_DELETE 语义最强、索引最干净,代价是每次都要全量对账、且必须信任本批输入是完整集合。术语
DocstoreStrategy(去重策略枚举); _handle_upserts(按 ref_doc_id 判新增/更新/跳过); _handle_duplicates(仅按 hash 拦重复); ref_doc_id(节点所属源文档 id)📖 "Document de-duplication de-deduplication strategies work by comparing the hashes or ids stored in the document store." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
📖 " else:
🧪 实例
python
from llama_index.core.ingestion import IngestionPipeline, DocstoreStrategy
from llama_index.core.storage.docstore import SimpleDocumentStore
pipeline = IngestionPipeline(
transformations=[...],
docstore=SimpleDocumentStore(),
vector_store=vector_store,
docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE, # 输入里删掉的文档也会被清出索引
)🔍 追问
UPSERTS 判定"文档改了"依据什么? → existing_hash and existing_hash != node.hash,即 docstore 中该 ref_doc_id 已有哈希但与当前节点哈希不同,则删旧重跑。📚 拓展阅读
- DocstoreStrategy 枚举文档 — 三种策略的属性说明
- _handle_upserts / _handle_duplicates — 同步与异步两套去重实现
Q只设置了 docstore 却没设 vector_store,upsert 策略还生效吗?深挖·拓展中频
答 不生效,会被降级。
run/arun 一开始把 effective_strategy = self.docstore_strategy,随后检查:如果 docstore 存在、vector_store 为 None、而策略又是 UPSERTS 或 UPSERTS_AND_DELETE,就发一条 UserWarning 并把 effective_strategy 改成 DUPLICATES_ONLY。原因是 upsert/delete 语义需要把旧节点从向量库里删掉才能真正替换,没有 vector store 就无法执行这一半操作,硬跑会导致索引不一致。值得注意的是它只改本次运行用的 effective_strategy,实例上的 pipeline.docstore_strategy 保持不变(warning 里明确写了 unchanged),所以后续补上 vector store 再跑,upsert 语义会自动恢复。这种"运行期降级而非改配置"的设计避免了副作用式地悄悄篡改用户设定,同时用 warning 让使用者知道本次退化的原因。术语
effective_strategy(本次运行实际生效的策略,可能被降级); UserWarning(缺 vector store 时的降级告警); duplicates_only(降级后的兜底策略)📖 "to apply upsert/delete semantics; falling back to 'duplicates_only' for this run. " — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
📖 "pipeline.docstore_strategy is unchanged." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
🧪 实例
python
# 只挂 docstore、没挂 vector_store,且 strategy=UPSERTS
pipeline = IngestionPipeline(
transformations=[...],
docstore=SimpleDocumentStore(), # 无 vector_store
docstore_strategy=DocstoreStrategy.UPSERTS,
)
pipeline.run(documents=docs) # 触发 UserWarning,本次实际按 duplicates_only 执行🔍 追问 降级后写回 docstore 用哪个策略? →
_update_docstore 收到的是 effective_strategy(此处为 DUPLICATES_ONLY),因此走仅 add_documents 的分支,不写哈希映射。📚 拓展阅读
- run() 中的策略校验与降级 — effective_strategy 的计算与 warning
- _update_docstore 分支 — 不同策略下写回 docstore 的差异
Qnum_workers 是如何实现并行摄取的?缓存在多进程下怎么合并?深挖·拓展中频
答 当
num_workers 大于 1 时,run 先用 multiprocessing.cpu_count() 兜底——若指定值超过 CPU 数就发 warning 并压回到 CPU 上限,避免超额进程反而拖慢。随后用 multiprocessing.get_context("spawn").Pool(num_workers) 建进程池,_node_batcher 把待处理节点按 worker 数切成若干批,p.starmap 把每批连同 transformations、in_place、cache、cache_collection 分发给 _run_transformations_worker 并行跑(arun 版本用 ProcessPoolExecutor + loop.run_in_executor)。关键难点在缓存:子进程各自持有 cache 副本,写入不会自动回到父进程,所以每个 worker 返回 (nodes, cache_entries),父进程收集后把 cache_entries 逐条 put 回自己的 cache。而且只有内存后端(MutableMappingKVStore)才需要这样合并——外部后端是直写共享存储的,天然共享无需回填。num_workers=None 或不大于 1 时则退回单进程 run_transformations 顺序执行。这样设计在 CPU 密集的转换(如本地 embedding、复杂解析)上能吃满多核,又不牺牲缓存的正确性。术语
num_workers(并行进程数,None 为顺序执行); _node_batcher(按批切分节点的静态方法); _run_transformations_worker(返回节点与缓存条目的工作进程); MutableMappingKVStore(需回填合并的内存缓存后端)📖 "num_workers (Optional[int], optional): The number of parallel processes to use." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
📖 "External backends write through to shared storage and need no merge." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/ingestion/pipeline.py
🧪 实例
python
# 用 4 个进程并行跑转换;超过 CPU 数会被自动压回上限
nodes = pipeline.run(documents=documents, num_workers=4)🔍 追问 为什么内存后端才需要合并缓存? → 内存
MutableMappingKVStore 的写入只存在子进程副本中,父进程看不到,故需回填;外部后端(如 Redis/DB)子进程直接写共享存储,父进程已可见,无需 merge。📚 拓展阅读
- run() 的多进程分支 — Pool.starmap 与缓存回填
- _run_transformations_worker 说明 — 返回 (nodes, cache_entries) 的原因
中频
嵌入与全局配置源码(Settings)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.pyQLlamaIndex 的全局 Settings 是怎么设计的?为什么用"懒初始化 + 单例"而不是构造时就把 LLM、embedding 都建好?深挖·拓展🔥高频
答
Settings 本质是一个用 @dataclass 定义的类 _Settings,内部所有字段(_llm、_embed_model、_callback_manager、_tokenizer、_node_parser、_prompt_helper 等)都以 Optional[...] = None 作为下划线私有属性存在,文件末尾通过 Settings = _Settings() 实例化成进程级单例。它对外暴露的是一组 property/setter:读取 Settings.embed_model、Settings.llm、Settings.node_parser 时,如果对应的私有字段仍是 None,才会在 getter 里"就地"构造默认对象(如 node_parser 默认 SentenceSplitter(),llm/embed_model 走 resolve_*("default"))。这种"懒初始化"的关键权衡在于:很多用户只用到其中一部分组件——只做检索的人未必配了 LLM,只跑本地 embedding 的人未必需要 tokenizer;若在导入时就急切构造全部依赖(比如去下载模型、连 API),会带来无谓的启动开销甚至因缺少密钥直接报错。用单例是为了让分散在各处的 index、retriever、query engine 在不显式传参时共享同一份默认配置,写一次 Settings.embed_model = ... 全局生效;代价是它是可变全局状态,测试与并发场景下需要注意隔离。术语
_Settings(承载全局配置的 dataclass); lazy initialization(懒初始化,getter 里按需构造); Settings = _Settings()(模块级单例实例); resolve_embed_model(把字符串/对象解析成具体 embedding 模型)📖 "Settings for the Llama Index, lazily initialized." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
📖 "Settings = _Settings()" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
📖 "self._node_parser = SentenceSplitter()" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
🧪 实例 首次读取属性时才构造默认对象:
python
from llama_index.core import Settings
# 此刻 _embed_model 仍为 None,尚未构造任何模型
Settings.embed_model = my_embedding # 触发 setter -> resolve_embed_model
size = Settings.chunk_size # 走 node_parser 默认 SentenceSplitter🔍 追问
Settings.chunk_size 是直接存在 _Settings 上的字段吗? → 不是,它是 node_parser 的代理属性:getter/setter 通过 hasattr(self.node_parser, "chunk_size") 判断,再读写底层 node parser 的 chunk_size,若当前 parser 没有该属性就抛 ValueError。📚 拓展阅读
- settings.py 源码 —
_Settings全部 property/setter 定义 - base embeddings 源码 — 被
Settings.embed_model解析出的BaseEmbedding契约
Q读取 Settings.embed_model / Settings.llm 时具体发生了什么?callback_manager 是怎么被"注入"进去的?深挖·拓展🔥高频
答 以
embed_model 这个 property 为例,它做两件事:第一,若 self._embed_model is None 就调用 resolve_embed_model("default") 构造并缓存默认 embedding 模型(llm 同理走 resolve_llm("default"));第二,每次取值前都会检查 if self._callback_manager is not None:,若用户设置过全局 callback manager,就把它赋给模型的 callback_manager 字段。这意味着 callback 是"每次读取时重新对齐"而非一次性绑定——你先设 embed_model、后设 callback_manager,再取 Settings.embed_model 时依然能拿到最新的回调管理器,保证可观测性(instrumentation、事件回调)在全局层面一致。setter 侧则用 resolve_* 把用户传入的字符串或对象归一化成具体实例:这层"解析"让用户既能传 "local" 这样的简写,也能直接传一个构造好的模型对象,降低了配置心智。权衡是这种"getter 里带副作用"的写法虽方便,但读属性并非纯只读,理解时需要留意。术语
resolve_embed_model("default")(默认 embedding 解析入口); resolve_llm("default")(默认 LLM 解析入口); callback_manager 注入(每次 getter 重新对齐全局回调); EmbedType/LLMType(setter 接受的宽松输入类型)📖 "self._embed_model = resolve_embed_model("default")" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
📖 "self._embed_model.callback_manager = self._callback_manager" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
📖 "self._llm = resolve_llm("default")" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/settings.py
🧪 实例 全局回调后设也能生效的顺序:
python
Settings.embed_model = "local" # setter: resolve_embed_model("local")
Settings.callback_manager = my_manager # 之后设置
emb = Settings.embed_model # getter 再次把 my_manager 注入 emb🔍 追问 为什么 callback 注入放在 getter 里而不是 setter 里? → 因为
embed_model 和 callback_manager 的设置顺序不确定,放在 getter 里"取用时对齐"能保证无论先设哪个,最终读到的模型都带着当前全局 callback manager。📚 拓展阅读
- settings.py 源码 —
embed_model/llm/callback_managerproperty 实现 - base embeddings 源码 —
callback_manager字段与事件如何被使用
QBaseEmbedding 的 get_text_embedding_batch 是怎么做批处理的?embed_batch_size 起什么作用?深挖·拓展🔥高频
答
get_text_embedding_batch 逐条遍历输入文本,把它们累积进 cur_batch;触发"flush"的条件是 if idx == len(texts) - 1 or len(cur_batch) == self.embed_batch_size:——也就是要么到了最后一条,要么当前批已攒满 embed_batch_size 条。每次 flush 才真正调用底层 _get_text_embeddings 生成一批向量,并成对发出 instrumentation 的 EmbeddingStartEvent/EmbeddingEndEvent 和 callback 事件,然后清空 cur_batch 继续攒下一批。embed_batch_size 是一个带约束的字段(gt=0, le=2048,默认 DEFAULT_EMBED_BATCH_SIZE),它决定"多少条文本合成一次调用":调大能减少 API/前向传播的往返次数、提升吞吐,但单次请求更大、更容易触发服务端 token/尺寸上限,也让失败重试的粒度变粗;调小则更稳、内存占用更低但调用次数增加。异步版本 aget_text_embedding_batch 逻辑类似,但把每批包成协程,再根据 num_workers 决定用 run_jobs 并发还是顺序 asyncio.gather,从而在并发维度进一步压缩总耗时。术语
embed_batch_size(单批文本条数,默认 DEFAULT_EMBED_BATCH_SIZE,gt=0, le=2048); cur_batch(累积中的当前批); _get_text_embeddings(子类实现的批量底层调用); num_workers(异步批处理并发度)📖 "Get a list of text embeddings, with batching." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "if idx == len(texts) - 1 or len(cur_batch) == self.embed_batch_size:" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "The batch size for embedding calls." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
🧪 实例 设
embed_batch_size=2、输入 5 条文本时的分批:text
文本: [t0, t1, t2, t3, t4]
批1(满2条 flush): [t0, t1]
批2(满2条 flush): [t2, t3]
批3(到末尾 flush): [t4]🔍 追问 异步批处理里
num_workers 怎么影响执行? → aget_text_embedding_batch 先把各批攒成协程列表;当 num_workers and num_workers > 1 时用 run_jobs 以 workers=self.num_workers 并发跑,否则退化为 asyncio.gather 顺序 await。📚 拓展阅读
- base embeddings 源码 —
get_text_embedding_batch/aget_text_embedding_batch完整实现 - settings.py 源码 — 批处理所用 embedding 由
Settings.embed_model提供
QBaseEmbedding 内置的 embeddings_cache 和 rate_limiter 各解决什么问题?它们在取向量的路径上如何配合?深挖·拓展中频
答 这两个都是
BaseEmbedding 上可选的字段,针对"embedding 调用又贵又受限"这一现实。embeddings_cache 若为 None 则不缓存;一旦提供(经 model_validator 校验必须是 BaseKVStore),get_query_embedding/get_text_embedding 等会先按文本 key 去 collection="embeddings" 里查,命中就直接返回缓存向量、跳过模型调用,未命中才计算并 put 回缓存——这对重复文本(如反复检索同一批文档)能显著省钱省时。批量路径 _get_text_embeddings_cached 更细致:它保留一个 (index, text) 元组列表 non_cached_texts,只对未命中的文本发起底层批量计算,再按原始下标写回,以确保返回顺序与输入严格一致。rate_limiter 则解决"打爆服务端配额"的问题:在真正发起(未命中缓存的)计算前调用 self.rate_limiter.acquire()(异步版 async_acquire())来节流。二者配合的关键顺序是——命中缓存时压根不碰 rate limiter,只有在需要真正调用模型时才申请令牌,从而把限流开销只花在真实请求上。术语
embeddings_cache(可选 KV 缓存,None 即不缓存); BaseKVStore(缓存必须满足的类型,由校验器强制); rate_limiter(节流器,调用前 acquire); non_cached_texts(未命中文本及其原始下标)📖 "Cache for the embeddings: if None, the embeddings are not cached" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "Rate limiter instance to throttle API calls." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "Tuples of (index, text) to be able to keep same order of embeddings" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
🧪 实例 缓存命中/未命中的分流(伪代码,取自同步文本路径):
python
cached_emb = self.embeddings_cache.get(key=text, collection="embeddings")
if cached_emb is not None:
text_embedding = cached_emb[next(iter(cached_emb.keys()))] # 命中,跳过模型
else:
if self.rate_limiter is not None:
self.rate_limiter.acquire() # 未命中才限流
text_embedding = self._get_text_embedding(text)🔍 追问 如果传给
embeddings_cache 的不是 BaseKVStore 会怎样? → check_base_embeddings_class 这个 @model_validator(mode="after") 会在校验时判断 not isinstance(self.embeddings_cache, BaseKVStore) 并抛出 TypeError("embeddings_cache must be of type BaseKVStore")。📚 拓展阅读
- base embeddings 源码 —
_get_text_embeddings_cached与 rate limiter 调用点 - settings.py 源码 — 通过
Settings.embed_model统一挂载带缓存/限流的模型
Qsimilarity() 支持哪几种相似度模式?为什么欧氏距离要取负号?to_payload 又是干什么用的?深挖·拓展低频
答
SimilarityMode 是个字符串枚举,给出三种度量:默认 DEFAULT = "cosine"(余弦)、DOT_PRODUCT(点积)、EUCLIDEAN(欧氏距离)。模块级函数 similarity() 按模式分派:余弦是点积除以两向量范数之积;点积直接 np.dot;欧氏则返回 -‖e1-e2‖。之所以给欧氏距离取负号,是因为"距离越小越相似"和"相似度越大越靠前"排序方向相反,取负后就能与余弦/点积统一成"越大越相似",让上层排序逻辑无需为不同模式区别对待。另一个易被问到的点是 to_payload:它返回模型的"非敏感"表示(class_name、model_name、embed_batch_size),专供 instrumentation 事件与 callback payload 使用,docstring 明确要求它绝不能包含 api_key 之类凭据或鉴权头——这是把"可观测性"和"密钥安全"分离的设计:既能在事件流里看到用哪个模型、批大小多少,又不会把敏感信息泄进日志/追踪系统;子类可覆写以添加安全的额外字段。术语
SimilarityMode(相似度模式枚举:cosine/dot_product/euclidean); -euclidean(欧氏距离取负以对齐排序方向); to_payload(可观测性用的非敏感模型表示); EmbeddingStartEvent/EmbeddingEndEvent(承载 payload 的 instrumentation 事件)📖 "DEFAULT = "cosine"" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "Using -euclidean distance as similarity to achieve same ranking order" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
📖 "Non-sensitive representation of this embedding model for observability." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/base/embeddings/base.py
🧪 实例 三种模式的核心计算:
python
# EUCLIDEAN: 取负距离,保证"越大越相似"
return -float(np.linalg.norm(np.array(embedding1) - np.array(embedding2)))
# DOT_PRODUCT
return np.dot(embedding1, embedding2)
# DEFAULT (cosine)
product = np.dot(embedding1, embedding2)
norm = np.linalg.norm(embedding1) * np.linalg.norm(embedding2)
return product / norm🔍 追问
to_payload 为什么要强调"non-sensitive"? → 因为它会随 instrumentation 事件和 callback payload 一起被发出,若混入 api_key/鉴权头就会泄露到观测/日志链路,所以只放 class_name、model_name、embed_batch_size 这类安全字段。📚 拓展阅读
- base embeddings 源码 —
SimilarityMode、similarity()、to_payload定义 - settings.py 源码 — 全局
embed_model与其 callback/可观测性配置的挂载点
第4章 Agent/Workflow 源码
🔥高频
Agent/Workflow 源码(FunctionAgent)
source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/base_agent.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.py source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/workflow_events.pyQFunctionAgent.take_step 是如何完成"一步"推理的?scratchpad 起什么作用?深挖·拓展🔥高频
答
take_step 是 FunctionAgent 的核心单步逻辑。它先校验 self.llm.metadata.is_function_calling_model,不是 function calling 模型就直接 raise ValueError("LLM must be a FunctionCallingLLM")——因为整个实现依赖 LLM 的原生 tool-calling 能力。接着它从 Context store 里按 scratchpad_key(默认 "scratchpad")取出本轮尚未写入长期 memory 的中间消息,拼成 current_llm_input = [*llm_input, *scratchpad]:llm_input 是外层从 memory 取的历史,scratchpad 则是本次 .run() 循环内累积的 assistant/tool 消息。这样设计的权衡是:每一步的 LLM 调用都能看到"历史 + 本轮已发生的工具往返",但这些临时消息不会污染 memory,直到 finalize 才批量落盘。发出 AgentInput 事件后,根据 self.streaming 走流式或非流式取 response,再用 get_tool_calls_from_response(..., error_on_no_tool_call=False) 解析工具调用(没有工具调用也不报错,表示要收尾)。最后把这条 assistant 消息 append 进 scratchpad 并写回 store,返回 AgentOutput(带 response、tool_calls、raw、current_agent_name)。术语
take_step(单步推理入口,由外层 workflow 的 run_agent_step 调用); scratchpad(存于 ctx.store 的本轮临时消息列表,循环结束才写入 memory); is_function_calling_model(LLM metadata 标志,决定能否用 FunctionAgent); AgentOutput(封装本步 LLM 输出与解析出的 tool_calls 的事件)📖 "Take a single step with the function calling agent." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "current_llm_input = [*llm_input, *scratchpad]" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "raise ValueError(\"LLM must be a FunctionCallingLLM\")" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
🧪 实例 take_step 内部数据流:
python
scratchpad: List[ChatMessage] = await ctx.store.get(
self.scratchpad_key, default=[]
)
current_llm_input = [*llm_input, *scratchpad]
# ... 取 response ...
scratchpad.append(last_chat_response.message)
await ctx.store.set(self.scratchpad_key, scratchpad)🔍 追问 为什么中间消息要放 scratchpad 而不是直接写 memory? → 因为一次
.run() 可能循环多步(多轮工具调用),中途写 memory 会让历史被半成品消息污染;scratchpad 让本轮消息集中暂存,finalize 时再一次性 aput_messages 落盘。📚 拓展阅读
- base_agent.py — run_agent_step — 外层 workflow 的
run_agent_stepstep 调用self.take_step。 - function_agent.py — finalize — scratchpad 最终如何写回 memory。
Qhandle_tool_call_results 如何处理工具结果?return_direct 和 handoff 有什么特殊逻辑?深挖·拓展🔥高频
答 工具执行完后,外层
aggregate_tool_results 会调用 agent 的 handle_tool_call_results,把结果并回 scratchpad。FunctionAgent 的实现:先取出 scratchpad,对每个 ToolCallResult 追加一条 role="tool" 的 ChatMessage,用 tool_call_result.tool_output.blocks 作为内容、并在 additional_kwargs 里塞入 {"tool_call_id": tool_call_result.tool_id}——tool_call_id 是让 LLM 把结果对应回它发起的那次 tool call 的关键。关键分支在于 return_direct:如果某个工具标记了 return_direct 且 tool_name != "handoff",就再补一条 role="assistant" 消息(内容为工具输出的 content),然后 break 提前结束循环——表示这个工具的输出直接作为最终答案,不再让 LLM 二次组织语言。这里刻意排除了 "handoff":handoff 工具本身也是 return_direct 的,但它不是"最终答案",而是要把控制权转交给另一个 agent,所以不能 break 收尾。最后把更新后的 scratchpad 写回 store。术语
handle_tool_call_results(把工具结果转成 ChatMessage 并入 scratchpad); tool_call_id(通过 additional_kwargs 关联工具结果与发起的 tool call); return_direct(工具输出直接作为答案返回,不再交 LLM 复述); handoff(保留工具名,return_direct 但表示转交而非收尾)📖 "Handle tool call results for function calling agent." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "only add to scratchpad if we didn't select the handoff tool" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
🧪 实例 return_direct 且非 handoff 时提前收尾:
python
if (
tool_call_result.return_direct
and tool_call_result.tool_name != "handoff"
):
scratchpad.append(
ChatMessage(
role="assistant",
content=str(tool_call_result.tool_output.content),
additional_kwargs={"tool_call_id": tool_call_result.tool_id},
)
)
break🔍 追问 为什么 handoff 工具是 return_direct 却不能 break? → 在 multi_agent_workflow 里 handoff 工具用
return_direct=True 创建,但它的语义是转交给 next_agent;若 break 当成最终答案会中断整个多 agent 流程,因此 handle_tool_call_results 和外层 aggregate_tool_results 都用 tool_name != "handoff" 把它排除在"停止"之外。📚 拓展阅读
- multi_agent_workflow.py — handoff 工具 — handoff 工具以
return_direct=True创建。 - base_agent.py — aggregate_tool_results — return_direct 且非 handoff 时才
StopEvent。
QFunctionAgent.finalize 做了什么?scratchpad 是怎样落回 memory 的?深挖·拓展🔥高频
答
finalize 是一次 agent 执行的收尾钩子,当外层判定没有更多工具调用(或 return_direct 收尾)时被调用。FunctionAgent 的实现非常简洁:从 ctx.store 取出整轮累积的 scratchpad,调用 memory.aput_messages(scratchpad) 把所有"in-progress"消息一次性写入长期 memory,然后 await ctx.store.set(self.scratchpad_key, []) 把 scratchpad 清空,最后原样返回传入的 output。这种"暂存-批量落盘"的设计权衡在于:整个多步循环期间 memory 保持干净(只含真正的对话历史),避免中间的 tool 消息和半成品 assistant 消息被后续步骤重复读取或污染;直到确认收尾才落盘,保证 memory 里是一段连贯完整的记录。注意外层 parse_agent_output 里有一句注释强调 messages 必须在 finalize 之后再从 memory 取,否则拿不到 agent 本轮的回复——正因为回复此刻才刚被 finalize 写进 memory。术语
finalize(收尾钩子,把 scratchpad 落盘并清空); aput_messages(BaseMemory 的批量异步写入接口); in-progress messages(本轮循环中暂存于 scratchpad 的临时消息); output(AgentOutput,finalize 原样透传给调用方)📖 "Adds all in-progress messages to memory." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "await memory.aput_messages(scratchpad)" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
🧪 实例 finalize 的落盘与清空:
python
scratchpad: List[ChatMessage] = await ctx.store.get(
self.scratchpad_key, default=[]
)
await memory.aput_messages(scratchpad)
# reset scratchpad
await ctx.store.set(self.scratchpad_key, [])
return output🔍 追问 外层为什么强调"messages 要在 finalize 之后再取"? → base_agent 的
parse_agent_output 注释指出,只有 finalize 把 scratchpad 写进 memory 后,memory.aget() 才包含 agent 本轮的响应,因此 structured output 等后续逻辑必须排在 finalize 之后。📚 拓展阅读
- base_agent.py — parse_agent_output — 无工具调用时调用 finalize 并在其后取 messages。
- function_agent.py — take_step — scratchpad 在此累积,finalize 时统一落盘。
Qtake_step 中 initial_tool_choice、allow_parallel_tool_calls、streaming 分别控制什么?深挖·拓展中频
答 这三者是 FunctionAgent 调 LLM 时的可调旋钮。
allow_parallel_tool_calls(默认 True)决定是否允许一次响应里并行返回多个工具调用——True 时并行、False 时串行,它作为 chat_kwargs 直接透传给 achat_with_tools / astream_chat_with_tools。initial_tool_choice(默认 None)用于"强制第一轮调某个工具":只有当它非 None 且 current_llm_input[-1].role == "user" 时才把 tool_choice 加进 chat_kwargs——用最后一条消息是否为 user 来近似判断"这是不是第一轮迭代",避免在后续轮次仍强制工具调用而卡死循环。streaming(定义在 BaseWorkflowAgent,默认 True)决定走 _get_streaming_response 还是 _get_response:流式版本在 async for 循环里对每个增量 write_event_to_stream 一个 AgentStream 事件(含 delta、response、tool_calls、thinking_delta),适合长任务实时展示,但不是每个 LLM 都支持流式;非流式则一次性 achat_with_tools 拿完整结果。术语
allow_parallel_tool_calls(是否并行调用多工具); initial_tool_choice(仅首轮强制某工具,靠 role=="user" 判断首轮); streaming(流式发 AgentStream 事件 vs 一次性返回); achat_with_tools(带工具的异步 chat 接口)📖 "If True, the agent will call multiple tools in parallel. If False, the agent will call tools sequentially." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "The tool to try and force to call on the first iteration of the agent." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
📖 "Only add tool choice if set and if its the first response" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/function_agent.py
🧪 实例 仅首轮注入 tool_choice:
python
# Only add tool choice if set and if its the first response
if (
self.initial_tool_choice is not None
and current_llm_input[-1].role == "user"
):
chat_kwargs["tool_choice"] = self.initial_tool_choice🔍 追问 streaming 模式下如何知道循环结束前的最后一个响应? →
_get_streaming_response 预先初始化 last_chat_response = ChatResponse(message=ChatMessage()),用 async for last_chat_response in response 不断覆盖,循环结束后返回它,这样即便流为空也有合法返回值。📚 拓展阅读
- base_agent.py — streaming 字段 — streaming 定义于 BaseWorkflowAgent,默认 True。
- workflow_events.py — AgentStream — 流式增量事件的字段结构(delta/response/tool_calls/thinking_delta)。
QBaseWorkflowAgent 用哪些 step 把 agent 串成 workflow?max_iterations 和 early stopping 怎么防死循环?深挖·拓展低频
答 FunctionAgent 只实现单步(take_step / handle_tool_call_results / finalize)三个抽象方法,而"循环调度"由基类
BaseWorkflowAgent 的一串 @step 编排:init_run(建 context、把 user_msg/chat_history 写 memory)→ setup_agent(拼 system_prompt、按 state_prompt 注入 state)→ run_agent_step(调 take_step 得到 AgentOutput)→ parse_agent_output(决定停止/继续/发工具调用)→ call_tool → aggregate_tool_results(收集所有工具结果、回灌成新的 AgentInput 形成闭环)。防死循环的机制在 parse_agent_output:每进一次就 num_iterations += 1,当 num_iterations >= max_iterations 时按 early_stopping_method 分流——"generate" 走 _generate_early_stopping_response 追加一条 early stopping 提示让 LLM 生成一个收尾回答;否则("force",默认)直接 raise WorkflowRuntimeError,提示可以调大 max_iterations 或改用 early_stopping_method='generate'。max_iterations 默认 DEFAULT_MAX_ITERATIONS = 20,可在 .run(.., max_iterations=...) 覆盖。若没有 tool_calls 就调 finalize 并 StopEvent(result=output) 正常结束;有则 send_event(ToolCall(...)) 继续循环。术语
@step(Workflow 的步骤装饰器,事件类型驱动); parse_agent_output(判定停止/重试/继续的决策节点); max_iterations(默认 20 的最大迭代数,防无限循环); early_stopping_method("force" 抛错 / "generate" 生成收尾回答); WorkflowRuntimeError(force 模式超限时抛出)📖 "if num_iterations >= max_iterations:" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/base_agent.py
📖 "Method to handle max iterations. 'force' raises an error (default). 'generate' makes one final LLM call to generate a response." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/base_agent.py
🧪 实例 agent 单步与 workflow 循环的关系:
flowchart TD
A[init_run] --> B[setup_agent]
B --> C[run_agent_step 调 take_step]
C --> D[parse_agent_output]
D -->|无 tool_calls| E[finalize + StopEvent]
D -->|有 tool_calls| F[call_tool]
F --> G[aggregate_tool_results]
G -->|回灌 AgentInput| B
D -->|num_iterations >= max| H{early_stopping_method}
H -->|force| I[raise WorkflowRuntimeError]
H -->|generate| J[生成收尾回答 StopEvent]🔍 追问 为什么判定用
>= 而不是 >? → num_iterations 先自增再比较,>= max_iterations(默认 20)确保恰好达到上限的那一步就触发停止/生成,不会多跑一轮。📚 拓展阅读
- function_agent.py — take_step/finalize — 基类 step 调用的具体单步实现。
- workflow_events.py — 事件类型 — AgentInput/AgentSetup/AgentOutput/ToolCall/ToolCallResult 驱动各 step。
Q多 Agent 场景下 handoff 怎么工作?FunctionAgent 与 AgentWorkflow 如何配合?深挖·拓展低频
答
AgentWorkflow 管理多个 agent 并通过 handoff 转交控制权。它在 get_tools 里为当前 agent 动态附加一个 handoff 工具:_get_handoff_tool 只有在 agent 数量大于 1 时才创建(单 agent 直接返回 None),并按 can_handoff_to 过滤掉当前 agent 自己和不允许转交的目标,再用 handoff_prompt.format(agent_info=...) 作为描述,以 FunctionTool.from_defaults(async_fn=handoff, ..., return_direct=True) 生成。handoff 函数本身校验目标 agent 是否在 agents 列表、是否在 can_handoff_to 允许集合内,合法则 await ctx.store.set("next_agent", to_agent) 记录下一个 agent 并返回一段 handoff 输出提示。之后 aggregate_tool_results 读取 next_agent 并把 current_agent_name 切过去;由于 handoff 是 return_direct 但 tool_name == "handoff",流程不 StopEvent 而是继续,把控制权交给新 agent。至于用哪种 agent,from_tools_or_functions 会看 LLM 是否为 function calling 模型:是则用 FunctionAgent,否则用 ReActAgent。术语
AgentWorkflow(编排多 agent + handoff 的 workflow); handoff(转交控制权的内置异步工具,写 next_agent); can_handoff_to(白名单,限制某 agent 能转交给谁); next_agent(store 键,aggregate_tool_results 据此切换 current_agent_name); from_tools_or_functions(按是否 function calling 选 FunctionAgent 或 ReActAgent)📖 "Handoff control of that chat to the given agent." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.py
📖 "Do not create a handoff tool if there is only one agent" — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.py
📖 "If the LLM is a function calling model, the workflow will use the FunctionAgent." — source: https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.py
🧪 实例 handoff 记录下一个 agent:
python
await ctx.store.set("next_agent", to_agent)
handoff_output_prompt = await ctx.store.get(
"handoff_output_prompt", default=DEFAULT_HANDOFF_OUTPUT_PROMPT
)
return handoff_output_prompt.format(to_agent=to_agent, reason=reason)🔍 追问 handoff 是 return_direct 工具,为什么不会像普通 return_direct 那样停止 workflow? →
aggregate_tool_results 里有 if return_direct_tool.tool_name != "handoff": 才 StopEvent;handoff 命中这个排除条件,因此只做 finalize 后继续,让切换后的 agent 接手。📚 拓展阅读
- multi_agent_workflow.py — aggregate_tool_results — 读取 next_agent 切换 current_agent_name 的逻辑。
- function_agent.py — handle_tool_call_results — 单 agent 侧对 handoff 的排除处理。