多 AI 协作的调度机制
多 AI 协作的调度机制:信箱与投递器分离、三段式收信接口、至少一次投递与幂等键。读完能落一套可用的最小实现。
面向需要自己实现多 AI 协作调度的工程场景。只讲机制与实现取舍,不含产品推广。
目标:读完能落一套可用的最小实现。
1. 问题定义
单 AI 方案有三个硬约束:
| 约束 | 根因 | 解法 |
|---|---|---|
| 上下文窗口有限 | 长任务历史占满窗口 | 切开任务,各持部分上下文 |
| 串行吞吐低 | N 个子任务串行执行 | 并行 |
| 自校验失效 | 同一模型验收自身产出,错误自洽 | 交叉校验(A 产出、B 验收) |
常见错误起点:把多 AI 协作理解成「两个模型互相对话」。
该模型隐含两个不成立的前提:
正确的抽象是消息传递,不是对话。
2. 核心模型
把「消息传递」拆成两个独立设计的部分:
A. 存储与寻址 消息放哪、如何检索、如何不丢 → 目标是「稳」
B. 送达与消费 何时送、送给谁、送达确认、去重 → 目标是「柔」
2.1 为什么必须分开
耦合实现(如直接函数调用)会遇到三个死结:
2.2 分离后的职责
信箱决定「消息在哪」,投递器决定「消息何时被看见」。
3. 调度实现
按自下而上的顺序。
3.1 通信层:pull 还是 push
实现选择:传输层用 pull,体验层模拟 push。
| 方案 | 服务端状态 | 断线恢复 | 排查难度 |
|---|---|---|---|
| 长连接 push | 需维护在线表、连接池 | 需重连逻辑 | 高 |
| **pull(轮询)** | **无状态** | **下一轮自动恢复** | **低(可 curl、可抓包)** |
选择 pull 的理由:
延迟处理:轮询间隔设为秒级,接收端即可视为准实时。用户侧无感知。
注意:轮询间隔由接收端自行配置,因此不同会话可采用不同投递策略(例如:执行中的会话拉长间隔,空闲会话缩短间隔)。
3.2 存储层:文件还是数据库
实现选择:小规模用文件(Maildir 式),规模上升后换数据库,接口保持不变。
/data/<agent>/inbox/<message-id>.json
一条消息一个 JSON 文件。该目录同时充当队列、数据库、审计日志。
| 文件方案优点 | 文件方案缺点 |
|---|---|
| 零依赖(无需数据库服务) | 无事务、无索引 |
| 可直接 grep / cat / 手工修改 | 万级条目后 `listdir` 性能下降 |
| 备份 = 拷贝目录 | 并发写需自行加锁 |
| 故障可人工介入(翻原始文件) | —— |
关键设计 1:消息 ID 全局唯一且时间有序
<YYYYMMDD-HHMMSS>-<from>-<to>-<毫秒末4位>
关键设计 2:并发控制
迁移阈值:消息量到万级,或需要复杂查询时,换 SQLite/Postgres。上层接口不变。
3.3 收信:三段式接口
不要把「拉列表」和「读正文」合并为单一接口。
① 探测 GET /inbox?user=X&unread=1 (user = AI 会话标识,下同)
→ 仅返回元信息:id / from / subject / ts / 正文字数
② 取正文 GET /message?user=X&id=...
→ 确认需要处理时才拉取完整 body
③ 确认消费 POST /ack {user, ids:[...]}
→ 置为已读
设计收益:
3.4 送达层:会话注入
这是接收端真正「收到」消息的环节,也是故障密度最高的环节。
① 轮询发现未读
② 探测目标会话忙闲 ← 关键
③ 注入消息摘要到目标会话
④ 注入成功后才 ack
四个要点:
① 忙闲探测
hasActiveRun)② ack 时序
正确:注入成功 → ack
错误:ack → 注入
先 ack 后注入时,若注入失败,消息将被永久丢弃(已置已读,不再被拉取)。
该错误隐蔽性强:正常路径下不报错,仅在故障路径暴露。
③ 幂等键
idempotencyKey(幂等键:同一 ID 只执行一次,重复投递直接跳过;如 <source>-<message-id>)④ 长时间未读处理
3.5 投递语义:at-least-once vs exactly-once
| 语义 | 实现 | 代价 |
|---|---|---|
| **at-least-once** | 落盘 + ack + 幂等键 | 存在重复(由幂等键消解) |
| exactly-once | 需消费位点 / 去重表 | 复杂度显著上升 |
选择 at-least-once。
4. 两种调度模式
上述讨论的是「消息如何传递」。而「任务如何派发」有两种模式。
模式 A:消息总线(异步 / 解耦)
调度方 → 发消息 → 信箱 →(投递器)→ 执行方 → 执行 → 回消息 → 信箱 → 调度方
| 特性 | 说明 |
|---|---|
| 在线要求 | 双方无需同时在线 |
| 适用任务 | 长任务(小时级) |
| 适用范围 | 跨会话、跨进程、跨机器 |
| 延迟 | 存在(受轮询间隔影响) |
| 结果获取 | 异步,需等待回信 |
模式 B:直接调用(同步 / 即时取结果)
调度方 → 启动子会话 → 执行方执行 → 返回结果 → 调度方继续
| 特性 | 说明 |
|---|---|
| 结果获取 | 即时 |
| 适用任务 | 短任务、边界明确的任务 |
| 并行能力 | 强(可一次启动 N 个) |
| 代价 | 调度方需等待或挂起;子会话默认隔离,不掌握全局上下文 |
选型
| 场景 | 选择 |
|---|---|
| 任务分钟级、结果需即时消费 | B |
| 任务小时级 | A |
| 跨机器 / 跨信任域 | A |
| 需并行多个子任务 | B |
| 需留痕、支持人工介入 | A |
工程实践中通常混用。
5. 实现清单与陷阱
5.1 最小可用清单
1. 存储 文件信箱:一 AI 一目录、一信一文件、ID 含时间戳
2. 接口 三段式:探测 / 取正文 / 确认消费
3. 投递 轮询(间隔可配)+ 忙闲探测 + 注入成功才 ack
4. 幂等 注入携带 idempotencyKey
5. 加固 ID 防目录穿越(拒绝 "/" 与 "..")+ 字段长度上限
6. 扩展 规模上升后换 SQLite(接口不变)
7. 跨信任域 加签名 / 令牌 + 防重放
实现 1-5 即可得到一台可用的最小调度件。
5.2 陷阱清单
① 信箱与投递器必须分离
服务只负责存取,「何时送达」由投递端决定。
② ack 必须晚于注入
顺序写反会导致消息永久丢失。该错误正常路径不暴露。
③ 必须先探忙闲
直接向运行中的会话注入会中断任务并污染上下文。
④ 幂等键不可省
轮询与重试天然产生重复。缺失幂等键会导致重复处理。
⑤ 文件即事实来源,且需支持人工介入
全程落盘、纯文本:任一环节故障均可人工查看、补齐、删除。
相较黑盒队列,排查成本显著更低。备份即拷贝目录。
⑥ 信任模型应与场景匹配
⑦ 元信息与正文分离
列表仅返回摘要,正文按需拉取。高频轮询下可显著降低流量。
⑧ 不要为接收端实现常驻工具接口
工具 schema 每轮都进入 prompt,持续占用上下文。
采用「脚本 + 拉取」模式,常驻开销为零。
6. 取舍汇总
| 维度 | 选择 | 理由 |
|---|---|---|
| 通信 | pull | 服务端无状态、断线自愈、可观测 |
| 存储 | 文件(小规模) | 零依赖、可审计、可人工介入 |
| 语义 | at-least-once + 幂等 | 复杂度可控,效果等价 |
| 同步性 | 按任务长度 | 短任务同步,长任务异步 |
| 信任 | 按场景 | 内网宽松,公网严格 |
| 消息体 | 元信息/正文分离 | 降低流量 |
本方案的边界
7. 验收:最难的部分
多 AI 协作的真正难点不是消息传递,而是判定「任务已完成」。
消息传递是工程问题;验收是定义问题。
验收的三个前提:
典型陷阱:将「流程结束」等同于「结果正确」
completed ≠ 目标达成该混淆会使自动化系统「自信地产生错误」。
结论:
无人值守的前提不是模型能力,而是任务定义足够清晰、验收标准足够客观。
消息调度解决「能否连通」,验收机制解决「连通后是否正确」。
8. 参考实现特征
| 组件 | 实现 |
|---|---|
| 服务 | 单文件、标准库 HTTP 服务,零外部依赖 |
| 存储 | `/ |
| 接口 | `/send` `/inbox` `/message` `/ack` `/health` |
| 身份 | 请求参数传递(内部信任模型);预留 token 校验路径 |
| 并发 | 全局锁保护写操作;目录级天然隔离 |
| 投递 | 各接收端自带 watcher,轮询 + 忙闲探测 + 注入后 ack |
| 备份 | 拷贝数据目录 |
该结构在千级消息量下运行稳定;量级上升时替换存储层即可,接口不变。