- AI Agent
- 大模型
- 后端
- 任务调度
【免费下载链接】XAgent
An Autonomous LLM Agent for Complex Task Solving
TaskSearchTree 是 XAgent 内部用于组织"复杂任务求解过程"的核心树状数据结构,它以ToolNode为节点,记录 Agent 在解决每个子任务时逐步调用工具、产生思考与输出结果的完整链路。本文以 XAgent/data_structure/tree.py 与配套文档 Markdown_Docs/XAgent/data_structure/tree.md 为骨架,结合节点实现与 ReACT 搜索算法源码,深入讲解树的构造、深度/子树统计、父子关系建立,以及它在真实任务执行中的调用方式,帮助你理解 XAgent 是如何把一次次的 LLM 推理与工具调用"沉淀"成一棵可回溯、可统计、可提交的任务树。
TaskSearchTree 类概览:一棵承载任务搜索行为的树
在 XAgent 的代码库中,TaskSearchTree被定义在 XAgent/data_structure/tree.py,其类注释明确说明:TaskSearchTree 表示一棵具有特定任务搜索行为(specific task searching behavior)的树数据结构。它的职责不是通用意义上的"多叉树工具类",而是为内层循环搜索算法(inner loop search)提供一棵记录"当前子任务从开始到结束每一步操作"的链式树。
类定义如下:
class TaskSearchTree: """ TaskSearchTree represents a tree data structure with specific task searching behavior. Attributes: root (ToolNode): Root node of the tree. now_expand_num (int): Maintains current expanding number for nodes during traversal. """ def __init__(self): self.root: ToolNode = ToolNode() self.root.expand_num = 0 self.now_expand_num = 1从属性设计上可以看到它的两个核心成员:
| 属性 | 类型 | 语义 |
|---|---|---|
root | ToolNode | 树的根节点,默认是一个新建的空ToolNode,其expand_num被固定为 0 |
now_expand_num | int | 遍历过程中维护的"当前扩展编号",用于给新加入的节点按扩展顺序编号 |
与 Plan 树的区别:两种树各司其职
XAgent 中还存在另一棵树——由 XAgent/data_structure/plan.py 中的Plan类构成的计划树(Plan Tree),它管理任务计划(含子任务 ID、状态、父子关系)。而TaskSearchTree管理的是每个子任务内部的执行过程:当某个子任务被真正执行时,Agent 每推理并调用一次工具,就会在树上新增一个节点。因此两者是"计划级"与"执行级"两个不同粒度的树结构,读者不应混淆。
构造方法__init__:初始化根节点与扩展编号
__init__方法不接收任何参数,内部只做三件事:
def __init__(self): self.root: ToolNode = ToolNode() self.root.expand_num = 0 self.now_expand_num = 1- 创建根节点:直接实例化一个
ToolNode并赋给self.root。根节点代表"任务尚未开始"的初始状态,它不携带任何真实的 Agent 行为数据。 - 根节点不参与扩展:
self.root.expand_num = 0,将根节点的扩展编号固定为 0,表示根节点本身不会被当作一次"扩展"。 - 从 1 开始计数:
self.now_expand_num = 1表示当前"下一个将要被扩展的节点"编号为 1,即树中第一个真实操作节点将从编号 1 开始。
注意点(沿用文档说明并结合源码):
- 该函数无参数,创建
TaskSearchTree()即可完成初始化; - 初始化的根节点默认不会被扩展;如果业务上需要根节点也参与扩展,可以通过修改其
expand_num属性实现(不过在当前 ReACT 实现中根节点始终只作为起始锚点); now_expand_num表示"下一个可分配的扩展编号",它随每次建立父子关系自增,实际反映树上真实节点(不含根)的数量。
查询方法:get_depth与get_subtree_size
TaskSearchTree的深度与子树大小查询都采用"委托给根节点"的实现方式:
def get_depth(self): return self.root.get_depth() def get_subtree_size(self): return self.root.get_subtree_size()这里的关键在于:树本身不维护任何统计信息,所有统计逻辑都定义在ToolNode上(见 XAgent/data_structure/node.py)。
ToolNode 上的深度计算
ToolNode.get_depth通过递归回溯父节点计算深度:
def get_depth(self): if self.father == None: return 0 return self.father.get_depth() + 1- 根节点的
father为None,因此深度为0; - 每个子节点深度 = 父节点深度 + 1;
- 由于
TaskSearchTree.get_depth()委托给self.root.get_depth(),返回的正是整棵树的最大深度。
使用注意:
get_depth依赖父子关系被正确建立(即father指针正确),否则计算结果会出现偏差;同时因为它采用递归实现,极端情况下过深的链可能引起递归开销,需要配合配置中的max_subtask_chain_length限制链长(详见后文)。
ToolNode 上的子树大小计算
ToolNode.get_subtree_size采用递归累加的方式统计以当前节点为根的子树节点总数:
def get_subtree_size(self): if self.children == []: return 1 now_size = 1 for child in self.children: now_size += child.get_subtree_size() return now_size- 叶子节点(
children为空)子树大小为1; - 非叶子节点的大小 =自身 1+ 所有子节点子树大小的累加;
- 对
TaskSearchTree而言,调用get_subtree_size()即得到整棵任务树的总节点数。
值得注意的语义细节:在ToolNode层面,"子树大小"包含当前节点自身(叶子返回 1);而关联文档对TaskSearchTree.get_subtree_size的说明中提到"子树的节点数不包括根节点本身"这一说法,与node.py的实现存在表述差异。以源码为准:TaskSearchTree.get_subtree_size()返回的是root.get_subtree_size(),其中根节点计入统计(根没有子节点时返回 1)。读者在实际阅读旧文档或调试时,应以 XAgent/data_structure/node.py 的实际行为为准,避免被注释误导。
建边方法make_father_relation:建立父子关系并编号
make_father_relation(father, child)是树从"单节点"生长为"链/树"的唯一入口,源码如下:
def make_father_relation(self, father, child): if not (isinstance(father, ToolNode) and isinstance(child, ToolNode)): raise TypeError("Father and child both need to be instances of ToolNode.") child.expand_num = self.now_expand_num self.now_expand_num += 1 child.father = father father.children.append(child)其执行流程分为三步:
- 类型校验:
father与child必须同时是ToolNode实例,否则抛出TypeError,提示信息为"Father and child both need to be instances of ToolNode."; - 分配扩展编号:把当前的
now_expand_num写入child.expand_num,然后now_expand_num += 1,从而保证树中每个真实节点都拿到唯一的、按加入顺序递增的扩展编号; - 双向建边:将
child.father指向father,并把child追加到father.children列表中,完成"父认子、子认父"的双向关联。
注意:
- 使用前必须确保
father与child节点均已创建并存在于树中; - 传入非
ToolNode类型会直接抛异常,因此调用方(如 ReACT 算法)总是用agent.message_to_tool_node(...)生成的ToolNode来调用; expand_num不仅用于标识顺序,还能配合now_expand_num推导当前树上"真实扩展节点"的数量。
节点基石:ToolNode 的完整结构
要真正用好TaskSearchTree,必须理解其节点类型ToolNode。它继承自抽象基类Node(见 XAgent/data_structure/node.py),初始化时定义了如下字段:
self.father: ToolNode = None self.children: list[ToolNode] = [] self.expand_num = 0 self.data = { "content": "", "thoughts": { "properties": { "thought": "", "reasoning": "", "plan": "", "criticism": "", }, }, "command": { "properties": { "name": "", "args": "", }, }, "tool_output": "", "tool_status_code": ToolCallStatusCode.TOOL_CALL_SUCCESS, } self.history: MessageHistory = MessageHistory() self.workspace_hash_id = ""各字段含义:
| 字段 | 类型 | 说明 |
|---|---|---|
father | ToolNode | 父节点指针 |
children | list[ToolNode] | 子节点列表 |
expand_num | int | 扩展顺序编号,由make_father_relation分配 |
data | dict | 节点核心数据:内容、thoughts(思考/推理/计划/批评)、command(命令名与参数)、工具输出、工具调用状态码 |
history | MessageHistory | 该节点对应的消息历史(见 XAgent/message_history.py) |
workspace_hash_id | str | 工作区哈希 ID,用于关联文件系统快照 |
此外ToolNode还提供两个对树的运行至关重要的方法:
process(属性):从当前节点一路回溯到根节点,把沿途每个节点的data按"根→当前"顺序拼成一个列表,供 ReACT 算法构造"你已经完成的步骤"提示词使用;to_json:对data做深拷贝,并把tool_status_code枚举值转换成其名称字符串(如"TOOL_CALL_SUCCESS"),得到 JSON 兼容格式,便于持久化或回放展示。
ToolNode的详细字段说明与示例可见配套文档 Markdown_Docs/XAgent/data_structure/node.md。
实战:TaskSearchTree 在 ReACT 内层搜索中的调用链
TaskSearchTree并非孤立存在,它被内层循环搜索算法ReACTChainSearch直接使用,实现在 XAgent/inner_loop_search_algorithms/ReACT.py 中。该算法继承自 XAgent/inner_loop_search_algorithms/base_search.py 的BaseSearchMethod,在初始化时维护了一个树列表:
class ReACTChainSearch(BaseSearchMethod): def __init__(self, xagent_core_components: XAgentCoreComponents): super().__init__() self.tree_list = [] self.finish_node = None self.xagent_core_components = xagent_core_components每轮尝试生成一棵新树
在generate_chain方法中,每次尝试(attempt)都会追加一棵全新的TaskSearchTree:
self.tree_list.append(TaskSearchTree()) now_attempt_tree = self.tree_list[-1] now_node = now_attempt_tree.root也就是说,tree_list中每棵树对应一次完整的"链式搜索尝试",多次尝试(max_try)失败或成功后由run方法统一判定搜索状态(SearchMethodStatusCode.HAVE_AT_LEAST_ONE_ANSWER/FAIL,见 XAgent/utils.py 中的枚举定义)。
循环生长:深度受限的链式扩展
树的生长发生在while循环中,其终止条件直接使用树的深度:
while now_node.get_depth() < config.max_subtask_chain_length: ... new_tree_node = agent.message_to_tool_node(new_message) ... tool_output, tool_output_status_code, need_for_plan_refine, using_tools = \ self.xagent_core_components.function_handler.handle_tool_call(new_tree_node) ... now_attempt_tree.make_father_relation(now_node, new_tree_node) ... now_node = new_tree_node关键点:
- 深度即进度:
now_node.get_depth()表示当前链已走了多少步,当它达到配置的max_subtask_chain_length时循环停止,防止无限生长; - 节点来源:
new_tree_node由agent.message_to_tool_node(new_message)生成——该方法(见 XAgent/agent/tool_agent/agent.py)把 LLM 返回的 message(含content、arguments、function_call)转换为一个携带思考与命令的ToolNode,其中data["command"]["properties"]["name"]就是 Agent 决定调用的工具名; - 边即操作记录:
make_father_relation(now_node, new_tree_node)把上一步节点与新节点连成链,expand_num按 1、2、3……依次分配; - 状态即结束信号:当
tool_output_status_code为SUBMIT_AS_SUCCESS或SUBMIT_AS_FAILED时中断循环,self.finish_node = now_node记录终点节点,供上层(如 XAgent/workflow/working_memory.py 中注册子任务并记录finish_node.get_depth()作为处理长度)使用。
节点数据如何回放给 LLM
树的链式结构还被用于构造下一轮推理的上下文:make_message(now_node, ...)读取now_node.process(即从根到当前节点的所有data序列),并在config.enable_summary开启时用summarize_action压缩后作为"你已经完成的步骤"注入用户消息。这样 LLM 每走一步都能"看到"整条历史链,而历史链正是由TaskSearchTree一步步累积起来的。
配置联动:用max_subtask_chain_length约束树高
树的高度上限来自全局配置项max_subtask_chain_length,默认配置见 assets/gpt-3.5-turbo_config.yml:
max_subtask_chain_length: 15配套的常用配置还包括:
max_plan_refine_chain_length: 3 # 计划精炼链长度 max_plan_tree_depth: 3 # 计划树最大深度 max_plan_tree_width: 5 # 计划树最大宽度 enable_ask_human_for_help: False # 是否允许向人类求助同一套配置也出现在 assets/xagentllama.yml。从源码看,max_subtask_chain_length在 ReACT.py 中被三处使用:作为while循环终止条件、判断是否强制调用subtask_submit(当now_node.get_depth() == config.max_subtask_chain_length - 1时,最后一步必须提交子任务)、以及作为提示词中的max_length占位符。由此可见,调大该值可让 Agent 在单个子任务内执行更多步骤(链更深),但也意味着更长的上下文与更多工具调用;调小则会更快进入subtask_submit收尾。
最小可运行示例:手动搭建一棵任务树
综合 Markdown_Docs/XAgent/data_structure/tree.md 的示例输出与源码实现,可以手动构造一棵任务树并验证各方法行为:
from XAgent.data_structure.node import ToolNode from XAgent.data_structure.tree import TaskSearchTree # 1. 初始化一棵任务树 tree = TaskSearchTree() print(tree.get_depth()) # 0:初始只有根节点 print(tree.get_subtree_size()) # 1:根节点自身计为 1 # 2. 构造两个真实操作节点并建立父子关系 father = ToolNode() child = ToolNode() tree.make_father_relation(tree.root, father) # father 的 expand_num = 1 tree.make_father_relation(father, child) # child 的 expand_num = 2 print(tree.get_depth()) # 2:root -> father -> child print(tree.get_subtree_size()) # 3:三个节点 print(father.expand_num, child.expand_num) # 1 2 print(child.father is father, father.children) # True [child] # 3. 类型校验:非 ToolNode 会抛 TypeError try: tree.make_father_relation(father, "not a node") except TypeError as e: print(e) # Father and child both need to be instances of ToolNode.小结:TaskSearchTree 的设计要点
- 组合而非继承:
TaskSearchTree内部持有ToolNode根节点,统计逻辑全部下沉到节点层,树类只做转发,职责清晰; - 编号机制:
now_expand_num与expand_num配合,为每个操作节点提供全局唯一的扩展顺序号; - 深度受限:树的生长深度由配置
max_subtask_chain_length控制,从源头规避了递归统计与上下文无限膨胀的风险; - 贯穿执行主链路:从 ReACT 搜索到工作记忆注册,
TaskSearchTree提供的深度、终点节点与process链式数据,是 XAgent 实现"复杂任务多步求解、可回溯、可总结、可提交"的底层支撑。
如果需要进一步了解节点细节与搜索算法整体流程,可继续阅读仓库内的 Markdown_Docs/XAgent/data_structure/node.md 与 Markdown_Docs/XAgent/inner_loop_search_algorithms/ReACT.md。
- AI Agent
- 大模型
- 后端
- 任务调度
【免费下载链接】XAgent
An Autonomous LLM Agent for Complex Task Solving
相关推荐
快速完整的微信聊天记录导出:备份、统计一次搞定
快速完整的微信聊天记录导出:备份、统计一次搞定 WeChatMsg 是一款本地运行的微信聊天记录导出工具:能把记录导出为 HTML、Word、CSV 三种格式,
Swift Algorithm Club 之 Trie 字典树:Swift 前缀树数据结构的原理与实现详解
Swift Algorithm Club 之 Trie 字典树:Swift 前缀树数据结构的原理与实现详解 导读 Trie(又称前缀树 prefix tree、
示例工程教程高级树结构解析:B树、三元搜索树在C-Sharp-Algorithms中的实现原理
高级树结构解析:B树、三元搜索树在C Sharp Algorithms中的实现原理 C Sharp Algorithms是一个功能强大的C 算法库,提供了标准数
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考