Multica 架构解读:它为何更像 Agent 团队操作系统,而不是 Agent Framework

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,这样才能回答两个关键问题:

  1. 这次执行究竟使用了什么配置?
  2. 使用同一输入重放时,怎样尽可能得到可解释的结果?

核心不变量:Issue 与 Run 必须分离

Issue 表示业务目标,Run 表示执行事实。

这带来四条领域不变量:

  1. 一个 Issue 可以没有 Run,也可以有多个 Run;
  2. 一个 Run 只属于一个 Issue;
  3. Run 状态不能直接等同于 Issue 状态;
  4. Runtime 只能报告执行结果,不能越权关闭 Issue。

所以:

Issue = Assigned

不意味着:

Run = Running

同样:

Run = Succeeded

也不意味着:

Issue = Completed

如果把两套状态机压成一套,重试会覆盖历史,失败会污染业务状态,并发 Run 会互相踩踏,Review 也失去明确的挂载点。

Assign 的事务边界

给 Issue 分配 Agent 时,系统通常要同时完成两件事:

  1. 更新 Issue 的分配信息;
  2. 创建一个 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 任务还有三个额外难点:

  1. 输入通常包含非结构化上下文;
  2. 输出质量需要语义验收,而不仅是进程健康检查;
  3. 工具调用可能产生不可逆的外部副作用。

因此,Agent 控制面不能只复制容器编排。它还需要版本化输入、Artifact 血缘、Review 门禁、幂等事件和副作用治理。

结论

Multica 的核心不是“再造一个更聪明的 Agent”,而是把 Agent 周围最容易失控的工程问题变成明确模型:

  • Issue 保存长期业务目标;
  • Run 保存每次不可混淆的执行尝试;
  • Agent 与 Skill 通过 Revision 固定可复现配置;
  • Server 维护状态、规则和权限;
  • Daemon 通过租约协调执行;
  • Runtime 专注完成具体工作;
  • Review 决定业务是否真正完成。

当状态所有权、事务边界、租约、幂等和验收门都被明确之后,Agent 才从一次性的交互工具变成可治理、可重试、可审计的团队执行资源。