Skip to content

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

OpenCode 整体架构

如果一个终端里能跑的 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——核心引擎由三个包构成:corellmopencode

三大引擎包:core 底座 / llm 模型层 / opencode 实现层

2.1 core:契约 + 厂商接入基座,是整个系统的底座

packages/core 是整个系统里唯一一个不依赖任何内部包的底座。它放的东西有两类:

  • 类型与契约sessionmessageagentmodeltool-outputprovider……这些是系统的「名词表」,规定“一个会话长什么样”“一个模型长什么样”。
  • 厂商接入基座:它的 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最顶,同时依赖 corellmsdkpluginuiscript——它是「装配链」上把别的包拼成一个可运行品的那一段。

大小上看(按 src 下 TypeScript 计):core ≈ 1.2 万行、llm ≈ 8.7 千行、opencode ≈ 8.7 万行——三包合计约 10 万行量级,其中实现层占了绝大部分。

顺着这条装配链,我们先把「层怎么分、链怎么接」看清楚;主循环、工具、上下文等子系统,之后会逐一展开。

三、一条装配链:从你敲命令到它跑起来

三块放在那还只是一堆包;真正让系统活起来的,是它们被装配成一条链。这条链的入口只有一主。

OpenCode 装配链:入口 → 运行时 → 会话主循环 → 工具 → 持久化与扩展

3.1 单一入口:一个 index.ts 拉起整个运行时

packages/opencode/src/index.ts 是唯一入口,用 yargs 装好 20+ 个子命令(run / serve / tui / acp / mcp / providers / agent / …)。真正长驻的是 serve:它拉起一个 Engine runtime(Effect 装配的运行时),同一个进程里同时把 HTTP APIWebSocket 两个面开出来。

  • 这也意味着:终端里敲 opencode(CLI) 和网页里连它 (Web/HTTP) 是同一套引擎,只是面不同入口——设计上省了一大份「双实现」。

单一入口 -> 引擎运行时 -> 同进程开 HTTP API 与 WebSocket

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 上叠加策略」的思路是一脉相承的。

持久化 + 扩展面:SQLite 为底,plugin/mcp/skill/snapshot 由 permission 门控

五、解构这套设计背后的工程哲学

5.1 五条好用的抽象

回顾整篇,这套体系最值得带走的是五条抽象,而非某段代码:

  1. 契约底座先行core 只放类型与厂商接入基座,不依赖任何内部包——「名词」和「动词」分干净。
  2. 模型层独立llm 把厂商协议独立成包,模型接入是可评测、可换的。
  3. 单一入口:CLI 和 Web 用同一引擎,一个 index.ts 装配起所有子命令。
  4. 一条装配链:入口 → 运行时 → 会话主循环 → 工具 → 持久化 / 扩展,层层咬合。
  5. 扩展不拆:插件 / 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 「装配链」为什么是开这篇的核心?

Agent Src — AI Agent 源码精读