
Multica 架构解读:它为何更像 Agent 团队操作系统,而不是 Agent Framework
深入拆解 Multica 的控制面架构:Issue/Run 双状态机、事务与 Outbox、Daemon 租约认领、事件幂等、失败恢复和 Review 验收门。
Multica 架构解读:它为何更像 Agent 团队操作系统,而不是 Agent Framework
Claude Code、Codex、OpenCode 解决的是“如何执行任务”,但团队真正把它们接入生产流程后,困难往往出现在执行器之外:
- 谁负责保存业务目标,而不是只保存一段对话?
- 一次执行失败后,如何安全重试并保留完整历史?
- 多个 Daemon 同时抢任务时,如何保证同一次 Run 不被重复执行?
- Runtime 报告完成后,谁有权把业务需求标记为完成?
Multica 的价值就在这里。它不需要重新实现底层 Agent,而是建立一个控制面,管理任务、执行、状态、协作和验收。与其称它为 Agent Framework,不如把它理解为 Agent Team Operating System。
控制面与执行面
Multica 位于用户和底层 Runtime 之间:
User
│ create / assign / review
▼
Multica Server
│ persist state / schedule / authorize
▼
Daemon
│ prepare workspace / claim run / stream logs
▼
Claude Code / Codex / OpenCode
这里最重要的不是组件数量,而是状态所有权:
- Server 拥有业务状态和调度事实;
- Daemon 拥有一次执行期间的租约与本地工作环境;
- Runtime 只负责执行,不拥有业务完成权。
因此,Runtime 退出码为 0 最多能证明本次进程正常结束,不能直接证明需求已经通过验收。
领域模型:五个对象、三种生命周期
Workspace:租户与协作边界
Workspace 是最外层聚合,可对应团队、项目或组织:
Workspace
├── Issues
├── Runs
├── Agents
├── Skills
└── Config
它不仅是目录结构,还应承担隔离职责:
- Issue、Run 与 Artifact 不能跨 Workspace 混用;
- Agent 与 Skill 的可见性由 Workspace 决定;
- 权限、配额、审计与保留策略在这一层生效。
Issue:长期存在的业务目标
Issue 描述“要完成什么”,例如“为登录页增加 OAuth 登录”。它关心业务状态:
Open → Assigned → In Review → Completed
└──────────────→ Cancelled
Issue 的生命周期通常长于任何一次执行。Run 失败不应删除 Issue,Run 成功也不应绕过 Review 自动关闭 Issue。
Run:一次不可混淆的执行尝试
Run 表示一次 Execution Attempt。一个 Issue 可以对应多次 Run:
Issue: 修复登录 Bug
├── Run #1: Queued → Running → Failed
├── Run #2: Queued → Running → Timed Out
└── Run #3: Queued → Running → Succeeded
Run 不是普通日志。它至少要记录:
| 字段 | 作用 |
|---|---|
issue_id |
关联业务目标 |
agent_revision |
固定本次使用的 Agent 定义版本 |
input_snapshot |
固定执行输入,避免重试时语义漂移 |
state |
Queued、Claimed、Running、Succeeded、Failed 等 |
attempt |
重试序号 |
lease_owner / lease_until |
Daemon 认领租约 |
started_at / finished_at |
执行时间线 |
result_ref |
结构化结果或 Artifact 引用 |
error_code |
可分类、可统计的失败原因 |
一旦 Run 开始执行,输入快照和 Agent 版本应保持不可变。要使用新配置重新执行,应创建新 Run,而不是修改历史记录。
Skill:独立版本化的能力
Skill 描述 React、Python、Docker、PPT Generation、Code Review 等能力。它与 Agent 分离后,才能独立安装、升级、复用和审计。
一个 Skill 版本发生变化,不应悄悄改变正在运行的 Run。更稳妥的做法是让 Agent Revision 固定其 Skill Revision 集合。
Agent:执行者定义
Agent 是可复用定义,不是运行实例:
Frontend Agent Revision 7
├── React Skill @ 3
├── Tailwind Skill @ 2
└── Playwright Skill @ 5
Run 创建时引用具体 Revision,这样才能回答两个关键问题:
- 这次执行究竟使用了什么配置?
- 使用同一输入重放时,怎样尽可能得到可解释的结果?
核心不变量:Issue 与 Run 必须分离
Issue 表示业务目标,Run 表示执行事实。
这带来四条领域不变量:
- 一个 Issue 可以没有 Run,也可以有多个 Run;
- 一个 Run 只属于一个 Issue;
- Run 状态不能直接等同于 Issue 状态;
- Runtime 只能报告执行结果,不能越权关闭 Issue。
所以:
Issue = Assigned
不意味着:
Run = Running
同样:
Run = Succeeded
也不意味着:
Issue = Completed
如果把两套状态机压成一套,重试会覆盖历史,失败会污染业务状态,并发 Run 会互相踩踏,Review 也失去明确的挂载点。
Assign 的事务边界
给 Issue 分配 Agent 时,系统通常要同时完成两件事:
- 更新 Issue 的分配信息;
- 创建一个 Queued Run。
这两步必须位于同一个事务中,否则会出现两类悬空状态:
- Issue 已显示 Assigned,但没有可执行 Run;
- Run 已进入队列,但 Issue 仍显示未分配。
伪代码可以写成:
BEGIN;
UPDATE issues
SET assignee_id = :agent_id,
state = 'assigned',
version = version + 1
WHERE id = :issue_id
AND version = :expected_version;
INSERT INTO runs (
id, issue_id, agent_revision, input_snapshot, state
) VALUES (
:run_id, :issue_id, :agent_revision, :input_snapshot, 'queued'
);
INSERT INTO outbox_events (
event_id, event_type, aggregate_id, payload
) VALUES (
:event_id, 'run.queued', :run_id, :payload
);
COMMIT;
这里的 expected_version 用于乐观锁;Outbox Event 与业务数据同事务写入,避免“数据库提交成功但队列消息丢失”。
Daemon 如何安全认领 Run
多个 Daemon 可以并行拉取任务,但一次 Run 在同一时刻只能由一个 Daemon 持有。仅靠“先查询、再更新”无法保证这一点,因为两个进程可能同时读到 queued。
认领必须是原子条件更新:
UPDATE runs
SET state = 'claimed',
lease_owner = :daemon_id,
lease_until = :now + :lease_duration,
version = version + 1
WHERE id = :run_id
AND state = 'queued';
只有受影响行数为 1 的 Daemon 才认领成功。执行期间,Daemon 周期性续租:
Claimed → heartbeat → heartbeat → Running
如果 Daemon 崩溃且租约过期,调度器可以把 Run 标记为 orphaned,再根据策略决定:
- 恢复同一 Run;
- 终止并创建新的重试 Run;
- 转入人工处理。
是否允许恢复同一 Run,取决于底层操作是否幂等。涉及外部写入时,盲目重跑可能造成重复提交。
命令、事件与幂等
控制面向 Daemon 下发的是命令,Daemon 和 Runtime 上报的是事实事件:
Command: StartRun(run_id, lease_token)
Event: RunStarted
Event: LogChunkAppended
Event: ArtifactProduced
Event: RunSucceeded | RunFailed
网络重试会让相同事件重复到达,因此每个事件都需要稳定的 event_id,消费端要维护幂等约束:
UNIQUE (event_id)
状态迁移也应校验前置状态。例如,只有 running 才能迁移到 succeeded;迟到的 RunSucceeded 不能把已经 cancelled 的 Run 重新复活。
Server、Daemon、Runtime 的详细职责
| 层 | 应负责 | 不应负责 |
|---|---|---|
| Server | 状态机、持久化、权限、调度、审计、Review | 执行具体编码任务 |
| Daemon | 认领租约、准备执行沙箱、启动 Runtime、心跳、日志与 Artifact 上传 | 决定 Issue 是否完成 |
| Runtime | 推理、工具调用、文件修改、测试、生成结果 | 修改控制面中的业务状态 |
这条边界让 Server 可以同时接入多个 Runtime,也让 Daemon 能针对不同操作系统、资源规格和安全策略部署。
失败不是一个布尔值
Failed 只能作为展示层概括,调度系统需要更细的失败分类:
| 失败类型 | 示例 | 默认处理 |
|---|---|---|
| Transient | 网络抖动、临时限流 | 指数退避后重试 |
| Runtime | 模型进程崩溃、工具异常 | 新建重试 Run |
| Invalid Input | 缺少凭据、参数不合法 | 返回 Issue 等待补充 |
| Policy | 权限不足、危险操作被拒绝 | 不自动重试 |
| Timeout | 超过租约或执行时限 | 终止、检查副作用后重试 |
| Review Rejected | 产物未达到要求 | 带 Review 反馈创建新 Run |
重试应创建新的尝试记录,保留父子关系:
Run #4
retry_of: Run #3
retry_reason: transient_network_error
这样才能计算真实成功率、平均尝试次数和失败分布,而不是用最后一次结果覆盖整个过程。
Review 是业务完成的门
Agent 的自我判断不能替代验收:
Run Succeeded
↓
Artifact Ready
↓
Review Requested
↓
Approved ─────────→ Issue Completed
Rejected ─────────→ New Run with feedback
Review 可以由人或另一个 Agent 执行,但必须生成结构化记录:
- 评审对象及版本;
- 评审者身份;
- 结论与理由;
- 修改建议;
- 时间戳。
关键点是 Review 绑定具体 Artifact Revision。否则产物更新后,旧的批准结果可能被错误复用。
可观测性与审计
一个可治理的 Agent 系统,至少要能沿 issue_id → run_id → artifact_id → review_id 追踪完整链路。
建议同时保留三类数据:
- 状态快照:快速回答“现在是什么状态”;
- 领域事件:回答“为什么变成这个状态”;
- 执行日志与 Artifact:回答“Runtime 实际做了什么”。
这三者不能互相替代。日志适合诊断,却不适合驱动业务状态;事件适合审计,却不应承载大体积文件;状态快照适合查询,却无法还原完整过程。
Kubernetes 类比的边界
Multica 像 Kubernetes 的地方,是它们都把声明、调度、执行和状态协调分开。但 Agent 任务还有三个额外难点:
- 输入通常包含非结构化上下文;
- 输出质量需要语义验收,而不仅是进程健康检查;
- 工具调用可能产生不可逆的外部副作用。
因此,Agent 控制面不能只复制容器编排。它还需要版本化输入、Artifact 血缘、Review 门禁、幂等事件和副作用治理。
结论
Multica 的核心不是“再造一个更聪明的 Agent”,而是把 Agent 周围最容易失控的工程问题变成明确模型:
- Issue 保存长期业务目标;
- Run 保存每次不可混淆的执行尝试;
- Agent 与 Skill 通过 Revision 固定可复现配置;
- Server 维护状态、规则和权限;
- Daemon 通过租约协调执行;
- Runtime 专注完成具体工作;
- Review 决定业务是否真正完成。
当状态所有权、事务边界、租约、幂等和验收门都被明确之后,Agent 才从一次性的交互工具变成可治理、可重试、可审计的团队执行资源。