10 · 委派与多智能体

tools/delegate_tool.py,5,071 行。这是 Hermes 里最长的单个工具文件 —— 比整个审批系统的核心还长。这一章讲一个智能体怎么派另一个智能体去干活。

10.1 为什么需要委派

任务:"把这个项目的 30 个模块都加上类型注解" 不委派: 主智能体自己做 30 遍 → 每读一个模块,上下文就长一截 → 读到第 12 个模块时上下文爆了,触发压缩 → 压缩把前 11 个模块的细节丢了 → 后面的工作质量下降 委派: 主智能体派 30 个子智能体,每个只管一个模块 → 每个子智能体的上下文只有它自己那一个模块 → 主智能体只收到 30 条"完成了/失败了"的摘要 → 主智能体的上下文始终很短

委派的本质是「上下文分区」。

它不是为了「并行更快」(虽然确实更快),核心价值是:让每个子任务在一个干净、专注、不会被无关信息污染的上下文里执行,而主智能体只承担协调成本。

10.2 深度限制:只允许一层

MAX_DEPTH = 1

这一行是整个多智能体系统里最重要的一个常量。它的意思是:主智能体可以派子智能体,但子智能体不能再派孙智能体。

如果不限制深度会怎样

假设每个智能体可以派 10 个子智能体,不限深度: 深度 0:主智能体 1 个 深度 1:子智能体 10 个 深度 2:孙智能体 100 个 深度 3:曾孙智能体 1,000 个 深度 4: 10,000 个 ★ 指数爆炸。每一个都在烧钱、占内存、发 API 请求。 ★ 而且没有任何一个环节会"觉得不对" —— 每一层都只是 在做它被设计要做的事情。 更糟的是:调试时你根本不知道是哪一层出的问题, 因为日志里有一万个智能体在同时说话。

这是一个「用最简单的手段消灭一整类问题」的典型例子。

想做「智能地限制递归」很难:要估算成本、要判断任务复杂度、要有熔断机制、要有预算传递……

MAX_DEPTH = 1 一行代码就让整类问题不存在了。代价是失去了「深层任务分解」的能力 —— 但实践中,一层委派已经覆盖了绝大多数场景,而两层带来的复杂度是指数级的。

在做架构设计时,先问「能不能用一个硬限制消灭这类问题」,再考虑「怎么智能地处理这类问题」。

10.3 并发限制

_DEFAULT_MAX_CONCURRENT_CHILDREN = 10
_RECENT_SUBAGENTS_CAP = 200
常量作用
_DEFAULT_MAX_CONCURRENT_CHILDREN = 10 同时最多跑 10 个子智能体。第 11 个要排队。防止一次性打爆模型供应商的速率限制,也防止本机内存和文件句柄耗尽
_RECENT_SUBAGENTS_CAP = 200 「最近的子智能体」记录最多保留 200 条。这是给「查看子智能体状态」这类功能用的历史缓冲,超出就丢弃最老的。防止长时间运行的会话把内存吃光

这两个数值配合 MAX_DEPTH = 1,把整个多智能体系统的资源占用锁在一个可预测的范围内:任意时刻最多 1 + 10 = 11 个智能体在跑,历史记录最多 200 条。

10.4 子智能体的工具限制

DELEGATE_BLOCKED_TOOLS

子智能体不能使用某些工具。最重要的一条是:子智能体不能再调用 delegate 工具 —— 这是 MAX_DEPTH = 1 在工具层面的强制实现。

注意这是「双重保险」:

· 逻辑层MAX_DEPTH = 1 在委派时检查深度
· 能力层DELEGATE_BLOCKED_TOOLS 让子智能体根本看不到 delegate 这个工具

第二层更彻底 —— 模型连「我可以委派」这个念头都不会有,因为工具列表里没有。不给能力,比给了能力再拦截更可靠。这和第 4 章「webhook 只投放 4 个只读工具」是同一个思路。

10.5 子智能体的审批策略

def _subagent_auto_deny(...)
def _subagent_auto_approve(...)

这里有一个必须解决的问题:子智能体跑起来后要求审批,谁来批?

场景:主智能体派了 10 个子智能体去改代码 子智能体 #3 想执行 `rm -rf build/` ↓ 审批系统说:"这需要人工确认" ↓ 可是…… · 用户可能不在(半夜跑的定时任务) · 就算在,10 个子智能体同时问,用户会疯 · 主智能体在等结果,全部卡住 → 需要一个"不问人"的策略
函数语义
_subagent_auto_deny 自动拒绝。子智能体收到「被拒绝」的结果,它可以换个方式做,或者报告失败。安全但可能卡住任务
_subagent_auto_approve 自动批准。不问人直接放行。能跑通但风险大

这是自动化系统里最难的一个权衡。

自动拒绝是安全的默认,但会让很多合法任务失败 —— 而且失败方式很隐蔽(子智能体报告「我做不到」,但真实原因是权限被拒)。

自动批准能跑通,但意味着红线之外的所有操作在无人监督下执行。第 5 章那 12 条红线依然生效(那是绝对禁止的),但「需要确认」这一档就被跳过了。

