![]()
5個AI編碼助手同時跑,怎么知道誰該干什么、什么已經(jīng)有人干了、什么干完了?標(biāo)準(zhǔn)答案是消息總線。我試過,然后把它扔了——換成一個Markdown文件夾。6個月過去,我沒回頭。
消息總線的數(shù)學(xué)題:5個節(jié)點是10條線,20個節(jié)點是380條
消息總線架構(gòu)聽著優(yōu)雅:Agent發(fā)布、訂閱、協(xié)商任務(wù)、廣播狀態(tài)。CrewAI、AutoGen,帶"多智能體"標(biāo)簽的框架都這么干。
Agent A干完任務(wù)27,廣播"我完事了"。Agent B、C、D、E全收到。B搶任務(wù)28,再廣播"我的了"。剩下的人得更新狀態(tài),避免重復(fù)認(rèn)領(lǐng)。
5個Agent還能忍。10個是90對潛在通信。20個是380對。通信開銷按O(n2)膨脹,每條消息都是競態(tài)條件、狀態(tài)過期、更新丟失的溫床。
更糟的是看不見。協(xié)調(diào)狀態(tài)在飛行中——消息隊列、內(nèi)存緩沖、Agent上下文窗口里。出問題時,你在調(diào)試幽靈。
Batty的解法:把任務(wù)板寫進文件系統(tǒng)
我寫的Batty換了個思路。每個任務(wù)是一個Markdown文件,扔在目錄里:
.batty/board/tasks/
├── 027-add-jwt-auth.md # status: in-progress, claimed_by: eng-1
├── 028-user-registration.md # status: todo
├── 029-add-rate-limiting.md # status: backlog
└── 030-fix-dashboard-css.md # status: done
文件頭部是YAML,機器讀;正文是Markdown,人看:
id: 28
title: User registration endpoint
status: todo
priority: high
depends_on: [27]
claimed_by:
tags: [api, auth]
# User registration endpoint
Add POST /api/register with email validation,
password hashing, and duplicate detection.
## Done when
- Endpoint returns 201 with user object
- Duplicate email returns 409
- Tests cover happy path and validation errors
Agent不訂閱主題,不跟同伴談判。讀一個文件。一個文件,一次讀取,一個任務(wù)。O(1)。
調(diào)度算法:10秒輪詢,5步?jīng)Q策
Batty的守護進程跑輪詢循環(huán),每10秒掃一遍任務(wù)板:
1. 掃描任務(wù)目錄
2. 找出空閑Agent(沒活干的)
3. 對每個空閑Agent,找優(yōu)先級最高的任務(wù),條件是:
- status: backlog 或 todo
- 沒人認(rèn)領(lǐng)
- 沒阻塞
- 依賴已解決(depends_on里的任務(wù)都done了)
4. 更新任務(wù)文件:status → in-progress,claimed_by → eng-1
5. 啟動Agent,帶上任務(wù)上下文
這就是全部調(diào)度邏輯。優(yōu)先級隊列、依賴解析、死鎖檢測,都在文件系統(tǒng)里原子化完成。
消息總線那套?協(xié)商、廣播、狀態(tài)同步——全刪了。不需要最終一致性,因為只有一個真相來源:那個Markdown文件。
文件系統(tǒng)當(dāng)數(shù)據(jù)庫:原子寫、可見性、時間旅行
用文件系統(tǒng)聽起來像開倒車,但現(xiàn)代OS的文件操作比你想象的硬。
原子寫入:Batty用write-to-temp-then-rename模式。Agent先寫.task-28.md.tmp,寫完原子重命名為.task-28.md。系統(tǒng)崩潰也留不下半成品。
可見性保證:rename()成功瞬間,新內(nèi)容對所有讀者可見。沒有"寫入中"的灰色狀態(tài)。
時間旅行:Git提交就是快照。任務(wù)板的歷史全在.git里。上周三下午3點誰認(rèn)領(lǐng)了什么、為什么任務(wù)27卡了4小時,git log --all -- '*.md' 一目了然。
消息總線的歷史?在日志輪轉(zhuǎn)里,在ELK集群的保留策略里,在"哎呀?jīng)]開持久化"的懊悔里。
調(diào)試體驗:從"猜幽靈狀態(tài)"到"cat一下"
上周任務(wù)31卡住了。我干了什么?
cd .batty/board/tasks && cat 031-refactor-db-layer.md
YAML frontmatter告訴我:status: in-progress,claimed_by: eng-3,started_at: 2024-01-15T09:23:00Z。正文里Agent自己寫的筆記:"發(fā)現(xiàn)users表有循環(huán)依賴,需要拆成兩個遷移"。
消息總線架構(gòu)下呢?查日志。查哪個Agent的日志?它廣播"我認(rèn)領(lǐng)了31"了嗎?消息丟了嗎?還是Agent自己崩潰了沒發(fā)心跳?
在Batty里,調(diào)試就是讀文件。ls -lt按時間排序,最近改過的任務(wù)浮上來。grep -r "循環(huán)依賴" . 跨任務(wù)搜上下文。fzf交互式模糊搜索,3秒定位。
我把這叫做"可cat的架構(gòu)"。
擴展性實測:從5個到50個Agent
6個月里我的場景從5個Agent漲到峰值47個。文件系統(tǒng)扛住了。
瓶頸在哪?不是文件讀取——現(xiàn)代SSD隨機讀能到50萬IOPS,我的任務(wù)板撐死幾百個文件。是Git。47個Agent并發(fā)提交,.git/index鎖競爭明顯。
解法?批量提交。Agent不每完成一個任務(wù)就git commit,而是攢一批,或者讓守護進程統(tǒng)一提交。把O(n)的提交變成O(1)。
消息總線在這個階段會面臨什么?380對通信變成2200對,broker的內(nèi)存隊列開始告警,消費者組重平衡拖垮吞吐。我不用猜,去年在Kafka上踩過一模一樣的坑。
什么場景不適合
得誠實。文件系統(tǒng)協(xié)調(diào)有硬邊界。
跨機器不行。NFS、SMB、甚至云盤同步,都破壞原子性假設(shè)。Batty目前限定單節(jié)點,用Docker Compose綁在一臺機器上。
亞秒級響應(yīng)不行。10秒輪詢是設(shè)計選擇。任務(wù)粒度是"實現(xiàn)一個API端點"級別,不是"查詢向量數(shù)據(jù)庫"級別。后者用消息總線合理。
復(fù)雜工作流不行。DAG依賴 Batty 能處理,但條件分支、循環(huán)、動態(tài)任務(wù)生成?YAML frontmatter里塞不下,得正經(jīng)上工作流引擎。
我的判斷:AI編碼Agent的任務(wù)特性——粗粒度、可并行、依賴明確、需要人審——恰好落在文件系統(tǒng)的甜蜜區(qū)。
6個月后的真實數(shù)據(jù)
從2023年7月到現(xiàn)在,Batty調(diào)度了2,847個任務(wù)。平均完成時間從消息總線方案的4.2小時降到2.7小時。不是算法變快了,是調(diào)試時間少了。
任務(wù)沖突(兩個Agent認(rèn)領(lǐng)同一任務(wù))發(fā)生了3次。全是早期代碼bug:rename()前沒檢查文件是否存在。修了之后,零次。
消息丟失?零次。狀態(tài)不一致需要人工修復(fù)?零次。
Git倉庫大小:47MB。包含完整歷史、所有任務(wù)描述、Agent的中間筆記、甚至失敗的嘗試記錄。
給想試的人:最小可行版本
你不需要Batty。核心邏輯50行Python:
import os, yaml, glob, subprocess
from datetime import datetime
BOARD = ".batty/board/tasks"
def scan():
tasks = []
for path in glob.glob(f"{BOARD}/*.md"):
with open(path) as f:
content = f.read()
if '---' not in content: continue
_, frontmatter, body = content.split('---', 2)
meta = yaml.safe_load(frontmatter)
meta['path'] = path
tasks.append(meta)
return tasks
def claim(task, agent_id):
tmp = task['path'] + '.tmp'
with open(task['path']) as f, open(tmp, 'w') as out:
content = f.read()
new = content.replace('status: todo', f'status: in-progress\\nclaimed_by: {agent_id}')
out.write(new)
os.rename(tmp, task['path'])
def dispatch(idle_agents):
tasks = scan()
ready = [t for t in tasks if t.get('status') in ('todo','backlog') and not t.get('claimed_by')]
for agent in idle_agents:
if not ready: break
task = max(ready, key=lambda t: t.get('priority', 0))
claim(task, agent)
subprocess.Popen(['python', 'agent.py', '--task', task['path']])
ready.remove(task)
沒有依賴,沒有broker,沒有分布式系統(tǒng)的博士學(xué)位。
一個意外發(fā)現(xiàn):Agent開始寫更好的任務(wù)描述
消息總線時代,任務(wù)描述是"實現(xiàn)用戶注冊"。Agent收到后經(jīng)常問:密碼策略?郵箱驗證?速率限制?
換成Markdown文件后,Agent知道這文件會被下一個Agent、被我、被未來的自己反復(fù)閱讀。描述變長了,變具體了,"Done when" checklist成了標(biāo)配。
有個任務(wù)文件我印象很深。Agent eng-2在031-refactor-db-layer.md底部加了節(jié)"踩坑記錄":"別用Alembic的batch_alter_table,SQLite不支持外鍵重命名。用了2小時才發(fā)現(xiàn)。"
下一個接數(shù)據(jù)庫遷移任務(wù)的Agent,自動繼承了這2小時的經(jīng)驗。
消息總線能廣播經(jīng)驗嗎?能,但誰訂閱"我踩坑了"這個主題?
文件系統(tǒng)不講訂閱,講可見。ls一下,所有歷史攤在桌上。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.