本文基于 OpenCode 源码分析,带你深入理解这个 AI 编程助手从接收用户输入到返回结果的完整链路。
OpenCode 采用客户端-服务端分离架构,即使在 CLI 模式下也是如此。TUI(终端 UI)运行在主线程,Server 运行在 Worker 线程,二者通过 HTTP + SSE 通信:
展开代码┌─────────────────────┐ ┌─────────────────────────────────────┐ │ TUI (SolidJS) │ SSE/ │ Server (Hono) │ │ 运行在主线程 │ ◄────► │ 运行在 Worker 线程 │ │ │ HTTP │ │ │ prompt/input │ │ session / provider / tool / llm │ │ message render │ │ storage (SQLite + Drizzle) │ └─────────────────────┘ └─────────────────────────────────────┘
技术栈一览:
| 层级 | 技术选型 |
|---|---|
| CLI 框架 | yargs |
| TUI 渲染 | @opentui/solid (SolidJS) |
| HTTP 服务 | Hono |
| 数据库 | SQLite + Drizzle ORM |
| LLM 调用 | Vercel AI SDK (ai 包) |
| 运行时 | Bun |
| 包管理 | Bun Workspaces + Turborepo |
当你在终端输入 opencode 命令时,发生了以下事情:
packages/opencode/src/index.ts 是真正的入口文件,使用 yargs 注册所有 CLI 命令。默认命令(不带子命令直接运行 opencode)是 TuiThreadCommand,即交互式终端模式。
src/cli/cmd/tui/thread.ts 中,主线程会启动一个 Worker 线程。Worker 负责:
src/cli/cmd/tui/app.tsx 用 @opentui/solid 渲染 SolidJS 组件树。组件层级很深,包含一系列 Provider:
展开代码ArgsProvider > ExitProvider > KVProvider > ToastProvider > RouteProvider > TuiConfigProvider > SDKProvider > SyncProvider > ThemeProvider > LocalProvider > KeybindProvider > PromptStashProvider > DialogProvider > CommandProvider > FrecencyProvider > PromptHistoryProvider > PromptRefProvider > App
其中 SDKProvider 负责创建与 Server 的连接,SyncProvider 负责将 SSE 事件同步到 SolidJS 响应式状态。
src/cli/cmd/tui/component/prompt/index.tsx 是核心的输入组件,包含一个 textarea,支持:
@agent、文件路径、/command)! 前缀)当用户按下 Enter,submit() 函数触发(约在 prompt/index.tsx:528):
ts展开代码// 简化的逻辑流程
async function submit() {
// 1. 验证输入非空、模型已连接
// 2. 如果还没有 session,创建一个
if (!session) {
session = await sdk.client.session.create({})
}
// 3. 根据模式发送不同请求
if (shellMode) {
sdk.client.session.shell(...)
} else if (slashCommand) {
sdk.client.session.command(...)
} else {
// 普通提问
sdk.client.session.prompt({ sessionID, parts, model, agent })
}
// 4. 清空输入框,导航到 session 页面
}
SDK 客户端发送 POST /session/:sessionID/message 到 Worker 中的 Hono 服务。
src/server/routes/session.ts 接收请求,验证参数后调用核心函数 SessionPrompt.prompt()。
src/session/prompt.ts 中的 createUserMessage() 负责:
@explore)ReadTool 读取文件内容MessageV2.Event.Updated 事件,经 SSE 推送到 TUI 显示这是 OpenCode 最核心的部分。src/session/prompt.ts 中的 loop() 函数实现了一个 Agent 循环:
ts展开代码// 简化的 loop() 逻辑
async function loop() {
while (true) {
// ① 从数据库加载完整消息历史
const msgs = await MessageV2.stream(sessionID)
// ② 转换为 AI SDK 格式(包含历史的 tool_calls 和 tool_results)
const messages = MessageV2.toModelMessages(msgs, model)
// ③ 解析 agent,确定可用的 tools
const tools = await resolveTools(agent, model)
// ④ 构建 system prompt(根据模型选择模板)
const system = buildSystemPrompt(agent, model)
// ⑤ 创建 processor,调用 LLM
const processor = SessionProcessor.create(...)
const result = await processor.process(messages, tools, system)
// ⑥ 根据返回值决定下一步
if (result === "stop") break // 模型回答完毕
if (result === "compact") compact() // 上下文溢出,压缩消息
if (result === "continue") continue // 有 tool_calls,继续循环
}
}
关键点:这不是单次 LLM 调用,而是一个循环。每当 LLM 请求调用工具,工具执行完毕后,循环会将结果连同历史消息重新送入 LLM,直到模型自行决定停止。
src/session/llm.ts 中的 LLM.stream() 是实际发起 LLM 请求的地方:
ts展开代码// 简化逻辑
function stream({ model, messages, tools, system }) {
// 1. 获取 LanguageModelV2 实例
const languageModel = Provider.getLanguage(model)
// 2. 组装 system prompt
// agent prompt + 环境信息 + 自定义指令(AGENTS.md)
const systemPrompt = [agentPrompt, envPrompt, customPrompt]
// 3. 应用 provider 特定的 transform
// 消息格式化、prompt caching、reasoning effort 等
const transformedMessages = ProviderTransform.message(messages, model)
// 4. 触发 plugin hooks
await plugins.trigger("chat.params", params)
// 5. 调用 Vercel AI SDK 的 streamText()
return streamText({
model: languageModel,
messages: transformedMessages,
tools,
system: systemPrompt,
temperature,
maxTokens,
// ...
})
}
src/provider/provider.ts 管理 20+ 家 LLM 提供商的统一接入:
| Provider | SDK 包 |
|---|---|
| Anthropic (Claude) | @ai-sdk/anthropic |
| OpenAI (GPT) | @ai-sdk/openai |
| Google (Gemini) | @ai-sdk/google |
| Amazon Bedrock | @ai-sdk/amazon-bedrock |
| Azure OpenAI | @ai-sdk/azure |
| xAI (Grok) | @ai-sdk/xai |
| Groq | @ai-sdk/groq |
| Mistral | @ai-sdk/mistral |
| DeepInfra | @ai-sdk/deepinfra |
| OpenRouter | @openrouter/ai-sdk-provider |
| GitHub Copilot | 自定义 SDK(src/provider/sdk/copilot/) |
| ... | ... |
Provider 层负责:
ProviderTransform 处理各 provider 的差异(消息格式、prompt caching、token 限制等)src/session/system.ts 根据不同的模型选择不同的 system prompt 模板:
| 模型 | Prompt 模板 |
|---|---|
| Claude 系列 | anthropic.txt |
| GPT-3.5/4/o1/o3 | beast.txt |
| GPT-5/Codex | codex_header.txt |
| Gemini 系列 | gemini.txt |
| Qwen 及其他 | qwen.txt(默认兜底) |
这些模板定义了 AI 的角色、行为规范、工具使用策略等。
src/session/processor.ts 消费 LLM 返回的流式响应,处理各类事件:
| 流事件 | 处理逻辑 |
|---|---|
text-delta | 创建/更新 text part,实时保存到数据库 |
reasoning-delta | 创建/更新 reasoning part(模型的思考过程) |
tool-input-start | 创建 tool part,状态标记为 pending |
tool-call | 状态标记为 running,检测 doom loop(同一工具+参数连续调用 3 次) |
tool-result | 状态标记为 completed,保存工具输出 |
tool-error | 状态标记为 error |
finish-step | 记录 token usage 和费用 |
error | 分类错误:context_overflow → 触发压缩;api_error → 触发重试 |
每次数据库更新都会通过 Bus 发布事件 → 经 SSE 推送到 TUI 客户端 → SolidJS 响应式更新 UI。这就是你看到回答"一个字一个字蹦出来"的原因。
当 LLM 返回 tool_calls 时,Vercel AI SDK 自动调用 resolveTools() 中注册的 execute() 函数:
展开代码LLM 返回: "我需要调用 bash 工具执行 git status" │ ▼ resolveTools() 中的 wrapper: ├── ① 权限检查 → PermissionNext.ask() │ (可能暂停,等待用户在 TUI 中点击"允许") ├── ② 触发 plugin hooks ├── ③ 调用 Tool.execute(args, context) │ (实际执行 shell 命令 / 文件读写 / 网络请求等) └── ④ 返回 {output, title, metadata, attachments} │ ▼ AI SDK 将结果作为 tool_result 送回 LLM │ ▼ loop() 继续下一轮迭代(带着工具结果再次请求 LLM)
OpenCode 内置了 18+ 种工具,定义在 src/tool/ 目录下:
| 工具 | 文件 | 功能 |
|---|---|---|
bash | bash.ts | 执行 shell 命令,支持 AST 解析提取路径 |
read | read.ts | 读取文件/目录,支持行号偏移、图片/PDF 处理 |
write | write.ts | 写入完整文件,生成 diff 供权限审查 |
edit | edit.ts | 搜索替换编辑,9 种匹配策略级联 |
multiedit | multiedit.ts | 单文件多处编辑 |
glob | glob.ts | 文件名模式匹配(基于 ripgrep) |
grep | grep.ts | 内容正则搜索(基于 ripgrep) |
list | ls.ts | 目录树展示 |
task | task.ts | 派生子 Agent(创建子 session 并行处理) |
batch | batch.ts | 并行执行最多 25 个工具调用 |
webfetch | webfetch.ts | 抓取 URL 内容,HTML 转 Markdown |
websearch | websearch.ts | 网络搜索(通过 Exa) |
codesearch | codesearch.ts | 代码/API 文档搜索 |
question | question.ts | 向用户提问(带选项) |
skill | skill.ts | 加载领域特定工作流指令 |
todo | todo.ts | 会话级任务列表管理 |
apply_patch | apply_patch.ts | 应用 unified diff 补丁(GPT 模型专用) |
lsp | lsp.ts | LSP 操作(跳转定义、查引用等,实验性) |
此外,通过 MCP(Model Context Protocol) 还可以接入外部工具服务器,扩展工具能力。
src/agent/agent.ts 定义了不同的 Agent,每个 Agent 有不同的工具权限:
| Agent | 特点 |
|---|---|
build(默认) | 全部工具可用,完整的编码能力 |
plan | 只读 + 规划,禁止编辑(除 plan 文件) |
explore | 只读工具(grep, glob, list, bash, read, webfetch) |
compaction | 用于消息压缩的内部 agent |
title | 用于生成会话标题的内部 agent |
展开代码Server: Session.updatePart() → DB 写入 → Bus.publish() │ ▼ SSE 推送 │ ▼ TUI: SDKProvider 接收事件 → SyncProvider 状态同步 → SolidJS 响应式更新 │ ▼ 组件重新渲染(增量更新)
src/cli/cmd/tui/routes/session/index.tsx 负责渲染会话页面:
<UserMessage> — 渲染用户消息<AssistantMessage> — 渲染助手回复(实时流式更新)
<PermissionPrompt> — 工具权限请求弹窗<QuestionPrompt> — AI 向用户提问的弹窗<Prompt> — 底部输入框展开代码用户输入 "帮我分析这个 bug" │ ▼ Prompt.submit() [TUI 主线程] │ sdk.client.session.prompt(...) ▼ POST /session/:id/message [HTTP → Worker 线程] │ ▼ SessionPrompt.prompt() [Server Worker] ├── createUserMessage() │ └── 消息入库 → Bus 事件 → SSE → TUI 显示用户消息 │ ▼ loop() ────────────────────────────── [Agentic Loop 开始] │ ├── 加载消息历史 ├── 构建工具集(ToolRegistry + MCP) ├── 构建 system prompt │ ▼ LLM.stream() → streamText() [请求 LLM API] │ ▼ SessionProcessor.process() [消费流式响应] │ ├── text-delta ────→ DB → Bus → SSE → TUI 实时显示文字 │ ├── tool-call("read", {filePath: "src/bug.ts"}) │ ├── 权限检查 ✓ │ ├── 执行 ReadTool → 返回文件内容 │ └── tool-result → DB → Bus → SSE → TUI 显示工具卡片 │ ├── tool-call("grep", {pattern: "error", path: "src/"}) │ ├── 执行 GrepTool → 返回匹配结果 │ └── tool-result → DB → Bus → SSE → TUI 显示工具卡片 │ ├── text-delta ────→ "根据分析,这个 bug 的原因是..." │ ├── finish_reason == "tool-calls" → loop() 继续 ↩ ├── finish_reason == "stop" → loop() 退出 ✓ │ ▼ 最终消息发布 → TUI 显示完整回答
不是简单的"一问一答",而是 LLM → tool → LLM → tool → ... → 最终回答 的循环。模型可以自主决定调用哪些工具、调用多少次,直到认为已经有足够信息来回答问题。
通过 Vercel AI SDK + 自定义 ProviderTransform 层,统一支持 20+ 家 LLM 提供商。每个 provider 的差异(消息格式、prompt caching 策略、reasoning effort 配置等)都在 transform 层处理,上层代码无需感知。
从 LLM API 的每个 token → SessionProcessor 处理 → 数据库持久化 → Bus 事件 → SSE 推送 → SolidJS 响应式更新 → 终端渲染,整条链路都是流式的,用户可以实时看到回答逐字生成。
工具执行前会检查权限。对于敏感操作(文件写入、shell 命令等),会暂停执行并在 TUI 中弹出权限请求,等待用户明确授权后才继续。
自动检测上下文溢出。当 token 用量接近模型的 context limit 时,触发 compaction(压缩):用一个小模型将历史消息摘要化,裁剪旧的工具输出,从而释放上下文空间继续对话。
retry-after 响应头experimental_repairToolCall 自动修复(小写化工具名、路由到 invalid tool)通过 task 工具,主 Agent 可以派生子会话,让子 Agent 独立执行任务。子 Agent 拥有独立的消息历史和上下文,完成后将结果返回给主 Agent。这实现了复杂任务的分治和并行处理。
如果你想深入阅读源码,以下是最关键的文件:
| 文件 | 作用 |
|---|---|
src/index.ts | CLI 入口,yargs 命令注册 |
src/cli/cmd/tui/thread.ts | TUI 主线程,启动 Worker |
src/cli/cmd/tui/app.tsx | TUI 根组件,Provider 树 |
src/cli/cmd/tui/component/prompt/index.tsx | 用户输入组件,submit() |
src/server/server.ts | Hono HTTP 服务器 |
src/server/routes/session.ts | Session API 路由 |
src/session/prompt.ts | 核心:消息处理 + Agentic Loop |
src/session/llm.ts | LLM 流式调用桥接 |
src/session/processor.ts | 流式响应处理状态机 |
src/session/message-v2.ts | 消息/Part 类型定义与转换 |
src/session/system.ts | System prompt 选择 |
src/provider/provider.ts | Provider 注册与模型解析 |
src/provider/transform.ts | Provider 特定转换 |
src/tool/tool.ts | Tool 基础框架 |
src/tool/registry.ts | Tool 注册表 |
src/agent/agent.ts | Agent 定义与权限 |
src/bus/index.ts | 事件总线 |
src/storage/db.ts | SQLite 数据库连接 |


本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!