OpenCode 整体架构:一个 10 万行 Agent 怎么被组织成一条装配链

如果一个终端里能跑的 AI 编程助手,源码拆成几十个包、上万个文件,能接 30 多家模型厂商,还要在同一个进程里把命令行、HTTP 接口、WebSocket、插件、快照挨个伺候好……
它凭什么不乱?
停一下,先别翻答案。反问一句:如果让你来设计这个东西,你会从哪儿下手?大多数人第一反应是「先写 runLoop,让模型能一轮轮转起来」。但真到几万行的规模你会发现——能让系统不塌的,不是某一个主循环写得有多漂亮,而是它怎么被组织成一条「装配链」:入口、运行时、模型、持久化、扩展,每一段守好自己那一段,再拼成一个可运行的整体。这就是本系列第一篇要交付的东西。
看完这篇,你会 get 到三个问题:
- 第一,这个 10 万行引擎被拆成了哪几块,为什么是「契约 / 模型 / 实现」三体;
- 第二,一个命令从你手上按下,是怎么被装配成一条能跑起来的链的;
- 第三,持久化和扩展是怎么长出来、却不把主干拖乱的。
我们先从「设计一个通用 Agent 引擎到底要摊开哪些摊子」说起。
一、先把问题摆出来:做一个「通用 Agent 引擎」的摊子有多大
1.1 一个能想象的任务
想象你被要求做一个通用的、开源可扩展的终端编程 Agent。不是给自家某个模型写个玩具,而是要让它:
- 能接几十家不同厂商的模型——Anthropic、OpenAI、Google、以及一堆开源可兼容的协议端点;
- 既能当 CLI 命令行跑,又能被 Web / SDK 拉起来当服务;
- 还得让人写插件扩展它的行为,而不需要改它核心。
在你动手敲第一行 run() 之前,先把这件事摊开看:它不是一个「循环」,而是四类完全不同的负担叠在一起——模型差异、运行时主循环、持久化、可扩展性。
1.2 先拆出四类负担
把「通用 Agent 引擎」的复杂度摊到桌面上,其实是四堆互不相干的问题:
| 负担 | 它的本质 | 要回答的问题 |
|---|---|---|
| 模型差异 | 各家厂商的接口、协议、能力都不一样 | 怎么用一个抽象把几十家 说得不同 的模型统一进来 |
| 运行时主循环 | 模型不是一次性,是要「感知→分析→行动→反馈」反复迭代 | 这个迭代的骨架怎么保持着,又不散落一地 |
| 持久化 | Agent 是一次又一次的会话,中途崩了要能续上 | 状态存哪、怎么查、怎么升级 |
| 可扩展 | 第三方要能用、能改 | 怎么让别人叠加能力而不改内核 |
这四个问题,正是后面所有章节要解决的四个「为什么」。而 OpenCode 给的答案有一个共同的形式:把每一种负担,都变成一个独立的、可替换的「层次」,再用一条装配链把它们拼起来。 下面我们先把引擎本身分成哪几块讲清楚。
二、三大引擎包:为什么是「契约 / 模型 / 实现」三体
先看 OpenCode 真正的代码骨架。它不是一个包包打天下的巨石,而是一个有边界、有依赖方向的 monorepo——核心引擎由三个包构成:core、llm、opencode。

2.1 core:契约 + 厂商接入基座,是整个系统的底座
packages/core 是整个系统里唯一一个不依赖任何内部包的底座。它放的东西有两类:
- 类型与契约:
session、message、agent、model、tool-output、provider……这些是系统的「名词表」,规定“一个会话长什么样”“一个模型长什么样”。 - 厂商接入基座:它的
src/plugin/provider/下放了 30+ 个 Provider 适配器(Anthropic、OpenAI、Google、Bedrock、Azure、以及一堆 openai-compatible、openrouter、togetherai……),每个都是一个「把一个厂商装进系统」的接线器。
这就点破一层:厂商接入 OpenCode 不是「写死在代码里的 N 家」,而是「往基座上插一个适配器」。你想支持一个新厂商,代码量不至于很小——新写一个 adapter,在 provider/ 目录挂上即可。这是它能「接得住 30 多家」而代码不乱的根本原因。
2.2 llm:独立的模型协议层,把「厂商协议」抽象成可换的一块
如果你注意上一条,会发现模型相关的逻辑其实有两层:一层是「每个厂商怎么接」(core 的 adapter),另一层是「怎么按协议对齐」。OpenCode 把它单独抽成 packages/llm。
llm 包是个独立的模型协议层——它把各家厂商的报文、认证、路由整理到底层的公共 abstraction(Protocol / Endpoint / Auth / Framing 四个维度),依赖面很轻、基本只与基础件(如 effect)耦合,不和 session、运行时缠绕。它的存在意味着:「模型怎么接」这件事,是整个引擎里可以独立演进、独立替换的一块。这不是实现细节,而是设计上的一个刻意分层决策:模型层的稳定性,不该被运行时里那些频繁变动的逻辑一起裹挟。
2.3 opencode:所有「运行时实现」的家
packages/opencode 是三个包里最大的一块,也是真正干活的引擎:session/(会话与主循环)、tool/(几十个内置工具)、server/(HTTP 服务)、mcp/、skill/、snapshot/、storage/(SQLite 持久化)、git/、permission/……几乎除了「契约」和「纯模型协议」之外,一切实现都在这里。
2.4 三者怎么咬合:依赖方向决定「谁在底座」
把三块放一起,依赖方向非常清晰:
core在最底,什么都不依赖(不依赖任何内部包,只靠纯 npm 外部件);llm独立中立,也和内部无强耦合;opencode在最顶,同时依赖core、llm、sdk、plugin、ui、script——它是「装配链」上把别的包拼成一个可运行品的那一段。
大小上看(按 src 下 TypeScript 计):core ≈ 1.2 万行、llm ≈ 8.7 千行、opencode ≈ 8.7 万行——三包合计约 10 万行量级,其中实现层占了绝大部分。
顺着这条装配链,我们先把「层怎么分、链怎么接」看清楚;主循环、工具、上下文等子系统,之后会逐一展开。
三、一条装配链:从你敲命令到它跑起来
三块放在那还只是一堆包;真正让系统活起来的,是它们被装配成一条链。这条链的入口只有一主。

