2026-05-18 · 技术方案
Multica 核心运行机制
CLI 连接器 · 服务端数据状态机 · Agent 互相 @ 协作链
Multica 是什么?
你的下一批员工,不是人类。
Multica 是开源的 Managed Agents 平台—— 把编码 Agent 变成真正的队友:像分配给同事一样分配 Agent, 它自主接手工作、写代码、报告阻塞、更新状态。 不再复制粘贴 prompt,不再盯着运行过程。
功能特性
Multica 管理完整的 Agent 生命周期:从任务分配 → 执行监控 → 技能复用。
Agent 即队友
有头像、上看板、发评论、建 Issue、主动报阻塞。像分配给同事一样分配给 Agent。
Squads 小队
Leader agent 带队,团队扩容路由不变。@前端组 替代 @张三或李四。
自主执行
完整生命周期管理(排队 / 认领 / 执行 / 完成 / 失败),WebSocket 实时进度。
可复用技能
每个解决方案都沉淀为全团队可复用的 Skill。部署、迁移、Code Review 都能累积。
统一运行时
一个控制台管所有算力。本地 daemon + 云端 runtime,自动探测可用 CLI。
多工作区
按团队组织工作,workspace 级别隔离 Agent / Issue / Settings。
⬇️ 下面进入这套机制的技术内部
本页讲什么
本页聚焦三条让"Agent 当队友"真正能跑起来的核心机制:
① multica CLI 作为连接器,承接用户、本地 11 种 agent CLI、Git 仓库、云端 / 自建服务端;
② 服务端 4 张表的状态机,所有任务流转都是单语句 SQL,关键转移靠 FOR UPDATE SKIP LOCKED 在数据库层做无锁分发;
③ Agent 之间通过评论 @mention 互相触发,靠三层防环逻辑避免死循环。
核心结论
连接器,而不是命令大全
单二进制把用户、本地 agent 工具链、Git 仓库、云 / 自建服务端 串成一条命令链;daemon 是 CLI 的常驻模式而不是独立进程。
SQL 即真理
所有状态转移都是 UPDATE ... RETURNING;
Claim 用 SKIP LOCKED 无锁分发;
失败按 failure_reason 决定重试 / 重置 session。
三层防环的接力链
评论里的 Markdown 链接是协作主线; "自触发 / Leader 循环 / 待处理去重"三层保护; Squad leader 自然成为兜底路由层。
详细分析
multica CLI:一站式连接器
CLI 不是一组孤立命令,而是把外部世界的若干系统
缝合到 Multica 内部状态机的唯一胶水层。
一台机器装一个 multica 二进制 + 一份认证态,就能让本地工具链、Git 仓库、服务端
(云或自建)按统一协议协作。
CLI 承接的五个外部系统
用户 → CLI
Cobra 框架 · 三组命令(core / runtime / additional)·
--output table|json 双输出 ·
短 ID 自动展开(MUL-42 ↔ UUID)
CLI ↔ Server
统一 REST API · Bearer Token 鉴权 · workspace_id 路由 ·
multica setup 一行切换云 / 自建(profile 隔离)
CLI → Daemon(同二进制)
multica daemon start/status/logs 是 subcommand 不是另一个 binary。
平台差异落在 cmd_daemon_unix.go / cmd_daemon_windows.go。
Daemon → 本地 Agent CLI
启动时探测 PATH 上的 11 种 CLI;执行任务时按 provider 拼参数 spawn 进程,
注入 MULTICA_TASK_ID / SESSION_ID / WORK_DIR 等环境变量。
Daemon → Git 仓库
multica repo 把项目和 GitHub 仓库绑定(resource_type=github_repo);
daemon 在任务隔离工作目录里做 worktree checkout,agent 直接在该目录跑命令、写代码、提交。
几个关键承接细节
① 用 setup 同一条命令切换"云 / 自建"
multica setup # 默认接入 Multica Cloud
multica setup self-host \ # 接入自建后端
--server-url https://api.internal.co \
--app-url https://app.internal.co \
--profile staging # profile 隔离多环境
配置写入 ~/.multica/config.json(带 profile 时为 config-{profile}.json),
Token 落 OS keychain。同台机器可以在多个 profile 间切换 ——
日常云、办公自建、客户私有,互不污染。
② Daemon 探测本地 agent 工具链
server/internal/daemon/config.go:145-160// 先用 exec.LookPath 快路径
for _, name := range []string{"claude", "codex", "copilot", "gemini",
"openclaw", "opencode", "hermes",
"pi", "cursor-agent", "kimi", "kiro-cli"} {
if p, err := exec.LookPath(name); err == nil { detected = append(detected, p); continue }
}
// 失败再走"读 ~/.zshrc / ~/.bashrc"的兜底
// 原因:GUI 启动的进程不继承交互式 shell 的 PATH
// (fnm/nvm/volta/brew 都靠 rc 文件初始化)
resolveAgentsViaLoginShell(...)
③ CLI 还是 agent "回写"的入口
Agent 在 daemon 起的进程里跑完后,可以直接调
multica issue update --status done /
multica issue comment add 把结果写回。
服务端通过请求头里的 X-Task-ID + X-Agent-ID 识别是 agent 身份的调用 ——
同一套 CLI,既是用户终端又是 agent 的"回写 SDK"。
服务端数据状态机
Multica 没有内存态调度器,所有状态都落在四张表上,所有转移都是单语句的
UPDATE ... RETURNING。DB 即真理,server 重启 0 影响。
2.1 Task 状态机
关键设计:
"失败"是终态,但满足条件的失败会复制一行新任务(attempt+1,
parent_task_id 指回上一次),实现轻量重试且历史可追溯。
2.2 失败原因 × 重试策略
| failure_reason | 语义 | 自动重试 | Session 继承 |
|---|---|---|---|
runtime_offline | Runtime 离线时被 sweeper 标记 | ✓ | ✓ |
runtime_recovery | daemon 重启时孤儿任务回收 | ✓ | ✓ |
timeout | dispatch 5min / running 2.5h 超时 | ✓ | ✓ |
iteration_limit | Agent 迭代次数上限 | 手动 | ✗ 重置 |
agent_fallback_message | Agent 输出退化(兜底文案) | 手动 | ✗ 重置 |
api_invalid_request | 上游 API 400(session 中毒) | 手动 | ✗ 重置 |
queued_expired | 排队超过 2h TTL 自动失败 | 否 | — |
cancelled / user_cancelled | 用户主动取消 | ✗ | — |
agent_error | 默认值(其他未分类错误) | ✓ | ✓ |
重试白名单 retryableReasons 见 service/task.go:1237-1241。
其余失败需要人介入或新 session 重跑。
2.3 Claim 的核心 SQL(最值得看的一段)
多个 daemon 并发抢同一个 agent 的队列,靠
FOR UPDATE SKIP LOCKED 在数据库层做无锁分发;
同时用 NOT EXISTS 子查询实现 per-(agent, issue) 序列化 ——
同一个 (agent, issue) 上不会同时跑两个任务。
UPDATE agent_task_queue
SET status = 'dispatched', dispatched_at = now()
WHERE id = (
SELECT atq.id FROM agent_task_queue atq
WHERE atq.agent_id = $1 AND atq.status = 'queued'
AND NOT EXISTS (
-- 关键:保证 (agent, issue) 维度串行
SELECT 1 FROM agent_task_queue active
WHERE active.agent_id = atq.agent_id
AND active.status IN ('dispatched', 'running')
AND (
(atq.issue_id IS NOT NULL AND active.issue_id = atq.issue_id)
OR (atq.chat_session_id IS NOT NULL AND active.chat_session_id = atq.chat_session_id)
OR (atq.issue_id IS NULL -- quick-create 任务用全 NULL 去重
AND atq.chat_session_id IS NULL
AND atq.autopilot_run_id IS NULL)
)
)
ORDER BY atq.priority DESC, atq.created_at ASC
LIMIT 1 FOR UPDATE SKIP LOCKED
)
RETURNING *;
两个细节:
① SKIP LOCKED 让多 daemon 抢锁时直接跳过已被别人锁住的行,零等待;
② 同一 (agent, issue) 在 dispatched/running 时新任务无法 claim ——
天然实现"同一 ticket 不会两个任务并发跑"。
2.4 四张表的联动
2.5 超时 / 阈值速查
Dispatch 超时
5 min
claim 后未 start
Running 超时
2.5 h
agent 执行硬上限
Queued TTL
2 h
排队太久自动失败
Runtime 离线
150 s
心跳静默阈值
Daemon heartbeat
15 s
最大 60s DB 延迟
Sweeper 扫描
30 s
批失败 stale 任务
Max attempts
2
默认重试次数
Runtime GC
7 d
离线后回收
2.6 任务取消全链路
- 用户在 UI 点 Cancel →
CancelTask(service/task.go:708-731) - DB 状态转移:
queued / dispatched / running → cancelled - Daemon 端 watcher
watchTaskCancellation每秒查GetTaskStatus - 检测到 cancelled → 优雅 kill agent 进程
- 广播
task:cancelled事件 → UI 隐藏进度条 + 重算 agent.status
Agent 互相 @ 的协作链
Multica 没有专门的 mention 表,评论正文本身就是协议。 agent 之间互相 @、用户 @ agent、@ squad,统一靠 Markdown 链接里的 schema 解析。 一段评论可能包含若干 mention,每个都可能触发一个新任务 —— 因此防环逻辑是这套机制能不能用的关键。
3.1 @mention 的 Markdown 协议
评论里的所有 mention 都长这样:
[@Alice](mention://member/uuid-of-alice)
[@CodeReviewer](mention://agent/uuid-of-agent)
[@BackendTeam](mention://squad/uuid-of-squad)
[MUL-123](mention://issue/uuid-of-issue)
[@all](mention://all/all)
解析靠一条正则就能拿到全部信息:
server/internal/util/mention.go:6-16type Mention struct {
Type string // "member" | "agent" | "squad" | "issue" | "all"
ID string
}
var MentionRe = regexp.MustCompile(
`\[@?(.+?)\]\(mention://(member|agent|squad|issue|all)/([0-9a-fA-F-]+|all)\)`,
)
设计取舍:
mention 不入独立表,留在评论 content 里动态解析;
评论删除时关联的 task 通过 trigger_comment_id ON DELETE SET NULL 自动断关联。
3.2 评论 → 任务的触发链
3.3 三层防环(最关键的工程细节)
Agent A 评论里 @ Agent B、B 完成后又 @ A —— 如果不防环就是个无限循环。 Multica 把防护拆成三层,每层互不替代:
① 自触发防护
server/internal/handler/comment.go:490-492// 评论作者就是被 @ 的那个 agent,直接跳过
if authorType == "agent" && authorID == m.ID {
continue
}
② Squad Leader 级防环
server/internal/handler/squad.go:638-640// 上一个任务已经是 leader 角色,再被 @ 就跳过
// 不然 squad leader 评论后会立刻给自己再派一个 leader 任务
if authorType == "agent" &&
authorID == uuidToString(squad.LeaderID) &&
h.lastTaskWasLeader(ctx, issue.ID, squad.LeaderID) {
continue
}
③ Pending 去重
server/internal/handler/comment.go:514-520hasPending, _ := h.Queries.HasPendingTaskForIssueAndAgent(ctx,
db.HasPendingTaskForIssueAndAgentParams{
IssueID: issue.ID,
AgentID: agentUUID,
})
if hasPending {
continue // 该 agent 在这个 issue 上已经有 queued/dispatched/running 任务
}
3.4 Squad Leader 自动充当路由层
@BackendTeam 这种 squad mention 在
enqueueMentionedAgentTasks 里走特殊分支:
if m.Type == "squad" {
squad, _ := h.Queries.GetSquad(ctx, parseUUID(m.ID))
leaderID := squad.LeaderID
// Squad leader 防环:见 3.3 ②
if authorType == "agent" && authorID == uuidToString(leaderID) &&
h.lastTaskWasLeader(ctx, issue.ID, leaderID) {
continue
}
// 入队时标记 is_leader_task = true
h.TaskService.EnqueueTaskForSquadLeader(ctx, issue, leaderID, comment.ID)
continue
}
if m.Type != "agent" { continue } // member / issue / all 不直接派任务
补充规则: 一条 member 评论里完全没有 @ 时,会自动触发 squad leader 帮忙分配 —— leader 因此天然成为"如果没人接,我兜底"的路由层。
3.5 端到端例子:用户 → Agent A → Agent B
整条链上没有中心调度器:每一步都是"一条评论 + 解析 + 防环 + 一条 SQL"。 实现简单但很有韧性 —— DB 是唯一真相源,任何环节中断后只要再有评论进来, 链条就能从断点接着走。
值得借鉴的设计
- CLI 即连接器 —— 一个二进制把用户、本地工具链、Git 仓库、云 / 自建服务端缝在一起;daemon 是 subcommand 不是独立进程。
- Polymorphic assignee(
assignee_type+assignee_id) —— 让 agent / member / squad 共用一套分配机制,业务改造最小化。 - FOR UPDATE SKIP LOCKED 替代 Redis 锁 —— 多 daemon 抢同一队列的标准答案,Postgres 原生支持,零额外组件。
- 把"评论正文"当作协议 —— mention 不开独立表,正则解析就完事;删评论自动级联清理任务。
- 三层独立的防环 —— 个体 / 角色 / 主题各管一段,任何一层都能独立兜底。
- failure_reason 驱动重试策略 —— "什么错该重试 / 该不该继承 session"做成纯枚举映射,service 代码极薄。