从任务树到自我修正:XAgent Plan 数据结构与 Agent 协作机制

1. 背景与目标

XAgent 的执行对象不是一条简单 prompt,而是一个可能需要多阶段推进、动态调整、工具协作和人工介入的复杂任务。用户输入的 query 通常不能直接交给一个工具执行器一次性完成,因此系统需要先把原始任务拆成多个子任务,再逐个执行。

这套设计的核心目标是把"任务理解、任务拆解、工具执行、执行反思、计划修正"串成一个闭环。Plan 数据结构承担的是任务骨架的角色:它保存任务层级、执行状态、任务目标、里程碑、执行结果和后续 refine 所需的信息。

从运行流程看,Plan 至少要满足几类需求:

  • 表达复杂任务的层级拆解,例如 1 -> 1.1 -> 1.1.1
  • 支持按固定顺序执行子任务。
  • 支持执行过程中失败后继续拆分任务。
  • 支持执行成功后删除或调整后续不再需要的任务。
  • 支持把工具调用结果、提交结果、反思结果绑定回具体任务节点。
  • 支持 UI 展示、日志归档和人工确认。

因此,XAgent 没有把 plan 设计成一个扁平数组,而是设计成一棵可递归展开、可动态修改的任务树。

2. Plan 的整体设计

Plan 定义在 XAgent/data_structure/plan.py。它本质上是一个树节点,每个节点代表一个任务或子任务。节点之间通过父子指针组织成树,节点自身的数据由 TaskSaveItem 保存。

核心字段如下:

python 复制代码
class Plan():
    def __init__(self, data: TaskSaveItem):
        self.father: Optional[Plan] = None
        self.children: List[Plan] = []
        self.data: TaskSaveItem = data
        self.process_node: ToolNode = None

各字段含义:

  • father: 当前任务的父任务节点。根节点没有父节点。
  • children: 当前任务被继续拆分后得到的子任务列表。
  • data: 当前任务的业务元数据,包括名称、目标、里程碑、执行状态、反思等。
  • process_node: 当前任务实际执行时最后停留的工具执行节点,通常包含 subtask_submit 的提交结果。

这个结构把"计划结构"和"执行结果"放在同一个节点上:children 描述计划如何展开,data 描述任务是什么,process_node 描述任务实际做成了什么样。

2.1 Plan 不是执行器

Plan 本身不执行任务,它只描述任务。真正执行当前任务的是 tool_tree_search agent 和 ReACTChainSearchPlan 提供当前任务的上下文,执行器读取 plan.data,调用工具,最后把执行结果写回 plan.process_nodeplan.data.status

也就是说:

text 复制代码
Plan                  负责表达任务结构
TaskHandler           负责调度当前执行哪个 Plan 节点
tool_tree_search      负责执行当前 Plan 节点
ReACTChainSearch      负责工具调用循环
plan_refinement       负责修改后续 Plan 结构

2.2 Plan 是树,不是数组

初始 plan 可能长这样:

text 复制代码
1  根任务:用户原始 query
├── 1.1 初始子任务
├── 1.2 初始子任务
└── 1.3 初始子任务

执行过程中,如果 1.2 太难、失败或需要更细拆解,可以进一步 split:

text 复制代码
1
├── 1.1
├── 1.2 状态变为 SPLIT
│   ├── 1.2.1
│   ├── 1.2.2
│   └── 1.2.3
└── 1.3

这就是树结构的价值:任务可以逐层展开,而不是一开始就把所有可能步骤都摊平成一个很长的列表。

3. TaskSaveItem:任务元数据

TaskSaveItem 定义在 XAgent/utils.py,它是每个 Plan 节点真正保存业务内容的地方。

核心字段如下:

python 复制代码
@dataclass
class TaskSaveItem:
    name: str = ""
    goal: str = ""
    milestones: List[str] = field(default_factory=lambda: [])
    prior_plan_criticism: str = ""
    status: TaskStatusCode = TaskStatusCode.TODO
    action_list_summary: str = ""
    posterior_plan_reflection: List[str] = field(default_factory=lambda: [])
    tool_reflection: List[Dict[str,str]] = field(default_factory=lambda: [])

字段含义:

  • name: 子任务名称,来自 planner 或 refiner 输出的 "subtask name"
  • goal: 子任务目标,来自 planner 或 refiner 输出的 "goal": {"goal": ...}
  • milestones: 判断任务是否完成的重要依据,描述该任务应达成的中间结果。
  • prior_plan_criticism: 初始规划阶段对该任务潜在问题的预判,来自 "goal": {"criticism": ...}
  • status: 当前任务状态,默认是 TODO
  • action_list_summary: 当前任务执行完成后的动作总结,由 posterior process 写入。
  • posterior_plan_reflection: 执行后的计划反思。
  • tool_reflection: 执行后的工具使用反思。

3.1 根任务的 goal 从哪里来

根任务在 PlanAgent.__init__() 里创建:

python 复制代码
self.plan = Plan(
    data = TaskSaveItem(
        name=f"act as {query.role_name}",
        goal=query.task,
        milestones=query.plan,
    )
)

所以根节点的目标就是用户原始 query:

text 复制代码
root.name       = act as <role_name>
root.goal       = query.task
root.milestones = query.plan

根节点代表"整个用户任务",它通常不会像普通子任务一样直接由工具执行,而是作为整棵任务树的根上下文存在。

3.2 子任务的 goal 从哪里来

普通子任务的 goal 由 planner 或 refiner 生成。无论是初始拆分,还是执行过程中的 split/add,都会先得到一个 subtask JSON,然后通过 TaskSaveItem.load_from_json() 解析。

解析逻辑是:

python 复制代码
if "goal" in function_output_item.keys() and "goal" in function_output_item["goal"].keys():
    self.goal = function_output_item["goal"]["goal"]

典型输入结构如下:

json 复制代码
{
  "subtask name": "收集需求",
  "goal": {
    "goal": "明确用户要完成的功能、边界和约束",
    "criticism": "用户需求可能不完整,需要避免过早实现"
  },
  "milestones": [
    "整理用户输入",
    "确认任务边界",
    "形成可执行需求说明"
  ]
}

其中 goal.goal 会写入 TaskSaveItem.goalgoal.criticism 会写入 TaskSaveItem.prior_plan_criticism

3.3 status 的作用

status 是调度逻辑的关键字段。当前实现里主要状态包括:

python 复制代码
class TaskStatusCode(Enum):
    TODO = 0
    DOING = 1
    SUCCESS = 2
    FAIL = 3
    SPLIT = 4

执行器只会继续寻找 TODO 节点执行。当前任务进入 inner_loop() 时会被置为 DOING,执行完成后根据 ReACTChainSearch 的结果变成 SUCCESSFAIL。如果某个任务被 refine 拆分,它会变成 SPLIT,后续执行器会跳过该父任务,转而执行它下面新生成的 TODO 子任务。

4. Plan 的 JSON 表示

Plan.to_json() 负责把任务树转成可展示、可总结、可传给 agent 的 JSON 结构。

核心逻辑如下:

python 复制代码
def to_json(self, posterior=True):
    root_json = self.data.to_json(posterior=posterior)
    if self.process_node:
        root_json["submit_result"] = self.process_node.data["command"]["properties"]

    root_json["task_id"] = self.get_subtask_id(to_str=True)
    if len(self.children) > 0:
        root_json["subtask"] = [subtask.to_json() for subtask in self.children]
    return root_json

它做了几件事:

  1. 先把当前节点的 TaskSaveItem 转成 JSON。
  2. 如果当前任务已经执行过,并且有 process_node,把提交结果写入 submit_result
  3. 动态计算当前节点的 task_id
  4. 如果有子节点,递归输出 subtask

最终结构大致如下:

json 复制代码
{
  "name": "收集需求",
  "goal": "明确用户要完成的功能、边界和约束",
  "prior_plan_criticsim": "用户需求可能不完整,需要避免过早实现",
  "milestones": [
    "整理用户输入",
    "确认任务边界"
  ],
  "exceute_status": "TODO",
  "task_id": "1.1",
  "subtask": [
    {
      "name": "分析输入",
      "goal": "提取用户输入中的约束条件",
      "milestones": [],
      "exceute_status": "TODO",
      "task_id": "1.1.1"
    }
  ]
}