正确的做法是:让主智能体在委派时显式声明子智能体的权限档位,而不是有一个全局默认。做「只读分析」的子智能体应该自动拒绝一切写操作;做「批量重构」的子智能体则需要预先授权写文件。

10.6 运行中的控制

子智能体不是「发出去就不管了」。有三个控制接口:

def interrupt_subagent(...)    # 中断某个子智能体
def steer_subagent(...)        # 向运行中的子智能体注入指令
def set_spawn_paused(...)      # 暂停/恢复新子智能体的派生
接口用途
interrupt_subagent 发现某个子智能体走偏了 / 卡住了 / 在烧钱,单独把它停掉,不影响其他 9 个
steer_subagent 不打断,但插一句话。对应第 3 章讲过的 /steer 机制 —— 把新指令注入到最后一条工具消息里,让智能体在下一轮就能看到。
比如:「顺便也检查一下类型注解」
set_spawn_paused 暂停派生新的子智能体,但已在跑的继续。用途:发现整批任务方向不对时,先止血 —— 不再派新的,让已经跑起来的自然结束,然后重新规划

set_spawn_paused 这个设计值得单独说。

最朴素的做法只有「全部继续」和「全部杀掉」两种。但实际场景里最常见的是第三种:「别再开新的了,让手上的跑完」

这在运维上叫「排空」(drain)—— 优雅停机、滚动更新、限流降级都是这个模式。一个成熟的并发系统必须区分「停止接受新工作」和「终止现有工作」。

10.7 亲缘关系检查

def _is_descendant_of(..., max_hops: int = 8)

「判断智能体 A 是不是智能体 B 的后代」。用途:

那个 max_hops = 8 是防御性的。既然 MAX_DEPTH = 1,理论上最多只需要查 1 跳。设成 8 是为了:

这是「即使不变量被破坏,程序也不能挂死」的写法。

正常情况下永远不会走到第 8 跳。但如果某个 bug 导致父子关系成了环,有这个上限的版本会返回一个(可能错误的)答案并继续跑,没有上限的版本会把整个进程挂死

在遍历任何「理论上应该是树,但数据由运行时构造」的结构时,都要加这样一个跳数上限。

10.8 看板:智能体之间的协作

plugins/kanban/ 提供了另一种多智能体模式 —— 不是「派下去等结果」,而是「共享一块任务板」

kanban_create_task       创建任务
kanban_claim_task        认领任务
kanban_update_task       更新进度
kanban_complete_task     完成任务
kanban_list_tasks        查看任务列表
kanban_heartbeat         ★ 心跳
看板模式: ┌───────────────── 共享任务板 ─────────────────┐ │ #1 [待认领] 重构 auth 模块 │ │ #2 [进行中] 写单元测试 ← agent-B 认领 │ │ #3 [待认领] 更新文档 │ │ #4 [已完成] 修复登录 bug ← agent-A 完成 │ └──────────────────────────────────────────────┘ ↑ ↑ ↑ agent-A agent-B agent-C (各自独立,通过任务板协调,没有父子关系)
委派模式看板模式
关系父子 —— 主智能体明确指派对等 —— 谁有空谁认领
谁决定做什么主智能体各个智能体自己
生命周期子智能体做完就结束智能体长期存在,持续认领新任务
适合已知的、可分解的批量任务持续的、来源不定的工作流

kanban_heartbeat 为什么必须存在

agent-B 认领了任务 #2 ↓ agent-B 崩溃了 / 进程被杀 / 机器重启 ↓ 任务 #2 永远停在 "进行中" ↓ 没有别的智能体会去做它 —— 因为看起来"有人在做" ↓ ★ 任务永久丢失 解决:agent-B 每隔 N 秒调一次 kanban_heartbeat → 超过 N×k 秒没心跳,任务自动回到 "待认领"

任何「认领 - 执行 - 完成」的分布式任务系统,都必须有心跳或租约(lease)机制。

否则「认领了但没做完就死掉」的任务会永久卡住。这在消息队列、任务调度器、分布式锁里是同一个问题,解法也一样:认领是有时效的,需要持续续期。

10.9 委派系统为什么有 5,071 行

回到开头那个数字。真正做「派一个子智能体」的核心逻辑可能只要 200 行。剩下 4,800 行在做什么?

类别内容
生命周期管理创建、启动、监控、中断、清理、超时、僵尸回收
并发控制并发上限、排队、暂停派生、优先级
结果聚合收集 10 个子智能体的结果、处理部分失败、超时的怎么算
状态查询「现在有几个在跑」「花了多少钱」「卡在哪一步」
控制通道中断、注入指令、暂停 —— 每个都要跨进程/跨线程安全地送达
安全边界工具屏蔽、审批策略、深度检查、亲缘关系
可观测性每个子智能体的日志、成本、耗时都要单独记录并能关联回父任务
失败处理子智能体崩溃、模型报错、上下文爆炸、无限循环 —— 每种都要有对策

「让智能体调用智能体」的 demo 是 20 行,生产系统是 5,000 行。

这个 250 倍的差距全部来自「出问题时怎么办」。多智能体系统的难点从来不是「怎么派」,而是「派出去的东西失控了怎么收场」。

面试里如果被问到多智能体,能说清楚这一点,比能画出漂亮的架构图有用得多。