3.1 单一入口:一个 index.ts 拉起整个运行时
packages/opencode/src/index.ts 是唯一入口,用 yargs 装好 20+ 个子命令(run / serve / tui / acp / mcp / providers / agent / …)。真正长驻的是 serve:它拉起一个 Engine runtime(Effect 装配的运行时),同一个进程里同时把 HTTP API 和 WebSocket 两个面开出来。
- 这也意味着:终端里敲
opencode(CLI) 和网页里连它 (Web/HTTP) 是同一套引擎,只是面不同入口——设计上省了一大份「双实现」。

3.2 会话主循环:一条可以被反复走的循环
引擎起来之后,核心是 session 里的主循环(session/processor.ts):它承接一个 prompt,进入「感知 → 分析 → 行动 → 反馈」的迭代,直到模型决定停下来/输出。工具的执行、压缩的触发(上下文溢出)都在这个循环里被安排。
——这一篇不停在它的一格一格。**首要任务只是理解:它是那条「装配链」的中心骨,所有其它层(模型、工具、持久化、插件)都挂在它身上来回打转。**至于每一格怎么走、事件怎么处理,这里就不再展开了。
3.3 一条链的结果:各守一段,化整为零
把上往下整个走一遍: 入口(index.ts/serve)→ 运行时 → 会话主循环(processor)→ 模型(core adapter + llm)→ 工具执行 → SQLite 持久化 / plugin-mcp-skill 扩展。
每一步都清楚自己「从谁手里接、往谁手里送」。这遵循了模块化架构的思想:各节点高内聚、低耦合,接口定义清晰,单点替换不影响整体系统的稳定性。
四、持久化与扩展:让系统可核、可生长
装好了主链之后,还有两块把「系统从能跑到可用」补齐:持久化让你能续、扩展让你能长。
4.1 持久化:状态可查,还能安全升级
Agent 是反复的会话,状态不能都压在内存。OpenCode 用 SQLite + Drizzle 做持久化(schema 在 session.sql.ts / storage 里定义)。这里想说的是一个抽象:把状态「落到更可查询的结构」,而不是 append-only 的纯文本——这样程序能审计、能恢复。
4.2 扩展面:改行为指针不动核心
它让第三方能扩展,靠的是 plugin(公共包,供外部插件作者用)+ mcp + skill + snapshot + background 这些挂在运行时之上的能力。关键是:你要叠加一个新能力,不需要碰核心引擎——插件做成一个「机制」外挂在运行时,还受 permission 约束。这和后面聊 plan-execute-verify 时「插件在原生 hook 上叠加策略」的思路是一脉相承的。

五、解构这套设计背后的工程哲学
5.1 五条好用的抽象
回顾整篇,这套体系最值得带走的是五条抽象,而非某段代码:
- 契约底座先行:
core只放类型与厂商接入基座,不依赖任何内部包——「名词」和「动词」分干净。 - 模型层独立:
llm把厂商协议独立成包,模型接入是可评测、可换的。 - 单一入口:CLI 和 Web 用同一引擎,一个
index.ts装配起所有子命令。 - 一条装配链:入口 → 运行时 → 会话主循环 → 工具 → 持久化 / 扩展,层层咬合。
- 扩展不拆:插件 / mcp / skill 栓在运行时外围,受 permission 门控,不动内核。
5.2 设计哲学:抽象要与直觉,Scale 在于「大纲」
把这些条反过来看,OpenCode 的设计哲学其实是一句朴素的话:一个系统能多大而不乱,不靠某一行代码,而靠「是否一开始就把复杂度切成有边界的层次」,以及「每层次是否只守一件事」。
它把全栈用 Effect 一统(贯穿 core 的 Schema、opencode 的运行时装配、HTTP 服务),因为「依赖注入 + 声明式装配」,在模块很多的地方反而更稳——效果是:引擎是「装配出来的」,而不是「内联写死」的。
章节小测
本章 Quiz — 核心机制自检
1 OpenCode 把引擎拆成 core / llm / opencode 三包,依赖方向最准确的是?
2 厂商接入的核心设计是什么?
3 关于「单一入口」,正确的是?
4 把持久化用 SQLite + Drizzle + 迁移链路做,而非 JSONL,主要说明什么抽象?
5 「装配链」为什么是开这篇的核心?