4.1 submit_result 的作用

当前任务执行完成后,TaskHandler.outer_loop() 会把 search_method.get_finish_node() 保存到当前 plan:

python 复制代码
self.now_dealing_task.process_node = search_method.get_finish_node()

之后 to_json() 会把 process_node 里的 command.properties 放到 submit_result。如果最后一步是 subtask_submit,这里就会包含:

  • submit_type
  • result.success
  • result.conclusion
  • result.milestones
  • suggestions_for_latter_subtasks_plan.need_for_plan_refine
  • suggestions_for_latter_subtasks_plan.reason

这让完整 plan 不只包含"原计划",也包含"执行后结果"和"对后续计划的建议"。

4.2 当前 JSON 字段的拼写问题

当前实现里有两个字段名拼写错误:

  • prior_plan_criticsim 应为 prior_plan_criticism
  • exceute_status 应为 execute_status

这两个字段已经出现在 to_json() 输出里,如果外部展示层、总结逻辑或历史记录已经依赖这些 key,直接修改可能造成兼容问题。比较稳妥的方式是:

  1. 新增正确字段名。
  2. 暂时保留旧字段名兼容。
  3. 更新所有内部引用。
  4. 在后续版本移除旧字段。

5. task_id 的生成机制

task_id 是每个 plan 节点的层级编号,例如:

text 复制代码
1
1.1
1.2
1.2.1

它不是持久存储在节点上的字段,而是每次调用时根据父子关系动态计算。

5.1 get_subtask_id()

get_subtask_id() 提供两种输出形式:

python 复制代码
def get_subtask_id(self, to_str=False):
    subtask_id_list = self.get_subtask_id_list()
    if to_str:
        subtask_id_list = [str(cont) for cont in subtask_id_list]
        return ".".join(subtask_id_list)
    else:
        return subtask_id_list

如果 to_str=False,返回数组:

python 复制代码
[1, 2, 1]

如果 to_str=True,返回字符串:

python 复制代码
"1.2.1"

数组形式适合做层级和顺序判断,字符串形式适合展示、日志、UI current task 和 recorder 归档。

5.2 get_subtask_id_list()

真正计算编号的是 get_subtask_id_list()

python 复制代码
def get_subtask_id_list(self):
    if self.father == None:
        return [1]
    father_subtask_id = self.father.get_subtask_id()
    child_id = self.father.children.index(self) + 1
    father_subtask_id.append(child_id)
    return father_subtask_id

规则很简单:

  • 如果当前节点没有父节点,它就是根节点,编号为 [1]
  • 如果有父节点,先拿父节点编号。
  • 再找到自己在父节点 children 数组里的位置。
  • 子节点位置从 1 开始,所以要 index + 1
  • 把这个位置追加到父编号后面。

例如:

text 复制代码
1
├── 1.1
└── 1.2
    ├── 1.2.1
    └── 1.2.2

对于 1.2.2 节点:

text 复制代码
父节点 1.2 的 id list = [1, 2]
当前节点是父节点 children 里的第 2 个
所以当前 id list = [1, 2, 2]
字符串形式 = "1.2.2"

5.3 为什么每次动态计算

当前设计没有把 task_id 存到节点上,是因为 plan 会被动态修改。

执行过程中可能发生:

  • split: 给某个任务增加子节点。
  • add: 在某个任务后增加兄弟节点。
  • delete: 删除尚未执行的兄弟节点。

这些操作都会改变节点在 children 数组里的位置。如果 task_id 是持久字段,就需要在每次结构变化后重新维护一批编号,容易出现"树结构变了但 id 没同步"的问题。

动态计算的好处是:

  • 编号永远和当前树结构一致。
  • add/delete/split 后不需要额外维护 id。
  • 实现简单,减少状态不一致风险。

代价是:

  • 每次调用都要沿父链递归计算。
  • self.father.children.index(self) 是线性查找。
  • 如果 plan 树非常大、频繁调用 get_subtask_id(),会有额外开销。
  • 如果外部系统把 "1.2" 当成稳定 ID 保存,add/delete 兄弟节点后可能导致引用漂移。

在当前 XAgent 的使用场景里,plan 树一般不会特别大,所以这个开销可以接受。它更偏向正确性和实现简单性,而不是追求大规模任务树下的性能最优。

5.4 task_id 和 recorder 的关系

task_id 还会被用于运行记录归档。每次开始处理一个子任务时,TaskHandler.outer_loop() 会调用:

python 复制代码
self.recorder.change_now_task(task_id)

这会把 recorder 的当前任务上下文切到对应编号,例如 "1.2"。后续工具调用、plan refine 记录都会归到这个任务下面。

因此,task_id 同时服务三类场景:

  • 执行调度:定位当前节点在执行顺序中的位置。
  • 人机交互:告诉前端当前执行的是哪个任务。
  • 运行记录:把工具调用和 refine 过程归档到对应子任务。

6. 为什么使用树形结构

XAgent 的 plan 使用树形结构,主要是因为复杂任务天然存在"粗粒度目标"和"细粒度步骤"的层级关系。一个任务最开始可以只拆成少量高层子任务,只有当某个子任务执行失败、过大或不够明确时,才继续向下拆分。

如果使用扁平数组,初始计划可能会被迫一次性列出很多步骤:

text 复制代码
step 1
step 2
step 3
step 4
step 5
...

这种方式的问题是:

  • 一开始很难知道需要多少步骤。
  • 后续某一步失败时,很难把补救步骤和原步骤建立层级关系。
  • 某一步如果已经解决了后面多个步骤,需要删除或合并后续步骤。
  • 执行记录、反思和 UI 展示都会变得扁平,难以表达"这个子任务为什么存在"。

树形结构更适合表达"任务展开过程":

text 复制代码
1  原始任务
├── 1.1 高层子任务
├── 1.2 高层子任务
│   ├── 1.2.1 失败后进一步拆分出来的子任务
│   └── 1.2.2 失败后进一步拆分出来的子任务
└── 1.3 高层子任务

这样,父节点表达"为什么要做这件事",子节点表达"具体怎么补救或落实"。

6.1 树结构带来的语义优势

树结构至少带来四个语义优势。

第一,任务上下文更清晰。执行 1.2.1 时,可以知道它是从 1.2 拆出来的,继承了父任务的目标、失败原因和 prior criticism。

第二,失败后的处理更自然。一个任务失败后,不一定代表整个任务失败,可以把它标记为 SPLIT,然后生成更细的子任务继续执行。

第三,计划修改更局部。refine 可以只修改当前执行位置之后的任务,而不需要重建整张任务列表。

第四,展示结构更符合人类理解。UI 可以按树展示任务进度,用户能看到"当前在整个任务结构中的位置"。

6.2 树结构和动态 refine 的关系

split 操作依赖树结构。它的语义是"把一个任务展开成多个更小的子任务",而不是"在数组后面插入几步"。

例如当前任务 1.2 失败:

text 复制代码
1.2  分析数据源

refine 后:

text 复制代码
1.2  分析数据源,状态变为 SPLIT
├── 1.2.1 检查数据文件是否存在
├── 1.2.2 解析数据 schema
└── 1.2.3 生成数据质量报告

原来的 1.2 不再作为一个普通 TODO 任务执行,而是变成一个被拆解过的父任务。后续执行器会继续找它下面的 TODO 子任务。

7. 为什么执行时要打平整棵树

虽然 plan 的存储结构是树,但执行器每次要解决的问题是:"当前任务做完了,下一个要执行谁?"

这个问题本质上需要一个线性顺序。树适合表达层级,但不直接表达"下一个"。所以代码会先把整棵树打平成一个按执行顺序排列的数组。

相关函数是 Plan.get_inorder_travel()

python 复制代码
@classmethod
def get_inorder_travel(cls, now_plan):
    result_list = [now_plan]
    for child in now_plan.children:
        result_list.extend(Plan.get_inorder_travel(child))
    return result_list

