摘要
这不是一篇“某个字段填错了所以改一下就好了”的故障记录,而是一次典型的多层控制面误判案例:表面上,系统里的 agent 已经注册、路由配置也看起来正确、前端还能正常拉起新会话;实际上,真正承担派工动作的 `sessions_spawn` 链路依然持续失败。更具迷惑性的是,日志里出现了 `Gateway: agents.defaults.tools failed`,让排障方向一度偏向 tools 配置;与此同时,又因为旧 session 与新 session 在行为上呈现出不一致,团队短时间内误以为这是“缓存未刷新”或“会话绑定没切干净”的问题。
最终确认,问题根因并不在 `tools.agentToAgent.allow`,它并不是这条派工链路的关键控制面;真正决定某个 agent 能否继续向下分派子 agent 的,是 `agents.list.<agent>.subagents.allowAgents`。换言之,**路由是否能到达目标 agent**,和**目标 agent 是否被授权继续派工**,是两个不同层面的控制问题。前者通了,不代表后者正确;后者错误时,前端表面看起来仍可能“像是通的”。
本文完整复盘这次修复过程,包括:问题现象、错误假设、日志误导、配置层级陷阱、后台验证与前台验证如何配合、为什么旧/新 session 会制造“绑定错觉”,以及升级与回归测试应如何设计。对工程师与架构师而言,这次案例的价值不只在于改对了一个字段,更在于识别多-agent 系统里“控制面分层失配”这类故障的排障方法论。
引言
多-agent 系统最容易让人产生一种错觉:只要能看到 agent 列表、能发起路由、能生成新 session,系统就已经“差不多是通的”。这在单层调用模型里有时成立,但在带有分工派发、会话代理、权限继承和子代理授权的体系里,这种判断经常是错的。
本次故障恰好落在这个误区中心。系统中存在一个 `main`,它负责接收上游请求,并通过内部路由把任务分配给下游 agent。下游 agent 中,有的只是执行端点;有的则还要继续进行 `sessions_spawn`,把任务拆给更细分的子 agent。问题正出在这里:
- 从 `main` 看,目标 agent 能被路由到;
- 从配置文件看,agent-to-agent 工具似乎已经打开;
- 从前台表现看,新会话有时还能创建出来;
- 但真正的派工链路,仍然失败。
如果只看表面,这类问题很容易被归结为“配置还没生效”“旧 session 没清掉”“Gateway 读了旧版本”“前端展示和后台状态不同步”。这些猜测并非完全不合理,但都不是这次问题的核心。
真正值得关注的是:**在多-agent 架构中,一条业务链路常常跨越多个独立控制平面。你在 A 平面上看到的“允许”,并不等于 B 平面上的“允许”;而错误日志里提到的模块名,也未必就是导致失败的实际控制点。**
因此,这次修复的价值并不只是“找到一个正确字段”,而是确认了一种更稳健的排障范式:
- 先拆清链路;
- 再区分控制面;
- 然后分别做后台验证与前台验证;
- 最后通过新旧 session 对比,识别哪些现象只是历史绑定残留,哪些才是真配置生效。
架构背景
1. 系统角色分层
为了匿名化说明,本文统一使用以下角色名称:
- `main`:入口 agent,负责接收请求、判断意图、路由派发
- `agent1` ~ `agent7`:不同职责的下游 agent,其中部分 agent 还会继续调用 `sessions_spawn` 派发子任务
本次问题涉及的不是简单的“main 调 agentX”,而是更深一层的链路:
- 上游请求进入 `main`
- `main` 将任务路由给某个目标 agent
- 该目标 agent 在自己的上下文中再次执行派工
- 派工通过 `sessions_spawn` 创建或绑定新的子 session
- 子 session 对应的 agent 继续执行任务
因此,系统至少存在以下几层控制逻辑:
- **路由控制**:某个 agent 能不能被选中、能不能被调用
- **工具控制**:当前 agent 能不能使用某类工具
- **子代理控制**:当前 agent 被允许派给哪些下游 agent
- **会话控制**:spawn 出来的 session 绑定给谁、是否复用旧上下文、是否使用新的配置快照
而故障恰恰来自这些控制面的边界不清。
2. 为什么 `sessions_spawn` 容易制造错觉
`sessions_spawn` 与普通工具调用不同,它往往同时涉及三件事:
- 新建或绑定会话对象
- 选择目标 agent
- 把当前上下文、权限和执行环境一起传递下去
这意味着,一个“spawn 成功”的表象,并不能证明整条链路真正正确。可能出现的情况包括:
- session 创建成功,但目标 agent 不具备继续下派的权限
- session 绑定的是旧配置上下文,而不是当前修改后的 agent 定义
- 前台看起来切到了新 agent,实际后台仍在沿用旧的会话身份
- 路由命中了某 agent,但该 agent 的 `subagents.allowAgents` 未授权,导致二次派工失败
也正因为此,排障时如果只看“有没有新 session”“UI 上显示的 agent 名称对不对”,很容易得出错误结论。
3. 配置层级的潜在误导
本次故障涉及两类容易混淆的配置:
#### 3.1 工具允许项
例如:
- `tools.agentToAgent.allow`
这类字段从语义上看非常像“允许 agent 之间互相调度”,因此极具误导性。团队最早把它当成关键控制面,认为只要这里允许了,agent 就应该可以继续派工。
#### 3.2 agent 的子代理白名单
真正关键的,是:
- `agents.list.<agent>.subagents.allowAgents`
这个配置不是“工具是否存在”,而是“某个具体 agent 被授权把任务派给哪些 agent”。它的控制粒度更高,也更贴近 `sessions_spawn` 的实际语义。
如果没有这个字段,或字段内容不包含目标 agent,那么即使:
- 路由层面可达,
- agent-to-agent 工具表面已启用,
- 前端界面看起来能切换 session,
真正的派工仍然可能失败。
4. 为什么日志会带偏方向
故障期间出现了一个高频提示:
`Gateway: agents.defaults.tools failed`
从字面看,它像是在说“默认 tools 配置失败了”。这会自然引导工程师去怀疑:
- tools 段落格式不对;
- agentToAgent 的工具默认项没加载成功;
- Gateway 对 tools 做了 schema 校验但没通过。
但后续验证发现,这条信息本质上只是**路径层级错误**,即 Gateway 在读取 `agents.defaults.tools` 时发现该路径不符合预期或不存在。它并不直接等同于“agent-to-agent 派工失败的根因”。
换言之:
- 这条日志是真的;
- 但它不是这次派工失败的主因;
- 它属于“配置结构异常的伴随噪音”,而不是业务控制面的最终答案。
这类日志最危险的地方,不在于它错,而在于它“只说了部分真相”。
排障过程
第一阶段:从“表面配置正确”出发的错误自信
问题最早暴露时,现象非常典型:
- `main` 能看到多个 agent;
- 路由时目标 agent 可选;
- 某些前台操作会显示 session 已切换或新建;
- 但真正执行到多-agent 派工时,链路中断。
第一反应通常是检查几类显性配置:
- agent 是否注册成功;
- 路由配置是否包含目标 agent;
- tools 是否启用了 agent-to-agent 相关能力;
- Gateway 是否重载了最新配置。
这些检查的结果都“看起来没问题”。也正因为如此,团队最开始做了一个危险假设:**既然表面配置是对的,那失败大概率是生效时机、缓存、旧 session 复用、或者前后端状态不一致。**
这个假设听起来合理,但它把“配置看起来对”与“控制面真正对”混为一谈了。
第二阶段:误把 `tools.agentToAgent.allow` 当关键控制面
随着日志和配置文件被逐段核对,注意力逐渐集中到工具开关上。因为从命名直觉看,`tools.agentToAgent.allow` 很像是“允许 agent 相互调用”的总开关。
于是排障路径一度演变成:
- 检查这个字段是否存在;
- 检查它是否为允许状态;
- 怀疑 Gateway 是否只读取默认值而没有读取 agent 级别覆写;
- 进一步对照 `agents.defaults.tools` 与单 agent tools 定义的层级。
这个阶段的排障并非没有价值,它至少帮助确认了几个事实:
- tools 相关配置确实存在历史残留和层级混用问题;
- `Gateway: agents.defaults.tools failed` 确实说明某些路径写法不合法;
- 部分配置看似“被读到了”,但并不能解释为什么某些 agent 可以路由、却不能继续 spawn 子 agent。
真正的问题在于:团队把“工具是否打开”当成了“派工是否被授权”的同义词。事实上,这两者不是一回事。
可以把它理解为:
- `tools.agentToAgent.allow` 更像“系统里有没有这把刀”;
- `agents.list.<agent>.subagents.allowAgents` 才是“这个人被不被允许拿刀去做指定操作”。
前者存在,不等于后者授权。
第三阶段:被 `Gateway: agents.defaults.tools failed` 带偏
日志中的 `Gateway: agents.defaults.tools failed` 一度成为最强线索。因为它与“tools 配置”高度关联,团队自然将其视为阻断点。
但在进一步拆解后发现,这条日志的性质更接近:
- 某个配置路径层级写错;
- 或默认配置段写在了错误位置;
- Gateway 在解析默认 tools 配置时校验失败。
也就是说,它说明“有配置结构问题”,但不能推出“sessions_spawn 失败一定由它引起”。
这个阶段真正的突破,不是修掉了那条日志,而是意识到:
> 如果一个 agent 已经能够被路由命中,那说明至少在部分控制面上系统是通的。 > 如果它仍无法继续派工,那根因更可能位于“该 agent 的下游授权”而不是“全局 tools 总开关”。
这是排障方向第一次从“全局工具层”转向“agent 局部授权层”。
第四阶段:旧 session 与新 session 制造了“绑定错觉”
在修配置的过程中,另一个非常干扰判断的现象出现了:同样一组配置修改后,某些操作在旧 session 里仍然失败,而在某些新开的 session 里似乎又能走得更远;还有一些情况下,前台显示已经切换到了目标 agent,但后台的行为像是仍然沿用了旧身份。
这引发了几种常见猜测:
- 是不是 Gateway 没有完全重载;
- 是不是前端缓存了旧会话元数据;
- 是不是 session 复用时,agent 绑定信息没有更新;
- 是不是不同入口创建的 session 使用了不同配置快照。
这些猜测部分成立,但它们不是根因,而是**观察层被历史会话污染后的典型噪声**。
在多-agent 场景中,旧 session 与新 session 的最大区别不是“有没有 ID 变化”,而是:
- 是否在创建时读取了新配置;
- 是否继承了旧的 agent 身份或上下文;
- 是否沿用了旧的 spawn 授权状态;
- 前台展示名称是否真的等于后台执行身份。
如果没有把这些变量隔离开来,排障结论就会被“绑定错觉”污染:工程师会误以为某配置“有时生效,有时不生效”,但实际上比较的根本不是同一种运行对象。
第五阶段:从“路由是否可达”转向“该 agent 是否允许继续派工”
真正的转折点,是把问题拆成两个判断:
判断一:路由是否通
即 `main` 能否找到并把任务送到目标 agent。
这个层面如果是通的,说明:
- agent 注册没问题;
- 基础路由信息基本没问题;
- 至少不是“目标 agent 根本不存在”。
判断二:目标 agent 是否被授权继续派工
即目标 agent 在收到任务之后,能否通过 `sessions_spawn` 再次把任务分派给指定子 agent。
这一层才真正对应:
- `agents.list.<agent>.subagents.allowAgents`
当排障按这两个层面拆开后,很多之前彼此冲突的现象突然统一了:
- 为什么前台看起来能选中目标 agent?因为路由层是通的;
- 为什么实际派工还失败?因为子代理授权层没通;
- 为什么改 tools 有时似乎有点变化却不彻底?因为碰到的是伴随配置问题,不是根授权;
- 为什么新 session/旧 session 现象不同?因为它们读取或继承的上下文不一致。
到这里,根因已经基本浮出水面。

