TL;DR
多 Agent 协作的核心难点不是「让 Agent 干活」,而是状态同步和死锁预防。本文给出一个基于 Kanban 的架构方案:每个 Agent 只负责一个泳道(Lane),任务通过 WIP 限制和状态机流转,避免 Agent 之间的竞态和资源争抢。核心设计要点:
- 泳道隔离:一个 Agent = 一个泳道,Agent 之间不直接调用
- WIP 限制:每个 Agent 同时只处理 N 个任务,超限则排队
- 状态机流转:任务状态转移通过原子操作保证一致性
- Dead Letter Queue:异常任务不阻塞管道,走 DLQ 兜底
一、问题:多 Agent 为什么比单 Agent 难 10 倍?
单 Agent 的世界很简单:你给一个任务,它调用工具,返回结果。多一个 Agent,问题就爆炸了:
Agent A: 正在修改 app/config.py
Agent B: 也准备修改 app/config.py
→ 💥 竞态条件,互相覆盖
Agent A: 等待 Agent B 完成前置任务
Agent B: 等待 Agent A 释放锁
→ 💥 死锁
Agent A: 崩溃了,任务状态卡在 "进行中"
其他 Agent: 永远等不到下一步
→ 💥 管道阻塞
本质问题是:Agent 之间没有共享的执行上下文,但操作的是同一套资源。这跟分布式系统的经典难题一模一样。
二、Kanban 架构:三条核心原则
2.1 泳道隔离
┌──────────────────────────────────────────────────────────┐
│ Kanban Board │
├────────────┬────────────┬────────────┬───────────────────┤
│ Planner │ Coder │ Reviewer │ Deployer │
│ (Lane 1) │ (Lane 2) │ (Lane 3) │ (Lane 4) │
├────────────┼────────────┼────────────┼───────────────────┤
│ 📋 待规划 │ 📝 待编码 │ 🔍 待审查 │ 🚀 待部署 │
│ │ │ │ │
│ → 分析需求│ → 实现代码 │ → Code │ → 部署验证 │
│ → 拆分子任│ → 跑测试 │ Review │ → 监控回滚 │
│ │ │ │ │
│ WIP: 3 │ WIP: 2 │ WIP: 2 │ WIP: 1 │
└────────────┴────────────┴────────────┴───────────────────┘
核心规则: - 一个 Agent 只属于一个泳道 - Agent 之间不直接调用,只通过 Board 传递任务 - 任务从左向右单向流动(不允许回退,回退 = 新建任务) - 每个泳道有 WIP(Work In Progress)上限
2.2 WIP 限制——防止系统过载
WIP 是整个架构最重要的保护机制。没有 WIP 限制的多 Agent 系统 = 把 100 个任务同时扔给 4 个 Agent = 上下文爆炸 + 互相覆盖。
class Lane:
def __init__(self, name: str, wip_limit: int):
self.name = name
self.wip_limit = wip_limit
self.in_progress: list[Task] = []
self.queue: list[Task] = []
def try_pull(self, task: Task) -> bool:
"""尝试接任务,WIP 满了就排队"""
if len(self.in_progress) < self.wip_limit:
self.in_progress.append(task)
task.status = TaskStatus.IN_PROGRESS
return True
else:
self.queue.append(task)
return False
def complete(self, task: Task, next_lane: "Lane"):
"""完成当前任务,推入下一泳道"""
self.in_progress.remove(task)
task.status = TaskStatus.READY
self._feed_next_lane(task, next_lane)
WIP 设置的黄金法则:
| Agent 类型 | 推荐 WIP | 原因 |
|---|---|---|
| Planner(分析需求) | 3-5 | 分析不修改文件,可以并行 |
| Coder(写代码) | 1-2 | 同一文件不能两个 Agent 同时改 |
| Reviewer(审查) | 2-3 | 审查不需要锁文件 |
| Deployer(部署) | 1 | 部署必须串行 |
2.3 状态机——每个任务只有一个主人
┌─────────┐
│ READY │ ← 任务进入泳道队列
└────┬─────┘
│ try_pull() 成功
┌────▼─────┐
│ IN_PROGRESS│ ← 只有一个 Agent 持有
└────┬─────┘
│ Agent 完成
┌────▼─────┐
┌──────┤ DONE │ → 推入下一泳道
│ └──────────┘
│
│ Agent 遇到不可恢复错误
│ ┌──────────┐
└──────► FAILED │ → 人工接管的 Dead Letter Queue
└──────────┘
状态转移必须是原子操作。用 Redis 的 WATCH + MULTI 或 SQL 的 SELECT ... FOR UPDATE 实现:
# Redis 原子拿任务
def atomic_claim_task(lane_id: str, agent_id: str) -> Optional[Task]:
"""只有一个 Agent 能拿到同一任务"""
# Lua 脚本保证原子性
script = """
local tasks = redis.call('ZRANGE', KEYS[1], 0, 0)
if #tasks == 0 then return nil end
local task_id = tasks[1]
redis.call('ZREM', KEYS[1], task_id)
redis.call('HSET', 'task:'..task_id, 'owner', ARGV[1], 'status', 'IN_PROGRESS')
return task_id
"""
return redis.eval(script, 1, f"lane:{lane_id}:queue", agent_id)
三、死锁预防——四个铁律
3.1 单向流转
任务只能从左向右移动:Planner → Coder → Reviewer → Deployer。
禁止回退。如果一个任务需要返工(比如 Code Review 不通过),不是把任务退回去,而是:
1. 当前任务标记为 FAILED
2. 创建新任务,放入正确的泳道
3. 新任务携带旧任务的上下文和 Review 意见
这样保证了状态机的 DAG(有向无环图)属性,从根本上避免了循环等待。
3.2 Agent 不等待 Agent
# ❌ 错误:Agent A 等 Agent B
result = agent_b.execute(subtask) # 死锁高发区
do_next_step(result)
# ✅ 正确:通过 Board 流转
# Agent A 只负责提交子任务到下一个泳道
board.push(subtask, lane="Coder")
# 然后继续处理自己的下一个任务
next_task = board.pull("Planner")
3.3 超时释放
每个任务必须带 TTL。如果 Agent 崩溃或卡死,超时后自动释放回队列:
# 定时扫描僵尸任务
def scan_zombie_tasks():
for task in redis.scan_iter("task:*"):
if task.status == "IN_PROGRESS" and task.elapsed > task.ttl:
task.status = "READY"
task.owner = None
redis.zadd(f"lane:{task.current_lane}:queue", {task.id: task.priority})
alert(f"Task {task.id} timed out, requeued")
3.4 Dead Letter Queue
不可恢复的错误不进主流程。每个泳道有对应的 DLQ:
def handle_failure(task: Task, error: Exception, lane: Lane):
if isinstance(error, RecoverableError):
task.retry_count += 1
if task.retry_count <= task.max_retries:
lane.requeue(task) # 重试
return
# 不可恢复 → DLQ
task.status = TaskStatus.DLQ
task.error_context = error.traceback
dlq.push(task)
notify_human(f"Task {task.id} moved to DLQ: {error}")
四、安全边界——Agent 隔离的第三条腿
安全专题的前两篇(Prompt Injection 防御、Tool Sandbox)解决了「外部不可信输入」的问题。多 Agent 场景下还有一个同样致命的问题:Agent 之间的互信不该是无条件的。
4.1 最小权限——每个泳道只给必要的工具
| 泳道 | 允许的工具 | 禁止的工具 |
|---|---|---|
| Planner | 读文件、搜索代码 | 写文件、执行命令 |
| Coder | 读/写 ~/projects/、run test |
操作 ~/.hermes/、网络出站 |
| Reviewer | 读文件、git diff | 写文件、执行命令 |
| Deployer | scp、systemctl restart | 任意读写(只跑部署脚本) |
4.2 互不信任——Agent 的输出永远是「数据」而非「指令」
Agent A (Coder): "代码写好了,Reviewer 你去审查吧"
Agent B (Reviewer): 收到的是 {task_id, diff_url}
→ 不会直接执行 A 的任何建议
→ 只从 Board 拉任务,不看 A 的私人消息
禁止 Agent 之间直接传消息。所有交互必须通过 Board(带状态验证)。
4.3 审计日志——不可抵赖
[2026-07-14 10:23:01] Task #1421 | Planner → Coder | Agent: plan-01
[2026-07-14 10:23:15] Task #1421 | CLAIMED by Coder agent: code-03
[2026-07-14 10:25:42] Task #1421 | DONE → Reviewer | 3 files changed
[2026-07-14 10:26:10] Task #1421 | CLAIMED by Reviewer agent: review-02
[2026-07-14 10:28:33] Task #1421 | FAILED → DLQ | Reason: 安全漏洞
每条记录不可篡改。出问题时,谁接的任务、做了什么、结果如何,一清二楚。
五、完整架构图
┌─────────────────────────────────────────────────────────────────┐
│ Kanban Board │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Planner │→│ Coder │→│ Reviewer│→│ Deployer│ │
│ │ WIP:3 │ │ WIP:2 │ │ WIP:2 │ │ WIP:1 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Redis (状态存储 + 队列) │ │
│ │ lane:{name}:queue (有序集合, 按优先级) │ │
│ │ task:{id} (哈希, 状态+owner+上下文) │ │
│ │ lane:{name}:dlq (DLQ 队列) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Agent │ │ Agent │ │ Agent │ │ Agent │ │
│ │ Planner │ │ Coder │ │ Reviewer │ │ Deployer │ │
│ │ #1-3 │ │ #4-5 │ │ #6-7 │ │ #8 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Dead Letter Queue (人工接管) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 审计日志 (不可变追加) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
六、跟「并行 Agent 一把梭」的对比
很多框架的做法是:把 10 个 Agent 同时启动,各自领活干,用一个「协调器」汇总结果。
| 维度 | 并行一把梭 | Kanban 流水线 |
|---|---|---|
| 文件冲突 | Agent A/B 同时改同一文件,互相覆盖 | 泳道隔离,同时只有 1 个 Coder 持有任务 |
| 死锁 | A 等 B,B 等 A | 单向流转,无循环依赖 |
| 故障恢复 | 一个 Agent 挂了,它的任务石沉大海 | TTL 超时释放 + DLQ 兜底 |
| 状态可见性 | 不知道谁在做什么 | Board 实时显示每个任务的状态 |
| 安全 | Agent 之间直连,输出即指令 | 隔离+审计,不可抵赖 |
| 吞吐量 | 高(10 个并行)但乱 | 中等,但可控、可预测 |
Kanban 方案牺牲了「理论最高吞吐量」,换来了「可预测性和安全」。对生产环境来说,后者远比前者重要。
七、实现建议:从最小可行开始
不需要一上来就建 Redis、写 Lua 脚本。最小可行方案 30 分钟就能跑:
# 最小 Kanban:文件系统 + 目录结构
board/
├── lane-1-planner/
│ ├── queue/ # 任务文件 = READY
│ ├── in_progress/ # Agent 把这个文件移到这里 = IN_PROGRESS
│ └── done/ # 移到这里 = DONE
├── lane-2-coder/
│ ├── queue/
│ ├── in_progress/
│ └── done/
├── lane-3-reviewer/
└── lane-4-deployer/
# 原子性用 os.rename() 保证(同文件系统 rename 是原子的)
def claim_task(lane: str, agent_id: str) -> Optional[str]:
queue_dir = f"board/{lane}/queue"
progress_dir = f"board/{lane}/in_progress"
tasks = sorted(os.listdir(queue_dir))
if not tasks:
return None
task = tasks[0]
src = os.path.join(queue_dir, task)
dst = os.path.join(progress_dir, task)
try:
os.rename(src, dst) # 原子操作!
return task
except FileNotFoundError:
return None # 被别的 Agent 抢走了
八、什么时候不需要 Kanban?
| 场景 | 结论 |
|---|---|
| 单 Agent,顺序执行任务 | 不需要。一个 Agent 串行执行就够了 |
| 多 Agent,但任务完全独立(互不修改共享资源) | 不需要。并行一把梭更高效 |
| 多 Agent,共享文件/数据库/配置 | 需要 Kanban。WIP 限制 + 泳道隔离是刚需 |
| 生产级 Agent 编排平台 | 需要的就不只是 Kanban 了——还需要 Kafka 消息队列、分布式锁、Service Mesh |
总结
三层理解:
| 层级 | 理解 |
|---|---|
| 初级 | Kanban 就是个任务板,Agent 从上面领任务 |
| 中级 | WIP 限制是防止 Agent 互相踩踏的关键机制,状态机用原子操作保证一致性 |
| 终极 | 多 Agent 协作的本质是分布式共识问题。Kanban 用「单向流转+泳道隔离+WIP 限制」这个约束集,把分布式共识降级为顺序一致性——代价是牺牲最高吞吐,换来确定性和安全 |
记忆锚点:一个泳道、一个 Agent、一个方向、一个 WIP 上限。
本文由 admin 原创,转载请注明出处。
评论
0