这个函数会返回一个数组。例如:

text 复制代码
1
├── 1.1
│   ├── 1.1.1
│   └── 1.1.2
└── 1.2

打平后是:

text 复制代码
[1, 1.1, 1.1.1, 1.1.2, 1.2]

然后 pop_next_subtask() 会从当前节点之后寻找第一个 TODO

python 复制代码
root_plan = now_plan.get_root()
all_plans = Plan.get_inorder_travel(root_plan)
order_id = all_plans.index(now_plan)
for subtask in all_plans[order_id + 1:]:
    if subtask.data.status == TaskStatusCode.TODO:
        return subtask
return None

这段逻辑的意思是:

  1. 先找到整棵树的根节点。
  2. 把最新的整棵树打平成执行数组。
  3. 找到当前任务在数组里的位置。
  4. 从当前位置之后开始扫描。
  5. 返回第一个状态为 TODO 的任务。
  6. 如果没有,说明没有后续任务了。

7.1 为什么不是直接找 children

当前任务做完后,下一个任务不一定是它的 child。可能有几种情况:

  • 当前任务被 split 后,下一个是它的第一个子任务。
  • 当前任务没有子任务,下一个是它的后续兄弟节点。
  • 当前任务是某个子树的最后一个节点,下一个是祖先节点的后续兄弟节点。
  • 当前任务后面有些节点已经 SUCCESSFAILSPLIT,要跳过它们找下一个 TODO

如果不用打平数组,就需要写一套复杂的树游标逻辑:

text 复制代码
先看 child
没有 child 看 next sibling
没有 sibling 就回到 parent
parent 也没有 sibling 就继续回溯
过程中还要跳过非 TODO 节点

打平后,这些逻辑都简化成"从数组当前位置往后扫"。

7.2 每次重新打平的好处

执行过程中 plan 会被 refine,树结构可能发生变化。如果维护一个长期存在的队列,add/delete/split 后队列就可能过期。

当前实现每次都从 root 重新打平,可以保证:

  • 获取的是最新 plan 结构。
  • 新增子任务会自然进入执行顺序。
  • 删除任务不会再出现在后续扫描里。
  • 被 split 的父任务会被跳过,子任务会被执行。

这是一种简单可靠的设计。它牺牲了一点性能,但避免了执行队列和树结构不同步的问题。

8. 先序遍历与执行语义

get_inorder_travel() 这个名字并不准确。它实际实现的是先序遍历,也就是 preorder traversal。

先序遍历的规则是:

text 复制代码
先访问当前节点,再从左到右访问子节点

对应代码:

python 复制代码
result_list = [now_plan]
for child in now_plan.children:
    result_list.extend(Plan.get_inorder_travel(child))

当前节点先进入 result_list,然后递归访问所有子节点,所以这是典型的先序 DFS。

8.1 为什么先序 DFS 适合 XAgent

XAgent 的执行语义是:先尝试执行一个较高层任务,如果这个任务可以完成,就不需要继续展开;如果它失败或过大,再把它拆成更小的子任务。

因此执行顺序应该是:

text 复制代码
先处理父任务
必要时再处理子任务
再处理后续兄弟任务

这正好对应先序 DFS:

text 复制代码
1 -> 1.1 -> 1.1.1 -> 1.1.2 -> 1.2

如果使用后序遍历,就会先执行所有子任务,再处理父任务,这不符合"失败后再拆分"的执行模型。如果使用广度优先遍历,则会先处理所有同级任务,再进入子任务,这也不适合"当前失败任务需要立刻补救"的场景。

8.2 命名问题

当前函数名 get_inorder_travel 有两个问题:

  • inorder 通常指中序遍历,主要用于二叉树。
  • 当前 plan 是一般树,不是二叉树,且实现不是中序而是先序。

更合适的命名是:

  • get_preorder_traversal
  • flatten_preorder
  • flatten_execution_order

其中 flatten_execution_order 可能最贴近业务语义,因为它不只是一个树算法函数,它实际上是在生成任务执行顺序。

9. 初始 Plan 如何生成

初始 plan 的生成发生在 PlanAgent.initial_plan_generation() 中。

TaskHandler.outer_loop() 一开始会调用:

python 复制代码
self.plan_agent.initial_plan_generation(
    agent_dispatcher=self.agent_dispatcher)

PlanAgent.__init__() 中,系统已经先创建了根节点:

python 复制代码
self.plan = Plan(
    data = TaskSaveItem(
        name=f"act as {query.role_name}",
        goal=query.task,
        milestones=query.plan,
    )
)

然后 initial_plan_generation() 会调用具备 RequiredAbilities.plan_generation 能力的 agent:

python 复制代码
agent = agent_dispatcher.dispatch(
    RequiredAbilities.plan_generation,
    target_task=f"Generate a plan to accomplish the task: {self.query.task}",
)

这个 planner agent 的 prompt 要求它使用 subtask_split_operation 把 query 拆成 2-4 个子任务。

9.1 subtask_split_operation 的输出结构

planner 输出的每个 subtask 大致遵循如下结构:

json 复制代码
{
  "subtask name": "子任务名称",
  "goal": {
    "goal": "子任务主要目标",
    "criticism": "该计划潜在问题"
  },
  "milestones": [
    "完成该子任务必须达成的里程碑"
  ]
}

然后代码会解析 planner 的 function call arguments:

python 复制代码
subtasks = json5.loads(new_message["function_call"]["arguments"])

for subtask_item in subtasks["subtasks"]:
    subplan = plan_function_output_parser(subtask_item)
    Plan.make_relation(self.plan, subplan)

plan_function_output_parser() 会创建 TaskSaveItem,把 JSON 字段加载进去,然后包装成一个新的 Plan 节点:

python 复制代码
def plan_function_output_parser(function_output_item: dict) -> Plan:
    subtask_node = TaskSaveItem()
    subtask_node.load_from_json(function_output_item=function_output_item)
    subplan = Plan(subtask_node)
    return subplan

最后通过 Plan.make_relation(self.plan, subplan) 把子任务挂到根节点下面。

9.2 初始生成和运行时 split 的区别

初始生成和运行时 split 都会产生子任务,但语义不同。

初始生成是把用户原始 query 拆成顶层子任务:

text 复制代码
1
├── 1.1
├── 1.2
└── 1.3

运行时 split 是某个具体子任务执行后发现需要细化,于是把这个任务拆成更小的子任务:

text 复制代码
1.2
├── 1.2.1
├── 1.2.2
└── 1.2.3

初始生成由 plan_generation agent 完成,运行时 split 由 plan_refinement agent 完成。

RequiredAbilities.tool_tree_search 表示"工具树搜索"能力。它不是规划器,而是执行器。它负责拿到当前 subtask 后,决定下一步调用什么工具、观察结果、继续推理,直到提交当前子任务。

TaskHandler.inner_loop() 中,当前 plan 节点会被交给这个能力的 agent:

python 复制代码
agent = self.agent_dispatcher.dispatch(
    RequiredAbilities.tool_tree_search,
    json.dumps(plan.data.to_json(), indent=2, ensure_ascii=False),
)

随后创建 ReACTChainSearch

python 复制代码
search_method = ReACTChainSearch(
    xagent_core_components=self.xagent_core,
)

再调用:

python 复制代码
search_method.run(
    config=self.config,
    agent=agent,
    arguments=arguments,
    functions=self.function_handler.intrinsic_tools(
        self.config.enable_ask_human_for_help),
    task_id=task_ids_str,
    now_dealing_task=self.now_dealing_task,
    plan_agent=self.plan_agent
)

执行器会看到几类信息:

  • 当前 subtask 的 namegoalmilestonesstatus
  • 完整 plan 的摘要或 JSON。
  • 已经执行过的步骤和工具结果。
  • 当前文件系统结构。
  • 可调用工具列表。
  • 当前工具调用步数和最大步数限制。

