夜雨聆风学习资料网

ARTICLE · 1190984

open-Agent-Team源码阅读:一个多智能体运行时的兜底哲学

open-Agent-Team源码阅读:一个多智能体运行时的兜底哲学

前几篇分别整理了 multi-agent 与 sub-agent 的概念区别、Claude Code 里 Subagent / Fork / Teammate 三套机制、coordinator 模式怎么把指挥官的手捆住,以及一个第三方 DSH 插件怎么把"队长带团队"落成十件小事。

今天换一个方向,一个用 Python 实现的多智能体运行时。

它和前面几篇最大的不同是——它不依赖 Claude Code,是一个独立的 runtime。但作者在 README 里交代得很清楚,它是"基于 Claude Code 的 Agent Teams 模式所搭建的一个 multi-agent 系统"。也就是说,有人读懂了那套设计之后,试图用最朴素的技术栈把它重新实现一遍。

整个仓库大约 40 个 Python 文件。读下来最强烈的感受是:它的复杂度几乎全花在"防止 agent 犯傻"上,而不是"让 agent 更聪明"上。

一次不依赖 Claude Code 的重写

项目的自我定位是一个 multi-agent orchestration runtime。README 里列了它想解决的三件事:

  • 单 Agent 难以处理复杂、多步骤任务
  • 上下文窗口有限,容易溢出
  • 缺乏任务分工与并行执行能力

这三条,其实就是第一篇文章里讨论过的老问题。

结构很直白。team-lead 跑在主进程里,负责理解和编排;teammates 跑在独立子进程里,每个都是一个 multiprocessing.Process,daemon=True。角色分了五种:coder、researcher、tester、reviewer、architect,分别对应提示词里五段写好的行为规则——architect 负责先立契约,coder 负责实现,tester 负责验证,reviewer 负责挑刺,researcher 负责先把代码摸清。

它对 Subagent 和 Agent Teams 的区别总结得很干净:

  • Subagent:一个人使用多个工具
  • Agent Teams:一组 agent 分工协作

这跟第一篇文章的结论完全一致,只不过它是站在实现者的角度说的。执行模型也很熟悉:Plan → Parallel → Merge,Lead 拆解、多 Agent 并行、Lead 汇总。

团队的状态,全都在文件夹里

每次运行,会在项目目录下生成一个 .open_Agent_Team/,这就是整个系统的真实状态:

.open_Agent_Team/

├── teams/{team_name}/config.json       # 团队信息与成员表

├── tasks/{team_name}/task_*.json       # 任务看板,一个任务一个文件

├── inboxes/{team_name}/{agent}.json    # 每个 agent 一个邮箱

└── logs/{team_name}/{agent}.jsonl      # 活动日志

没有数据库,没有消息队列,没有共享内存。任务板的自增 ID 靠一个 _counter.json 维护;一张任务卡就是一个 JSON 文件,文件名是 task_<id>.json;一个邮箱就是一个 JSON 数组,每个 agent 一个文件。README 里那句话写得很直接:"任务卡住了?Worker 没响应?依赖没解锁?直接看这里。"

代价是并发安全得自己兜。所以它写了一个 FileLock,用 os.open 带 O_CREAT | O_EXCL | O_WRONLY 去创建 .lock 文件,谁创建成功谁就拿到锁:

self._fd = os.open(

    str(self.lock_path),

    os.O_CREAT | os.O_EXCL | os.O_WRONLY,

)

os.write(self._fd, str(os.getpid()).encode())

等锁超过 10 秒就放弃;如果发现锁文件的修改时间超过 60 秒,就判定它过期、直接抢过来——这是防止上一个进程崩了、把锁永远留在磁盘上。所有 JSON 写入统一走"临时文件 + rename"的原子替换,避免写到一半崩掉、留下半截文件。

这套设计简单到可以用记事本手动改任务状态,也脆弱到必须给每一次读写都套上一层锁。它的工程含量不在精妙,而在把朴素的东西用对。

派活不是丢一句话,是给一份带验收标准的简报

spawn_agent 是整个项目里字段最多的工具。它不是简单传一句任务描述,而是一份完整的 briefing,可以带这些字段:

mission / task_ids / owned_paths / required_reads /

deliverables / definition_of_done / quality_bar /

coordination_notes / startup_checklist / completion_report_template

