跳到主要内容

数据截至 (上游 commit 0004b748b71c)

Kilo Code — 架构与原理

30 秒导读: Kilo Code 是一个开源编码 agent。它把「读代码、改文件、跑命令、调模型」这套能力做成一个后端进程,再让 VS Code 插件、JetBrains 插件、终端 TUI、云端 Web 各自当这个进程的 HTTP 客户端。换句话说:内核只有一套,前端有四张脸。


1. 这是什么(零基础也能懂)

一句话定义: 一个能听懂自然语言、自己动手改你项目代码的 AI 编码助手,开源、可自托管、模型可换。

解决什么问题、给谁用。 假设你在一个几十万行的仓库里,要「把所有旧的日志调用换成新的 logger,并补上测试」。你不想一个个文件翻。你打开编辑器侧边栏(或终端)说这句话,agent 自己去搜、去读、去改、去跑测试,改到哪一步都问你一句「这个文件我要改成这样,行吗」。用户是日常写代码的工程师,以及要在 CI 里跑无人值守自动化的团队

它能做什么:

  • 跨多文件的代码生成与修改;
  • 内联自动补全(ghost text);
  • 终端命令执行与浏览器自动化;
  • 通过 MCP(Model Context Protocol)接第三方工具;
  • 500+ 模型任选、任务中途可切换(README.md:24)。

用起来什么样。 最小的一次真实使用是这样(依据:README.md:44-64、README.md:146-148):

npm install -g @kilocode/cli
cd ~/my-project
kilo # 无参数 = 起终端 TUI,进入交互式会话

# 或者在 CI 里无人值守跑:
kilo run --auto "run tests and fix any failures"

kilo 这个可执行名来自 packages/opencode/package.json:19-21bin 映射(kilokilocode 都指向 ./bin/kilo);无参数进 TUI 是因为 TUI 命令注册的 yargs 名字就是 $0packages/opencode/src/cli/cmd/tui.ts:139TuiThreadCommandcommand: "$0 [project]":140)。

