多 Agent 协作:委托、调度与看板
delegate_task、cron、kanban 三大支柱,让 Hermes 从单兵变成团队
系列:通过 Hermes 探秘 Agent 工程 · 补遗篇 关联篇:安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事
问题:一个 Agent 干不了所有事
前 10 篇文章讲的都是"单个 Agent 如何运行得很好"。但真实场景中,单 Agent 有三个根本性瓶颈:
- 上下文爆炸:一个大型任务(比如审查 100 个文件)的中间工具结果会撑爆上下文窗口
- 时间跨度:有些任务需要等待(部署后验证、定时检查),一个持续运行的 Agent 不现实
- 并行度:多个独立子任务串行执行太慢,而一个 Agent 同一时间只能做一件事
Hermes 的解决方案是多 Agent 协作——不是让一个 Agent 变强,而是让多个 Agent 协同工作。它有三个支柱:delegate_task(子代理委托)、cron(定时任务)、kanban(看板调度)。
支柱一:delegate_task 子代理委托
核心思想
当你面对一个大项目(比如"重构这个 10 个文件的模块"),主 Agent 不亲自做,而是把任务分解成 3 个独立的子任务:
Main Agent
├─ delegate_task("审查 file1-3 的接口设计", role="leaf")
├─ delegate_task("审查 file4-6 的接口设计", role="leaf")
└── delegate_task("审查 file7-10 的接口设计", role="leaf")
三个子代理并行运行,各自用独立的上下文窗口。完成后,主 Agent 收集三份总结报告,整合成最终结果。
关键收益:主 Agent 的上下文窗口里只看到"委托了三件事"和"收到三份总结",看不到中间几百次工具调用的细节。
子代理隔离
每个子代理获得:
- 全新的空白对话——不继承父 Agent 的历史(防止上下文泄漏)
- 自己的 task_id——独立的终端会话、文件操作缓存
- 受限的工具集——禁止递归委托、禁止用户交互、禁止修改共享记忆、禁止写文件(
execute_code)
受限工具集的工程实现是白名单+“减法”:从父 Agent 已开启的工具里去掉 DELEGATE_BLOCKED_TOOLS 列表中的几个(delegate_task、clarify、memory、execute_code、cronjob)。
子代理类型
leaf(默认):专注工人——拿到任务,自己完成,返回总结。不能再 spawn 子代理。
orchestrator:协调者——拿到任务后可以自己分解成更小的子任务,委托给孙代理。但有深度限制(max_spawn_depth,默认 1),防止树形嵌套失控。
安全默认
子 Agent 在后台线程里跑,没有 TUI 交互。当执行到危险命令时,默认行为是自动拒绝——避免"子代理 hung 在 input() 上等待永远不会出现的确认"的 deadlock。
配置 delegation.subagent_auto_approve: true 可以切换到自动批准(opt-in)但会打印审计日志。
超时与心跳
子代理默认没有硬性超时——合理的重度任务可能运行很久。替代方案是心跳监控:
- 每 30 秒发送一次心跳
- 如果 15 个周期(450 秒)没有新的 API 调用 → 判定为卡住
- 如果 40 个周期(1200 秒)还在执行同一个工具 → 也判定为卡住
这比固定超时更灵活:快速失败的任务不会被误杀,真正的僵死任务也能被发现。
支柱二:Cron 定时任务
核心思想
有些任务不是即时响应的——“每天凌晨 3 点检查服务器状态”、“每周五下午生成本周总结”。Cron 是 Gateway 级别的定时任务系统。
两种执行模式
no_agent 模式:不调用 LLM。直接执行 script 字段的 shell 脚本,stdout 作为输出。适合纯运维任务(日志清理、备份、健康检查)。
agent 模式:用 LLM 执行自然语言 prompt。每一轮是独立的 Agent 运行,有独立的上下文。
Cron 任务通过 delivery 字段路由输出到任意渠道(Telegram、Discord、飞书……),实现了"任务执行"和"结果消费"的解耦。
Delivery 机制
Cron Job 的结果通过 gateway/delivery.py 路由到目标平台。关键设计:cron 输出不在 Gateway session 历史里——避免消息交替违规,也避免 cron 日志污染真正的对话。
Cron 审批的特殊规则
Cron Job 没有人在场确认。Cron 的审批模式由 approvals.cron_mode 独立控制,默认 deny。这是一个 fail-closed 的设计——无人值守时,宁可任务失败,也不能执行危险操作。
支柱三:Kanban 看板调度
核心思想
delegate_task 是"父 Agent 主动分解",Kanban 是"中央调度器自动分配"——一个多 Agent 的工作队列系统。
架构
Orchestrator(编排者)
│
├─ /kanban create "实现用户认证模块"
├─ /kanban assign task-001 worker-1
├─ /kanban list
│
▼
┌──────────────────────────────┐
│ SQLite 数据库 │
│ tasks / events / runs / subs │
└──────────┬───────────────────┘
│
┌─────┴─────┐
▼ ▼
Dispatcher Notifier
(每 60s) (每 5s)
│ │
▼ ▼
spawn worker Telegram /
Discord / Slack 通知
│
▼
Worker (hermes chat -q)
任务生命周期
unassigned → assigned → in_progress → completed
→ blocked(遇到障碍,等待人工介入)
每个状态变迁都记录在 SQLite 里,实现持久化 + 可审计。
Worker 完整启动流程
调度器(每 60s)→ 争抢文件锁(.flock Singleton 锁)→ 7 个步骤:
- 回收僵尸:清理上次 spawn 但已死掉的 worker PID
- 释放过期 claim:worker 超过 TTL 仍持有任务锁 → 强制释放
- 检测心跳超时:450 秒没有新的 API 调用 → 判定为卡住
- 检测进程死亡:worker 的 PID 已不存在(被 OOM / Ctrl+C)→ crashed
- 强制 max runtime:任务有最大运行时长限制,超时强制结束
- 重新计算就绪:检查依赖——前置任务全部完成 → 从
todo晋升到ready - 分配 worker:按优先级遍历 ready 任务 → 原子性 claim + spawn
每次 spawn 调用 subprocess.Popen(hermes -p <profile> chat -q)——一个完整的 Hermes 子进程。Worker 被 spawn 后和调度器不再通信,只通过数据库状态判断完成。
Worker 隔离
Kanban worker 不是普通 Agent——它们被 HERMES_KANBAN_TASK 环境变量打上"单一任务一辈子"的印记:
- 工具集受限——只有
kanban工具集 + 核心工具。不能操作其他任务 - session 固定——一个 worker 对应一个 task_id
- 工作目录隔离——每个 worker 的终端 CWD 指向任务的专属目录
- 任务所有权保护——
HERMES_KANBAN_TASK与参数里的 task_id 不匹配时,调用被拒绝(纵深防御:prompt injection 不能影响其他任务)
心跳与自动回收
调度器的心跳检测:如果 worker 卡住没有心跳 → 自动把任务标记为"orphan"(可被重新分配的孤儿)。kanban 任务可以被另一个 worker 接手,这是和 delegate_task 的最大区别。
核心对比:delegate_task vs kanban
类比
- delegate_task = 函数调用:传参数、等返回值、用完即弃
- kanban = 消息队列:发到队列、消费者异步处理、结果存在队列里随时可查
什么时候用哪个?
用户在线 + 任务小 + 结果要立刻汇总?
→ delegate_task
用户离线 + 任务多 + 失败要重试 + 状态要追踪 + 人工要介入?
→ kanban
核心区别
| delegate_task | kanban | |
|---|---|---|
| 同步/异步 | 同步——父 Agent 阻塞等子代理完成 | 异步——调度器周期扫描,spawn 后不管 |
| 临时/持久 | 临时——任务完成,子 Agent 销毁 | 持久——状态在 SQLite,随时可查 |
| 调度者 | 父 Agent 自己判断(声明式) | 中央调度器自动扫描 |
| 失败恢复 | 父 Agent 自己重试 | 调度器自动回收/重试/重新分配 |
| 审计跟踪 | 无 | 完整——每次运行/失败/event 都有日志 |
| 人工介入 | 不支持中断/暂停 | 支持 blocked 状态,人工 /kanban unblock |
六大 kanban 使用场景
① 长任务需要持久化
“这个项目有 8 个模块要重构。“用户关了终端,子 Agent 完成了也找不到父 Agent 汇报。kanban 状态存在数据库里,用户下次上线时通过通知收到结果。
② 任务需要人工中间介入
“review 这个 PR → 如果有问题 block → 等待开发者修复 → 修复后重新 review”。delegate_task 无法"暂停,等明天人工修复后再继续”。kanban 支持 blocked 状态。
③ 优先级队列 + 路由
5 个 bug 修复(高优先级)、3 个 feature 开发(中优先级)、不同 assignee 路由给不同 profile。
④ 大量并行超出 delegate_task 并发限制
默认 delegate_task 最多同时跑 3 个子代理。大项目可能有 20+ 个独立子任务。kanban 的 max_spawn / max_in_progress 灵活控制全局并发度,每种 profile 也有独立并发限制(max_in_progress_per_profile)。
⑤ 审计跟踪
“这个任务是谁做的?做了多久?失败了几次?为什么被 block?"——kanban 数据库里每次运行都有 event 日志。
⑥ 自动分解(optional)
用户在看板里创建 triage 任务"优化项目性能”,辅助 LLM 可以自动将其分解为"分析 CPU 瓶颈”、“分析内存使用”、“优化数据库查询"等子任务(auto_decompose 功能)。
通知机制
Notifier Watcher 每 5 秒扫描数据库里的事件表。当发现新事件(completed / blocked / crashed),沿着订阅表找到"谁要被通知到这个 chat” → 通过对应平台适配器发消息。投递后推进 cursor 去重——不会出现重复通知。
三支柱对比
| delegate_task | cron | kanban | |
|---|---|---|---|
| 触发方式 | LLM 判断 | 时间 / 人工 | 看板调度器 |
| 执行者 | 子 Agent 实例 | Agent / Script | Worker Agent |
| 上下文 | 全新独立 session | 全新 session | 固定 task 专属 |
| 生命周期 | 任务完成即销毁 | 持久化 | 持久化 |
| 交互方式 | 同步(父等待子完) | 无人值守 | 无人值守 |
| 失败恢复 | 无(父 Agent 自己重试) | 重试策略 | 自动重新分配 |
工程启示
1. 并发不只是性能,是功能
execute_code 通过 RPC 让模型可以使用工具,主要价值不在性能——它让 LLM 可以做"有副作用的事"而不污染上下文。并发执行之于 Agent 不只是快,是让单 Agent 变成多 Agent 组织的前提。
2. 隔离是信任的基础
子代理不继承父 Agent 历史、kanban worker 不能操作其他任务——这些隔离不是"不信任模型",而是给 prompt 攻击设置纵深。
3. 声明式编排
Hermes 没有"多 Agent 协调框架"的硬编码流程。delegate_task / cron / kanban 都是工具,由 LLM 自己决定什么时候用。场景变了,不需要改框架,换一个 prompt 就够了。
4. 人类退出,系统继续运转
kanban 和 cron 对"无人值守"的特殊处理提醒我们:设计 Agent 系统时,不是所有人都在看屏幕——系统必须有能力在人类不在场时安全运行。
本系列的完结
补写了这最后一篇后,系列的完整地图如下:
| # | 主题 | 核心工程概念 |
|---|---|---|
| 1 | Agent Loop | 主循环、迭代预算、中断 |
| 2 | 工具系统 | 自注册、toolset、并发执行 |
| 3 | System Prompt | Stable/Semi-Stable/Volatile 三层缓存 |
| 4 | 上下文压缩 | 边界对齐、结构化摘要 |
| 5 | 记忆系统 | 内置记忆、外部 Provider 语义查询 |
| 6 | 工具调度 | registry 单例、搜索桥延迟加载 |
| 7 | 安全防护 | 工具守卫、审批、结果清洗 |
| 8 | Gateway 网关 | Session Key、双层守卫 |
| 9 | Provider 抽象层 | Profile 声明式、Fallback 链 |
| 10 | 沙箱与代码执行 | RPC 存根、环境变量清洗 |
| 11 | 技能系统 | 程序化记忆、渐进式披露 |
| 12 | 多 Agent 协作 | 委托分解、看板的声明式编排 |
从循环到调度、从安全到扩展、从记忆到经验——这就是 Hermes 的 Agent 工程全景。