LlamaIndex TreeIndex 四大检索器深度解析:TreeAllLeaf、TreeSelectLeaf、TreeRoot 与 Embedding 变体的源码级实现
【免费下载链接】llama_indexLlamaIndex is the leading document agent and OCR platform项目地址: https://gitcode.com/GitHub_Trending/ll/llama_index
本文围绕 LlamaIndex 中树索引(TreeIndex)配套的四个检索器展开:TreeAllLeafRetriever、TreeSelectLeafRetriever、TreeRootRetriever与TreeSelectLeafEmbeddingRetriever。这四个类是官方 API 参考(retrievers API 文档 中列出的llama_index.core.retrievers成员)在树结构索引上的具体实现。读完本文,你将理解每种检索模式的适用场景、child_branch_factor等关键参数的行为、as_retriever工厂方法的分发逻辑,以及“LLM 选路”与“向量相似度选路”两条核心调用链的源码细节,能够在 RAG 系统中正确地为树索引选择并配置检索器。
树索引(TreeIndex)与四种检索模式的关系
在讨论检索器之前,需要先明确它们依附的索引结构。TreeIndex定义在 tree 索引基类 中,其设计目标是:
树索引是一个树状索引,每个节点是其子节点的摘要。索引构建时自底向上(bottoms-up)地构建树,直到得到一组根节点(root_nodes)。查询时的主要选项是从根节点向下遍历树(traverse down the tree from the root nodes),次要选项是直接从根节点合成答案(directly synthesize the answer from the root nodes)。
TreeIndex的关键构造参数包括:
| 参数 | 默认值 | 说明 |
|---|---|---|
num_children | 10 | 每个节点应有的子节点数量(决定树的宽度与层级深度) |
build_tree | True | 构建索引时是否真正构建树(生成摘要节点)。设为False时只保留叶子节点 |
summary_template | DEFAULT_SUMMARY_PROMPT | 节点摘要所用的提示词 |
insert_prompt | DEFAULT_INSERT_PROMPT | 增量插入时决定新文本挂在哪一层的提示词 |
use_async | False | 是否使用异步构建 |
树的结构体是IndexGraph(TreeIndex.index_struct_cls),检索器通过index_struct.root_nodes拿到根节点、通过index_struct.get_children(node)拿到某节点的子节点、通过docstore.get_node(node_id)取出节点内容。下面四种检索器正是围绕这三个访问点实现了不同的查询策略。
四种模式由枚举TreeRetrieverMode(base.py 第 25-29 行)定义:
class TreeRetrieverMode(str, Enum): SELECT_LEAF = "select_leaf" SELECT_LEAF_EMBEDDING = "select_leaf_embedding" ALL_LEAF = "all_leaf" ROOT = "root"其中SELECT_LEAF、SELECT_LEAF_EMBEDDING、ROOT三种模式要求索引在构建时生成了树(build_tree=True),ALL_LEAF则不要求。这一约束由REQUIRE_TREE_MODES集合(base.py 第 32-36 行)和_validate_build_tree_required校验方法共同保证:若对未建树的索引使用需要树结构的模式,会抛出ValueError(base.py 第 130-136 行)。
工厂方法:as_retriever 如何分发到四种检索器
树索引并没有把“检索策略”写死在索引里,而是通过as_retriever工厂方法按需创建具体检索器实例。完整实现位于 base.py 第 94-128 行:
def as_retriever( self, retriever_mode: Union[str, TreeRetrieverMode] = TreeRetrieverMode.SELECT_LEAF, embed_model: Optional[BaseEmbedding] = None, **kwargs: Any, ) -> BaseRetriever: self._validate_build_tree_required(TreeRetrieverMode(retriever_mode)) if retriever_mode == TreeRetrieverMode.SELECT_LEAF: return TreeSelectLeafRetriever(self, object_map=self._object_map, **kwargs) elif retriever_mode == TreeRetrieverMode.SELECT_LEAF_EMBEDDING: embed_model = embed_model or Settings.embed_model return TreeSelectLeafEmbeddingRetriever( self, embed_model=embed_model, object_map=self._object_map, **kwargs ) elif retriever_mode == TreeRetrieverMode.ROOT: return TreeRootRetriever(self, object_map=self._object_map, **kwargs) elif retriever_mode == TreeRetrieverMode.ALL_LEAF: return TreeAllLeafRetriever(self, object_map=self._object_map, **kwargs) else: raise ValueError(f"Unknown retriever mode: {retriever_mode}")从源码结构看,有几点值得注意:
- 默认模式是
select_leaf,即 LLM 逐层选路,这是树索引的“正统”查询方式。 select_leaf_embedding模式需要embed_model:未显式传入时会回落到全局Settings.embed_model,这与TreeSelectLeafEmbeddingRetriever构造器中的回退逻辑一致(见下文)。kwargs会被透传给具体检索器,例如child_branch_factor、verbose、query_template等,因此可以通过工厂方法自定义提示词与分支因子。- 四个检索器都同时从
llama_index.core.indices.tree包和llama_index.core.retrievers包导出(见 retrievers 包初始化文件 与 indices.tree 包初始化文件),CLI 的组件映射(mappings.json)也确认这四个类的“官方地址”是llama_index.core.retrievers。
典型用法是配合RetrieverQueryEngine:
from llama_index.core import TreeIndex, Settings from llama_index.core.indices.query.engine import RetrieverQueryEngine tree_index = TreeIndex.from_documents(documents, num_children=10) # 默认:LLM 逐层选路到叶子 retriever = tree_index.as_retriever() # 或使用向量相似度选路 # retriever = tree_index.as_retriever( # retriever_mode="select_leaf_embedding", child_branch_factor=2 # ) query_engine = RetrieverQueryEngine.from_args(retriever) response = query_engine.query("这篇文档的核心结论是什么?")TreeAllLeafRetriever:把整篇文档的叶子节点全部作为检索结果
TreeAllLeafRetriever实现于 all_leaf_retriever.py,是四种模式中逻辑最简单的一种。类 docstring 明确说明了它的定位:
这个类从叶子节点出发构建一个查询相关的树来返回回答。使用该查询模式意味着树索引在初始化时不需要构建(因为每次查询都会重建树)。
核心检索逻辑只有三行(all_leaf_retriever.py 第 47-56 行):
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """Get nodes for response.""" logger.info(f"> Starting query: {query_bundle.query_str}") index_struct = cast(IndexGraph, self._index_struct) all_nodes = self._docstore.get_node_dict(index_struct.all_nodes) sorted_node_list = get_sorted_node_list(all_nodes) return [NodeWithScore(node=node) for node in sorted_node_list]可以归纳出它的特点:
- 不依赖树结构:取的是
index_struct.all_nodes(索引中所有叶子节点),而非root_nodes或get_children。这也正是为什么ALL_LEAF不在REQUIRE_TREE_MODES中——即使构建索引时build_tree=False,该模式仍然可用。 - 返回全部叶子:没有打分、没有筛选,所有节点以
score=None包装成NodeWithScore返回,节点顺序由get_sorted_node_list(indices/utils.py 第 16 行)按节点 id 排序保证确定性。 - 适用场景:它本质上是“把树索引当作文档容器用”——把全部源文档内容交给 LLM/响应合成器来回答,等价于忽略层级结构的全量上下文检索。文件顶部还残留着
DEFAULT_NUM_CHILDREN = 10常量,与TreeIndex的默认子节点数保持一致。 - 注意其构造参数:只接受
index、callback_manager、object_map、verbose,没有 LLM 相关参数,因为检索过程本身不调用模型,问答发生在下游的响应合成阶段。
TreeRootRetriever:直接从根节点摘要中取答案
TreeRootRetriever实现于 tree_root_retriever.py,docstring 对其适用前提有非常关键的说明:
这个类直接从根节点检索答案。与 GPTTreeIndexLeafQuery 不同,它假设图中已经存储了答案(因为它是带 query_str 构建的),因此不会试图沿着图往下解析信息来合成答案。
检索实现(tree_root_retriever.py 第 42-50 行):
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """Get nodes for response.""" logger.info(f"> Starting query: {query_bundle.query_str}") root_nodes = self._docstore.get_node_dict(self._index_struct.root_nodes) sorted_nodes = get_sorted_node_list(root_nodes) return [NodeWithScore(node=node) for node in sorted_nodes]从源码可以读出它的设计边界:
- 只取
index_struct.root_nodes:这些根节点是整棵树的最高层摘要。对于“文档级/章节级宏观问题”(如“这份报告的整体结论”),根节点摘要往往已经足够,无需下钻。 - 前提条件苛刻:注释里说的“带 query_str 构建”指的是旧版
GPTTreeIndex的查询式建图方式——建图时就围绕某个问题逐层摘要,根节点直接就是答案。而当前TreeIndex是问题无关的通用摘要树,因此用TreeRootRetriever时,根节点返回的是文档摘要,最终回答仍由下游响应合成器基于这些摘要节点生成。 - 该模式要求
build_tree=True:ROOT属于REQUIRE_TREE_MODES,未建树的索引调用as_retriever(retriever_mode="root")会被_validate_build_tree_required拒绝。
TreeSelectLeafRetriever:LLM 驱动的逐层“迷宫式”检索
这是树索引的默认、也是最能体现树结构价值的检索器,实现于 select_leaf_retriever.py。类 docstring 概述为:
这个类遍历索引图,搜索最能回答查询的叶子节点。
child_branch_factor(int):每一层考虑的子节点数量。若为 1,则任何父节点只会选择 1 个子节点继续遍历;若为 2,则选择 2 个。
构造参数一览
| 参数 | 默认值 | 说明 |
|---|---|---|
index | 必填 | TreeIndex实例 |
query_template | DEFAULT_QUERY_PROMPT | 单分支(child_branch_factor=1)时的选路提示词 |
query_template_multiple | DEFAULT_QUERY_PROMPT_MULTIPLE | 多分支(child_branch_factor>1)时的选路提示词 |
text_qa_template | DEFAULT_TEXT_QA_PROMPT | 叶子节点问答提示词(响应合成器使用) |
refine_template | DEFAULT_REFINE_PROMPT_SEL | 多节点结果精化提示词 |
child_branch_factor | 1 | 每层保留的子节点数 |
verbose | False | 打印每层选择过程 |
选路提示词定义在 default_prompts.py 第 46-78 行。单分支模板要求模型在编号列表中“只返回最相关的一个编号(ANSWER: <number>)”;多分支模板则要求返回“至多{branching_factor}个按相关度排序的编号”。两个模板都强调“只依据上述选项、不使用先验知识”,以减少模型幻觉干扰选路。
两条工作路径:_query 与 _retrieve
该类的实现同时保留了“查询即回答”和“纯检索”两条路径,这也是理解它的关键:
路径一:_query(问答路径,select_leaf_retriever.py 第 282-299 行)
def _query(self, query_bundle: QueryBundle) -> Response: source_nodes: List[BaseNode] = [] response_str = self._query_level( self._index_struct.root_nodes, query_bundle, level=0, source_nodes=source_nodes, ).strip() return Response( response_str, source_nodes=[NodeWithScore(node=node) for node in source_nodes], )它直接覆写了基类的_query方法(注释明确写了this overrides the _query method in the base class),因此以查询引擎方式直接query时,走的是完整的“逐层选路 → 叶子问答 → refine 聚合”流程:
_query_level从根节点集合开始,把每个节点的摘要文本编号后拼进选路提示词,用PromptHelper.get_text_splitter_given_prompt按模型上下文窗口自适应截断,再调用self._llm.predict获得选择结果(select_leaf_retriever.py 第 165-224 行)。- 用
extract_numbers_given_response(response, n=self.child_branch_factor)从 LLM 回复中解析编号(indices/utils.py 第 22 行)。解析失败(回复中没有数字)或编号越界时,源码采取降级策略:直接把该层的 LLM 原始回复当作最终答案返回,而不是崩溃(select_leaf_retriever.py 第 231-249 行)。 - 对每个选中的编号(1-indexed,代码中
number - 1转 0-indexed),递归进入_query_with_selected_node:若该节点是叶子,用get_response_synthesizer(基于text_qa_template)从节点文本生成回答;否则继续对其子节点调用_query_level(select_leaf_retriever.py 第 110-163 行)。 - 当一次查询命中多个叶子(
child_branch_factor > 1)时,后面的叶子会携带前一个答案prev_response,通过refine_template对既有答案进行精化——即“逐叶累进 refine”的响应聚合方式。 - 沿途访问过的叶子节点被收集进
source_nodes,作为最终Response.source_nodes返回,便于溯源。
路径二:_retrieve(纯检索路径,select_leaf_retriever.py 第 398-441 行)
def _retrieve_level(self, cur_node_ids, query_bundle, level=0) -> List[BaseNode]: cur_nodes = {index: self._docstore.get_node(node_id) for index, node_id in cur_node_ids.items()} cur_node_list = get_sorted_node_list(cur_nodes) if len(cur_node_list) > self.child_branch_factor: selected_nodes = self._select_nodes(cur_node_list, query_bundle, level=level) else: selected_nodes = cur_node_list children_nodes = {} for node in selected_nodes: node_dict = self._index_struct.get_children(node) children_nodes.update(node_dict) if len(children_nodes) == 0: # NOTE: leaf level return selected_nodes else: return self._retrieve_level(children_nodes, query_bundle, level + 1) def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: nodes = self._retrieve_level(self._index_struct.root_nodes, query_bundle, level=0) return [NodeWithScore(node=node) for node in nodes]这条路径与路径一的区别在于:它只负责“选路到叶子”,不在叶子处调用 LLM 生成答案,而是把最终选中的叶子节点作为NodeWithScore列表返回。这样当把TreeSelectLeafRetriever装进RetrieverQueryEngine时,选路过程照常发生,而真正的问答由外部注入的response_synthesizer对返回的叶子节点完成——这正是“检索器/合成器解耦”的典型体现。注意一个细节:_retrieve_level中当当前层节点数不超过child_branch_factor时跳过 LLM 选路、直接全部保留,能减少一次模型调用;_select_nodes中的编号越界节点会被静默continue跳过(而非像_query_level那样整体降级返回)。
child_branch_factor 的取舍
child_branch_factor=1:每层只保留 1 条路径,LLM 调用次数最少(每层 1 次),但一旦某层选错分支就无法回头;child_branch_factor>1:每层保留多条路径做“束搜索”,召回更稳,但 LLM 调用与 refine 步骤随之增加,且要求使用多分支提示词query_template_multiple。
从源码结构看,两类提示词的切换完全由if self.child_branch_factor == 1分支决定(select_leaf_retriever.py 第 188-224 行),如果手动覆盖提示词,应保证与所用分支因子匹配。
TreeSelectLeafEmbeddingRetriever:用向量相似度替代 LLM 选路
TreeSelectLeafEmbeddingRetriever继承自TreeSelectLeafRetriever(select_leaf_embedding_retriever.py 第 20 行),docstring 说明:
这个类使用查询与节点文本之间的嵌入相似度来遍历索引图。
它在父类基础上增加了一个embed_model参数(Optional[BaseEmbedding],缺省回落到Settings.embed_model,见 第 46-72 行),并重写了选路的核心方法:
问答路径的重写——_query_level(第 74-109 行):不再调用 LLM 做选择,而是对当前层所有节点计算与查询的相似度,取前child_branch_factor个节点,再复用父类的_query_with_selected_node完成“叶子问答 + refine 聚合”。选路阶段的模型调用从“每层一次 LLM”变成了“每节点一次 embedding”,对于宽树或高频查询,成本特征明显不同。
相似度计算与缓存(第 111-154 行):
def _get_query_text_embedding_similarities(self, query_bundle, nodes) -> List[float]: if query_bundle.embedding is None: query_bundle.embedding = self._embed_model.get_agg_embedding_from_queries( query_bundle.embedding_strs ) similarities = [] for node in nodes: if node.embedding is None: node.embedding = self._embed_model.get_text_embedding( node.get_content(metadata_mode=MetadataMode.EMBED) ) similarity = self._embed_model.similarity(query_bundle.embedding, node.embedding) similarities.append(similarity) return similarities def _get_most_similar_nodes(self, nodes, query_bundle): similarities = self._get_query_text_embedding_similarities(query_bundle, nodes) selected_nodes, selected_indices = [], [] for node, _ in sorted(zip(nodes, similarities), key=lambda x: x[1], reverse=True): if len(selected_nodes) < self.child_branch_factor: selected_nodes.append(node) selected_indices.append(nodes.index(node)) else: break return selected_nodes, selected_indices从源码结构看有三个值得注意的实现细节:
- 查询向量只算一次:查询 embedding 缓存在
query_bundle.embedding上,同一查询在逐层下钻过程中复用;节点向量则缓存在node.embedding字段上,同一节点多次访问时不重复计算。 - 节点取的是
MetadataMode.EMBED模式下的内容,即参与嵌入的是纯文本内容而非带元数据的 LLM 视图。 - 排序选取:对
(node, similarity)按相似度降序排序,取前child_branch_factor个,语义上等价于每层 top-k 束搜索。
检索路径的重写——_select_nodes(第 156-163 行):父类_retrieve_level在需要选路时调用_select_nodes,子类把它替换为纯向量相似度的 top-k 选择。因此把select_leaf_embedding模式的检索器装进RetrieverQueryEngine时,整个“逐层选路”过程零 LLM 调用,只有 embedding 计算,这是它与TreeSelectLeafRetriever最本质的性能差异。
四种检索器对比与选型建议
| 检索器 | 选路依据 | 是否调用 LLM 选路 | 是否要求已建树(build_tree=True) | 典型适用场景 |
|---|---|---|---|---|
TreeAllLeafRetriever | 无(返回全部叶子) | 否 | 否 | 文档不长、希望全量上下文作答;或build_tree=False时唯一可选模式 |
TreeRootRetriever | 无(直接返回根节点摘要) | 否 | 是 | 宏观/概括性问题;根节点摘要已包含答案 |
TreeSelectLeafRetriever | LLM 逐层选择子节点编号 | 是(每层 1 次) | 是 | 长文档精准问答,默认模式,精度优先 |
TreeSelectLeafEmbeddingRetriever | 查询与节点文本的向量相似度 top-k | 否(仅 embedding) | 是 | 宽树、高频查询、希望压低 LLM 成本的精准问答 |
选型上可以按以下思路判断:问题粒度越宏观,越靠近根(root);越具体,越需要下钻到叶子(select_leaf/select_leaf_embedding);文档总长度在单次上下文可容纳范围内且不需要分层定位时,all_leaf最省事。若使用select_leaf系模式,child_branch_factor是精度与成本的调节旋钮:从1起步,遇到明显“选错分支”的漏召回时再调大。
小结与源码索引
本文以官方 API 参考(retrievers/tree.md)列出的四个树检索器成员为主体,结合仓库源码梳理了它们的实现与协作关系:
- 模式定义与工厂分发:indices/tree/base.py(
TreeRetrieverMode、as_retriever、_validate_build_tree_required) - TreeAllLeafRetriever
- TreeRootRetriever
- TreeSelectLeafRetriever
- TreeSelectLeafEmbeddingRetriever
- 公共导出:retrievers 包、indices.tree 包
- 选路提示词:default_prompts.py
- 工具函数(节点排序、编号解析):indices/utils.py
掌握这四种检索器的差异与child_branch_factor、retriever_mode等参数的行为后,你就可以在 LlamaIndex 的树索引 RAG 管线中,针对文档规模、问题粒度与延迟成本,做出有据可依的检索策略选择。
【免费下载链接】llama_indexLlamaIndex is the leading document agent and OCR platform项目地址: https://gitcode.com/GitHub_Trending/ll/llama_index
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考