cc Agent Teams 没用 AutoGen,怎么做的?

22 阅读8分钟

CC 源码系列 #3|CC 没用 AutoGen,自己写了 8000 行调度代码

上一篇《CC 记忆凭啥不用向量数据库》发出后,有读者留言问:Agent Teams 怎么实现的?有没有用 AutoGen 或者 CrewAI?这篇带你扒开 Claude Code 多智能体团队系统的源码,看看它底层到底是怎么调度的。


目录


一行框架代码都没用

我本来以为 Claude Code 的多智能体系统会包一层 AutoGen 或 CrewAI——毕竟现成的轮子摆在那里,不用白不用。

翻完源码之后,发现完全不是这回事。

CC 的 Agent Teams 实现在 packages/cli/src/services/teams/ 下,加上相关类型定义和工具层,合计接近 8000 行代码,一行现成框架都没有。

整个系统基于文件系统构建,没有数据库,没有消息队列,没有 Redis,没有 RPC。

读完的第一感受是:很多框架党想多了。


目录结构:就是一堆 JSON 文件

目录结构

CC Teams 的运行时目录长这样:

~/.claude/teams/<team-name>/
  members.json          # 团队成员注册表
  tasks.json            # 任务列表(全局共享)
  inboxes/
    team-lead.json      # team-lead 的收件箱
    agent-1.json        # agent-1 的收件箱
    agent-2.json        # agent-2 的收件箱
    ...
  state/
    <agent-name>.json   # 各成员运行时状态

这就是整个多智能体系统的"基础设施"——一个目录,几个 JSON 文件。

没有消息队列服务要启动,没有数据库要初始化,没有网络端口要监听。文件系统即协议。


核心机制:文件 inbox

inbox 机制

每个团队成员的"收件箱"是一个独立的 JSON 文件,路径为 inboxes/<name>.json

消息格式如下:

[
  {
    "id": "msg_abc123",
    "from": "team-lead",
    "to": "agent-1",
    "text": "开始处理任务 #3",
    "timestamp": "2026-04-07T10:00:00Z",
    "read": false
  },
  {
    "id": "msg_def456",
    "from": "agent-1",
    "to": "team-lead",
    "text": "{\"type\": \"idle_notification\", \"agent\": \"agent-1\"}",
    "timestamp": "2026-04-07T10:05:00Z",
    "read": true
  }
]

SendMessage = append 一条消息到目标 inbox 文件。

收消息 = 读文件,筛选 read: false 的条目。

协议消息(shutdown_request / idle_notification / plan_approval_request)也走同一个 inbox,靠 text 字段里的 JSON type 字段区分。没有单独的控制通道,没有优先级队列。

这个设计意味着:如果某个 Agent 卡住了,你可以直接 cat inboxes/agent-name.json 看消息有没有送到、有没有被 read。调试体验极其直接。


Agent 生命周期:Idle ≠ Dead

Idle 机制

这是整个系统最反直觉的一点。

处理完一个 turn 之后,Agent 进程不会退出。

它会:

  1. 向 inbox 发一条 idle_notification
  2. 进入轮询状态,持续读取自己的 inbox
  3. 检测到新消息后立即唤醒,开始处理下一个 turn

轮询间隔很短,唤醒是毫秒级的。

为什么不重启?重启的成本是重新加载所有上下文。 进程保活意味着 context window 里的内容还在,下一个任务可以直接衔接。这对于有状态的长期任务尤其重要。

所以当你看到一个 Agent 长时间没有输出时,不要以为它挂了——它可能只是在 idle,等消息。

正确的排查方式:直接 cat inboxes/<name>.json,看最后几条消息的 read 状态。


任务系统:无锁竞争设计

任务系统

任务存储在 tasks.json 里,格式大致如下:

[
  {
    "id": "task_001",
    "subject": "修复登录页面 Bug",
    "description": "用户反馈登录后跳转失败,复现路径:...",
    "status": "in_progress",
    "owner": "agent-1",
    "blockedBy": [],
    "blocks": ["task_002"]
  },
  {
    "id": "task_002",
    "subject": "更新登录相关文档",
    "status": "pending",
    "owner": null,
    "blockedBy": ["task_001"],
    "blocks": []
  }
]

没有分布式锁。

CC 的任务竞争靠的是应用层规范:team-lead 主动派活,不依赖 Agent 自主抢占。这规避了多 Agent 同时 claim 同一个任务的并发问题——因为 team-lead 是单点分派的。

blocker 依赖(blockedBy / blocks)由 Agent 手动维护。清理不及时会导致任务链卡住。依赖链越长,出问题的概率越高。这是这套设计里明确的 trade-off。


这套设计的 7 条代价

设计代价

这套方案听起来很美("就是几个 JSON 文件"),但不是没有代价:

1. 并发写冲突

多个 Agent 同时向同一个 inbox 文件 append 消息,存在并发写丢消息的风险。CC 没有加锁,靠的是写入频率较低(每次写入有 LLM 处理间隔)来规避。

2. 文件只增不减

