Skip to content

学习目标:建立 OpenCode 的整体代码地图,把第2章的5个概念组件对应到真实源码位置 前置知识:第2章"AI Agent 的核心组件" 阅读时间:20 分钟


第2章讲了 AI Agent 的五个核心组件:LLM、Tools、Memory、Planning、Execution Loop。

这一章做一件事:把这五个组件对应到 OpenCode 仓库的实际代码位置,建立一张"源码地图"。有了这张地图,后面每一章深入某个模块时,你知道自己在哪、周围是什么。


3.1 OpenCode 是什么

一句话定位:OpenCode 是一个开源的 AI 编码 Agent,可以在终端、浏览器或桌面应用里运行,支持多种 LLM 提供商。

它和 GitHub Copilot、Cursor 等工具的核心区别:

维度GitHub Copilot / CursorOpenCode
开源100% 开源
提供商固定(OpenAI)可选(Claude/GPT/Gemini/本地模型)
数据上传到云端本地优先,可控
部署SaaS 服务本地/自托管/云端均可
可扩展有限插件系统,支持 MCP

核心设计理念:Agent 逻辑一份,多端共用。CLI、Web、Desktop 三种界面背后是同一套服务端逻辑,不是三套独立实现。


3.2 仓库整体结构

OpenCode 是一个 Monorepo(多包仓库),用 Bun workspaces 管理:

text
opencode/
├── packages/
│   ├── opencode/      ← 核心:CLI + Server + Agent 逻辑
│   ├── app/           ← Web 应用(SolidJS)
│   ├── desktop/       ← 桌面应用(Tauri + Rust)
│   ├── ui/            ← 共享 UI 组件库
│   ├── sdk/js/        ← JavaScript SDK(封装 API 调用)
│   ├── console/       ← 控制台(app/core/function/mail/resource)
│   └── ...
├── sdks/
│   └── vscode/        ← VSCode 扩展
├── infra/             ← 基础设施代码(SST + Cloudflare)
└── docs/              ← 文档(本书所在位置)

最重要的包是 packages/opencode。它是整个系统的核心,其他包要么是它的客户端(app、desktop),要么是它的工具链(ui、sdk)。


3.3 packages/opencode:核心包详解

这是本书大部分内容聚焦的地方。它的 src/ 目录:

text
packages/opencode/src/
├── index.ts           ← CLI 总入口
├── agent/             ← Agent 定义(配置协议)
├── session/           ← 会话与执行循环(核心)
├── tool/              ← 工具系统
├── provider/          ← LLM 提供商抽象
├── server/            ← HTTP API 服务器
├── cli/               ← CLI 命令(tui/run/serve/web)
├── mcp/               ← MCP 协议集成
├── storage/           ← 数据持久化(SQLite)
├── lsp/               ← LSP 代码智能
├── config/            ← 配置系统
├── permission/        ← 权限控制
├── project/           ← 项目(工作区)管理
├── file/              ← 文件系统抽象
├── share/             ← 会话分享
├── worktree/          ← Git worktree 支持
└── ...

把第2章的5个组件对应到代码

概念组件对应目录/文件核心作用
LLMprovider/session/llm.ts多模型抽象,调用模型
Toolstool/工具注册表与执行
Memorysession/message-v2.tsstorage/对话历史与持久化
Planningagent/agent.tssession/system.tsAgent 配置与 System Prompt
Execution Loopsession/processor.ts驱动 LLM + 工具循环

这张对应表是理解 OpenCode 的核心索引。后面每一章深入某个模块时,先回来看这张表定位自己的位置。


3.4 一次任务的完整代码路径

光看目录不够,更重要的是看代码怎么流动。我们用一个具体任务追踪完整路径:

任务opencode "帮我读取 config.ts,把 port 改成 8080"

第一步:入口初始化

text
packages/opencode/src/index.ts

用户在终端输入命令,index.ts 是第一个被执行的文件。它做三件事:

  1. 解析命令行参数(用 yargs)
  2. 初始化运行时(日志、环境变量、数据库 migration)
  3. 根据命令分发:run → TUI,serve → 无头服务器,web → Web 服务器
typescript
// 简化示意
let cli = yargs(hideBin(process.argv))
  .scriptName("opencode")
  .command(RunCommand)    // opencode(默认,启动 TUI)
  .command(ServeCommand)  // opencode serve
  .command(WebCommand)    // opencode web

await cli.parse()

第二步:进入共享服务边界

text
packages/opencode/src/server/server.ts
packages/opencode/src/server/routes/session.ts

无论是 TUI、Web 还是 Desktop,任务都通过 HTTP API 进入系统。server.ts 创建 Hono 应用,注册中间件(认证、CORS、日志)和路由。

为什么要经过 HTTP 服务器?因为这样三端共享同一套逻辑,不用各自实现一遍。

text
TUI 进程  ──┐
Web 浏览器 ──┤ → HTTP → server.ts → session.ts → processor.ts
Desktop   ──┘