光看名字就知道每一项在管什么:mission 一句话讲清你独占负责什么;owned_paths 划定你能改哪些文件;required_reads 是动手前必须先读的东西;definition_of_done 是"什么算做完";quality_bar 是质量标准;startup_checklist 是开工前的检查步骤。工具本身只把 name / agent_type / mission / task_description 四个设成必填,其余是强烈建议——但提示词里把话说得很重:

Treat each spawn_agent call as prompt engineering. Weak teammate briefs produce weak execution.

任务卡本身也是结构化的,字段是 goal / scope / deliverables / acceptance / constraints / interfaces / handoff。提示词里直接给了一组正反例。反面写法:

Implement frontend

正面写法:目标是"照着共享的 API 契约搭任务列表 UI",范围精确到三个文件,交付物是 API client 方法、列表视图、错误态、加载态,验收标准指明要用 /api/tasks、要为空的列表渲染出空状态。

对照上一篇 DSH 插件:那边是 persona 规则加提示词约束,这边是 schema 化的工具字段。思路不同,但落点一致——都把"把话说清楚"当成第一生产力,因为一个含糊的委派,会在下游放大成一堆返工。

依赖是推的,进度不是轮的

任务之间的依赖用 blockedBy 表示。这个项目处理依赖的方式,和上一篇 DSH 插件的调度器同源,但实现更朴素。

当一个任务被标记完成,TaskBoard.update_task 会顺手把它从所有其他任务的 blockedBy 列表里剔掉;带依赖的版本 update_task_with_deps 还会返回一份"刚刚被解锁"的任务清单。然后 TaskUpdateTool 拿这份清单,主动给这些任务的下游 owner 发一条 [TASK READY],消息里还额外附上这个任务的契约接口和 handoff 期望:

[TASK READY] Task #3 '...' is now unblocked and ready for you to work on.

Call task_get(task_id="3") for full details, then start working.

也就是说,下游 worker 不需要自己去问"我能不能开工了",它会收到推送。

配套的另一半是"别轮询"。提示词里反复强调:系统会在任务就绪时通知你,不要反复调 task_list。工具层也真的动过手——task_list 和 task_get 都带指纹去重,如果这次调用和上次比任务板没有任何变化,它不会重复返回全量,而是回你一句:

No task-board changes since your last task_list call.

Avoid polling task_list repeatedly. Wait for inbox updates...

把"别轮询"从一句提示词,做成一条代码里的反馈,治的是模型的毛病,用的是工具自己的记忆。

同一套思路还延伸到了 worker 的启动。worker 进程起来之后,不会立刻进入 LLM loop,而是在 _wait_for_actionable_task 里阻塞着,每 2 秒看一眼任务板,直到自己名下出现一个非 blocked 的任务才继续。这个函数的注释写得很直:

Block (zero LLM cost) until the agent has at least one non-blocked task.

依赖没就绪就进循环,等于让模型坐在那儿空转烧 token。与其让它等,不如让系统等——等的成本是零,转的成本是真金白银。

谁在管 Leader 的手

这个项目里有一处设计:spawn_agent 工具本身其实什么都没干。

它执行之后,只是返回一段 JSON,里面写着 {"action": "spawn_agent", ...}。真正去起子进程的,是 Leader 在主进程里 hook 住的那段逻辑——执行完工具之后,它会遍历这一批调用,发现是 spawn_agent 就真正去拉进程,然后检查"是不是所有未完成任务都有人认领了"。

工具只负责声明意图,真正的副作用由运行时来做。这样,"模型说它派活了"和"系统真的派活了"就是两件事,可以分别校验。

围绕这个校验,它做了一道"派发守卫":leader 跑完一轮,如果任务板上还有没分配 owner 的未完成任务,运行时就会往它上下文里插一条 [SYSTEM ORCHESTRATION RULE],大意是"这些任务还没有主人,在你结束这轮之前必须处理掉:要么用 spawn_agent 把它们派出去,要么明确改任务板让它们不再是无人认领的工作",然后逼它再跑一轮,最多重试 4 次。同时,一旦派发完成,会立刻触发 request_early_exit,让它马上收工,别在那儿傻等 worker。

