第一次有人找我聊 opencode,多半是带着一条报错来的:
error from provider (console): opencode's free tier can only be used from within opencode
这句话初看像客服话术,但我建议你把它当成理解 opencode 工程定位最好的一把钥匙。它说明了两件事:opencode 的免费模型不是普通的 OpenAI 兼容 API,你没法把它抄到别的客户端里当"免费 ChatGPT"用;同时,opencode 自己就是一个完整的运行时——它不只是一个终端聊天工具,而是一个带着会话管理、Agent 循环、事件落盘、快照机制的编码智能体运行时。这次这篇文章想聊的,就是这个运行时本身:它的工程全景、双会话内核,以及贯穿全部状态管理的事件溯源机制。如果你已经用过一段时间的 opencode,或者正在决定要不要从 Claude Code / Codex 迁过来,这篇文章应该能回答你不少"它到底是怎么工作的"这类问题。
1. 先拨开第一层迷雾:opencode 不是"命令行版 ChatGPT"
1.1 那条报错为什么值得多读几遍
先说那个免费额度报错的场景。很多人第一次遇见它,是因为看到 opencode 支持自定义 provider,就把内置的opencodeprovider 的 baseURL 拿出来填到别的地方去,结果被拒。报错里有个关键词:can only be used from within opencode。
这句话按我的理解,是 opencode 在服务端校验了客户端的来源特征,不只是简单的 API Key 检查。也就是说,免费额度的使用条件是"你人在 opencode 的会话里",而不是"你拿到了一个可以用任意 HTTP 客户端调用的令牌"。这个设计在工程上挺聪明:它能有效防止免费额度被脚本批量薅走,同时也把"opencode 运行环境"和"外部任意客户端"区分开。理解了这一点,你就理解了 opencode 的核心哲学——它更接近一个"本地优先的 Agent 运行时",而不是"模型的转发面板"。
1.2 从聊天 CLI 到 Agent 运行时,中间隔了一个"状态机"
如果你用过早期的终端 ChatGPT 客户端,大概会有这种印象:启动、提问、等回复、退出,完事。opencode 完全不是这个逻辑。它启动之后是一个交互式 TUI,你可以在同一个会话里让 agent 改代码、跑命令、读文件、反复调试;它会维护项目的上下文,会在关键节点打快照,会记录每一次工具调用和模型输出——整套东西的核心不是"聊天界面",而是一个围绕项目状态运转的 Agent 循环。
我把这个差异总结成一句话:聊天 CLI 管理的是"对话状态",Agent 运行时管理的是"工作状态"。对话状态只需要记住谁说了什么;工作状态则要记住当前项目改了哪些文件、跑了哪些命令、哪次工具调用失败了、失败后 agent 打算怎么办。为了管理这种复杂的工作状态,opencode 必须做两件事:一是把会话结构拆成合理的粒度(这就是双会话内核的由来),二是把每一次状态变化以事件的形式留存(这就是事件溯源)。这两点后面会分别展开。
1.3 工程全景:CLI、TUI、Agent、Provider、Session 各就各位
在深入内核之前,先给 opencode 的工程全景画一张地图。它不是一个单体巨石,而是由多个清晰模块组成的协同系统,我用表格整理一下各模块的职责:
| 模块 | 职责 | 说明 |
|---|---|---|
| CLI / TUI | 用户交互入口 | 终端界面,负责展示消息、接受输入、操作 session |
| Session 管理 | 会话的创建、恢复、切换 | 区分不同项目、不同任务,是双会话内核的外层 |
| Agent Loop | 模型推理与工具调用的循环引擎 | 一条用户消息触发多轮"思考-行动-观察"循环 |
| Provider 抽象 | 对接不同模型服务商 | 内置 opencode、Anthropic、OpenAI、Gemini、DeepSeek 等 |
| Tool / MCP | 文件读写、命令执行、外部工具 | 通过 MCP 协议接入更丰富的外部能力 |
| Event Store | 事件溯源存储 | 把每次交互、每次工具调用追加写入日志 |
| Snapshot | 项目状态快照 | 在关键节点保存现场,支持回滚与恢复 |
这张表里的每个模块单独拎出来都能写一篇,但我觉得对读者最有价值的是中间的三个:Session 管理、Agent Loop、Event Store。它们三个组合起来,就是标题里说的"双会话内核 + 事件溯源"。下文的展开顺序是:先讲双会话内核长什么样,再讲事件溯源为什么是这个架构的底座,最后回到真实使用场景看这些机制如何表现为你能感知到的功能和边界。
2. 双会话内核:屏幕上有一层会话,屏幕后面还有一层会话
2.1 "双会话"到底拆的是哪两层
先说判断依据。我在实际使用 opencode 时注意到一个现象:在 TUI 界面上,一个 session 就是一条条消息组成的列表,看起来跟普通聊天软件没有区别;但当我打开它落盘的历史日志时,发现同一个 session 内部的记录粒度要细得多——不仅有用户消息和助手消息,还有模型内部推理、工具调用、工具返回结果、错误重试等等。这两种粒度并存,说明系统内部一定有两层会话结构在同时运转。
我把这两层分别叫作交互会话和引擎会话:
- 交互会话是用户能感知的那一层:它管理"我当前在哪个项目""我这条消息是延续之前的对话还是新开一个话题""历史消息怎么展示"。它负责的是人机边界。
- 引擎会话是用户不太能直接感知、但决定一切行为的那一层:它管理"这一轮任务到底要执行到哪一步""工具调用序列怎么编排""重试策略怎么执行"。它负责的是任务边界。
用餐厅做类比的话,交互会话是前厅的点餐记录本,用户看得到;引擎会话是后厨的工单流转卡,前台点一份牛排,后厨可能要拆成"解冻、腌制、煎制、装盘"好几个步骤,一步步执行完才会上菜。opencode 的过人之处在于,这两层不是割裂的,而是共用同一个事件流:前台记录的"点了什么菜"和后厨记录的"煎了几分钟"最终都沉淀到同一份日志里。这也是为什么它能做到"对话可以恢复、任务也可以恢复"。
2.2 交互会话:轻量、可见、可恢复
交互会话的设计目标我总结为四个字:轻量、可靠。它不需要把每一步执行细节都塞给用户看,只需要呈现最终结果和关键动作。你退出 opencode 再重新进入,可以在历史会话列表里找到之前的 session,继续往下聊;换一台电脑,只要把数据目录拷贝过去,同样的会话上下文也能接着用。这个"可恢复"能力的实现基础,就是事件溯源——它会话里的所有上下文,都从事件日志里重建出来,而不是靠一个维护在内存里的"当前状态快照"硬撑。
交互会话的轻量性还体现在它允许你随时"分叉"。我经常遇到的情况是:一个会话里 agent 改了几轮代码,但我想换个思路重来,又不想丢掉之前的记录。在交互会话层面,我可以基于同一份事件历史开出一条新线,之前的对话保留,新的尝试不影响旧记录。这种体验在 Claude Code 里也能做到,但在 opencode 里因为事件流的粒度更细,"分叉"的代价更低。
2.3 引擎会话:每一轮 Agent Loop 都是一段完整事件序列
引擎会话是更值得研究的一层。它要回答的问题是:一条用户消息进来之后,opencode 内部是怎么把它变成一连串动作的?
我的理解是,每次用户消息进入,opencode 会在引擎会话里启动一个 Agent Loop。这个循环大致长这样:把当前可用的上下文汇总给模型,模型产出决策(可能是回复文本,也可能是工具调用请求),Agent 执行工具调用,得到结果后把结果回填给模型,模型继续决策,直到它认为任务完成。这一整个循环里产生的每一步,都会被记录成一个事件:模型推理事件、工具调用事件、工具输出事件、错误事件、重试事件。
这里可以对比一下两层会话的存储与可见性:
| 对比项 | 交互会话 | 引擎会话 |
|---|---|---|
| 粒度 | 用户可见的消息级 | 模型推理、工具调用、结果的执行级 |
| 生命周期 | 跟随用户操作,可暂停可恢复 | 随每轮任务起止,任务完成即归档 |
| 主要用途 | 上下文延续、会话切换 | 任务编排、失败重试、行为审计 |
| 存储形式 | 同样落在事件日志中 | 同样落在事件日志中,事件类型更丰富 |
很多使用 opencode 时遇到的怪问题,其实都出在引擎会话这一层。比如 agent"绕圈"——同一个工具调用失败三次,它还在重试;比如模型明明收到了工具结果,却好像"没看见"一样重复请求。这些现象说明引擎会话在执行过程中对状态的维护是有状态的,而状态一旦出错,你的排查手段就是去看事件日志,而不是只盯着界面上的聊天记录。这正是事件溯源存在的意义:它能让你把一次"绕圈"事故还原成一条清晰的事件链,看到底是哪一步的状态出了偏差。
3. 事件溯源:为什么它敢让所有状态都"可重放"
3.1 以终态存储和以事件存储,差的不是一点半点
要理解事件溯源,最简单的方法是看一个反例。大多数应用存储会话状态的方式是"以终态为准":聊天记录表里存一条消息,更新了就覆盖,删了就没了。这种方式的优点是简单直接,缺点是过程不可见。你只能看到当前状态,看不到这个状态是怎么一步步变成的。
事件溯源换了一种思路:不存"当前是什么",只存"发生了什么"。每一条交互、每一个工具调用结果、每一次模型决策,都是追加写入日志的一个事件。当前状态是所有这些事件按照顺序"重放"出来的结果。打个比方,银行的账本永远不会只记一行"当前余额 5000 元",而是记每一笔存取款流水;余额随时可以由流水重算出来。事件溯源的账本思路,天然具备可审计、可回溯、可重建的能力。
opencode 选这个方案,我认为不是炫技,而是被 Agent 场景逼出来的刚需。Agent 的行为模式是"下一步决策依赖上一步的结果",而且过程中充满了不确定的工具返回值、失败和重试。如果你只保存"最终改好的代码",那一旦失败,你根本不知道失败发生在哪个环节。事件溯源让问题变得可定位:日志里清清楚楚记着哪次工具调用返回了非零状态码,agent 又是基于什么上下文做了下一步决定。
3.2 opencode 把事件写进了 JSONL,一行一个事件
opencode 的事件存储落地形式是 JSONL 日志文件。按 XDG 目录规范,它通常位于~/.local/share/opencode/下面,按照项目名和 session id 分层存放。每个 session 对应一个或多个 jsonl 文件,每一行是一个独立事件,追加写入。结构大致长这样(字段名以实际版本为准,这里只做示意):
{"type":"user_message","text":"帮我修一下登录报错","timestamp":"2025-01-01T10:00:00Z"} {"type":"assistant_message","content":"我先看一下后端日志","timestamp":"2025-01-01T10:00:01Z"} {"type":"tool_call","tool":"bash","input":"tail -50 server.log","timestamp":"2025-01-01T10:00:02Z"} {"type":"tool_result","exit_code":0,"output":"..."} {"type":"assistant_message","content":"日志里有一条数据库连接失败","timestamp":"2025-01-01T10:00:03Z"}我实际排查过几次问题,这套设计在"事故复盘"场景下非常好用。有一次 agent 在重构时意外删错了文件,界面上的对话历史完全看不出操作顺序,但打开事件日志,从tool_call事件能清清楚楚看到它是先读了目录列表,然后对错误的文件执行了删除。那一刻你就能理解,为什么 opencode 敢把状态管理做得这么"重"——因为重放能力就是安全的兜底。
3.3 重放带来的三个实际能力:回溯、快照与迁移
事件溯源不是理论概念,它直接转化成三个你在使用中能感知到的能力:
第一个是回溯。你可以在会话里往前翻,看到任意一个历史节点上 agent 当时看到了什么、做了什么。这个能力平时用不大上,但当 agent 说出"我不知道为什么这个文件被改坏了"这种话时,回溯事件日志可以很快定位到"是哪一步、哪一个工具调用引入了变更"。相当于给 Agent 装了一个黑匣子。
第二个是快照与回滚。opencode 会在任务的关键节点对项目做快照(当前实现里,对 Anthropic 系模型的支持最完整,部分模型可能不提供)。这个快照不是存储整个文件树副本,而是基于事件流标记一个"重放锚点"——需要回滚时,就从这个锚点重建项目状态。效果等同于 git reset,但比 git reset 更贴近 Agent 任务的节奏。我在做大规模重构实验时,只要 agent 准备动手改一批文件,都会确认快照已经打好,这样试错成本变得非常低。
第三个是迁移。因为所有会话状态都是由事件重放出来的,你不需要导出什么"记忆文件",直接把~/.local/share/opencode/下的项目目录整个拷贝到另一台机器,启动 opencode 选择继续会话,agent 会像什么都没发生过一样接着干。这个方法我实际验证过,项目上下文、工具调用历史全部保留。这在换电脑或多人协作时特别省事。
4. 真正跑起来:安装、Provider、配额边界与会话的协同
4.1 从零安装到最小可用配置
opencode 用 Go 编写,分发形态是单二进制,这一点在 Linux 上特别友好。如果你用的是 Ubuntu,最省事的方式是直接走官方安装脚本:
curl -fsSL https://opencode.ai/install | bash装完之后二进制会被放到~/.opencode/bin(或者你的 PATH 里),跑一下opencode --version确认版本。不想用脚本的同学也可以用 Go 直接装:
go install github.com/sst/opencode@latest需要注意一个细节:opencode 的默认数据目录依赖 XDG 环境变量,如果你之前改过XDG_DATA_HOME,那事件日志就不在默认位置了。找起来不难,但确实遇到过有人找不到日志来问我,先看一眼环境变量是最快的定位方式。
第一次启动之前,需要配认证。内置 provider 里,Anthropic、OpenAI、DeepSeek 这些都支持 API Key 登录;opencode 自己的免费模型也在,但它要求请求必须"from within opencode",内容前面已经讲过了。配置集中在~/.config/opencode/opencode.json里,核心是 provider 和 model 的声明,比如你要用自定义的 OpenAI 兼容端点,就在 provider 段里加baseURL。
4.2 Provider 接入与"会话级身份"问题
opencode 的 Provider 抽象做得比较规整:无论是官方模型服务,还是自建的 OpenAI 兼容网关,对上层 Agent Loop 来说都只是"一个能完成推理的端点"。我身边不少朋友在用社区里一个叫 opencode go 的聚合网关方案,它把多家模型商的 API 统一成一个端点,省去频繁切换 baseURL 的麻烦。用这类网关时要特别注意额度的计算方式:它往往不是按"总账户"统一扣费,而是每个模型各算各的额度。也就是说,你在 opencode 里把 claude 和 deepseek 都指向同一个网关时,两边是独立的计费池,别指望一边没额度了另一边还能顶上。这个"分开计算额度"的规则如果不清楚,很容易产生"明明充了钱却一直报 no quota"的错觉。
再配合开源工具 cc-switch v2,它解决的是多套配置之间的快速切换问题。比如同一台机器上,你有 Claude Code 的配置、有 opencode 的配置、有 Codex 的配置,手动改 JSON 文件很痛苦,用 cc-switch 可以直接在几套配置模板之间切换,选好之后自动写到对应工具的配置文件里。这类工具没有改变 opencode 本身,但它让"多 provider 轮换"的工作流变得顺滑很多。
这里想强调一个我踩过的坑:自定义 provider 的会话级身份和 API Key 是两回事。很多自定义网关同时服务多个用户,靠请求头里带的 Authorization 区分身份。opencode 在发起请求时会带上你在 provider 配置里写的 key,但如果你用的免费层 provider 有"来源校验"(比如前面那条free tier can only be used from within opencode),它检查的不只是 key,还包括整个请求上下文。为了确认这个问题,我的排查顺序是:先确认 auth 状态,再检查 baseURL 指向,最后确认我到底是在 opencode 自己的 TUI 进程里发起的请求,还是通过别的方式转发出去的。从涉及的报错信息反推,前两步通常都没问题,问题往往出在第三步。
4.3 Free Tier、VSCode 协同与其他使用边界
VSCode 和 opencode 的配合也是被问到很多的话题。最简单的方式是在 VSCode 的集成终端里直接跑 TUI,这样你左边是编辑器,下面是 opencode 的操作台,agent 改文件、跑测试,你都能在同一块屏幕里看到。想更深度集成的话,opencode 还支持以 serve 模式起一个本地服务,由 VSCode 扩展往这个服务发请求。我个人的建议是:日常轻量使用就用集成终端;要长期跑一个后台 agent 任务的时候再用 serve 模式,因为它把进程和终端解耦了,关掉编辑器 agent 还能继续跑。
最后再回到 free tier 那条报错。如果你在 TUI 里正常使用都遇到它,我的排查经验是:先看自己的网络连通性是否正常,再看 auth 状态是否过期,最后检查是不是在配置文件里把内置 provider 的某些字段覆盖掉了。排除这几点之后,最稳妥的做法是新建一个 session 再试。从工程视角看,这条边界其实是在提醒你:opencode 的免费额度绑定的是"opencode 运行时"这个整体,而不是某一把静态的钥匙。这也是为什么我始终坚持一个态度——把 opencode 当黑盒用,遇到问题会很难办;试着从事件日志和会话内核的角度去理解它,很多报错反而变得特别容易解释。
说了这么多,落到我自己的使用习惯上,其实只有一条:每次让 agent 做重操作之前,先看一眼事件日志和快照状态。opencode 的事件溯源设计,让"试错"真正变得廉价了,但前提是你愿意在它工作的时候,花十秒钟理解它是怎么记录你项目的每一步的。这个习惯养成了,你会发现 CLI 时代的 Agent 工具,比你想的更接近一个可靠的同事。