inbox 文件会持续增长,没有自动 GC。长时间运行后需要手动清理,否则读取成本上升。

3. 崩溃恢复弱

进程崩溃后,read: false 的消息会被重新处理(at-least-once 语义)。没有持久化的消费位点,幂等性由上层业务保证。

4. 无横向扩展

整个系统假设单机运行。不支持跨机器的 Agent,不支持动态扩容。这是设计目标决定的,不是遗漏。

5. 依赖链手动维护

blockedBy / blocks 需要 Agent 自己更新。如果 Agent 忘记清理,或者清理时机不对,任务链会卡死。

6. 消息无优先级

所有消息 FIFO 处理,没有优先级队列。紧急任务没有插队机制,只能靠 team-lead 提前发送。

7. 状态可观测性差

没有统一的 dashboard,观察整个团队状态需要手动读多个 JSON 文件。这在调试复杂场景时比较费力。

CC 在源码注释里对这些问题都有所承认。这是典型的 "worse is better":牺牲严格正确性换简洁,对目标场景(单机、小团队、可审计)来说是合理的取舍。


与主流框架对比

框架对比

维度CC TeamsAutoGenCrewAI
依赖零(文件系统)Python 生态Python 生态
消息机制文件 inbox内存 / Redis内存
持久化自动(文件即状态)需要配置需要配置
可观测cat JSON 即可需要日志系统需要日志系统
横向扩展不支持支持支持
学习成本极低中等中等
目标场景单机、小团队、可审计通用、分布式通用、角色化

CC Teams 明显是为单机、小团队、可审计优化的。横向扩展不是它的目标,也不应该是你对它的期待。


从源码反推的 5 条使用技巧

使用技巧

1. 卡住直接 cat inbox

cat ~/.claude/teams/<team-name>/inboxes/<agent-name>.json | jq '.[] | select(.read == false)'

这一条比任何日志都直接。消息在不在、有没有被读,一目了然。

2. 别频繁重启 Agent

重启意味着 context window 清空。除非你确认当前上下文已经失效,否则 idle 状态的 Agent 不需要重启。

3. shutdown 也是消息

要关闭某个 Agent,发一条 shutdown_request 消息到它的 inbox,不要直接 kill 进程。优雅关闭能保证当前 turn 处理完成后再退出。

{
  "type": "shutdown_request",
  "request_id": "req_xxx"
}

4. team-lead 主动派活,不要等 Agent 自主抢占

任务分配收敛更快,也避免并发 claim 冲突。

5. 依赖链扁平化

blockedBy 链越长,出问题越多。能并行的任务尽量不加 blocker,单链深度不超过 3 层是个经验值。


我的实际运行场景

我自己跑的是一个 commander 团队:1 个 lead + 7 个成员。

成员分工:

  • xhs-ops:每天巡查小红书笔记评论,识别用户需求,上报 commander
  • juejin-ops:同步发布技术文章到掘金(就是正在做这件事)
  • github-ops:巡查开源项目的 Issue,自动派发给开发成员
  • cc-stats-dev:负责 cc-statistics 的功能开发和 Bug 修复
  • stock-dev:AI 投研分析服务,接收用户的股票分析请求
  • album-dev:KMP 相册应用开发

典型流程:

用户在小红书评论"能不能支持 CSV 导出?"
    → xhs-ops 识别 → 上报 commander
    → commander 评估 P2(体验改善)
    → 派发给 cc-stats-dev
    → cc-stats-dev 进 ~/Claude/cc-statistics 仓库实现
    → 完成后通知 commander
    → commander 通知 xhs-ops
    → xhs-ops 回复用户 + 发布更新帖

整套流程全靠 inbox + task list 驱动,没有额外的协调层。

有一次团队成员 idle 半小时,我以为挂了,直接 cat inboxes/xxx.json 一看——消息根本没送到,是 SendMessage 时写错了目标名。这种调试体验在看日志的框架里是复现不了的。


一句话总结

CC Teams 用文件 inbox 替代消息队列,用应用层规范替代分布式锁,用进程保活替代冷启动,用 JSON 文件替代状态数据库。Idle ≠ Dead,cat inbox 即调试,worse is better 是它的设计哲学。

如果你之前因为 AutoGen / CrewAI 太复杂没敢入坑,CC Teams 是个很好的起点——理解成本几乎为零,跑起来就是几个 JSON 文件。


延伸:用 cc-statistics 观察多 Agent 的 token 消耗

如果你在跑 CC Teams,cc-statistics 可以帮你统计各 Agent 的 token 消耗分布。

多 Agent 并行时,token 消耗会比单 Agent 高出好几倍。cc-statistics 支持按 session 拆分,能看到每个 Agent 的 input/output token 占比,方便评估成本。


下一篇预告

CC 源码系列 #4:Tool 系统——为什么一个 LLM 工具调用框架要写 1 万多行?

从 Bash、Read、Write、Edit、Glob 这些基础工具,到权限沙箱、超时控制、hook 系统,下一篇扒开看。