关于血统:一件必须先说清楚的事。 这个仓库不是从零写的。

  • README 的 FAQ 直说:Kilo CLI 是 OpenCode 的 fork(README.md:171)。
  • 证据在包结构里:整个内核放在 packages/opencode/,包名却是 @kilocode/clipackages/opencode/package.json:4),而它依赖的基础库仍叫 @opencode-ai/core@opencode-ai/llm
  • package.json:22-26 用 bun workspaces 把 packages/* 全部纳管,是个单体 monorepo。
  • 源码里满地是 // kilocode_change 注释,用来标记 Kilo 相对上游 OpenCode 的改动(例如 packages/opencode/src/tool/registry.ts:7:243:272)。

一句话直觉。 把它当成一台装了轮子的编译器服务:内核是常驻服务(负责会话、工具、模型、文件),编辑器只是贴在上面的显示屏和键盘。你换显示屏(VS Code → JetBrains → 终端),服务本身一行不改。


2. 顶层全景(它大概怎么转)

这一节只给「大盘」:谁跟谁说话、数据往哪流。任何机制的实现都在后面的章节。

2.1 第一层:前端怎么接上后端

怎么读这张图: 从左到右。左边四种前端各有各的进程模型,但都收敛到同一个 HTTP 接口——差别只在「HTTP 请求走不走真的 TCP」。

前端(全是 HTTP 客户端) 怎么接 后端
┌──────────────┐
│ 终端 TUI │──► Worker 线程 + RPC 转 fetch ──┐
├──────────────┤ │ ┌────────────────────┐
│ VS Code 插件 │──► 子进程 `kilo serve --port 0`─┤──►│ Effect HttpApi 路由 │
├──────────────┤ │ │ + /event SSE 流 │
│ JetBrains │──► 子进程 `kilo serve --port 0`─┤ └─────────┬──────────┘
├──────────────┤ │ │
│ Web / 云 │──► 普通远程 HTTP ───────────────┘ ▼
└──────────────┘ 一个 instance

三条接法各有真源码为证:

前端接法依据
终端 TUI主线程渲染,后端跑在 Worker 里;fetch 被换成走 RPC 的假 fetch,由 worker 直接调进程内 handlercli/cmd/tui.ts:83 createWorkerFetch、装配在 :435cli/tui/worker.ts:75 Server.Default().app.fetch(request)
VS Code 插件spawn 一个 CLI 子进程,端口给 0 让系统分配,再从 stdout 里把端口捞出来packages/kilo-vscode/src/services/cli-backend/server-manager.ts:118
JetBrains 插件同上,Kotlin 侧拼的命令行一模一样packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt:154

值得单独点一句:TUI 那条路径连 TCP 都不开Server.Default()server/server.ts:67)导出的是一个纯 fetch(request) => Response 的 handler。同一套路由,既能挂到 Node HTTP 服务器上(server/server.ts:84 listen),也能当函数直接调。这就是「所有前端都是 HTTP 客户端」这句话在工程上不打折的原因。

注意: worker.ts 里那个 server() RPC 方法(:73)是可选支路——只有用户显式要一个真端口时才会 Server.listen。TUI 的默认路径不经过它。

事件回流走 SSE:GET /event,一条长连接把总线上的所有事件推给前端,还带 10 秒心跳(server/routes/instance/httpapi/groups/event.ts:14handlers/event.ts:30-42)。

2.2 第二层:一个 instance 内部长什么样

instance = 一个工作目录。它的身份就三个字段:directoryworktreeprojectproject/instance-context.ts:5-9)。所有带状态的服务都按 instance 隔离。

怎么读这张图: 从上往下是一次请求的下沉路径;虚线框内的东西共享同一个 instance 上下文。

HTTP 请求


┌─────────────── 一个 instance(= 一个工作目录)───────────────┐
│ │
│ ① 会话主循环 ──────► ② 模型层 ──────► 500+ 模型 API │
│ (转一圈拼一次 (统一成一个 │
│ 请求、收一次流) Model 对象) │
│ │ │
│ ▼ │
│ ③ 工具注册表 ──► ④ 权限闸门 ──► 文件系统 / shell / MCP │
│ │ │
│ ▼ │
│ ⑤ 影子 git 快照 ⑥ 一份 SQLite(会话/消息/权限) │
└──────────────────────────────────────────────────────────────┘

部件职责一览。 这几个文件就是本套文档反复回来的地方:

部件干什么文件
HTTP 门面把 Effect HttpApi 路由包成既可 listen 又可直接 fetch 的两用 apppackages/opencode/src/server/server.ts
会话主循环一轮一轮推进对话:拼消息 → 发模型 → 处理工具调用 → 再来一轮packages/opencode/src/session/prompt.ts
工具注册表内置工具 + 插件工具 + 目录里捡到的自定义工具,按模型/agent 裁剪后交给模型packages/opencode/src/tool/registry.ts
权限闸门每个危险动作先算规则、算不出就挂起等人回答packages/opencode/src/permission/index.ts
模型层把 models.dev 目录 + 各家 provider 归一成同一个 Model,并挑运行时packages/opencode/src/provider/provider.ts
存储单个 SQLite 文件 + drizzle 迁移,承载会话、消息、todo、已批准的权限规则packages/opencode/src/storage/db.ts

这些服务怎么被装配起来? 一张 Effect Layer 图,分三层合并(Core / Session / Feature),最后套上 instance 层做成一个 ManagedRuntimeeffect/app-runtime.tsAppLayerAppNodeBuilderV1.build 组装(:75),AppRuntimeManagedRuntime.make 导出(:137 / :144)。想知道「这个进程里到底活着哪些服务」,读那份 node import 清单最快。详见 01-runtime-skeleton


3. 主线走一遍(高层,不进代码)

追一次最典型的交互:用户说一句话 → 模型决定改文件 → 权限放行 → 文件真的变了 → 结果回到界面。

① 用户敲一句话
│ POST 会话接口

② 主循环起一轮
│ 从 SQLite 取历史 → 过滤已压缩部分 → 拼 system prompt

③ 发给模型,收流式回包
│ 模型吐出 tool_call: edit(file, oldString, newString)

④ 工具执行:先算旧内容→新内容的 diff


⑤ 权限闸门 ctx.ask("edit", 文件路径)

├── 规则里已 allow ──────────────► 直接放行
├── 规则里 deny ────────────────► 抛 DeniedError,变成给模型的错误文本
└── 需要问人 ──► 挂起 + 广播事件 ──► 前端弹窗 ──► 用户点「允许」──► 恢复


⑥ 写盘 + 格式化 + 发文件变更事件;同时打一个影子 git 快照


⑦ 工具结果回填进消息,SSE 推给前端;主循环判断:还有工具要调吗?

├── 有 ──► 回到 ②(step + 1)
└── 没有 ──► 本轮结束,助手消息落库

几处「这句话有源码撑着」的锚点:

  • 第 ② 步的「一轮」是字面意义的 while (true)session/prompt.ts:1395,循环体开头就 status.set(sessionID, { type: "busy" }) 并打 log("loop", { step })
  • 第 ⑤ 步的 ctx.ask 不是工具自己实现的,而是主循环在组装工具上下文时注入的闭包:session/tools.ts:96 起(ask 闭包注入 KiloSessionPrompt.askPermission),它把 session/agent 信息补齐后再交给真正的 Permission.ask
  • 「挂起等人」是用 Deferred 实现的:permission/index.ts:272-283 先创建 deferred、登记到 pending map、广播 permission.asked 事件,然后 Deferred.await 卡在那儿,直到 HTTP 那头调 reply 才继续。
  • 第 ⑥ 步的快照发生在处理器层,不在工具里:session/processor.ts:133:644:677 三处 snapshot.track(...)

这条链的每一环都有专章:主循环见 02-agent-loop,工具与权限见 03-tools-and-permission,写盘与快照见 04-edit-and-snapshot


4. 阅读地图

六章,由浅入深排好了。建议顺序就是编号顺序;赶时间的话看下表的「什么时候读它」。

讲什么什么时候读它
01-runtime-skeleton一个 instance 的边界、Effect 服务图怎么装配、SQLite 怎么当唯一真相你想搞清「这个进程里活着什么」
02-agent-loopwhile (true) 一轮里发生什么、何时停、子任务怎么套娃你要改 agent 行为、加中断/重试
03-tools-and-permission工具三来源、按模型裁剪、ask 的规则求值与挂起你要加工具、或调权限策略
04-edit-and-snapshot九级降级匹配、影子 git 的初始化与回滚你关心「模型给的旧文本对不上怎么办」
05-context-managementsystem prompt 组装、压缩、剪枝、prompt 缓存你在跟 token 成本 / 上下文溢出打架
06-model-layer500+ 模型归一、参数变换、原生运行时与回退你要接新 provider、或想读那套自研运行时

只想拿走「设计精华」的读者:第 5 节 + 04 + 05 两章足够。


5. 巧妙之处速览

五个不显然的设计。每条先说妙在哪,再给坐标;实现细节一律去对应章节

5.1 九级降级匹配 —— 把「模型记错了缩进」变成可恢复错误

模型给出的 oldString 几乎从不和磁盘上的文本一字不差。edit 工具不是直接失败,而是排了九个匹配器依次尝试:精确 → 逐行 trim → 首尾锚点 → 空白归一 → 缩进宽松 → 转义归一 → 边界 trim → 上下文感知 → 多处出现。命中即停,九个全废才报错。

坐标:入口函数 replacetool/edit.ts:702,九个 Replacer 的数组字面量在 :709-719,依次是 SimpleReplacer:268)到 MultiOccurrenceReplacer:568)。展开见 04-edit-and-snapshot

5.2 影子 git —— 在你的仓库之外,给你的仓库拍快照

要能撤销 agent 的改动,又不能污染用户的 .git(不能多出提交、不能动 index)。做法:在 ~/.local/share/.../snapshot/<projectID>/<worktreeHash>/ 下另开一个 git 目录--work-tree 指向用户的真实工作区。于是可以正常 git init / 提交 / diff / restore,而用户的 git log 里干干净净。

坐标:snapshot/index.ts:117 算出 gitdir:121 每条命令都拼 --git-dir <影子> --work-tree <真实>:362-369 首次初始化并关掉 autocrlf/fsmonitor。展开见 04-edit-and-snapshot

5.3 权限 arity —— 让「总是允许」总是允许对的东西

用户点「总是允许」时,该记住什么模式?记整条命令太窄(npm run build 允许了,npm run test 还得再问);记第一个词太宽(允许 git 等于允许 git push --force)。Kilo 的答案是给命令前缀标注 arity(算几个 token 才算一条「人能理解的命令」)git = 2、npm run = 3、ls = 1,最长前缀优先,flag 不算 token。于是 git checkout main 归一成规则 git checkout *

坐标:permission/arity.ts:1prefix() + 同文件 ARITY 字典(这张字典是拿 LLM 生成的,生成 prompt 原样留在 :11-23 的注释里,ARITY 本体从 :24 起);调用点在 tool/shell.ts:408。展开见 03-tools-and-permission

5.4 payload-limit 预剪枝 —— 在网关拒绝之前先自己瘦身

上下文压缩通常按 token 数触发。但真正会打回请求的往往是字节数(网关有 body 大小上限),而 token 估算和字节数不是一回事。Kilo 加了一道独立的兜底:消息组装完之后先 JSON.stringify 量一下,超过 1.25 MB 就立刻触发一次持久化剪枝,再重新组装一遍;还超就打 warn 但继续发。

坐标:session/prompt.ts:98 的常量 REQUEST_PRUNE_BYTES = 1_250_000,判定与重拼在 :1656-1667,复用 compaction.prune 并传一个专门的原因标签 "payload-limit"session/compaction.ts:103)。展开见 05-context-management

5.5 原生运行时 + 显式回退 —— 敢自研,也敢承认不支持

Kilo 在 AI SDK 之外自己写了一套 LLM 运行时(packages/llm,含各家 provider 与协议实现)。上线方式很克制:默认关,由 flag 打开;打开后每次请求先问原生运行时「这个组合你支不支持」,拿到的要么是一条现成的事件流,要么是一个具体的不支持理由,后者直接落回 AI SDK,并把理由写进日志字段 llm.native_unsupported_reason

坐标:session/llm.ts:289flags.experimentalNativeLlm:306native.type === "supported":327 打「falling back to ai-sdk」;flag 定义在 effect/runtime-flags.ts:64KILO_EXPERIMENTAL_NATIVE_LLM)。展开见 06-model-layer


6. 全库级代码地图

用法: 行号会随上游更新漂移,符号名一般不会——找不到就用第三列 grep。路径相对克隆根。

6.1 进程与装配

主题文件符号
CLI 入口、命令注册packages/opencode/src/index.tsTuiThreadCommandServeCommandRunCommand
可执行名 kilopackages/opencode/package.jsonbin:21-23
HTTP 服务(两用 app)packages/opencode/src/server/server.tsDefaultlistenlistenEffect
全部 API 组挂载packages/opencode/src/server/routes/instance/httpapi/api.tsaddHttpApi(...)
SSE 事件流packages/opencode/src/server/routes/instance/httpapi/handlers/event.tseventResponseeventHandlers
Effect 服务图packages/opencode/src/effect/app-runtime.tsCoreLayerSessionLayerFeatureLayerAppLayerAppRuntime
instance 身份packages/opencode/src/project/instance-context.tsInstanceContextcontainsPath
instance 局部状态packages/opencode/src/effect/instance-state.tsmakegetinvalidate
SQLite 客户端与迁移packages/opencode/src/storage/db.tsClientgetPathtransactionuse
会话相关表packages/core/src/session/sql.tsSessionTableMessageTablePartTableSessionMessageTable

6.2 会话与模型

主题文件符号
主循环packages/opencode/src/session/prompt.tsloopprompthandleSubtaskREQUEST_PRUNE_BYTES
流事件处理器packages/opencode/src/session/processor.tscreateprocesscompleteToolCallcleanup
运行时选择与回退packages/opencode/src/session/llm.tsLLMNativeRuntime.stream 调用点、experimentalNativeLlm 分支
上下文压缩/剪枝packages/opencode/src/session/compaction.tspruneisOverflowPRUNE_MINIMUMPRUNE_PROTECT
system prompt 组装packages/opencode/src/session/system.tsinstructionsprovidersoul
provider/model 归一packages/opencode/src/provider/provider.tsfromModelsDevProvidergetModelgetLanguagemodelSuggestions
请求参数变换packages/opencode/src/provider/transform.tsmessageoptionsmaxOutputTokensschema
自研运行时(独立包)packages/llm/src/@opencode-ai/llmproviders/protocols/route/

6.3 工具、权限、落盘

主题文件符号
工具注册与裁剪packages/opencode/src/tool/registry.tslayertoolsfromPluginwebSearchEnabled
工具上下文与 ask 注入packages/opencode/src/session/tools.tsresolvecontext
工具接口定义packages/opencode/src/tool/tool.tsContextDefExecuteResult
权限求值与挂起packages/opencode/src/permission/index.tsaskreplyresolveevaluatefromConfigtoConfig
命令前缀 aritypackages/opencode/src/permission/arity.tsprefixARITY
shell 工具的规则推导packages/opencode/src/tool/shell.tsBashArity.prefix 调用点(:443
编辑与降级匹配packages/opencode/src/tool/edit.tsreplaceSimpleReplacerMultiOccurrenceReplacerbuildFileDiff
影子 git 快照packages/opencode/src/snapshot/index.tsInterface.trackrestorerevertdiffFull

6.4 前端侧(只给入口,不展开)

主题文件符号
TUI 主线程 ↔ Workerpackages/opencode/src/cli/cmd/tui/thread.tsTuiThreadCommandcreateWorkerFetchcreateEventSource
TUI 后端 Workerpackages/opencode/src/cli/cmd/tui/worker.tsrpc.fetchrpc.server
VS Code 拉起后端packages/kilo-vscode/src/services/cli-backend/server-manager.tsServerManagerparseServerPort
VS Code 连回来packages/kilo-vscode/src/services/cli-backend/connection-service.tsConnectionServicecreateKiloClient 调用点(:722
JetBrains 拉起后端packages/kilo-jetbrains/backend/.../cli/KiloBackendCliManager.ktKiloBackendCliManager