第三步:创建/获取会话

text
packages/opencode/src/session/prompt.ts

会话(Session)是 OpenCode 中持久化的对话容器。每次用户发送消息,先找到或创建对应的 Session,然后调用 prompt() 把消息送进去。

prompt() 负责:

  • 创建本轮用户消息(附加文件、附件等)
  • 把消息持久化到数据库
  • 启动主处理循环

第四步:System Prompt 装配

text
packages/opencode/src/session/system.ts
packages/opencode/src/agent/agent.ts

在调用 LLM 之前,需要组装 System Prompt。这是 Planning 组件的核心实现:

typescript
// session/system.ts(概念示意)
async function buildSystemPrompt(agentName: string, context: Context) {
  const agent = Agent.get(agentName)  // 从 agent.ts 获取 Agent 定义

  return [
    agent.system,          // Agent 的基础行为准则
    await getProjectContext(context),  // 当前项目信息
    await getCustomInstructions(),     // 用户自定义指令(opencode.md)
    getCurrentWorkingDirectory(),      // 工作目录
  ].join("\n\n")
}

System Prompt 是 Agent 的"人格"——它定义了 Agent 应该如何思考、什么时候调用哪些工具、遇到歧义时如何决策。

第五步:主执行循环

text
packages/opencode/src/session/processor.ts
packages/opencode/src/session/llm.ts

这是整个系统最核心的文件。processor.ts 实现了第2章讲的 Execution Loop:

text
processor.ts 循环:

[1] llm.ts:组装 messages + tools + system prompt,调用 LLM

[2] 流式接收响应
     ├── text-delta → 广播 bus 事件(TUI/Web 实时更新)
     ├── reasoning  → 广播事件(显示思考过程)
     └── tool-call  → 进入工具执行分支

[3] 工具执行(tool-call 分支)
     ├── permission/next.ts:检查权限
     ├── tool/registry.ts:找到对应工具
     ├── 执行工具
     └── 把结果作为 tool_result 加入 messages

[4] finish_reason == "stop" → 退出循环
    finish_reason == "tool-calls" → 回到 [1]

对于我们的任务"读取 config.ts,把 port 改成 8080",这个循环会经历:

text
轮次1:
  LLM 调用 read_file("config.ts")
  工具执行,返回文件内容
  把内容加入对话历史

轮次2:
  LLM 看到文件内容,决定调用 edit_file("config.ts", "port = 3000", "port = 8080")
  工具执行,修改文件

轮次3:
  LLM 输出文字:"已将 config.ts 中的 port 从 3000 修改为 8080。"
  finish_reason = "stop",循环结束

第六步:工具执行

text
packages/opencode/src/tool/registry.ts
packages/opencode/src/tool/read.ts
packages/opencode/src/tool/edit.ts

registry.ts 是工具注册表,维护所有可用工具的 Map。每个工具是独立文件:

typescript
// tool/read.ts(简化示意)
export const ReadTool = {
  name: "read",
  description: "读取文件内容,返回文本",
  parameters: z.object({
    filePath: z.string().describe("要读取的文件路径"),
  }),
  async execute({ filePath }) {
    const content = await fs.readFile(filePath, "utf-8")
    return content
  }
}

当前 OpenCode 内置了这些工具:

text
文件操作:  read / write / edit / multiedit / glob / ls
搜索:     grep / webfetch / websearch / codesearch
代码执行:  bash / apply_patch
AI辅助:   task(子任务)/ plan(规划模式)
交互:     question(询问用户)

第七步:消息持久化与回流

text
packages/opencode/src/session/message-v2.ts
packages/opencode/src/storage/

每一轮循环的结果(用户消息、Assistant 消息、工具调用、工具结果)都持久化到 SQLite 数据库。这实现了:

  • 会话恢复:关掉重开,历史仍在
  • 多端同步:TUI 和 Web 看到同一份数据
  • 事件回放:新连接的客户端可以获取历史事件流

3.5 客户端/服务器分离:为什么这样设计

很多人第一次看 OpenCode 会困惑:为什么本地 CLI 还要启动一个 HTTP 服务器?

答案是复用。看这个对比:

如果没有服务器层

text
TUI 实现:读文件逻辑 + 调模型逻辑 + 工具执行逻辑
Web 实现:读文件逻辑 + 调模型逻辑 + 工具执行逻辑(重写一遍)
Desktop:读文件逻辑 + 调模型逻辑 + 工具执行逻辑(再重写一遍)

有了服务器层

text
服务器:读文件逻辑 + 调模型逻辑 + 工具执行逻辑(一份)
TUI:   HTTP 客户端 + 终端渲染
Web:   HTTP 客户端 + 浏览器渲染
Desktop:HTTP 客户端 + 原生渲染

服务器层让三端共享全部业务逻辑。这是 OpenCode 支持多端的根本架构基础。