它的 prompt 明确说明:

  • 需要处理当前 subtask。
  • 可以使用工具和真实环境交互。
  • 达成目标后必须调用 subtask_submit
  • 如果 milestone 太难,可以提交失败并建议拆分。

执行器每一步都会输出一个 function call。普通步骤可能调用文件系统、shell、notebook、搜索工具等。最后一步应该调用 subtask_submit

subtask_submit 里会包含:

json 复制代码
{
  "result": {
    "success": true,
    "conclusion": "当前子任务执行结论",
    "milestones": ["已完成的里程碑"]
  },
  "submit_type": "give_answer",
  "suggestions_for_latter_subtasks_plan": {
    "need_for_plan_refine": false,
    "reason": "是否需要修改后续计划及原因"
  }
}

这个提交结果决定当前任务状态,也可能触发后续 plan refine。

11. ReACTChainSearch 如何配合 Plan

ReACTChainSearch 是当前 subtask 的工具执行循环。它把当前任务执行过程记录成一棵 TaskSearchTree,每个工具调用是一个 ToolNode

虽然类名里有 Search 和内部树结构,但当前默认 max_try=1,整体更像一条 ReACT 链:思考、调用工具、观察结果、继续思考,直到提交。

11.1 执行开始

inner_loop() 开始时会把当前任务状态改为 DOING

python 复制代码
plan.data.status = TaskStatusCode.DOING

然后执行 ReACTChainSearch.run()run() 内部调用 generate_chain(),开始逐步生成工具调用。

11.2 每一步如何构造上下文

make_message() 会把当前 subtask 信息加入上下文:

python 复制代码
terminal_task_info = summarize_plan(now_dealing_task.to_json())
now_subtask_prompt = f'''Now you will perform the following subtask:
"""
{terminal_task_info}
"""
'''

同时还会加入当前已经完成的工具调用过程:

python 复制代码
user_prompt = f"""The following steps have been performed:
{action_process}
"""

这样执行器每一步都知道:

  • 当前要做哪个 subtask。
  • 这个 subtask 在 plan 中是什么状态。
  • 之前已经做过哪些工具调用。
  • 当前还剩多少工具调用预算。

11.3 什么时候停止

执行循环里,如果工具调用返回 SUBMIT_AS_SUCCESSSUBMIT_AS_FAILED,就会停止:

python 复制代码
if tool_output_status_code == ToolCallStatusCode.SUBMIT_AS_SUCCESS:
    self.status = SearchMethodStatusCode.HAVE_AT_LEAST_ONE_ANSWER
    break
elif tool_output_status_code == ToolCallStatusCode.SUBMIT_AS_FAILED:
    break

最后 run() 会把搜索状态转换成当前任务执行状态:

python 复制代码
if self.status == SearchMethodStatusCode.HAVE_AT_LEAST_ONE_ANSWER:
    self.status = SearchMethodStatusCode.SUCCESS
else:
    self.status = SearchMethodStatusCode.FAIL

TaskHandler.inner_loop() 再根据这个状态更新 plan 节点:

python 复制代码
if search_method.status == SearchMethodStatusCode.SUCCESS:
    plan.data.status = TaskStatusCode.SUCCESS
elif search_method.status == SearchMethodStatusCode.FAIL:
    plan.data.status = TaskStatusCode.FAIL

11.4 process_node 如何回写

outer_loop()inner_loop() 返回后,会把执行完成节点挂到当前 plan 上:

python 复制代码
self.now_dealing_task.process_node = search_method.get_finish_node()

这一步很重要。它把"计划节点"和"实际执行轨迹的最后节点"关联起来。之后 Plan.to_json() 才能把 subtask_submit 的结果作为 submit_result 输出。

12. subtask_submit 与 refine 触发

refine 是否执行,不是系统根据 success 自动推断的,而是由 subtask_submit 里的 need_for_plan_refine 字段决定。

subtask_submit 的 schema 中有:

yaml 复制代码
suggestions_for_latter_subtasks_plan:
  need_for_plan_refine:
    type: boolean
  reason:
    type: string

FunctionHandler.handle_subtask_submit() 的核心逻辑是:

python 复制代码
plan_refine = False
if arguments["result"]["success"]:
    tool_output_status_code = ToolCallStatusCode.SUBMIT_AS_SUCCESS
else:
    tool_output_status_code = ToolCallStatusCode.SUBMIT_AS_FAILED
if arguments["suggestions_for_latter_subtasks_plan"]["need_for_plan_refine"]:
    plan_refine = True

所以 successneed_for_plan_refine 是两个不同维度:

text 复制代码
success=true,  need_for_plan_refine=false  当前任务成功,后续计划不用改
success=true,  need_for_plan_refine=true   当前任务成功,但后续计划需要调整
success=false, need_for_plan_refine=true   当前任务失败,需要拆分或修改计划
success=false, need_for_plan_refine=false  当前任务失败,但 agent 认为不用改计划

12.1 refine 不是只能失败才触发

成功任务也可能触发 refine。例如当前任务提前完成了后续任务的一部分,那么后续某些 TODO 子任务就可能应该删除。此时 success=true,但 need_for_plan_refine=true

失败任务也不一定触发 refine。如果 agent 认为失败不影响后续计划,或者当前失败不需要继续拆分,它可以提交 need_for_plan_refine=false

12.2 "任务太大"怎么判断

当前系统没有单独的算法判断"任务太大"。这个判断主要依赖 tool_tree_search agent 自己根据 prompt 作出判断。

prompt 中的关键约束是:

text 复制代码
Use "subtask_submit" only when you achieve all milestones of the current subtask
or you make sure it's impossible with the given tools.

If milestone is too hard to achieve, you can use "subtask_submit"
to give up the subtask and divide it into smaller subtasks.

所以它判断任务过大的依据通常是:

  • 当前 milestone 无法在工具预算内完成。
  • 当前任务目标过宽,无法直接落地。
  • 工具调用步数接近 max_subtask_chain_length
  • 当前工具或信息不足以完成该目标。
  • 执行过程中发现需要更细的前置步骤。

但是这些都是 agent 自评,不是系统级 validator。

12.3 当前触发机制的风险

如果子任务不精准,但执行器直接给了一个看似完成的结果,并且提交:

json 复制代码
{
  "success": true,
  "need_for_plan_refine": false
}

那么当前系统就会认为该任务完成,并继续执行下一个 TODO。posterior_process() 虽然会做总结和反思,但它不会自动把 need_for_plan_refine 改成 true。

这意味着当前设计存在一个明显风险:

text 复制代码
refine 是 agent 自评驱动,不是质量评估驱动。

如果 agent 自评错误,低质量结果可能会被当成已完成的 milestone。更稳妥的设计应该加入独立的 result validator 或 critic,对提交结果、原始 goal、milestones 和实际工具产物做一致性检查。

13. Plan Refine 的执行时机

plan_refine_mode() 的执行时机在 TaskHandler.outer_loop() 中。每个子任务完成后,系统会按照固定顺序做几件事:

  1. 执行当前子任务的 inner loop。
  2. 把执行结束节点写入当前 plan 的 process_node
  3. 执行 posterior process,提取总结和反思。
  4. 把当前任务注册到 working memory。
  5. 向 UI/interaction 插入 refinement 数据。
  6. 如果 search_method.need_for_plan_refine 为 true,进入 plan_refine_mode()
  7. 调用 Plan.pop_next_subtask() 找下一个 TODO 任务。

相关代码结构如下:

python 复制代码
search_method = self.inner_loop(self.now_dealing_task)

self.now_dealing_task.process_node = search_method.get_finish_node()

self.posterior_process(self.now_dealing_task)

self.working_memory_agent.register_task(self.now_dealing_task)

if search_method.need_for_plan_refine:
    self.plan_agent.plan_refine_mode(
        self.now_dealing_task, self.toolserver_interface, self.agent_dispatcher)

self.now_dealing_task = Plan.pop_next_subtask(
    self.now_dealing_task)

这说明 refine 是发生在"当前任务已经执行完成之后、选择下一个任务之前"。这个位置很关键,因为当前任务的执行结果已经可用,后续任务还没开始,因此可以安全地修改未来计划。

