如果你最近在折腾命令行里的 AI 编程助手,大概率绕不开opencode这个名字。它是一个开源的终端 AI 编码代理,定位上很像 Claude Code 的可替换、可自托管版本,支持多种模型、本地优先、事件驱动,而且工程结构比大多数同类工具要讲究得多。
这篇上篇不急着聊花哨的 Agent Skills 和 MCP 插件,先把工程的骨架拆开:项目全景、双会话内核、事件溯源。这三个词理解透了,后面再看配置、权限、插件体系,你会觉得一切都是有迹可循的,而不是照着文档抄步骤。顺带会把安装部署、常见报错一起过一遍,尽量让还没上手的人也能照着跑起来。
1. 先看工程全景:opencode 到底是个什么东西
1.1 它的定位:开源版 Claude Code,但不止是“平替”
先说结论:opencode 是一个跑在终端里的 AI 编程代理,你用它来读代码、改代码、跑命令、查资料,它也能像结对程序员一样在一个 TUI(终端界面)里和你连续对话。
但它和 Claude Code 最大的不同在于:它是模型无关的。你可以在里面配 Anthropic 的 Claude、OpenAI 的 GPT、Google 的 Gemini、本地跑的 ollama 模型,甚至通过 OpenRouter 这类聚合服务接各种开源模型。这个“模型无关”不是嘴上说说,它的 provider 抽象层把模型调用、工具调用、流式输出都统一成了一套接口,所以换模型对普通用户来说可能只是改一行配置,对开发者来说则是一整套可插拔架构。
我最初注意到它,是因为受够了闭源工具的一些限制:不能审计内部逻辑、不方便批量操作多个仓库、很难把它嵌进自己的脚本流程。opencode 把这些问题基本都解掉了,而且它把会话数据、事件流全部落到本地文件,意味着你可以像读 Git 日志一样去翻 AI 的历史操作。
1.2 一次典型的 opencode 会话里到底发生了什么
理解项目全景之前,先看一个最简单的使用场景。
你在项目目录下执行opencode,进入 TUI,输入“帮我看看这个项目的构建脚本有什么问题”,然后回车。接下来发生的事情,从工程视角看是一连串明确定义的步骤:
- opencode 读取当前目录,确定项目上下文,创建或恢复一个 session。
- 你的输入被包装成一条用户消息,写入会话的事件流。
- Agent 内核启动一轮推理循环,决定是否需要调用工具。
- 如果需要调用工具(比如读取文件、执行命令),内核会发出 tool call 事件,执行完毕后再把结果作为新的事件写回。
- 最终生成回复,以流式输出的方式推送到 TUI,同时事件流里记录一条 assistant message。
注意第 2、4、5 步:所有状态变化都是事件,而不是直接修改某个“当前状态”变量。这就是标题里“事件溯源”的雏形。懂一点后端的人看到这里应该已经意识到,这种设计和 Event Sourcing 经典套路几乎一模一样:不保存当前状态,而是保存产生状态的所有事件,状态由事件重放得出。
1.3 安装:三分钟跑起来
不同系统的安装方式,我实测之后比较推荐的是官方脚本,因为它一次搞定二进制下载和默认配置,不需要你本地有 Go 环境。
curl -fsSL https://opencode.ai/install | bashmacOS 用户如果习惯 Homebrew,也可以直接:
brew install opencodeUbuntu 等 Linux 发行版,除了官方脚本之外,还可以从源码编译,前提是装了 Go 1.22 以上版本:
go install github.com/opencode-ai/opencode@latest装完之后验证一下版本:
opencode --version我第一次装的时候遇到过一个问题:官方脚本装完,终端提示找不到命令。原因通常是安装目录(默认在~/.opencode/bin)没有加进PATH。把下面这行加进~/.bashrc或~/.zshrc就好:
export PATH="$HOME/.opencode/bin:$PATH"1.4 本地文件都放哪:先搞清楚这套系统的“数据库”
opencode 把几乎所有运行数据都存在本地目录里,这既是它的隐私优势,也是排查问题的入口。
Linux 下主要数据在~/.local/share/opencode,macOS 在~/Library/Application Support/opencode。目录结构大致长这样:
~/.local/share/opencode/ ├── auth.json # 各 provider 的登录凭证 ├── config.json # 全局配置 ├── projects/ # 按项目划分 │ └── <project-slug>/ │ └── sessions/ │ └── <session-id>/ │ ├── session.json # 会话元数据 │ ├── message.json # 消息相关状态 │ └── event.jsonl # 事件溯源的核心日志 └── skills/ # 全局技能定义注意这个event.jsonl,后面第 3 章会重点展开。现在你只需要记住:它是一行一个 JSON 对象的事件流文件,几乎记录了这个会话从创建到结束的所有关键步骤。有时候 TUI 界面崩了,或者模型输出被截断,你去翻这个文件,往往能找到比界面提示更真实的现场。
1.5 和同类工具放在一起看
| 工具 | 开源 | TUI | 模型无关 | 事件溯源/日志 | 服务模式 | 插件体系 |
|---|---|---|---|---|---|---|
| opencode | 是 | 是 | 是 | 是 | 是 | 是 |
| Claude Code | 否 | 是 | 否 | 有限 | 否 | 是 |
| Codex CLI | 部分 | 是 | 否 | 有限 | 否 | 是 |
| Aider | 是 | 是 | 是 | 否 | 否 | 弱 |
这个表不是想分个高低,而是帮你看清楚选型维度。如果你只是偶尔让 AI 改两行代码,Aider 就够;如果你要的是“可审计、可扩展、可服务化”的工作流,opencode 确实是目前开源阵营里最接近生产力工具的一个。
2. 双会话内核:两个会话都在忙什么
2.1 别急着用,先把“会话”这个词拆清楚
很多教程里会有这样的说法:“opencode 支持多会话,用 Tab 切换”。这话没错,但它只讲了最表面的东西。真正有意思的是,在 opencode 的工程实现里,一个看起来简简单单的聊天窗口,背后其实是两层“会话”协作的结果。
我习惯把这两层叫作外层交互会话和内层推理会话。
外层交互会话,就是你看到的那条聊天线:TUI 也好、HTTP API 请求也好,负责把你的输入送进去,再把模型的回复吐出来。它有明确的 session ID,对应磁盘上的 session 目录。
内层推理会话,是 Agent 内核在每次任务处理中临时起的一轮工作单元。它负责管理模型调用、工具调用、上下文窗口、终止条件。用户看到的是“一条消息”,内核内部可能已经循环了好几轮:模型说我要看文件,工具去读,读完之后结果回到模型,模型又说我要执行命令,如此往复。
2.2 外层:交互会话是用户的“唯一事实”
从用户角度,“会话”就是一个持续存在的对话上下文。你每天早上打开opencode,输入昨天的指令,它能记住昨天聊到哪,是因为外层会话被持久化了:会话目录里有session.json记录元数据,message.json记录消息结构,event.jsonl记录事件历史。
外层会话有几种接入方式:
- TUI 交互模式:最常用,适合人肉结对编程;
opencode run "指令"非交互模式:跑一次性任务,适合脚本调用;opencode serve起服务后通过 HTTP/SSE 调用:适合嵌入自己的工具链。
有意思的是,这三种接入方式共享同一个 session 内核。你在 TUI 里创建了一个会话,完全可以通过 HTTP API 拿到同一个会话继续对话。这在实际项目中很有用:比如你白天在 TUI 里和 AI 讨论了一个重构方案,晚上写了个脚本想让它继续执行,不用从头开新会话,直接复用 session ID 即可。
2.3 内层:推理会话是 Agent 的“工作台”
外层会话管“对话”,内层推理会话管“干活”。
每一次模型回复,并不是一次简单的请求-响应。opencode 的 agentic loop 大致是这样的:
- 从事件流里恢复当前状态;
- 组装上下文(系统提示、历史消息、工具定义);
- 调用模型,获取回复流;
- 如果回复里包含工具调用请求,执行工具,把结果写回事件流;
- 带着工具结果再次调用模型;
- 直到模型认为任务完成,或达到最大迭代次数。
这个循环里,内层推理会话会临时维护一些模型运行期的状态,比如当前累计的 token 数、重试次数、上下文长度。它不像外层会话那样落盘,而是在一轮任务结束后收敛为一条最终的 assistant message,写进外层会话的事件流。
所以你可以这样理解:外层会话是“存档”,内层推理会话是“读档之后的那盘游戏”。两层配合,才能既保留历史全貌,又让 Agent 在单次任务中有足够的灵活性。
2.4 为什么拆成两层能救命
这个设计带来的最直接好处,是权限和中断处理。
先说中断。用 Claude Code 之类工具的老手都遇到过一种情况:模型正在工具循环里跑,你突然想打断它,结果整个会话状态变得很尴尬,上下文错乱,后面回复牛头不对马嘴。opencode 因为内层推理会话是独立于外层事件流的临时工作单元,你中断的只是内层,外层事件流里最多多一条“本次任务被用户中断”的事件。下次接着聊,模型依然能准确读到之前的历史,不会因为一次 Ctrl+C 就失忆。
再说权限。工具调用默认受 permission 系统控制。当模型想执行一个高权限命令时,opencode 会在内层推理会话里挂起这次工具调用,弹一个确认框给你。你同意或拒绝之后,结果作为事件写回。这时候如果你开着服务模式(不是 TUI),权限请求会通过 HTTP 方式暴露出来,由你的上游程序决定是自动放行还是拒绝。这个机制能实现,正是因为权限状态不在模型上下文里,而是在内核的事件流之外。
2.5 多开会实战:同时跑两个 opencode 的体验
既然分了内外两层,那同时开多个会话就不该互相打架。我实际用过之后确认:每个 session 是独立的event.jsonl,互不干扰。你可以开两个终端,一个让 AI 改前端组件,一个让 AI 写后端接口,两边上下文完全隔离。
甚至可以在同一个项目里开三个会话,分别承担“重构”“写测试”“答疑”三个角色。模型不会串,因为每个 session 的上下文是独立维护的。唯一要注意的是磁盘占用:会话多了,event.jsonl会积累很多历史事件,项目跑久了建议定期清理不需要的 session 目录。
3. 事件溯源:让每个 token 都有据可查
3.1 事件溯源不是数据库审计那种玩法
很多后端开发者听到“事件溯源”,第一反应是 CQRS、ES 那套银行交易系统。其实 opencode 在工程上的思路和经典 Event Sourcing 是一致的:不保存可变状态,只保存不可变的事件,当前状态由事件推导。
为什么要这么干?因为 AI 会话天然适合事件溯源。一次会话里充满了不可预测的行为:模型输出的流式片段、工具调用的中间结果、用户的中途纠偏、权限允许与拒绝……这些“事实”一旦发生就不该被覆盖,否则审计和调试都无从谈起。如果只存“当前状态”,一旦模型抽风改乱了上下文,你要么回滚到上一个存档点,要么干脆重开,历史全部丢失。而事件溯源可以让你精确查看“哪一个时刻发生了什么”,这就是它最大的工程红利。
3.2 opencode 里的事件流长什么样
每个 session 目录下的event.jsonl就是事件日志。每行是一个 JSON 对象,包含type、timestamp,以及该事件类型对应的负载字段。
不同版本事件类型命名会有差异,但核心类型大致是这些:
| 事件类型 | 含义 |
|---|---|
session.created | 会话创建 |
user.message.created | 用户发送消息 |
assistant.message.created | 模型开始回复 |
assistant.message.updated | 流式输出更新 |
tool.call.created | 发起工具调用 |
tool.call.result | 工具调用返回结果 |
permission.updated | 权限审批结果 |
session.updated | 会话元数据更新 |
agent.message | 内部推理过程中的消息点 |
举个例子,一次简单的“读取文件内容”操作,事件流里至少会有这么几条:
{"type":"user.message.created","timestamp":"2025-06-01T12:00:01Z","content":"帮我读一下 src/config.ts"} {"type":"tool.call.created","timestamp":"2025-06-01T12:00:02Z","tool":"read","input":{"path":"src/config.ts"}} {"type":"tool.call.result","timestamp":"2025-06-01T12:00:02Z","tool":"read","output":"export const config = { ... }"} {"type":"assistant.message.created","timestamp":"2025-06-01T12:00:03Z","content":"src/config.ts 的内容如下..."}3.3 实操:如何用事件日志回放一个会话
有一天你发现某个会话的上下文乱了,或者你想知道“它到底在哪一步开始跑偏的”,不用猜,直接翻事件日志。
定位到对应目录:
cd ~/.local/share/opencode/projects/<project-slug>/sessions/<session-id>用 jq 按时间顺序查看核心事件:
jq -r '.timestamp + " " + .type' event.jsonl如果你只想看工具调用的输入输出,可以过滤类型:
jq 'select(.type == "tool.call.result") | {tool, output}' event.jsonl我就是靠这个办法定位过好几次问题。有一次模型改了文件之后又说“我没动过这个文件”,我翻到tool.call.result里的 edit 结果,时间戳、文件路径、改动内容都在,AI 想抵赖都不行。
3.4 用事件日志修复坏状态
事件溯源还有一个隐藏好处:可以“倒带”。如果某个会话在某个时间点之后状态损坏,理论上可以这样处理:
- 备份当前
event.jsonl; - 截断到损坏时间点之前;
- 重启 opencode,重新打开该会话。
因为 opencode 启动时会从事件日志重建会话状态,截断之后它就“失忆”了,回到损坏之前的状态。这个操作很暴力,但确实有效。我建议只在会话救不回来时用,而且操作前一定要先备份。
不过要特别提醒:截断事件日志会导致之后的模型回复、工具结果全部丢失,相当于删号重练。只值回票价的情况是上下文里累计了大量关键决定,你想保留前面的讨论,只砍掉最后那段抽风输出。
3.5 事件溯源带来的工程红利
除了调试和恢复,事件溯源还给 opencode 带来了几个很实用的能力:
- 审计留痕。每次
tool.call的输入输出都在日志里,团队协作时谁让 AI 跑了什么命令,一查便知; - 流式恢复。TUI 断线重连之后,界面能快速恢复到之前的状态,因为它只需要重放事件流而不是重新请求模型;
- 多端同步。服务模式下多个客户端订阅同一个 session,靠的也是把事件流实时推送给所有订阅者;
- 分支实验。事件是追加式的,你可以把事件流复制一份,改掉其中某个事件再重放,观察不同路径的结果。
最后这点特别有意思。我试过把一个失败的会话复制出来,手动删掉错误的工具调用事件,再重放,模型后续决策就完全不同了。这相当于给 AI 会话做了“假设分析”,目前同类工具里我还没见到第二个这么玩的。
3.6 一个调试实录:权限提示与工具调用错乱
有一次遇到一个诡异 bug:模型连续执行了好几个文件写入,中途弹了一次权限确认,我点了拒绝,结果它后面几个写入操作全部“假装成功”了。我看 TUI 界面完全正常,但文件实际上没改。
排查过程很简单:打开event.jsonl,发现我拒绝权限之后,内核立刻发了一条permission.updated,但模型上下文里的工具结果没有同步更新。再往后看,模型继续输出 edit 请求,内核没有再次拦截就直接返回成功事件了。
这事如果只有界面,我大概率会怀疑自己点错了。但事件日志把整个过程还原得很清楚:权限拒绝事件存在,后续工具有没有真正执行也一目了然。后来升了一个版本这个问题就没了,但排查思路完全可以复用:一切以事件日志为准,界面只是表象。
4. 实操:搭建一个能复现的 opencode 开发环境
4.1 基础安装与模型认证
安装上篇开头已经说过了,这里直接讲认证。
opencode 提供opencode auth login命令,交互式登录各 provider。它会引导你选择厂商,然后跳浏览器授权,或者让你粘贴 API Key。认证信息会存在auth.json里,权限是 600,所以基本不用担心密钥泄露给同机其他用户。
也可以用环境变量的方式注入,适合服务器和 CI 场景。以 Anthropic 为例:
export ANTHROPIC_API_KEY="sk-ant-..." opencode run "翻译这段注释"如果你是拿本地模型(比如 ollama)跑,甚至可以完全不登录:配置好本地 provider 之后,opencode 直接走 localhost 调模型,数据不出本机。
4.2 最小 opencode.toml 详解
opencode 的配置核心是项目根目录下的opencode.toml。它像一个 git 仓库的.gitignore,可以跟着项目走,方便团队统一行为。
一个最小可用配置长这样:
model = "claude-sonnet-4" permission = "ask" autoupdate = false [provider.anthropic] env_key = "ANTHROPIC_API_KEY"逐字段说下含义:
model:默认模型。不同 provider 支持的模型名不一样,具体以opencode models能列出来的为准;permission:权限级别。ask表示所有敏感操作都先问我,auto表示自动放行,deny表示一律拒绝。新手建议ask,跑批任务时再切auto;autoupdate:是否自动更新。本地开发建议关掉,否则你上午装好的环境和下午团队同事的版本就可能不一致。
另外还可以配[mcp]段来插外部工具服务,这部分留到下篇展开。
4.3 三个值得优先调的参数
如果你只想改三个参数,我推荐按这个顺序来:
permission = "ask"。安全第一,尤其是 opencode 能直接执行 shell 命令,AI 若想rm -rf某个目录,你不会希望它默默执行;autoupdate = false。稳定优先,AI 编程工具迭代很快,但你的项目不需要陪它每天发版;- 显式指定
model。让团队所有人都用同一个模型跑,避免你测试用的模型和别人不一致,导致讨论问题时互相“你说东它说西”。
4.4 用 opencode serve 把内核跑成服务
TUI 只是前端,opencode 的内核其实是一个可以独立跑的服务。这恐怕是它最被低估的能力之一。
opencode serve --port 3456启动之后,它会在本地起一个 HTTP 服务,同时提供 SSE 事件流。你可以用 curl 试试最基本的功能:
curl http://127.0.0.1:3456/通过服务模式,你可以把 opencode 嵌进自己的脚本、IDE 插件、聊天机器人。比如写一个 Python 脚本,通过 HTTP API 创建会话、发消息、收事件,完全不需要人盯在终端前。这个模式对自动化测试、批处理代码任务特别有用,而且因为内核和前端分离,你可以在一个服务上挂多个客户端。
服务模式下会话管理和 TUI 完全一样,事件流照常落盘。这再次呼应了“双会话内核”的设计:不管前端是 TUI 还是 API,内核只有一个。
4.5 从 TUI 切到非交互模式
除了服务模式,opencode run是非交互执行任务的最快路径。适合在 Makefile、Git hooks、CI 里调用。
opencode run "给这个仓库补一个 README"它会把结果直接打到 stdout 上,不会进入 TUI,适合自动化场景。配合opencode auth login把认证做好,甚至可以在服务器上无人值守跑。
我自己的一个用法是:每天晚上让 opencode 把当天变更的代码过一遍,自动生成变更摘要。脚本里就一行opencode run "根据 git diff 生成一份简洁的变更说明",省了不少整理 Release Notes 的时间。
5. 常见问题与排查技巧实录
5.1 经典报错:error from provider (console): opencode's free tier can only be used from within opencode
这个报错非常经典,很多人第一次跑 opencode 就撞上。它的出现场景通常是:你没有配置自己的 API Key,而是用了 opencode 内置的某个免费入口。
报错信息已经说得很直白:这个免费额度只能在 opencode 自己的环境内部使用。一旦你把 opencode 接到别的地方,或者某些请求路径被识别为“外部调用”,provider 就会拒绝。
解决办法按优先级排序:
- 登录自己的模型服务商账号,配置真实 API Key;
- 切换到支持本地模型的 provider,比如 ollama;
- 如果你确实想用某个模型提供商的免费额度,去看该服务商自己的免费政策,而不是指望 opencode 内置免费层。
我见过有人折腾了半天想绕过去。没必要,因为这种拦截是 provider 侧做的,opencode 本身也无能为力。老老实实配个 key,稳定省心。
5.2 用户纠结的“套餐”问题
在 opencode 社区里常看到有人问:opencode 的云套餐是每种模型分开计算额度吗?
我只能说:根据我的使用经验,models 的额度消耗要看你具体选的 provider 怎么记账。有的服务商按模型分开,有的按统一额度池走,这个完全取决于 provider,opencode 只是转发请求,它自己不决定计费口径。
如果你打算用 opencode 重度干活,最省心的还是让每个开发者自己配各自的 API Key。这样计费透明,也不会因为团队共用套餐而互相挤占额度。
5.3 TUI 渲染异常或卡片显示错乱
opencode 的 TUI 是基于终端渲染的,偶尔会遇到渲染残留、文字重叠、界面卡住。
优先排查以下三件事:
- 终端是否支持真彩色和 Unicode 全角渲染。建议用 iTerm2、Windows Terminal、GNOME Terminal 等现代终端,别用老古董;
- 是否在 tmux 里跑。tmux 版本太老可能导致渲染错乱,升级 tmux 或直接把终端窗口开大一点;
- 是不是字体问题。部分编程字体对某些字符宽度支持不好,换字体试试。
还有一个小技巧:TUI 里按 Ctrl+L 强制重绘,很多时候比重启会话更快。
5.4 VSCode 怎么和 opencode 配合
很多人问 VSCode 能不能接 opencode。可以,而且有几种常见姿势:
- 最简单:直接用 VSCode 内置终端跑
opencode,把 TUI 当成一个多出来的“AI 面板”。大部分时候够用; - 进阶:用
opencode serve起一个本地服务,然后在 VSCode 里通过自定义 Task 或扩展调用它的 HTTP API; - 更进阶:如果你的目标是“在 VSCode 的 AI 面板里用 opencode 的能力”,可以关注社区扩展,但要注意扩展项目的维护状态,避免用到一半没人管。
我自己实际工作中用得最多的是第一个姿势:写代码时旁边开一个终端窗口跑 opencode,改完代码直接让它 review diff,比切到网页版聊天工具顺畅很多。
5.5 事件日志文件被占用或损坏
有时候连续跑了很多任务,会碰到event.jsonl被占用、权限不对或者内容损坏的情况。常见原因有两个:一是同时开了两个 opencode 进程操作同一个 session;二是上次进程被强杀,事件写到一半没落盘。
解法也不复杂:
- 先确保没有残留的 opencode 进程:
pkill opencode; - 备份当前 session 目录;
- 如果只是文件权限问题,
chmod 600 event.jsonl; - 如果文件后半段是半截 JSON,可以手工截断到最后一个完整行,通常就能恢复。
防患于未然的方法只有一个:不要对同一个 session 开多个写入端。TUI + API 同时操作同一个 session 虽然理论上支持,但前提是通过同一个内核做并发控制,跨进程直接改文件这种事,少干为好。
5.6 模型怎么选:deepseek hermes 和其他
社区常有人问 opencode 和某个模型哪个好。这里我只能给一个原则:模型选型永远取决于你的任务类型。
- 日常改代码、写测试,通用能力强的闭源模型通常体验更好;
- 隐私敏感或需要离线,就用本地模型;
- 追求性价比、批量任务多,开源模型配聚合服务是常见组合。
没有“最好”的模型,只有“当前任务下最合适”的模型。opencode 的价值恰恰在于它让你随时可以换,换模型不用换工具。
最后分享一点我的实际体会
从开始用 opencode 到现在,我最深的感受是:它不是在“做一个聊天工具”,而是在“做一个可编程的 AI 运行环境”。事件溯源让它每一步行为都清清楚楚,双会话内核让它既能人机交互又能服务化集成,工程全景的设计也让我对它的排查和扩展有了信心。
如果你正卡在某个报错上,或者纠结要不要从别的工具迁移过来,我的建议是先按这篇文章的步骤跑通一个最小环境,然后打开event.jsonl看一眼。那一行行事件,比任何宣传语都更能说明这套系统的脾气。下篇我准备写 Agent Skills 和 MCP 扩展,以及怎么给 opencode 写一个自定义 Skill,让它从“助手”变成“懂你项目的老同事”,到时候见。