Agent 的大脑:13 款工具怎么统一成一个接口
本文是 Multica 拆解系列的第 2 篇。上一篇讲了 Agent 的"工位"(runtime)。这一篇讲工位上那颗"大脑":13 款 AI 编程工具怎么被同一个接口管住。
这一篇要解决的问题
Multica 同时支持 13 款编码工具:Claude Code、Codex、Cursor、Copilot、Gemini、Hermes、Kimi、Kiro、OpenCode、OpenClaw、Pi、Antigravity、CodeBuddy。
问题来了:这 13 款工具差异极大。有的支持会话恢复,有的不支持;有的读 MCP 配置,有的无视它;skill 文件该放哪个目录每家都不一样;选模型的方式也分"静态"和"动态发现"。如果调度系统要为每款工具写一套专门逻辑,Multica 的代码会变成一个巨型 switch,每加一款工具改一遍。
我通读了 server/pkg/agent/ 下全部 13 个适配器文件,发现 Multica 用了一个经典解法——统一接口,差异下沉。
心智模型:一个 Backend 接口,13 个实现
整个适配层的核心是这一个 Go 接口:
// server/pkg/agent/agent.go
type Backend interface {
// Execute runs a prompt and returns a Session for streaming results.
Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error)
}
13 款工具,13 个结构体,全都实现这同一个 Execute 方法。调度系统(daemon)拿到任务后,根本不关心是哪款工具——它只调 backend.Execute(ctx, prompt, opts),拿到一个 Session,从里面读流式消息和最终结果。

打开智能体面板,每个 Agent 绑定了自己那款工具(Claude Code、Codex、Kimi……),但调度系统只认同一套执行接口:

13 款工具都用同一套接口,但能力差异巨大。这张图展示"统一接口"与"各家方言"的关系:

这就是适配器模式最纯粹的样子。差异在哪?差异在每个 .go 文件内部——把各自的 CLI 怎么调、stdout 怎么解析、会话怎么恢复,全关在各自的文件里,对外只暴露那个统一的 Execute。
深度点一(免费):差异被塞进了 ExecOptions
统一接口不代表忽略差异。差异被集中到 ExecOptions 这一个结构体里,适配器各自挑自己关心的字段、忽略不关心的:
// server/pkg/agent/agent.go
type ExecOptions struct {
Cwd string
Model string
SystemPrompt string
ThreadName string
MaxTurns int
Timeout time.Duration
ResumeSessionID string // 会话恢复
ExtraArgs []string
CustomArgs []string
McpConfig json.RawMessage // MCP 配置
ThinkingLevel string // 推理档
OpenclawMode string // openclaw 专用
}
注意这里有个设计哲学:接口里没有 IsClaude() 这种判断。每个适配器自己决定消费哪些字段。ThinkingLevel 注释写得很清楚:" honoured by claude, codex, opencode; other backends ignore it "——不支持的适配器静默忽略,而不是报错。
反直觉:这套接口的容错原则是优雅降级,不是"能力不匹配就失败"。你想给 Gemini 设推理档?它不支持,但不报错,只是当无事发生——Multica 宁可静默丢掉这个能力,也不让一个不相关的高级选项把整个任务搞崩。
这让"加一款新工具"变成只写一个新文件,完全不碰接口和调度逻辑。
深度点二:13 款工具的能力差异矩阵
接口统一了,但做产品决策时你得知道每款工具到底能干什么。我从代码和文档里整理出这张能力矩阵:
| 工具 | 会话恢复 | MCP | skill 注入路径 | 模型选择 |
|---|---|---|---|---|
| Claude Code | ✅ 真用 | ✅ | .claude/skills/ |
静态 + flag |
| Codex | ⚠️ 代码在但不可达 | ✅ | $CODEX_HOME/skills/ |
静态 |
| Cursor | ⚠️ 代码在但不可用 | ✅ | .cursor/skills/ |
动态发现 |
| Copilot | ✅ | ❌ | .github/skills/ |
静态(账号权益) |
| Gemini | ❌ | ❌ | .agent_context/skills/(fallback) |
静态 |
| Hermes | ✅ | ✅ | .agent_context/skills/(fallback) |
动态发现 |
| Kimi | ✅ | ✅ | .kimi/skills/ |
动态发现 |
| Kiro | ✅ | ✅ | .kiro/skills/ |
动态发现 |
| OpenCode | ✅ | ✅ | .opencode/skills/ |
动态发现 + variant |
| OpenClaw | ✅ | ✅ | .agent_context/skills/(fallback) |
绑定智能体 |
| Pi | ✅ | ❌ | .pi/skills/ |
动态发现 |
| Antigravity | ✅ | ❌ | .agents/skills/ |
动态发现 |
| CodeBuddy | ✅ | ✅ | 自有路径 | 动态发现 |
这张表解释了为什么用户会困惑"为什么我的 skill 在 Gemini 上不生效"——有 3 款(Gemini/Hermes/OpenClaw)走的是通用 fallback 路径 .agent_context/skills/,而这些工具本身未必从那里读 skill。接口帮你注入了,但工具收不收是它自己的事。
会话恢复这一列尤其关键。Claude Code 的恢复是"真用",意味着一个长任务被打断后能接上之前的上下文;而 Codex/Cursor 标了 ⚠️,意思是适配器代码里有恢复的逻辑,但上游 CLI 当前不可达/不可用——这是个诚实的降级状态,代码里写了但跑不通。
深度点三:推理档(ThinkingLevel)怎么屏蔽差异
最有技术含量的是推理档(ThinkingLevel)。每家工具的"思考强度"参数名和取值都不同:Claude 是 low|medium|high|xhigh|max,Codex 是 none|minimal|low|medium|high|xhigh,OpenCode 是模型 variant 名。如果让调度系统去理解每家的语义,又回到巨型 switch。
Multica 的做法在 server/pkg/agent/thinking.go 里。它维护一张映射表,把 Multica 内部的统一档位翻译成每家工具的原生取值:
// server/pkg/agent/thinking.go
var codebuddyEffortLabel = map[string]string{ ... }
// 每款工具一张映射表:统一档 → 原生值
// 不支持的工具:Execute 里压根不注入,等于忽略
Claude 适配器把统一档翻成 --effort flag,Codex 翻成 reasoning_effort,OpenCode 翻成 variant 名,其它 10 款直接忽略。对调度系统来说,它永远只设一个 ThinkingLevel,翻译是适配器的私事。
这个模式贯穿整个适配层:调度系统说一种语言,适配器负责翻译成各家的方言。这就是为什么 Multica 能同时指挥 13 款工具,而你看板上的体验完全一致——差异被关在各自的 .go 文件里了。
下一篇:大脑有了,同事还得能干活、能汇报——Multica 的任务状态机和三级超时怎么保证一个 Agent 既不偷懒也不会无限卡死。
想看 AI 拆完下一篇?留个邮箱,自动推送。