13.1 refine 使用哪些上下文

plan_refine_mode() 会把几类上下文交给 plan refinement agent:

  • 当前完整 plan。
  • 当前执行的 subtask id。
  • 当前任务提交时给出的 refine 建议。
  • 当前 workspace 文件结构。
  • 当前可用工具名称。
  • 最大 plan 宽度和深度配置。
  • 历史 refine chain。

其中,当前任务提交时的建议来自:

python 复制代码
refine_node_message = now_dealing_task.process_node.data["command"]["properties"]["args"]
refine_node_message = refine_node_message["suggestions_for_latter_subtasks_plan"]["reason"]

也就是说,tool_tree_searchsubtask_submit 中写的 reason,会成为 plan_refinement agent 修改计划的重要依据。

14. Refine 支持哪些操作

plan_refine_mode() 目前支持四类操作:

text 复制代码
split
add
delete
exit

对应分发逻辑如下:

python 复制代码
if function_input['operation'] == 'split':
    function_output, output_status_code = self.deal_subtask_split(function_input, now_dealing_task)
elif function_input['operation'] == 'add':
    function_output, output_status_code = self.deal_subtask_add(function_input, now_dealing_task)
elif function_input['operation'] == 'delete':
    function_output, output_status_code = self.deal_subtask_delete(function_input, now_dealing_task)
elif function_input['operation'] == 'exit':
    output_status_code = PlanOperationStatusCode.PLAN_REFINE_EXIT

14.1 split

split 的语义是:把目标任务拆成多个子任务。

执行逻辑是:

python 复制代码
for new_subtask in function_input["subtasks"]:
    new_subplan = plan_function_output_parser(new_subtask)
    Plan.make_relation(subtask, new_subplan)
subtask.data.status = TaskStatusCode.SPLIT

它会给目标 subtask 添加多个 children,然后把目标任务状态改成 SPLIT

例如:

text 复制代码
1.2  调研外部 API,状态 TODO

split 后:

text 复制代码
1.2  调研外部 API,状态 SPLIT
├── 1.2.1 搜索候选 API
├── 1.2.2 对比 API 返回格式
└── 1.2.3 写入调研结论

后续执行器会跳过 SPLIT 的父节点,执行它下面的 TODO 子任务。

14.2 add

add 的语义是:在某个后续任务之后新增兄弟任务。

它不是把任务加为当前节点的 child,而是插入到目标任务所在的兄弟列表里:

python 复制代码
index = subtask.father.children.index(subtask)

for new_subplan in new_subplans:
    new_subplan.father = subtask.father
subtask.father.children[index+1:index+1] = new_subplans

例如:

text 复制代码
1
├── 1.1
├── 1.2
└── 1.3

1.2 后 add 一个任务:

text 复制代码
1
├── 1.1
├── 1.2
├── 1.3 新增任务
└── 1.4 原 1.3 编号动态变更

因为 task_id 是动态计算的,插入兄弟节点会影响后续兄弟节点编号。

14.3 delete

delete 的语义是:删除尚未执行的 TODO 任务。

执行逻辑是:

python 复制代码
subtask.father.children.remove(subtask)
subtask.father = None

它从父节点的 children 数组中移除目标任务,并清空目标任务的 father

删除常见于两种情况:

  • 当前任务已经顺带完成了某个后续任务。
  • 后续任务因为前置结果变化而不再需要。

14.4 exit

exit 表示 refine agent 判断不需要继续修改计划,退出 refine 模式。

这通常发生在:

  • 当前计划已经足够好。
  • 当前任务虽然有建议,但 refine agent 判断不需要修改结构。
  • 达到 refine 最大步数前已经完成判断。

15. Refine 的修改边界

refine 不是只能修改"当前节点及其子节点",而是能修改"当前执行位置及之后"的计划。

这个边界是按先序 DFS 打平后的执行顺序定义的。假设当前执行到 1.2

text 复制代码
1
├── 1.1  已执行
├── 1.2  当前任务
│   ├── 1.2.1 未来任务
│   └── 1.2.2 未来任务
└── 1.3  未来任务

refine 可以影响:

  • 当前任务 1.2
  • 当前任务未来生成的子任务。
  • 当前任务的未来兄弟节点 1.3
  • 未来兄弟节点的子树。

一般不能影响:

  • 已经执行过的前序兄弟节点 1.1
  • 已经执行过的前序兄弟节点子树。
  • 已经完成的父任务内容。

15.1 split 的边界检查

split 通过 can_edit 控制只能修改当前任务及其之后的任务:

python 复制代码
can_edit = False
for k, subtask in enumerate(inorder_subtask_stack):
    if subtask.get_subtask_id(to_str=True) == now_dealing_task.get_subtask_id(to_str=True):
        can_edit = True

    if subtask.get_subtask_id(to_str=True) == target_subtask_id:
        if not can_edit:
            return error

这段逻辑的含义是:

  • 按执行顺序遍历整棵 plan 树。
  • 遍历到当前任务之前,can_edit=False
  • 遍历到当前任务后,can_edit=True
  • 如果目标任务出现在当前任务之前,则禁止修改。

15.2 delete 的边界检查

delete 更严格,它要求目标任务必须在当前任务之后,并且目标任务状态必须是 TODO

python 复制代码
if subtask.data.status != TaskStatusCode.TODO:
    return error

subtask.father.children.remove(subtask)

这避免删除已经执行过的任务。

15.3 add 的边界检查

add 通过比较目标任务和当前任务的 id list,限制只能在当前任务之后新增:

python 复制代码
former_subtask_id_list = former_subtask.get_subtask_id_list()
now_dealing_task_id_list = now_dealing_task.get_subtask_id_list()

for i in range(min(len(former_subtask_id_list), len(now_dealing_task_id_list))):
    if former_subtask_id_list[i] < now_dealing_task_id_list[i]:
        return error

这个检查比先序数组扫描更粗糙,主要是为了阻止向当前任务之前插入任务。

15.4 会不会涉及父节点和兄弟节点

业务上,refine 不应该修改已完成父节点的任务内容,也不应该修改已执行兄弟节点。但实现上,adddelete 必然会操作父节点的 children 数组。

例如删除未来兄弟节点时:

python 复制代码
subtask.father.children.remove(subtask)

这确实修改了父节点的 children 结构,但没有修改父节点的 data。所以更准确的说法是:

text 复制代码
refine 可以修改未来计划结构;
必要时会触碰父节点 children;
但不应该回头修改已完成任务的业务内容。

16. posterior_process 的作用

posterior_process() 是每个子任务执行完成后的总结和反思阶段。

它调用 get_posterior_knowledge()

python 复制代码
posterior_data = get_posterior_knowledge(
    all_plan=self.plan_agent.latest_plan,
    terminal_plan=terminal_plan,
    finish_node=terminal_plan.process_node,
    tool_functions_description_list=self.tool_functions_description_list,
    config=self.config,
    agent_dispatcher=self.agent_dispatcher,
)

输入包括:

  • 完整 plan。
  • 当前刚执行完的 terminal plan。
  • 当前任务的 finish node。
  • 可用工具描述。
  • 配置。
  • agent dispatcher。

16.1 写回哪些字段

posterior process 会把结果写回当前任务:

python 复制代码
summary = posterior_data["summary"]
terminal_plan.data.action_list_summary = summary

if "reflection_of_plan" in posterior_data.keys():
    terminal_plan.data.posterior_plan_reflection = posterior_data["reflection_of_plan"]

if "reflection_of_tool" in posterior_data.keys():
    terminal_plan.data.tool_reflection = posterior_data["reflection_of_tool"]

也就是:

  • action_list_summary: 当前任务执行动作总结。
  • posterior_plan_reflection: 对计划本身的执行后反思。
  • tool_reflection: 对工具使用的反思。

16.2 posterior_process 当前不触发 refine

当前实现里,posterior process 只负责写入反思结果,不负责决定是否 refine。

是否 refine 的判断已经在 subtask_submit 中由 need_for_plan_refine 决定,并通过 FunctionHandler.handle_subtask_submit() 写入 search_method.need_for_plan_refine