本地模式下,TUI 在同一进程内启动服务器(或连接已有服务器),没有网络开销;远程模式下,Web/Desktop 通过网络连接到服务器,支持团队共享或云端部署。


3.6 技术栈的选择逻辑

了解 OpenCode 为什么选这些技术,比记住技术名字更有价值:

Bun(运行时)

  • 内置 TypeScript 支持,无需 tsc 编译步骤
  • 启动速度比 Node.js 快 3-4 倍(CLI 工具的关键指标)
  • 内置 SQLite 驱动,省去额外依赖

Vercel AI SDK(LLM 接口层)

  • 统一接口对接 Anthropic、OpenAI、Google、本地模型
  • 内置流式输出支持
  • TypeScript 类型完整,函数调用格式标准化

Hono(HTTP 框架)

  • 极轻量(5KB),适合嵌入 CLI 进程
  • 原生 TypeScript,类型安全的路由
  • 支持 SSE(Server-Sent Events),用于流式推送事件

SolidJS(UI 框架)

  • 比 React 更小的包体积(TUI 和 Web 共用)
  • 真正的响应式,无虚拟 DOM,更接近原生性能
  • TUI 的组件树渲染比 React 更高效

Drizzle ORM(数据库)

  • 类型安全的 SQL,查询结果有完整 TypeScript 类型
  • 轻量,无运行时依赖注入
  • 支持 SQLite(本地)和 PostgreSQL/MySQL(云端)

3.7 配置系统:用户如何定制 Agent

OpenCode 支持多层配置,优先级从高到低:

text
1. 命令行参数                    (最高优先级)
2. 环境变量(OPENCODE_*)
3. 项目级配置(.opencode/config.json)
4. 全局配置(~/.config/opencode/config.json)
5. 内置默认值                    (最低优先级)

除了配置文件,OpenCode 还支持自定义指令文件

text
CLAUDE.md   ← 项目根目录,Agent 会自动读取并加入 System Prompt
opencode.md ← 同上,OpenCode 专用命名
~/.claude/CLAUDE.md ← 全局用户级指令

这意味着你可以直接在 Markdown 文件里告诉 Agent:

  • 这个项目用什么技术栈
  • 代码风格是什么
  • 哪些目录不要修改
  • 任何你希望 Agent 记住的事情

这是 OpenCode "配置即代码"的体现——Prompt 工程的结果以文件形式存在仓库里,而不是散落在 UI 配置面板里。


3.8 MCP:工具系统的扩展机制

MCP(Model Context Protocol)是 Anthropic 推出的开放协议,让 Agent 可以调用外部服务提供的工具,不需要修改 Agent 自身代码。

text
没有 MCP:
  工具都在 packages/opencode/src/tool/ 里
  想加新工具,要修改 OpenCode 源码

有了 MCP:
  任何人都可以写 MCP Server
  OpenCode 连接 MCP Server,自动获得它的工具
  不需要修改 OpenCode 本身

例如,接入一个数据库查询工具:

json
// .opencode/config.json
{
  "mcp": {
    "servers": {
      "my-database": {
        "command": "npx my-db-mcp-server",
        "args": ["--connection", "postgresql://..."]
      }
    }
  }
}

配置好之后,Agent 就能调用 my-database 提供的 query_tablerun_migration 等工具,就像内置工具一样。

MCP 是 OpenCode 工具系统的"插槽"——内置工具覆盖通用编码场景,MCP 工具覆盖特定项目的定制需求。


本章小结

建立了 OpenCode 的源码地图:

五个概念组件对应的代码位置

text
Planning     → agent/agent.ts + session/system.ts
LLM          → provider/ + session/llm.ts
Execution    → session/processor.ts
Memory       → session/message-v2.ts + storage/
Tools        → tool/registry.ts + tool/*.ts

一次任务的代码路径

text
index.ts → cli/cmd/run.ts
  → server/server.ts → server/routes/session.ts
    → session/prompt.ts
      → session/system.ts(装配 System Prompt)
      → session/processor.ts(执行循环)
        → session/llm.ts(调用 LLM)
        → tool/registry.ts(执行工具)
        → session/message-v2.ts(保存消息)

架构决策要点

  • 客户端/服务器分离 → 三端共享一套逻辑
  • Bun + TypeScript → 无编译,快速启动
  • MCP 协议 → 工具系统可扩展

思考题

  1. 为什么 tool/registry.ts 要统一管理工具,而不是让每个模块自己调用工具函数?
  2. System Prompt 和工具 description 都影响 Agent 行为,它们的职责边界是什么?
  3. 如果你要给 OpenCode 加一个"查询 Jira 工单"的能力,用 MCP 方式和直接修改源码各有什么优缺点?

下一章预告

第4章:工具系统

我们将深入 packages/opencode/src/tool/,学习:

  • 工具是如何定义的(Zod schema + execute 函数)
  • registry.ts 如何注册和过滤工具
  • basheditgrep 等核心工具的实现细节
  • 权限系统如何控制工具访问