监控一侧的逻辑也一样——靠上报,不靠猜。每个 worker 进程结束时都会自动给 team-lead 发一条收尾消息,内容不是干巴巴的"我结束了",而是把自己名下的任务按 completed / in_progress / blocked 分好类报上去;如果进程是崩溃退出的,消息里会直接写明"Agent 崩溃了,它名下的任务可能卡住"。leader 判断一个 worker 是正常收工还是出事,依据就是这条主动上报。

最后是提示词层面的一条硬规矩。leader 的角色说明里写着:

You do NOT write implementation code yourself.

规则清单里又强调了一遍:

NEVER write implementation code directly. Always delegate coding to a coder agent.

读到这儿,就能和之前 coordinator 那篇对上。两篇讲的是同一个母题——"管理者的手要被捆住",差别只在手段:Claude Code 是在工具层面把指挥官的读写执行能力直接没收,物理上做不到亲自下场;这个项目是在提示词加运行时守卫的层面去约束——先告诉它别写,再在它该派活没派活的时候把它拽回来。

剩下的全是兜底

崩溃与超时自愈。 进程退出被监测到之后,它会把那个 agent 名下还在 in_progress 的任务自动改成 blocked,并在描述里追加一段 [AUTO-RECOVERY] 注释,说明是哪个 agent 在什么状态下退出的,然后提醒 leader 重新分配。worker 自己的回合数用尽时也有一套兜底:把没做完的任务标成 blocked,通知 leader,走人。

催办。 如果一个 in_progress 的任务长时间没有任何状态变化(用一个 status + owner + updatedAt 拼成的"签名"来判断),leader 会主动给 owner 发一条 [STATUS CHECK]:你这个任务大概多久没动了,做完了就赶紧标 completed,卡住了就发消息说明并标 blocked。这条催办带冷却时间,防止刷屏。

轮次告警。 每个 agent 的回合数快到上限时——剩下 3 轮——系统会注入一条 [SYSTEM WARNING],要求它立刻把已完成的任务标记完成、给 leader 发一份最终总结、把做不完的标成 blocked。等于在"断电"之前留出收拾行李的时间。

上下文压缩。 当上下文超过上限的 80%、并且消息片段超过 7 段时,它会保留最开头的 1 段和最近的 6 段,把中间部分压成一段带 [CONTEXT SUMMARY] 的摘要。摘要里甚至塞了一句给模型的提醒——"不要去重读那些已出现在摘要里的、没有变化的文件"。

沙箱。 它有两条防线。命令层面有一张危险正则黑名单,rmdir、rm -r、del /s、带通配符的删除、shutdown、mkfs、sudo,以及"curl 管道给 shell"这种远程执行,全被拦下:

(re.compile(r"(^|\s)(sudo|su)\b", re.IGNORECASE), "Privilege escalation commands are blocked."),

(re.compile(r"curl\b.+\|\s*(sh|bash|pwsh|powershell)", re.IGNORECASE), "Piped remote shell execution is blocked."),

路径层面要求所有文件操作必须落在 project_root 里面,另外还单独拉了一个敏感目录黑名单:.ssh、.gnupg、.aws、.config、id_rsa 之类。

成本可见。 它内置了一张按模型前缀查价的表,opus、sonnet、gpt-4o、deepseek、qwen 各自的输入输出单价都列了,累计 token 会换算成估算成本;另外有一个 token_budget,一旦超了就直接停。

这一整节里,几乎没有一行代码是在让模型变强的,全是在给模型兜底。预等待、去重、守卫、催办、告警、压缩、沙箱——它们共同回答的是同一个问题:当一个 agent 开始犯懒、卡死、跑偏或者烧钱的时候,系统能不能自己接住。

写在最后

把这一篇放回系列里看,路径挺有意思:Claude Code 的官方设计是"应该长成什么样",DSH 插件是"产品化之后长成什么样",而这个 Python 运行时是"一个人从头实现一遍会长成什么样"。三种实现,同一个母题。

如果整篇只留一句话,大概是它的分工——提示词负责告诉 agent"该怎么做",运行时的职责是"当它没这么做的时候怎么办"。README 里有一句设计原则写得很清楚:

Prompt 负责行为引导,Runtime 负责状态管理、工具边界和自动协调。

把规则写进提示词,只是在希望模型照做;把规则写进运行时的守卫里,才是在保证事情会发生。

参考资料

[1] open-Agent-Team 源码(https://github.com/wangzuke/open-Agent-Team)

[2] open-Agent-Team 官方文档

相关学习资料