因此,当前流程是:

text 复制代码
subtask_submit.need_for_plan_refine -> 决定是否进入 plan_refine_mode
posterior_process                  -> 只记录总结和反思

这也是一个可以改进的点。更合理的设计是让 posterior reflection 或 validator 参与 refine 判断。例如:如果 reflection 发现任务结果没有覆盖 milestones,就应该触发重试或 refine。

17. Interaction Interrupt:运行中人工介入

self.interaction.interrupt 是运行时人工介入开关。它不是"强制停止程序"的意思,而是"是否在关键步骤等待人类确认或修改"。

它在 XAgentInteraction.__init__() 中初始化:

python 复制代码
def __init__(..., interrupt: bool = False, ...):
    self.interrupt = interrupt

默认是 False。如果调用方启动 interaction 时设置为 True,系统会在外层任务切换和内层工具步骤中等待用户输入。

17.1 外层 task 开始前的确认

TaskHandler.outer_loop() 中,每个 subtask 开始前会检查:

python 复制代码
if self.interaction.interrupt:
    goal = self.now_dealing_task.data.goal
    receive_data = self.interaction.receive(
        {"args": {"goal": goal}})

这表示系统把当前任务的 goal 发给用户,允许用户确认或修改。如果用户返回新的 goal,代码会覆盖当前任务目标:

python 复制代码
self.now_dealing_task.data.goal = new_intput

17.2 内层工具步骤中的确认

ReACTChainSearch 里也会检查 interaction.interrupt。如果开启,每一步工具调用之后可以等待人类查看和改写 agent 当前动作。

这让系统支持 human-in-the-loop:

  • 用户可以在任务执行前修改 goal。
  • 用户可以在工具调用过程中干预动作。
  • agent 可以在人类确认后继续执行。

17.3 和强制停止的区别

真正的停止逻辑不是 interrupt,而是 Redis 中 interaction 状态为 "close"。例如 insert_data() 中会检查:

python 复制代码
alive = redis.get_key(self.base.interaction_id)
if alive == "close":
    exit(0)

所以:

text 复制代码
interrupt=True     表示运行中等待人工输入
redis close        表示用户终止本次任务

18. Redis/DB 等待用户确认机制

当前系统通过"Redis 通知 + DB 存数据"的方式实现运行中等待用户确认。

整体流程是:

text 复制代码
Agent insert_data()
  -> 写 raw step 到数据库
  -> redis.set_key(interaction_id + "_send", 1)
  -> 前端知道有新 step 可展示

用户在前端提交
  -> 后端把 human_data 写回对应 raw step
  -> redis.set_key(interaction_id + "_" + current_step + "_receive", 1)

Agent receive()
  -> 每 2 秒轮询 Redis receive key
  -> key 存在后读 DB raw.human_data
  -> 返回用户输入,继续执行

18.1 receive 的阻塞等待

receive() 的核心逻辑:

python 复制代码
if self.call_method == "web":
    wait = 0
    while wait < self.wait_seconds:
        human_data = self.get_human_data()
        if human_data is not None:
            return human_data
        else:
            wait += 2
            time.sleep(2)

    raise XAgentTimeoutError("等待数据超时,关闭连接")

这是同步阻塞式轮询:

  • agent 当前线程会停在这里。
  • 每 2 秒查一次 Redis/DB。
  • 等到用户输入后继续执行。
  • 超过 wait_seconds 后抛超时异常。

18.2 Redis 和 DB 的分工

Redis 主要做轻量通知:

  • _send: 通知前端有新消息或新 step。
  • _receive: 通知 agent 用户已经提交数据。
  • interaction_id = close: 通知 agent 用户终止任务。

DB 主要存真实内容:

  • 当前 step 数据。
  • 当前文件列表。
  • human_data。
  • 当前 status。
  • 是否已发送、是否已接收。

这种设计实现简单,但不是最现代的事件驱动方式。

18.3 当前机制的问题

当前机制的问题主要是:

  • time.sleep(2) 会阻塞 worker 线程。
  • 多用户并发时,阻塞等待会浪费资源。
  • Redis key 只是信号,不是完整事件流。
  • 如果进程崩溃,恢复执行上下文会比较困难。
  • 等待状态没有被建模成显式 workflow state。

更现代的方式是:

  • WebSocket 或 SSE 推送 step 给前端。
  • Redis Streams、NATS、Kafka 或任务队列传递事件。
  • Agent runtime 使用 async/await 挂起等待,而不是 sleep 轮询。
  • 把人工确认建模成 WAITING_FOR_HUMAN 状态。
  • 用户提交后 resume 对应 checkpoint。

19. Running Recorder 与 task_id 归档

recorder.change_now_task(task_id) 的作用是把运行记录上下文切换到当前子任务。

调用位置在 TaskHandler.outer_loop()

python 复制代码
task_id = self.now_dealing_task.get_subtask_id(to_str=True)
self.recorder.change_now_task(task_id)

RunningRecorder.change_now_task() 做三件事:

python 复制代码
self.now_subtask_id = new_subtask_id
self.tool_call_id = 0
self.plan_refine_id = 0

含义是:

  • 当前日志归属的 subtask 改成 task_id
  • 当前 task 的工具调用编号从 0 开始。
  • 当前 task 的 plan refine 编号从 0 开始。

19.1 工具调用记录

后续工具调用会写到当前任务目录下:

text 复制代码
record_root_dir/<task_id>/tool_00000.json
record_root_dir/<task_id>/tool_00001.json

这让调试时可以按任务查看工具调用历史。

19.2 plan refine 记录

如果当前任务触发了 refine,PlanRefineChain.register() 会调用:

python 复制代码
XAgentCoreComponents.global_recorder.regist_plan_modify(...)

记录会写到:

text 复制代码
record_root_dir/<task_id>/plan_refine_00000.json

记录内容包括:

  • refine function name。
  • refine function input。
  • refine function output。
  • 修改后的 plan。

19.3 recorder 不影响执行逻辑

change_now_task() 不会改变 plan,也不会影响执行顺序。它只影响日志和审计。

如果没有这一步,工具调用和 plan refine 记录可能会混到上一个任务下面,导致后续调试困难。

20. RapidAPI 动态工具检索

rapidapi_retrieve_tool_count 是一个配置项,用来控制每个 subtask 执行前是否动态检索 RapidAPI 工具。

TaskHandler.inner_loop() 中:

python 复制代码
if self.config.rapidapi_retrieve_tool_count > 0:
    retrieve_string = summarize_plan(plan.to_json())
    rapidapi_tool_names, rapidapi_tool_jsons = self.toolserver_interface.retrieve_rapidapi_tools(
        retrieve_string, top_k=self.config.rapidapi_retrieve_tool_count)

如果配置值大于 0,系统会:

  1. 把当前 plan 总结成检索 query。
  2. 请求 ToolServer 的 /retrieving_tools
  3. 检索 top-k 个相关 RapidAPI 工具。
  4. 把工具 schema 注册到 function_manager
  5. 把工具追加到当前 function_handler 的可用工具列表。

20.1 retrieve_rapidapi_tools 做了什么

ToolServerInterface.retrieve_rapidapi_tools() 会发送请求:

python 复制代码
url = f"{self.url}/retrieving_tools"
payload = {
    "question": query,
    "top_k": top_k
}
response = requests.post(url, json=payload, timeout=20, cookies=self.cookies)

返回结果包括:

  • retrieved_tools: 工具名称列表。
  • tools_json: 工具 JSON schema 列表。

然后每个工具 schema 会被注册:

python 复制代码
for tool_json in tools_json:
    function_manager.register_function(tool_json)

20.2 如何加入当前 agent 可用工具

检索成功后,inner_loop() 会更新工具枚举和工具描述:

python 复制代码
self.function_handler.change_subtask_handle_function_enum(
    self.function_handler.tool_names + rapidapi_tool_names)
self.function_handler.avaliable_tools_description_list += rapidapi_tool_jsons

后续 function_handler.intrinsic_tools() 会把这些工具加入传给 agent 的 functions:

python 复制代码
tools = [self.subtask_submit_function]
if enable_ask_human_for_help:
    tools.append(self.ask_human_for_help_function)
tools.extend(self.avaliable_tools_description_list)
return tools

20.3 为什么默认关闭

配置文件里默认:

yaml 复制代码
rapidapi_retrieve_tool_count: 0

也就是关闭动态 RapidAPI 检索。

这个能力的设计目的,是避免一次性把大量 RapidAPI 工具塞进模型上下文。按当前任务检索 top-k 工具,可以减少上下文长度,也减少模型选错工具的概率。

但它也有风险:

  • 检索质量会影响工具可用性。
  • ToolServer 请求失败时当前实现只是打印错误并继续。
  • 动态加入工具可能造成不同 subtask 的可用工具集合不一致。
  • 如果 top-k 太小,可能漏掉关键工具;太大则又会增加上下文负担。

21. 当前设计的优点

XAgent 的 plan 设计虽然实现不复杂,但几个核心选择是有价值的:用树表达任务结构,用先序 DFS 表达执行顺序,用状态标记决定是否跳过节点,用 refine 修改未来计划。

21.1 树结构表达复杂任务自然

复杂任务往往不是线性步骤,而是"目标 -> 子目标 -> 更细子目标"的展开过程。树结构能直接表达这个过程。

例如:

text 复制代码
完成一个数据分析任务
├── 获取数据
├── 清洗数据
│   ├── 检查缺失值
│   ├── 统一字段格式
│   └── 输出清洗报告
└── 生成分析结论

这比扁平数组更容易理解,也更容易在失败时局部拆分。

21.2 先序 DFS 让执行顺序和展开顺序一致

先序 DFS 的执行方式是先尝试父任务,再进入子任务。这符合 XAgent 的动态执行模型:

text 复制代码
先尝试解决当前任务
如果失败或过大,再 split 成更小任务
然后继续执行子任务

这种设计避免了一开始过度拆解任务,也避免在没有执行反馈的情况下生成大量细碎步骤。

21.3 动态 task_id 简化结构维护

task_id 每次根据父子关系动态计算,不需要维护持久编号。这样 add/delete/split 后,编号会自动反映最新结构。

这让 plan 修改逻辑更简单,不需要在每次插入或删除后手动更新整棵树的 id。

21.4 打平执行降低调度复杂度

执行器每次从 root 打平整棵树,然后从当前位置往后找第一个 TODO。这比维护复杂树游标简单很多。

它统一处理了几类场景:

  • 当前节点的子任务。
  • 当前节点的兄弟任务。
  • 祖先节点的后续兄弟任务。
  • 跳过 SUCCESSFAILSPLIT 节点。

21.5 refine 只修改未来计划

refine 的边界是当前执行位置及之后的任务。这是一个重要约束。

它避免了一个常见问题:执行到后面时,agent 回头修改已经完成的历史任务,导致日志、结果和计划不一致。

当前设计虽然不是完全严格的不可变历史模型,但已经通过 can_editTODO 状态限制了大部分危险操作。

21.6 process_node 把计划和执行轨迹关联起来

Plan.process_node 保存当前任务最终执行节点,让 plan 不只是一份静态计划,而是能包含执行结果。

这对总结、反思、UI 展示很重要。因为系统可以从同一个 plan JSON 中看到:

  • 原始任务目标。
  • 当前执行状态。
  • 任务提交结果。
  • 是否建议 refine。
  • 任务执行总结。

21.7 recorder 让执行过程可追踪

recorder.change_now_task(task_id) 让每个子任务拥有独立的工具调用和 refine 记录。调试时可以按任务编号定位:

text 复制代码
1.2/tool_00000.json
1.2/tool_00001.json
1.2/plan_refine_00000.json

这让复杂 agent 运行过程具备审计能力。

22. 当前设计的问题与风险

当前设计也有不少明显问题,主要集中在命名、字段兼容、质量判断、状态建模和运行时调度上。

22.1 get_inorder_travel 命名错误

get_inorder_travel() 实际是先序 DFS,不是中序遍历。

当前命名会误导读代码的人,尤其是熟悉树算法的人。它更应该叫:

  • get_preorder_traversal
  • flatten_preorder
  • flatten_execution_order

如果不想大范围改调用点,可以先新增新函数名,让旧函数作为兼容 wrapper。

22.2 JSON 字段拼写错误

当前 JSON 输出存在拼写问题:

  • prior_plan_criticsim
  • exceute_status

这会影响可读性,也容易让后续开发者误用字段。直接修复可能破坏兼容,建议采用双写迁移。

22.3 refine 依赖 agent 自评

当前是否 refine 完全依赖 subtask_submit 里的 need_for_plan_refine。这意味着如果执行器自评错误,系统不会自动纠正。

典型风险:

text 复制代码
任务其实没完成
agent 提交 success=true
need_for_plan_refine=false
系统继续执行下一个任务
后续任务建立在错误结果上

22.4 "任务太大"没有系统级判定

当前任务是否太大,主要由 tool agent 根据 prompt 判断。系统没有从这些信号自动判断:

  • 工具调用次数接近上限。
  • 多次工具调用没有推进。
  • milestones 没有覆盖。
  • 输出文件不存在或质量不足。
  • submit conclusion 与 goal 不匹配。

这使得任务拆分质量高度依赖模型自觉。

22.5 posterior_process 不参与控制流

posterior process 会生成总结和反思,但它不会改变执行决策。即使 reflection 发现计划有问题,也不会自动触发 refine。

这导致系统里存在两套信息:

text 复制代码
subtask_submit.need_for_plan_refine  决定是否 refine
posterior_plan_reflection            只被记录

更好的方式是让 reflection 结果参与后续控制流。

22.6 task_id 不是稳定 ID

动态 task_id 的好处是和树结构一致,但它不是稳定 ID。

如果执行过程中新增或删除兄弟节点,后续节点编号可能变化:

text 复制代码
原来:1.3
在 1.2 后 add 新任务后:原 1.3 变成 1.4

如果外部系统、UI 或日志长期引用 "1.3",可能出现引用漂移。

22.7 Redis 轮询等待用户输入效率不高

当前 receive() 使用 time.sleep(2) 轮询等待用户输入。这种方式简单,但会阻塞 worker。

在低并发场景可以接受,但在多用户、多任务、长时间等待人工确认的场景下,会浪费资源。

22.8 每次全树打平在大规模 plan 下效率较低

pop_next_subtask() 每次都从 root 打平整棵树,然后查找当前节点。这对小 plan 没问题,但如果 plan 很大,频繁执行会有额外开销。

当前瓶颈不明显,但如果未来支持长期任务、大规模任务树或多 agent 协作,就需要考虑执行游标或增量队列。

22.9 SPLIT 状态无法表达部分成功

当前状态包括 SUCCESSFAILSPLIT,但没有 PARTIAL_SUCCESS

现实任务中经常出现:

  • 当前任务完成了一部分。
  • 一部分 milestone 已经达成。
  • 剩余部分需要拆分。

当前只能粗略地用 SPLITFAIL 表达,信息不够精细。

23. 改进建议

改进可以分成几个层次:低风险代码清理、数据结构增强、质量控制增强、运行时架构升级。

23.1 修正命名但保留兼容

新增更准确的函数名:

python 复制代码
@classmethod
def flatten_execution_order(cls, now_plan):
    result_list = [now_plan]
    for child in now_plan.children:
        result_list.extend(Plan.flatten_execution_order(child))
    return result_list

@classmethod
def get_inorder_travel(cls, now_plan):
    return cls.flatten_execution_order(now_plan)

这样既能改善可读性,又不破坏旧调用。

23.2 修复 JSON 字段拼写

建议在 TaskSaveItem.to_json() 中同时输出新旧字段:

python 复制代码
json_data = {
    "name": self.name,
    "goal": self.goal,
    "prior_plan_criticism": self.prior_plan_criticism,
    "prior_plan_criticsim": self.prior_plan_criticism,
    "milestones": self.milestones,
    "execute_status": self.status.name,
    "exceute_status": self.status.name,
}

