前阵子把OpenCode拉下来跑通之后,我顺手做了个一般人不太会做的事:把它的源码从头到尾读了一遍。原因很简单——用的时候它表现很聪明,但一旦涉及"它到底是怎么做到的",文档里很多东西语焉不详。比如Skills为什么能自动触发,LSP信息是怎么喂给模型的,"this model is not available in your country"到底是OpenCode的问题还是上游的问题。与其去issue里翻二手答案,不如直接把源码打开看一眼。这篇文章就是那轮源码阅读的笔记整理。它是一篇源码解析,不是OpenCode的新手教程,但我尽量把社区里问得最多的安装、配置、报错问题也带进去,因为很多问题的根因恰好就藏在源码里。适合已经在用或者准备深入OpenCode的开发者,也适合对AI编码Agent内部机制感兴趣的读者。
1. 先别急着看代码:OpenCode的定位和源码仓库结构
1.1 它不是聊天工具,是一个Agent运行时
很多人第一次打开OpenCode,觉得它是个带界面的ChatGPT。这个印象不准确。OpenCode的核心不是"聊天",而是"在代码仓库里自主执行任务"。它背后是SST团队开源的一个AI编码Agent,整个运行时的设计目标就是让模型能够观察代码、修改文件、执行命令、调用浏览器,并且在多轮工具调用中保持状态。从这个角度理解,读它的源码更像在读一个轻量级Agent框架,而不是在读一个聊天软件。
这也解释了为什么它的模块会分成工具、模型适配、Skill、LSP、浏览器自动化这么几大块。聊天只需要模型API,但Agent需要工具系统、上下文管理系统、权限系统和可扩展的接入层。你很难在一款闭源产品里看到这么完整的分层,这恰巧是OpenCode很适合做源码解析的原因。
1.2 从入口到核心模块:源码目录怎么读
源码阅读的第一步是把仓库拉到本地。日常使用OpenCode时,官方提供了CLI下载方式,桌面版和IDE插件(比如VSCode、JetBrains)也有对应安装包;但读源码需要直接clone仓库。拉到后你会发现核心代码是TypeScript写的,安装依赖用的是Bun。从功能上分,核心代码大致可以切成这么几块:
- TUI界面层:负责终端交互、渲染会话、处理输入输出。
- 会话层:记录对话历史、管理上下文窗口、处理附加文件。
- Agent编排层:核心决策循环,负责调用模型、解析工具调用、执行工具、回填结果。
- 工具层:所有工具的注册、参数校验和执行。
- 模型适配层:对接Anthropic、OpenAI、OpenRouter、Ollama等服务商。
- Skill层:技能的发现、加载、匹配与执行。
- LSP层:启动语言服务器,把语义信息转成模型上下文。
- 浏览器自动化层:封装浏览器动作,让Agent能真实操作页面。
我读源码建议的顺序是:先看会话层和Agent编排层,理解一次对话怎么流转;然后看工具层和模型适配层,理解能力从哪来;最后看Skill、LSP、浏览器自动化这些增强模块,理解它为什么比普通聊天更"懂代码"。这个顺序是我自己翻了两次源码后总结的,能最快建立全局观。不同版本的目录结构会有差异,比如2.x比0.x多了桌面端相关代码,但大的分层思路基本延续。
1.3 为什么选Bun和TypeScript
技术选型也值得聊一句。OpenCode选择Bun作为运行时,核心原因是性能——终端工具对启动速度和文件IO有很高要求,Bun的启动速度和内置工具链让整个CLI的体感响应很快。TypeScript则给这种复杂状态机提供了约束,消息、工具参数、Provider返回值都有明确类型,读代码时能省很多脑力。如果你打算自己改源码,本地Bun版本最好和仓库要求对齐,版本不对会在构建阶段报各种奇怪错误,这是第一个容易踩的坑。
2. Agent主循环:一次"让AI改代码"的请求在内部走了哪几步
2.1 从用户输入到工具调用的完整链路
这是全文最关键的一段。一次看似普通的"把这段代码重构一下"背后,OpenCode内部的流转是这样的:
- 用户输入进入会话层,作为user消息追加到消息列表。
- Agent层把系统提示词、历史消息、当前上下文里的文件内容、所有已注册工具的定义,一起打包成一个模型请求。
- 模型返回结果,可能是纯文本,也可能包含一个或多个工具调用。
- 如果有工具调用,Agent不会直接把它交给用户,而是先做参数校验和权限检查,再调用对应的工具执行函数。
- 工具返回结果被包装成tool消息追加回消息列表。
- Agent拿着更新后的历史,继续请求模型。
- 反复执行3到6步,直到模型不再产生新的工具调用,最终回复才展示给用户。
这就像你让一个实习生干活,他不是一口气给你答案,而是"查资料→给你看→你反馈→再查→再给你看"。OpenCode的Agent循环就是把这个实习生写成了代码。模型能不能完成任务,很大程度取决于这个循环设计得好不好。
2.2 工具调用与结果回流是整个Agent的发动机
工具调用这块,从源码上看核心可以用下面的类型来理解:
interface ToolCall { id: string; name: string; arguments: Record<string, unknown>; } interface ToolResult { ok: boolean; data: unknown; error?: string; }消息列表是Agent的"记忆",每轮工具调用和工具结果都会被完整记录。这样模型在下一次生成时,能够看到"我调用了read_file,读到了xxx内容",从而基于真实文件内容做判断。这也是为什么OpenCode改代码时不会凭空乱改——它的上下文里确实有文件内容。源码里的循环逻辑并不复杂,本质上就是一个while循环,关键在退出条件:只有模型不再请求工具时循环才停止。这个设计直接决定了Agent的自主程度。
提示:如果你在源码里看到某个工具一直没被模型调用,先检查工具定义里的description写得是否清楚。模型是靠description决定要不要调用工具的,工具名和参数说明写得模棱两可,它就不会用。
2.3 "导入一段代码进行修改"在源码上是如何实现的
很多网友问"opencode怎么导入一段程序代码并进行修改完善"。其实在源码里,这个动作并不神秘:会话层支持把一个或多个文件作为附加上下文注入。注入后文件内容会以明确的分隔格式出现在消息列表里,模型的后续回答就有了具体依据。实际操作时,你可以在会话里添加文件,也可以直接把文件路径拖进输入框,让Agent读取后再修改。比起笼统地说"帮我优化一下",带上文件后模型的输出完全是两个级别。从源码视角看,这里最值得关注的是上下文截断策略——文件内容过长时,会话层会怎么裁剪,这决定了Agent在大仓库里的可用性。
3. 模型适配层:invalid api key、地区限制、免费模型是同一件事的三面
3.1 Provider抽象:OpenCode是怎么统一各家模型API的
不同模型服务商的API格式、认证方式、流式协议都不一样。OpenCode在源码里做了一层Provider抽象,把各家差异挡住。每个Provider都要实现统一的接口:发请求、解析流式输出、转成内部Message和ToolCall。对上层Agent来说,它根本不关心模型来自Anthropic还是Ollama,它只跟统一的抽象打交道。
这也带来了一个非常实际的好处:你在一个OpenCode会话里可以随时切换模型服务商,甚至可以把同一个问题分别发给Claude和本地模型对比答案。切换模型时,会话历史是共享的,模型适配层负责把相同的历史翻译成不同服务商能理解的格式。源码里各Provider的差异主要集中在请求体构造和流式解析这两块,前者各家格式不同,后者大多走SSE但要处理的事件字段不一样。
3.2 API key到底从哪来:环境变量、配置文件和ccswitch
invalid api key是最常见的问题。源码里API key的读取顺序大致是:当前目录配置文件、全局配置文件、环境变量。对于大多数Provider,最终靠的还是环境变量,常见变量名如下:
| 模型服务商 | 常见环境变量 |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| Google Gemini | GEMINI_API_KEY |
| Ollama | 不需要key,本地服务 |
如果你报invalid api key,先按这个顺序查:环境变量有没有设置、设置的是不是当前选择Provider对应的变量、key有没有多余的空格或换行、key本身是否过期。社区里常见的ccswitch,本质上就是把不同服务商的key集中管理,在启动opencode前把当前要用的key注入环境变量。它不干预OpenCode内部逻辑,所以排查时记得先确认ccswitch注入之后,环境变量里真实的值是什么。另外,Linux下经常要手动修改JSON配置文件,改完注意JSON格式不能有语法错误,一个多余的逗号就会让配置加载失败。
3.3 "this model is not available in your country"是谁报的错
这个问题在热搜里出现频率很高。可以明确地说:这不是OpenCode源码报的错,OpenCode不会自己判断你所在的国家,它也没有任何地理探测逻辑。这个错误是模型服务商在服务端返回的,OpenCode的请求层只是把Provider返回的错误信息原样透传显示在界面上。
从源码角度看,这个问题发生在模型适配层:请求发出后,服务商返回了一个错误响应,适配层解析错误体,把里面的message字段透传出来。所以你不需要去改OpenCode源码,更不应该去搜索任何绕过方案。正确合法的做法是换一个在你当前区域可用的模型服务商,或者改用OpenRouter这类聚合平台上的可用模型,也可以直接用本地模型(比如Ollama),彻底绕开服务商区域校验的问题。
提示:当报错信息带有一段"请访问我们的支持页"之类的文案时,基本可以断定是上游服务商的业务限制,不是配置错误,也不是OpenCode的Bug。
3.4 免费模型和本地模型的接入配置
说回"opencode免费模型"。完全免费使用的方案里我实际用过两种:一种是OpenRouter上的免费模型,另一种是本地Ollama。OpenRouter的好处是它的API格式兼容OpenAI,绝大部分Provider适配成本很低。配置上可以这样理解:你在opencode.json的provider里指定自定义的base_url和模型id,把OpenRouter或者本地Ollama的服务地址填进去。下面是一个本地Ollama的配置思路示意,具体字段名以你所用版本为准:
{ "provider": { "ollama": { "base_url": "http://localhost:11434", "models": ["qwen2.5-coder:7b"] } } }本地模型虽然响应速度取决于机器配置,但优点是私密性强、没有区域和配额限制,适合用来跑一些敏感代码。如果你在意"opencode go套餐的响应速度"这类问题,我的经验是:响应速度的根源在模型服务商和网络链路,不在OpenCode这个壳。用同一个Provider和同一个模型,换任何客户端速度都差不多。与其纠结工具,不如从模型本身和网络质量去解决。
4. 读懂代码的两种能力:Skills与LSP的源码实现
4.1 Skills:为什么模型会在需要的时候"突然"学会新技能
Skills在OpenCode里是一组可复用的能力包。一个Skill通常包含一段指令文本,告诉模型在什么场景下应该做什么、按什么步骤做,可以附带脚本作为辅助工具。源码层面的Skill系统大概由三部分组成:加载器负责扫描内置和用户目录下的所有Skill;匹配器根据当前任务和Skill的描述判断是否值得使用;执行器在模型决定使用Skill后,把Skill内容注入上下文并运行关联脚本。
很多用户问"Skill为什么不生效",最常见原因是描述写得不够具体。模型是依靠描述来决定何时使用Skill的,如果描述含糊,比如"处理代码",它就无法判断该在什么时候用。正确的描述应该明确触发场景,例如"当用户要求进行前端可访问性审查时使用"。源码里匹配器的逻辑其实很简单,没有复杂的语义匹配,本质上是把描述和当前对话关键词做一次相关性判断,所以描述质量直接决定Skill的可用性。
4.2 LSP:语义信息是怎么"喂"给模型的
LSP是OpenCode另一个重要的上下文来源。说得直白一点,模型本身是看不到你工程里的类型定义、函数引用、报错诊断的。LSP的职责就是把语言服务器提供的语义信息,整理成模型能读懂的文本片段,在合适的时候注入消息上下文。比如Agent要修改一个函数,可以先通过LSP拿到该函数的定义位置、所有调用点、当前工作区里的诊断报错,这些信息作为工具结果回填给模型,让修改更精准。
这种设计非常聪明:它没有重新发明一套代码分析器,而是复用了编辑器生态里最成熟的Language Server。这意味着OpenCode支持的编程语言广度,基本等于社区语言服务器的广度。源码里LSP客户端的启动、初始化、请求挂钩、结果格式化都做得比较独立,读这一块时不需关注具体语言,关注协议交互就行。如果你在自己的工具里也想集成类似能力,直接参考LSP层的设计即可。
4.3 一个可落地的自定义Skill示例
讲完原理,给一个最小可运行的例子。假设我想给OpenCode加一个"代码审查"Skill。目录结构大致如下:
~/.config/opencode/skills/code-review/ SKILL.mdSKILL.md的内容是Markdown格式的指令,告诉模型触发时机、审查流程和输出模板,示意如下:
--- name: code-review description: 当用户要求审查代码质量、安全或性能问题时使用 --- # 代码审查流程 1. 先读取目标文件或目录。 2. 按可读性、性能、安全性、错误处理四个维度检查。 3. 输出问题列表,标注严重程度和建议修改。这个Skill注册后,当我在某个会话里提到"审查这段代码",模型就会在上下文中加载这个Skill的指令,按里面定义的步骤去读文件、找问题、按模板输出。整个过程不需要重新编译OpenCode,因为Skill是运行时加载的。这也解释了为什么OpenCode把Skills当成扩展体系的核心——它把自定义能力从"改代码"简化成了"写Markdown"。
5. 从Agent到浏览器:Playwright集成怎么做到"自己测前端Bug"
5.1 浏览器工具在源码里的设计
OpenCode的Playwright集成是很多人感兴趣的部分,因为它让Agent从"只能改代码"进化为"能打开浏览器验证代码"。源码里这块的思路和普通工具一样:在工具注册表里注册一组浏览器操作,比如打开页面、点击元素、读取控制台日志、截图、获取无障碍快照等。模型根据任务目标决定调用哪个浏览器工具,工具执行层负责实际驱动浏览器,把结果(截图路径、console报错、DOM状态摘要)转成文本返回给模型循环。
值得注意的细节是,浏览器工具返回给模型的内容需要精心设计,否则上下文会爆炸。源码里通常只返回DOM节点的摘要、关键可访问元素、控制台错误列表,而不是把整个页面HTML扔给模型。这个"摘要化"的设计是所有页面级Agent工具都必须考虑的问题。如果你自己写类似的工具,一定要在工具返回层做内容裁剪,不然一次页面操作就能把上下文窗口撑爆。
5.2 一个可复现的前端Bug检测工作流
实际使用中,我会让OpenCode做这样一件事:打开某个有Bug的页面,复现问题,收集报错,定位到代码文件。一个典型提示词是这样的:
"打开 http://localhost:3000 ,点击登录按钮,不要输入任何内容,观察页面是否报错。然后打开浏览器控制台,把出现的error信息整理给我,并猜测最可能出问题的前端文件。"
执行过程中,Agent会调用浏览器工具完成导航和点击,然后读取console内容,最后结合仓库代码定位到可疑文件。这个流程在传统开发里可能要手动开DevTools反复点,现在Agent可以半自动完成。需要注意,它适合用来做"冒烟验证"和"可复现Bug",还不能完全替代专业测试用例的编写。从源码角度看,这个工作流的本质是Agent在循环里反复使用浏览器工具组,每操作一步就观察一次页面状态,直到拿到足够信息得出结论。
5.3 这套设计可以迁移到什么场景
读源码的时候我在想,这种"Agent+浏览器工具"的组合完全可以迁移到其他工具上。类似的能力可以用在自动化回归冒烟、页面可访问性检查、视觉回归对比、甚至把多页面操作流程做成一个人工智能驱动的手动测试代理。理解了工具注册和结果摘要化这两点,你在任何Agent框架里都能复现这套设计。特别是"结果摘要化"这个思路,是防止上下文爆炸的关键,也是很多人自己做浏览器Agent时最容易忽视的地方。
6. 源码级排查与二次开发:我从踩坑里总结的经验
6.1 遇到问题先查日志,再反查源码
OpenCode报错时,很多人第一反应是重装或换key。但更高效的做法是打开日志。源码里日志系统是分模块的,会话、工具、模型适配各有各的日志段。遇到invalid api key时,先看模型适配段的日志有没有把请求发出、返回状态是什么;遇到工具不执行时,看工具段有没有参数校验失败。配合日志反查源码,基本能解决90%的配置问题。
我实际排查过一个case:某次切换Provider后一直报鉴权失败,配置文件和env都检查了没发现问题。后来翻日志发现,配置文件里的Provider id大小写和源码里注册的名字不一致,导致走到默认Provider的读取逻辑,自然就读不到正确key。这种问题纯靠看README是发现不了的,但日志一开、源码一查,原因立刻就清楚了。
6.2 修改源码重新构建的流程
如果你想给OpenCode改点东西,比如加一个自定义工具、改TUI配色,源码构建流程大致是:先确保本机装有合适版本的Bun,然后clone仓库、安装依赖、跑开发模式。开发模式里能实时看到改动效果。需要留意的是版本对齐——OpenCode对Bun和Node的版本有要求,版本不对会在构建阶段报各种奇怪错误,先把运行时版本对齐再动手。
构建命令本身不复杂,核心就几条:bun install装依赖、bun run dev进开发模式、bun run build出生产包。但终端TUI项目有个特点:开发模式下改动会热更新,但某些底层模块(比如原生的文件监听、终端渲染)改完必须重启才能生效。你要是改了工具层代码发现没反应,先别怀疑改错了,重启一下开发进程再试。
6.3 值得下手的几个扩展点
读完整套源码,我个人觉得最值得二次开发的位置有三个:
- 新增Provider适配:在模型适配层增加一个新的服务商,适合有内部模型平台或私有化模型的公司。
- 新增Tool:在工具注册表里加一个能力,例如执行SQL、操作GitHub PR、调用内部接口。
- 扩展Skill体系:把团队规范、代码风格检查做成Skill,全团队共享。
这几个扩展点的边界都很清晰,不会动到核心循环。新手建议从自定义Skill开始,零代码成本;有一定TypeScript经验的可以从自定义Tool入手,成就感最强。新增Tool时重点看工具注册表的数据结构,把工具名、参数schema、执行函数三样东西注册进去,模型就能在会话里自动发现并调用它。
最后再分享一个小体会:读OpenCode源码最大的收获,不是搞懂了一个具体工具,而是看到了一个成熟的Agent运行时该怎么组织模块。工具注册、上下文管理、模型适配、扩展机制这几件事,任何想自己做AI编码工具的人都绕不开。如果未来你想在某个垂直场景里做自己的AI助手,这份源码会是一个非常值得参考的范本。而且读源码这件事本身有复利效应——你读懂一次,后面再遇到任何Agent框架,都能一眼看出它的循环在哪、工具怎么注册、上下文怎么管理,不会再被花哨的界面和宣传词迷惑。