任务系统与多 Agent:从 Todo 到自主认领
多 Agent 看起来像"同时启动多个大模型",但真正困难的部分不是并发,而是任务、状态、通信、权限和文件冲突。
Learn CC 让我最清楚的一点是:
Todo、Task、Subagent 和 Agent Team 解决的是四个不同问题。
一、TodoWrite:提升规划能力,不增加执行能力
todo_write 本身不会读取文件,也不会执行命令。它只是让模型维护一张当前工作清单:
text
pending
in_progress
completed
典型流程:
text
先列步骤
→ 标记当前步骤 in_progress
→ 执行
→ 标记 completed
→ 进入下一项
它的价值是降低长任务中的注意力漂移。
例如,一个重构任务进行到测试失败时,模型很容易只顾修测试,忘记原始目标。Todo 可以持续提醒它当前任务边界。
但 Todo 有明显限制:
- 通常只存在当前会话内;
- 没有任务依赖;
- 没有并发认领;
- 不能作为多个 Agent 的共享事实。
所以 Todo 是"单个 Agent 的执行计划",不是完整任务系统。
二、Task System:持久化目标和依赖关系
Task System 需要把任务保存到磁盘或数据库:
json
{
"id": "task_001",
"subject": "实现用户表",
"status": "pending",
"owner": null,
"blockedBy": [],
"worktree": null
}
另一个任务可以声明依赖:
json
{
"id": "task_002",
"subject": "实现登录 API",
"blockedBy": ["task_001"]
}
只有依赖完成后,任务才能被认领。
Todo 与 Task System 可以同时存在:
| 维度 | Todo | Task System |
|---|---|---|
| 作用 | 当前执行步骤 | 全局任务事实 |
| 生命周期 | 当前会话 | 跨会话 |
| 依赖关系 | 无 | DAG/blockedBy |
| 多 Agent 认领 | 无 | owner/claim |
| 存储 | 内存 | 文件或数据库 |
任务依赖必须由代码判断,不能只让模型"记得先做哪个"。
三、Subagent:用独立上下文完成一次子任务
Subagent 的核心价值不是增加线程数量,而是隔离上下文。
主 Agent 可以委派:
text
请追踪这个函数的完整调用链,只返回结论和关键文件。
子 Agent 获得全新的 messages[],独立调用工具,完成后只把最终结论返回主 Agent。
这样,中间读取几十个文件产生的噪声不会污染主对话。
关键约束包括:
- 子 Agent 不应无限递归创建更多子 Agent;
- 子 Agent 仍然必须遵守权限系统;
- 返回结论,而不是返回全部历史;
- 设置最大循环次数和超时;
- 文件副作用需要被主 Agent感知。
Subagent 更像一次性的隔离进程。
四、Agent Team:持久队友需要消息系统
有些工作不是一次性委派,而是多个角色长期并行:
text
Lead
├── Backend Agent
├── Test Agent
└── Review Agent
这时仅返回一次结论不够,队友之间需要异步通信。
最小实现可以使用 Mailbox:
text
.mailbox/
├── lead.jsonl
├── backend.jsonl
└── test.jsonl
发送消息就是向目标邮箱追加一行 JSON:
json
{
"from": "backend",
"to": "lead",
"type": "message",
"content": "认证接口已经完成,等待测试。"
}
接收方读取并消费消息,再注入自己的上下文。
从工程角度看,Agent Team 本质上已经接近:
text
Actor Model + Message Bus + Shared Task Store
五、协议:不能只靠自然语言约定
普通消息适合传递信息,但关机、计划审批、权限确认等操作需要明确状态机。
例如安全关机:
text
Lead 发送 shutdown_request
→ 队友完成收尾
→ 队友发送 shutdown_response
→ Lead 更新请求状态
→ 队友退出
计划审批也类似:
text
队友发送 plan_approval_request
→ Lead 审查
→ 返回 approved/rejected
→ 队友决定是否执行
每个请求应有唯一 request_id:
json
{
"type": "plan_approval_request",
"request_id": "req_004281",
"status": "pending"
}
响应必须携带相同 ID,并校验响应类型,防止一条关机响应错误地批准了另一条计划请求。
协议的意义是把"大家应该这样做"变成确定性的状态转换。
六、权限冒泡:子 Agent 不能静默越权
如果队友在后台执行危险操作,而审批弹窗只存在它自己的隐藏线程中,用户就无法控制风险。
更合理的方式是权限冒泡:
text
子 Agent 请求危险工具
→ 请求发送给 Lead/主界面
→ 用户批准或拒绝
→ 结果返回子 Agent
→ 子 Agent 继续执行
因此权限系统不仅是工具前的一个函数,它还可能是一个双向协议。
七、自主智能体:本质更像有界 Worker Pool
Autonomous Agent 的关键不是"更聪明",而是空闲时主动扫描任务板:
text
WORK
→ 当前任务完成
→ IDLE
→ 检查 Mailbox
→ 扫描可认领任务
→ 原子认领
→ 回到 WORK
这和线程池非常相似:
| 线程池 | Autonomous Agents |
|---|---|
| Worker | Agent 实例 |
| Task Queue | 任务看板 |
| Runnable | Task |
| 线程池大小 | Agent 并发上限 |
| 抢占任务 | claim_task |
| 锁 | 任务文件锁 |
所以更准确的抽象是:
text
Actor 模型
+ 共享任务队列
+ 有界并发调度器
八、复用 Agent 不等于复用旧任务上下文
我最初的疑问是:Agent 完成一个任务后继续认领下一个任务,会不会造成上下文污染?
答案是:会,如果直接复用完整历史。
因此需要区分:
text
Worker 生命周期
≠
Task Context 生命周期
Agent Worker 可以长期存活,但每次认领新任务时应重新构建任务上下文:
text
稳定身份和权限
+ 当前任务描述
+ 必要项目背景
+ 相关记忆
+ 当前 Worktree
上一任务的临时日志、错误猜测和中间目标应被清理或压缩成必要摘要。
这类似线程池复用线程,但每个 Runnable 有独立的局部变量。
九、认领任务必须原子化
多个 Agent 可能同时看到同一个未认领任务:
text
Alice 读取 owner=null
Bob 读取 owner=null
Alice 写 owner=alice
Bob 写 owner=bob
最终会产生重复执行。
因此 claim_task 必须在锁内完成:
text
加锁
→ 重新读取
→ 检查 status、owner、依赖
→ 写入 owner 和 in_progress
→ 释放锁
仅仅在写入前检查一次 owner 不足以解决竞争条件。
十、冲突不能完全交给 LLM 兜底
即使任务分配正确,两个 Agent 仍可能修改同一个文件。LLM 可以尝试解决冲突,但 Harness 应优先减少冲突发生:
- 任务按模块切分;
- 明确文件所有权;
- 使用 Worktree 隔离;
- 合并前运行测试;
- 发生冲突时再由模型辅助处理。
确定性隔离优先于事后智能修复。
十一、总结
多 Agent 系统可以分为四层:
text
Todo 单个 Agent 的当前计划
Task System 共享任务事实和依赖图
Subagent 一次性隔离执行
Agent Team 长期并行协作
再加上:
text
Mailbox 消息通信
Protocol 状态机和请求响应
Permission 权限冒泡
Worker Pool 自主认领和并发限制
File Lock 原子任务认领
Worktree 文件修改隔离
多 Agent 的难点从来不是"调用多个模型",而是让多个不共享完整上下文的执行者,对同一组事实达成一致,并且不重复、不越权、不互相覆盖。