源码深读 XAgent:6 个 Agent 怎么分工?工具失败不崩、死循环怎么防?
本文基于 XAgent 主仓库源码,梳理多 Agent 架构的设计动机、工具调用可靠性机制,以及防死循环 / 防「扯皮」的工程手段。适合正在做 Agent 框架、或想从开源项目里抄作业的同学。
一、大纲
- 整体执行模型:双层循环(Outer Loop + Inner Loop)
- Agent 全景:6 个 LLM Agent + 工作流编排类
- 为什么这么拆:职责分离、认知任务解耦
- 工具调用可靠性:LLM 层 / Schema 层 / ToolServer 层三层防护
- 防死循环与防扯皮:硬上限、单向推进、提交驱动
- 关键配置参数速查
- 技术债与未实现能力
二、整体执行模型:双层循环
XAgent 的核心编排器是 TaskHandler(XAgent/workflow/task_handler.py),它把一次用户 Query 的处理拆成两层:
scss
用户 Query
→ PlanAgent.initial_plan_generation() [PlanGenerateAgent]
→ while (下一个 TODO 子任务): [Outer Loop,中序遍历计划树]
inner_loop() [Inner Loop,ReACT + ToolAgent]
→ 推理 → 工具调用 → 观察结果
→ 直到 subtask_submit 或达步数上限
posterior_process() [ReflectAgent]
working_memory.register_task()
if need_for_plan_refine:
plan_refine_mode() [PlanRefineAgent,最多 N 步]
pop_next_subtask() [线性前进,不回溯]
Outer Loop 负责按子任务顺序推进全局计划;Inner Loop 负责在单个子任务内用 ReACT 范式反复「思考 → 行动 → 观察」。两层解耦是后续所有可靠性设计的基础:计划修改和工具执行不在同一个循环里打架。
三、Agent 全景:谁负责什么?
3.1 注册在 available_agents 的 4 个执行 Agent
在 XAgent/core.py 中注册:
| Agent | 能力枚举 | 核心职责 |
|---|---|---|
| PlanGenerateAgent | plan_generation |
将用户 query 分解为 2--4 个子任务的初始计划树 |
| PlanRefineAgent | plan_refinement |
子任务完成后,按 split/add/delete/exit 操作迭代修正未来子任务 |
| ToolAgent | tool_tree_search |
Inner Loop 内:推理 → 调工具 / subtask_submit |
| ReflectAgent | reflection |
子任务结束后提取后验知识(summary、plan reflection、tool reflection) |
3.2 元 Agent:DispatcherAgent
DispatcherAgent 不执行任务本身 ,而是为其他 Agent 按当前子任务动态生成附加 user prompt 。它通过 XAgentDispatcher.dispatch() 被调用:
- 取目标 Agent 的 example prompt
- 若
enable=True:调用DispatcherAgent.parse()生成定制化附加 prompt - 解析失败则 fallback 到默认 prompt
build_agent()实例化目标 Agent
注意 :当前默认配置 enable=False(core.py),生产路径直接使用静态 prompt 模板,不经 Dispatcher 动态 refine。
3.3 工作流编排类(非 BaseAgent,但承担 Agent 职能)
| 类 | 路径 | 职责 |
|---|---|---|
| PlanAgent | workflow/plan_exec.py |
计划生命周期:初始生成 →(可选)记忆 refine → plan_refine_mode |
| TaskHandler | workflow/task_handler.py |
主编排器:outer loop + inner loop + posterior_process |
| ReACTChainSearch | inner_loop_search_algorithms/ReACT.py |
单链 ReACT 执行器,驱动 ToolAgent |
| WorkingMemoryAgent | workflow/working_memory.py |
跨子任务记忆(chat_with_other_subtask 工具,handle 未完整实现) |
3.4 摘要模块:summarize.py(函数式,非 Agent 类)
| 函数 | 调用时机 | 作用 |
|---|---|---|
summarize_action() |
ReACT 每步 make_message() |
压缩工具调用历史,提取 key_actions |
summarize_plan() |
ReACT / PlanRefine / Reflect / TaskHandler 多处 | 按 token 预算压缩计划树 |
与 ReflectAgent 的分工:summarize 压缩原始轨迹 ,Reflect 输出结构化后验知识供下一子任务消费。
3.5 枚举存在但未实现的 Agent
task_evaluator:枚举在utils.py,无实现类summarization:有SummarizationTrieTree骨架,未接入主流程
四、为什么这么拆?设计哲学
4.1 能力分派,而非单体超级 Agent
每个 Agent 通过 RequiredAbilities 枚举声明单一能力集合,由 AgentDispatcher.agent_markets 按能力路由。好处:
- Prompt 专精:PlanGenerate 专注全局分解,ToolAgent 专注逐步执行,互不污染
- 可替换:同一能力可注册多个 Agent 实现,Dispatcher 选第一个
- 可测试:每个 Agent 的输入输出边界清晰
4.2 计划生成 vs 计划修正:两种认知任务
| PlanGenerate | PlanRefine | |
|---|---|---|
| 时机 | 任务开始 | 子任务完成后 |
| 权限 | 创建完整初始计划 | 只能修改 subtask_id > now_dealing_task 的未来任务 |
| 操作 | 一次性分解 | split / add / delete / exit,最多 3 步 |
| 约束 | 2--4 个子任务 | 树深 ≤ 3、树宽 ≤ 5 |
把「从零规划」和「局部修正」拆开,避免执行中的 Agent 随意改已完成任务,这是防扯皮的第一道防线。
4.3 执行与反思分离:ToolAgent 不自我辩论
ToolAgent 在 Inner Loop 只做三件事:思考、调工具、提交(subtask_submit)。它不修改计划 ,若发现后续子任务需要调整,通过 subtask_submit.suggestions_for_latter_subtasks_plan.need_for_plan_refine 把意图交给 PlanRefineAgent。
ReflectAgent 在子任务结束后单次调用 ,输出 action_list_summary、posterior_plan_reflection 等,供后续子任务读取。没有「ToolAgent 和 PlanRefine 同环对话」的设计,从根本上杜绝多 Agent 来回辩论。
4.4 FunctionHandler 统一中介
所有工具调用(含 intrinsic tools:subtask_submit、ask_human_for_help)都经 FunctionHandler.handle_tool_call() 路由,集中做:
- 命令分发
- 状态码映射(
ToolCallStatusCode) - ToolServer 超时重试
- 长结果压缩(
long_result_summary)
Agent 层不直接碰 HTTP,降低耦合。
4.5 summarize 独立:控制 Token 预算
ReACT 每步可能积累大量工具输出。summarize_action / summarize_plan 在写入 message history 前做压缩(clip_text、LLM 摘要),防止上下文爆炸导致 LLM 调用失败或成本失控。
五、工具调用可靠性:三层防护
5.1 LLM 层:重试 + 上下文降级
| 位置 | 机制 | 配置 |
|---|---|---|
ToolAgent.parse() |
@retry(stop=stop_after_attempt(max_retry_times)) |
max_retry_times: 3 |
openai.py chatcompletion_request |
tenacity 指数退避 61--293s;不重试 Auth/Permission/BadRequest | max_retry_times + 3 次 |
obj_generator.py chatcompletion |
Schema 校验失败重试 3 次 | 内置 |
上下文超长时自动降级到更大模型(gpt-4 → gpt-4-32k 等),见 openai.py 第 69--90 行。
此外,obj_generator.chatcompletion() 通过 recorder.query_llm_inout() 查缓存;experiment.redo_action: false 时优先用缓存,避免重复 LLM 调用。
5.2 Schema 层:幻觉工具名与坏 JSON 修复
obj_generator.py:
function_call_refine():校验 function name 是否在允许列表;幻觉工具名时注入 system error message 再抛FunctionCallSchemaError触发重试dynamic_json_fixes():broken JSON 时让 LLM 修复后重新校验load_args_with_schema_validation():参数 schema 失败时自动修复一次
ToolAgent.parse()(OpenAI 模式):
python
# tool_agent/agent.py 第 116--125 行
jsonschema.validate(tool_call_args, schema)
# 失败 → objgenerator.dynamic_json_fixes() → 再 validate
双重校验确保 LLM 输出的 function call 结构合法,减少「调了个不存在的工具」的情况。
5.3 ToolServer 层:状态码映射 + 超时重试
HTTP 状态码 → ToolCallStatusCode:
| HTTP | 状态码 | 含义 |
|---|---|---|
| 200 | TOOL_CALL_SUCCESS |
成功 |
| 404 | HALLUCINATE_NAME |
工具名不存在 |
| 422 | FORMAT_ERROR |
参数格式错误 |
| 450 | TIMEOUT_ERROR |
超时 |
| 500 | TOOL_CALL_FAILED |
执行失败 |
| 503 | SERVER_ERROR |
服务不可用(抛异常) |
超时重试 (function_handler.py 第 219--231 行):
python
MAX_RETRY = 10
while retry_time < MAX_RETRY and status == TIMEOUT_ERROR and detail['type'] == 'retry':
time.sleep(3)
# 用 detail['next_calling'] + detail['arguments'] 重试
ToolServer 可通过异常 type='retry' 告知客户端「这次超时但可重试」,最多 10 次、每次间隔 3 秒。超过上限则返回友好错误信息,不抛异常阻断主流程。
关键设计:失败不阻断 ReACT 循环
工具失败时,tool_status_code 写入 ToolNode.data,结果作为 system message 追加到 history,ReACT 继续下一步 (除非触发 subtask_submit)。Agent 可以从错误中学习,而不是一失败就 crash。
5.4 长结果处理
long_result_summary():网页类工具调用parse_web_text压缩;字符串 >2000 字符有 summarize 占位clip_text(file_archi, 1000)限制文件系统结构展示summary.single_action_max_length: 4096、max_return_length: 8192控制摘要输出
5.5 尚未实现的可靠性机制
- 无 Circuit Breaker:ToolServer 503 直接抛异常
TIME_LIMIT_EXCEEDED枚举存在但未使用tool_call_count在 TaskHandler 中初始化为 0 但从未递增- ToolServer 请求 timeout 硬编码 10--20s,非统一配置
六、防死循环与防扯皮
6.1 ReACT Inner Loop:硬步数上限
ReACT.py 第 220--312 行:
python
while now_node.get_depth() < config.max_subtask_chain_length: # 默认 15
...
if now_node.get_depth() == config.max_subtask_chain_length - 1:
function_call = {"name": "subtask_submit"} # 最后一步强制提交
if tool_output_status_code == SUBMIT_AS_SUCCESS:
break
elif tool_output_status_code == SUBMIT_AS_FAILED:
break
| 机制 | 值/行为 |
|---|---|
| 硬循环上限 | max_subtask_chain_length: 15 |
| 最后一步强制提交 | 不允许无限调工具 |
| Prompt 预算提醒 | "You can at most use {{max_length}} steps" |
| 多链重试 | max_try=1(默认不重试整条链) |
| 退出条件 | subtask_submit 的 success/failed |
尽管类名含 Search 且维护 TaskSearchTree,实际是单链而非树搜索分支------没有 backtracking,不会在同一子任务内反复探索多条路径。
6.2 Plan Refine:有限修正 + 显式退出
plan_exec.py plan_refine_mode():
python
while modify_steps < max_step: # max_plan_refine_chain_length: 3
...
if operation == 'exit':
output_status_code = PLAN_REFINE_EXIT
if output_status_code in (PLAN_REFINE_EXIT, MODIFY_SUCCESS):
return
modify_steps += 1
- 最多 3 步 refine
exit操作可随时退出- 一次
MODIFY_SUCCESS即 return - 计划树深度 ≤ 3、宽度 ≤ 5 在代码中强制校验
6.3 子任务单向推进:不回溯、不辩论
- 中序遍历 + 单向前进 :
Plan.pop_next_subtask()只取当前任务之后第一个TODO子任务,不回溯已完成任务 - Plan Refine 编辑边界 :
deal_subtask_split/delete/add中can_edit逻辑限制只能改未来子任务 - 无多 Agent 对话环:Reflect 仅在子任务结束后单次调用;ToolAgent 不与 PlanRefine 同环运行
这三条合在一起,解决了 Agent 系统里最常见的两类「扯皮」:
- 执行中反复改计划 → 执行与修正分离,修正有步数上限
- 多 Agent 互相推翻结论 → 单向流水线,后验知识只写一次
6.4 人机交互中断(非死循环,但可打断)
interaction.interrupt时用户可改写 goal(outer loop)或 thoughts(inner loop)ask_human_for_help是同步阻塞等待用户输入,然后继续循环------这是「等人」,不是 Agent 间辩论
6.5 上下文压缩(间接防无限膨胀)
enable_summary: true 时 ReACT 每步用 summarize_action / summarize_plan 压缩历史,避免 message 无限增长导致 LLM 反复因 context 超长而失败重试,形成隐性死循环。
七、关键配置参数速查
来源:assets/gpt-3.5-turbo_config.yml
yaml
max_retry_times: 3 # LLM 调用重试
max_subtask_chain_length: 15 # ReACT 单链子任务最大步数
max_plan_refine_chain_length: 3 # 计划修正最大迭代
max_plan_tree_depth: 3 # 计划树深度上限
max_plan_tree_width: 5 # 计划树宽度上限
max_plan_length: 4096
enable_summary: true # 上下文压缩开关
summary:
single_action_max_length: 4096
max_return_length: 8192
enable_ask_human_for_help: false
experiment:
redo_action: false # false = 使用 LLM/Tool 缓存
八、架构关系图
九、技术债与展望
| 缺口 | 影响 |
|---|---|
SummarizationTrieTree 未接入主流程 |
Trie 增量摘要能力闲置 |
task_evaluator 无实现 |
无法自动评估子任务质量 |
tool_call_count 未使用 |
无法做全局工具调用预算 |
| 无 Circuit Breaker | ToolServer 故障可能拖垮整条链 |
| Dispatcher 默认关闭 | 动态 prompt 能力未在生产路径启用 |
ReACTChainSearch 实为单链 |
命名与实现不符,无真正树搜索 |
WorkingMemoryAgent handle 未完整 |
跨子任务对话能力预留但未落地 |
十、总结
XAgent 的多 Agent 拆分遵循一条清晰原则:不同认知任务用不同 Agent,用硬上限和单向流水线约束协作边界。
- 可靠性:LLM 重试 + Schema 修复 + ToolServer 超时重试 + 失败写入 history 继续执行
- 防死循环:ReACT 15 步硬上限 + 最后一步强制 submit + Plan Refine 3 步上限
- 防扯皮:执行/修正/反思三阶段分离 + 子任务单向推进 + 只能改未来计划
如果你在做自己的 Agent 框架,最值得抄的三件事是:FunctionHandler 统一中介 、subtask_submit 驱动计划变更 、summarize 控制 token 预算。其余部分(Dispatcher 动态 prompt、WorkingMemory 跨任务通信)在源码里还是半成品,正好留给你改进的空间。