然后逐步迁移内部引用,最后移除旧字段。

23.3 引入稳定节点 ID

保留动态 task_id 作为展示路径,同时给每个节点增加稳定 ID:

python 复制代码
self.node_id = uuid.uuid4().hex

这样可以区分:

text 复制代码
node_id  稳定身份,用于 DB、日志、外部引用
task_id  动态路径,用于展示当前树位置

add/delete 兄弟节点时,task_id 可以变化,但 node_id 不变。

23.4 增加 Result Validator

subtask_submit 后增加一个独立 validator,检查:

  • result.success 是否和实际产物匹配。
  • conclusion 是否覆盖原始 goal。
  • milestones 是否全部达成。
  • 如果任务要求写文件,文件是否真的存在。
  • 工具调用结果是否支持结论。

validator 输出可以是:

json 复制代码
{
  "valid": false,
  "reason": "缺少目标文件输出",
  "suggested_action": "retry_or_refine"
}

如果 validator 判定不通过,可以自动触发:

  • 重试当前 subtask。
  • 将当前 subtask split。
  • 要求人类确认。
  • 标记为 partial success。

23.5 让 posterior_process 参与 refine 决策

当前 posterior reflection 只是记录,建议改成控制信号之一。

例如:

text 复制代码
subtask_submit.need_for_plan_refine
OR validator.need_for_refine
OR posterior_reflection.need_for_refine

这样可以降低单一 agent 自评错误带来的风险。

23.6 增加 PARTIAL_SUCCESS 状态

建议扩展任务状态:

python 复制代码
class TaskStatusCode(Enum):
    TODO = 0
    DOING = 1
    SUCCESS = 2
    PARTIAL_SUCCESS = 3
    FAIL = 4
    SPLIT = 5
    SKIPPED = 6

这样可以更准确表达:

  • 当前任务部分完成。
  • 后续任务需要基于已有成果继续。
  • 某个任务因被其他任务覆盖而跳过。

23.7 把等待用户输入改成事件驱动

当前 receive() 可以改成更现代的机制:

text 复制代码
Agent emits WAITING_FOR_HUMAN event
runtime checkpoints current state
worker releases thread
frontend submits human_data
runtime resumes corresponding agent run

实现上可以选择:

  • WebSocket/SSE 推送前端。
  • Redis Streams 或消息队列传递事件。
  • async/await 挂起当前执行。
  • checkpoint 保存 plan、current node、tool state。

23.8 改善工具检索策略

RapidAPI 检索可以从"每个 subtask 固定 top-k"升级为更细的策略:

  • 根据 task type 判断是否需要检索外部 API。
  • 对检索到的工具做 rerank。
  • 加入工具使用成功率和历史反馈。
  • 避免重复向模型暴露无关工具。
  • 对每个 subtask 记录实际工具选择和效果,用于后续优化。

24. 推荐的新架构方向

如果要把这套系统演进成更现代、更可靠的 agent workflow,可以保留 Plan 树的核心思想,但把执行控制改成显式状态机。

推荐结构:

text 复制代码
Plan Tree
  -> Execution Cursor
  -> Tool Action Graph
  -> Result Validator
  -> Reflection
  -> Plan Refiner
  -> Checkpoint / Resume
  -> Human Approval Node

24.1 Plan Tree

继续用树表示任务结构:

  • 父子任务关系。
  • 任务目标。
  • milestones。
  • 执行状态。
  • refine 后的结构变化。

但建议增加稳定 node_id,并把动态 task_id 作为路径展示。

24.2 Execution Cursor

单独维护当前执行游标:

json 复制代码
{
  "current_node_id": "...",
  "execution_order_version": 12
}

这样调度逻辑不完全依赖每次全树打平,也更方便 checkpoint 和 resume。

24.3 Tool Action Graph

当前 TaskSearchTree 已经具备雏形。可以进一步明确它和 plan 的关系:

text 复制代码
PlanNode 1.2
  -> ActionGraph
      -> ToolNode 0
      -> ToolNode 1
      -> SubmitNode

Plan 表示要做什么,ActionGraph 表示实际怎么做。

24.4 Result Validator

在 submit 后新增质量门控:

text 复制代码
subtask_submit
  -> validator
      -> pass: mark SUCCESS
      -> fail: retry / split / ask human

这样 refine 不再只依赖执行 agent 自评。

24.5 Reflection

Reflection 不只是记录文本,而是输出结构化信号:

json 复制代码
{
  "plan_issue": true,
  "tool_issue": false,
  "missing_milestones": ["..."],
  "recommend_refine": true,
  "recommend_split": true
}

这些信号可以参与后续调度。

24.6 Plan Refiner

Plan Refiner 继续负责修改未来计划,但操作应该更结构化:

json 复制代码
{
  "operation": "split",
  "target_node_id": "...",
  "new_subtasks": [...]
}

使用 node_id 比使用动态 task_id 更安全。

24.7 Checkpoint / Resume

长期运行 agent 必须支持 checkpoint:

  • 当前 plan tree。
  • 当前 execution cursor。
  • 当前 action graph。
  • 当前 workspace snapshot。
  • 当前等待状态。

用户输入或系统重启后可以 resume,而不是依赖阻塞线程一直活着。

24.8 Human Approval Node

人工确认应该是 workflow 中的显式节点:

text 复制代码
WAITING_FOR_HUMAN
  input_schema: {"goal": "..."}
  resume_on: human_data

这样比 receive() 里 sleep 轮询更清晰,也更容易和前端、数据库、审计系统集成。

25. 总结

XAgent 的 Plan 是整个系统的任务骨架。它用树结构表达复杂任务,用 TaskSaveItem 保存任务元数据,用动态 task_id 表示节点在当前树中的路径,用先序 DFS 打平为执行顺序。

围绕 Plan,几个 agent 形成了一个闭环:

text 复制代码
plan_generation   生成初始任务树
tool_tree_search  执行当前子任务
reflection        总结和反思执行结果
plan_refinement   根据执行反馈修改未来计划

当前设计的关键优点是简单、直观、支持动态展开,并且能把计划、执行、记录和 UI 展示连接起来。它适合中小规模任务树,也符合 agent "先做、再反思、再修正"的工作方式。

但它也有明显短板:

  • refine 依赖 agent 自评。
  • 缺少独立质量验证。
  • posterior reflection 没有参与控制流。
  • 动态 task_id 不是稳定身份。
  • 人工等待是阻塞轮询。
  • 部分字段和函数命名存在历史问题。

如果继续演进,建议保留树形 plan 的核心模型,但引入稳定节点 ID、结果验证器、结构化 reflection、显式 workflow 状态、checkpoint/resume 和事件驱动的人机协作机制。

最终目标不是把 Plan 做成一个静态任务清单,而是把它做成 agent runtime 的"可执行任务状态树":它既能表达计划,也能承载执行结果,还能驱动后续调整。

相关推荐
武子康1 小时前
调查研究-202 SGLang 深度解析:为什么大模型推理框架不只是“把模型跑起来“
人工智能·openai·agent
前端双越老师2 小时前
我从 0 开发的 AI Agent 智语项目发布了
前端·node.js·agent
沉默王二2 小时前
DeepSeek这次招得太猛了,36个岗位,80%都要会Agent!
agent·ai编程·deepseek
古茗前端团队3 小时前
AI 乱改代码?试试这套 SDD 规范驱动工作流
agent
倾颜6 小时前
给受控 Agent 加 HITL:从 Graph interrupt 到 PostgreSQL checkpoint resume
agent
九酒13 小时前
AI Agent 开发踩坑记:口播功能非得用 APP 原生实现吗?
前端·人工智能·agent
Jackson__14 小时前
做了一段时间的AI coding后,我终于搞清了 CLI 和 MCP 的区别
前端·agent·ai编程
小孔菜菜20 小时前
LLM / Agent / Skills / MCP 协同关系深度解析
agent
JouYY21 小时前
聊一下多 Agent 编排架构的应用实践
架构·llm·agent