RELIABILITY PATTERN
多 Agent 协作隔离¶
When to Use
- 多个 Agent 并行写同一目录
- 共享 API 限流额度
- Cron 任务同时触发
When NOT to Use
- 单 Agent 顺序执行
- 无共享资源的独立任务
Skill Contract(本模式的模块声明)
版本 v1.0.0 | 分类 conventions | 相关 state-file-pattern · cron-job-pattern · maker-checker
对应 12-Factor Agents Factor 3: Own your own state
适用场景: 多 Agent 并行运行时的资源竞争防护
核心原则¶
并行 Agent 的第一风险不是逻辑错误,而是静默数据损坏 —— 两个 Agent 同时写一个文件,最后谁覆盖了谁,日志里看不出来。
资源竞争场景¶
1. 文件写入竞争¶
多个 Cron 任务或 Agent 同时写 reports/、STATE.md 或共享配置文件:
Agent-A ──write──> reports/weekly.md ──┐
├── 文件内容不确定(取决于谁最后写)
Agent-B ──write──> reports/weekly.md ──┘
症状: 报告内容混杂、STATE.md 字段被回退、JSON 格式损坏。
2. API 限流竞争¶
多个 Agent 共享同一个 API Key,各自独立计数,集体触发 rate limit:
症状: 批量 429 错误、指数退避导致任务超时、部分请求静默丢失。
3. 共享数据库/缓存竞争¶
多个 Agent 同时读-改-写同一个 Redis key 或 JSON 文件:
Agent-A read(amount=100) ──> write(amount=90)
Agent-B read(amount=100) ──> write(amount=80) # A 的修改被覆盖
症状: 经典 read-modify-write race condition。
隔离策略¶
策略一: 命名空间隔离¶
每个 Agent 拥有独立的工作目录,彻底消除写入冲突:
shared-data/
├── agent-market-watcher/
│ ├── STATE.md # 独立状态
│ └── reports/
│ └── weekly.md
├── agent-price-tracker/
│ ├── STATE.md
│ └── reports/
│ └── weekly.md
└── agent-news-curator/
├── STATE.md
└── reports/
└── weekly.md
规则: Agent 名称即目录名,mkdir -p 即授权,无需显式锁定。
策略二: 文件锁 (Lock File Pattern)¶
当必须写入共享资源时,用 .lock 文件做互斥:
import fcntl
import time
import json
from pathlib import Path
class FileLock:
"""基于 fcntl 的文件锁,支持超时。"""
def __init__(self, path: str, timeout: float = 30.0):
self.lock_path = Path(f"{path}.lock")
self.timeout = timeout
def __enter__(self):
start = time.monotonic()
while True:
try:
self.fd = open(self.lock_path, "w")
fcntl.flock(self.fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
self.fd.write(json.dumps({
"pid": __import__("os").getpid(),
"acquired": time.time()
}))
self.fd.flush()
return self
except (IOError, OSError):
if time.monotonic() - start > self.timeout:
raise TimeoutError(
f"Lock timeout after {self.timeout}s: {self.lock_path}"
)
time.sleep(0.1)
def __exit__(self, *exc):
try:
fcntl.flock(self.fd.fileno(), fcntl.LOCK_UN)
self.fd.close()
self.lock_path.unlink(missing_ok=True)
except Exception:
pass
# 使用示例
with FileLock("/shared-data/reports/weekly.md"):
data = Path("/shared-data/reports/weekly.md").read_text()
# ... 修改 data ...
Path("/shared-data/reports/weekly.md").write_text(data)
超时原则: 默认 30 秒,超时即报错而非无限等待 —— 避免死锁静默卡死。
策略三: 令牌桶限流 (Token Bucket)¶
共享 API 限流池,各 Agent 公平分配额度:
import time
import threading
class TokenBucket:
"""共享令牌桶,多 Agent 公平限流。"""
def __init__(self, rate: float, capacity: int):
"""
rate: 每秒补充的令牌数
capacity: 令牌桶最大容量
"""
self.rate = rate
self.capacity = capacity
self.tokens = float(capacity)
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self, tokens: int = 1, timeout: float = 30.0) -> bool:
"""尝试获取 tokens 个令牌,返回是否成功。"""
deadline = time.monotonic() + timeout
while True:
with self.lock:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(
self.capacity, self.tokens + elapsed * self.rate
)
self.last_refill = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
if time.monotonic() > deadline:
return False
time.sleep(0.05)
# 使用示例: 3 个 Agent 共享 15 req/s 配额
shared_bucket = TokenBucket(rate=15, capacity=30)
def agent_task(agent_name: str):
for url in urls:
if shared_bucket.acquire(timeout=10):
response = call_api(url)
else:
raise RuntimeError(f"{agent_name}: rate limit exhausted")
目录结构示例¶
多 Agent 并行报告的推荐目录布局:
hermes-production-patterns/
├── conventions/
│ ├── state-file-pattern.md # 每个 Agent 自己的 STATE.md
│ ├── cron-job-pattern.md # Cron 任务尊重共享限流
│ └── multi-agent-isolation.md # 本文件
├── shared-data/
│ ├── rate-limit.json # 共享限流配置
│ └── global-locks/ # 集中锁文件目录
│ └── .gitkeep
├── agents/
│ ├── market-watcher/
│ │ ├── STATE.md # 独立状态文件
│ │ └── reports/
│ │ ├── 2026-W36.md
│ │ └── 2026-W37.md
│ ├── price-tracker/
│ │ ├── STATE.md
│ │ └── reports/
│ │ └── 2026-W37.md
│ └── news-curator/
│ ├── STATE.md
│ └── reports/
│ └── 2026-W37.md
└── merged/ # 合并后的最终产出(只读)
└── weekly/
└── 2026-W37.md
与其他模式的集成¶
| 模式 | 集成点 |
|---|---|
| state-file-pattern | 每个 Agent 在自己的命名空间下维护独立的 STATE.md,不共享 |
| cron-job-pattern | Cron 任务启动时获取令牌桶配额,失败则推迟到下一个窗口 |
| maker-checker | Maker 和 Checker 使用相同命名空间,Checker 通过锁文件读取 Maker 的产出 |
| checkpoint-pattern | Checkpoint 写入 Agent 自己的目录,恢复时只扫描自身命名空间 |
反模式¶
| 反模式 | 正确做法 |
|---|---|
所有 Agent 共享一个 reports/ 目录 |
每个 Agent 一个子目录 |
| 各自独立计数 API 调用次数 | 共享 TokenBucket,集中限流 |
锁文件用 sleep() 轮询(无超时) |
设定超时,超时报错 |
锁文件不清理,残留 .lock |
__exit__ 中 unlink,异常也不留残渣 |
| 合并步骤和生产步骤用同一 Agent | 生产写自身目录,合并用独立 Agent 只读扫描 |
度量¶
- 锁等待时间 (p50/p99): 超过 5s 说明竞争严重,应扩大命名空间
- 令牌桶拒绝率: 超过 10% 说明总配额不足或 Agent 数需要限流
- 文件损坏率: 检查 JSON/YAML parse error 是否下降