ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kilo Code 架构解析:四层分层如何支撑 AI 编码工具的稳定扩展

Kilo Code 架构解析:四层分层如何支撑 AI 编码工具的稳定扩展 1. 从功能目标反推结构Kilo Code 到底想解决什么问题1.1 一个 AI 编码工具的核心使用场景很多人问我 Kilo Code 这个名字怎么来的其实很直白当时团队手里同时压着好几个编码辅助工具的原型有的偏代码补全有的偏仓库问答我就在想能不能做一个更贴近真实开发流程的东西——你给它一个含糊的自然语言需求它能自己在代码仓库里找到该改的文件、给出可落地的修改方案并且把改动以 diff 的形式交付给你。听起来和现在的各种 AI 编程助手差不多但在结构设计层面这完全是另一套思路。它不只是“问你一句、回你一段代码”而是要跑完一条完整的链路定位相关代码、理解上下文、生成修改计划、执行工具调用来改文件、最后验证结果。这意味着项目从一开始就必须按照一个多阶段流水线的思路去拆而不是做成一个传统的单入口命令行工具。我实际接触下来的典型使用场景有这么几类单文件改写比如“把这个日志模块的打印级别改成可配置”这种场景链路短但也要先找到文件、读懂当前实现、再决定是改函数签名还是加配置项。跨模块手术比如“把所有订单状态字段从字符串改成枚举类型”这种改动会波及多个文件工具需要跨目录搜索、批量修改还要做一致性检查。全局重构级任务比如“把项目里所有直接 new Service() 的地方改为从容器获取实例”这种任务已经不是简单搜索替换能搞定的需要理解调用的语义再按文件粒度逐个处理。1.2 这些场景对结构设计提出的四条硬约束如果你只是接个大模型 API、把用户输入丢进去再吐结果那确实不需要什么结构设计。但 Kilo Code 的场景逼着你必须认真对待架构原因有四条第一链路足够长。一次用户请求要经过意图解析、任务规划、代码搜索、文件读取、编辑生成、验证执行任何一环出问题用户都会觉得这个工具“蠢”。所以模块之间的边界必须清晰出了问题能迅速定位是规划层的事、工具层的事还是模型层的事。第二延迟敏感。编码辅助工具和聊天机器人不一样用户在等一个结果的同时脑子里还在写代码。首字输出时间必须做到可控这意味着不是所有请求都要攒够了完整 token 才返回上下文组装、工具调用、流式输出这些能力全都要在架构里提前留好位置。第三上下文资源是稀缺的。模型上下文窗口再大也装不下一整个中型仓库。项目结构设计直接影响“哪些上下文被放进窗口、哪些被丢弃、哪些被压缩”这种策略性决策而这个决策做得对不对决定了任务成功率。第四可观测性要求高。用户需要能看见工具“正在读哪个文件”“运行了什么命令”“为什么这么改”否则它就是一个黑盒没人敢对 AI 生成的 diff 点“接受”。所以结构上必须天然支持事件流、步骤回溯。1.3 为什么说结构设计决定工具型项目的生死我见过太多类似的工具型项目死在结构设计上现象基本一致功能只有两三个时跑得很顺一旦加入 IDE 插件、命令行入口、HTTP 服务、多模型切换代码就开始互相纠缠。改一个模型供应商的调用方式结果工具层的错误处理也跟着改想给某个插件加权限控制却发现权限判断散落在十几个函数里。Kilo Code 的结构设计原则说白了是在项目刚起步时就把“依赖方向”焊死。每个模块只允许知道自己下层的模块不允许反向依赖同层模块之间不允许互相调私有的东西必须通过各自暴露的接口协作。这样才能保证后面每加一个入口、每换一个模型改动都被限制在一个小范围内。一句话总结结构设计不是给项目添麻烦而是在给未来的改动上保险。接下来我把这套四层结构具体拆开说清楚每一层为什么存在、和相邻层怎么协作。2. 整体架构的分层逻辑入口、编排、能力、模型四层怎么切2.1 四层架构总览与依赖方向Kilo Code 的整体结构划分为四层从上到下依次是入口层Entry、编排层Orchestration、能力层Capability、模型层Model Provider。依赖方向严格单向上层可以依赖下层下层绝对不能反向依赖上层。用依赖方向来切层最大的好处是你拿到任何一个模块马上能判断它在图中的位置。比如“git diff 解析”这种功能只允许出现在能力层因为它是被编排层调用的工具而“把 OpenAI 的返回格式转成内部结构”这种代码只允许出现在模型层因为它是供应商适配的一部分。即使有人图省事把这种逻辑写在入口层代码审查阶段也能立刻拦住。为了直观各层职责是这样划分的层级核心职责对外暴露的形式入口层接收用户原始输入统一成标准请求CLI、IDE 插件、HTTP API编排层任务分解、状态流转、会话管理会话服务、任务状态机能力层操作代码仓库的具体工具搜索、读写文件、执行命令模型层统一调用各家大模型补全接口、流式接口2.2 入口层CLI、IDE 插件、HTTP 服务的共同抽象入口层最容易犯的错误就是想给每一种入口写一套独立逻辑。Kilo Code 在很早期就踩过一次这个坑因为我先写了 CLI后来又接 IDE 插件结果发现插件端要做的事和 CLI 高度重合解析用户指令、确定当前工作区路径、决定是否带上当前选中代码作为上下文区别只是触发方式不同。后来我把入口层收敛成一个标准请求对象大致结构是这样dataclass class StandardRequest: workspace_path: str # 项目根目录 instruction: str # 用户原始指令 selection: str | None # 当前选中代码可选 constraints: list[str] # 用户附加限制比如不要改测试文件 session_id: str # 会话标识用于多轮上下文所有入口——不管是从命令行敲入一句话还是 IDE 里选中代码后右键触发或者 HTTP 服务收到 JSON 请求——最终都翻译成这个 StandardRequest再交给编排层。这样做之后新增一个入口的成本就从“再写一套业务”降到了“写一个适配器”效率提升非常明显。2.3 编排层任务分解与状态机编排层是整个结构中我最强调“不能偷懒”的一层。很多人会把编排层和大模型调用混在一起觉得反正都是“把用户问题发给模型、拿结果转发给用户”。但 Kilo Code 的编排层要做的是一件事把一个大任务拆成多个子任务并且跟踪每个子任务的执行状态。我用的核心模型是一个任务状态机。一次用户请求会被编排层分解成若干个任务每个任务有明确的类型和状态。典型的任务类型包括定位任务搜索哪些文件可能和需求相关。阅读任务读取某个文件的具体内容并提炼要点。计划任务基于阅读结果生成改动方案。执行任务真正调用工具修改文件。验证任务运行相关命令或测试确认改动没破坏功能。任务状态包括 pending、planning、executing、verifying、succeeded、failed。编排层持有这个状态机每一步执行完都会把结果反馈给上层再决定是进入下一个任务、回退重试还是终止整个流程。这种显式的状态管理比“一个函数里顺序调用几个步骤”要可靠得多尤其在模型输出不可控的时候你能清晰地知道现在卡在了哪一步。2.4 能力层与模型层的边界为什么不能把 LLM 调用散落在各处这两层之间的边界是我在评审别人代码时最看重的地方。能力层管的是“具体操作”比如在仓库里搜一个函数名、读取一个文件的头部注释、跑一条测试命令。这一层的代码不关心模型是谁、输出格式是什么它只暴露干净的接口输入参数和返回结构都是项目自定义的。模型层管的是“跟外部模型打交道”把内部请求序列化成供应商需要的格式、处理鉴权、处理重试与超时、解析模型返回的 JSON。这一层不允许出现任何和代码仓库相关的业务逻辑比如“根据搜索结果排序决定下一步读哪个文件”这种判断必须上抛给编排层。为什么要把这个边界焊得这么死因为模型供应商的 API 变化太频繁了。今天多了一个参数明天返回结构加了一个字段如果你在每个工具函数里散落着对模型返回格式的解析那一次 API 升级就是一次全局修改。而把模型调用收敛到单独一层之后供应商适配只影响这一层其他层完全无感。3. 核心模块拆解与数据流设计3.1 从一条用户指令到最终补丁的完整链路分层讲清楚之后我拿一个实际案例把数据流串一遍。假设用户提交的需求是“把项目里所有 hardcode 的数据库表名前缀改成从配置读取。”这条指令进入编排层之后大概跑这么几步编排层的意图解析器先识别出这是一个全局重构类任务而不是单文件改写任务。分配一个定位任务能力层的 search_files 工具被调用按模式在仓库里搜出所有疑似硬编码前缀的位置。对每个候选文件派发阅读任务读取相关代码片段后模型层被调用来概括这些代码片段的共性。编排层基于概括结果生成改动方案决定是逐个文件生成编辑补丁还是抽取公共常量然后批量替换。执行任务调用 edit_file 工具逐个应用补丁。最后验证任务运行一次静态检查或单元测试确认没有引入语法错误。在这个链路里上下文管理器扮演的角色是“记忆管家”。每一步模型调用的输入、输出以及工具执行的中间结果都被它记录下来并在下一步组装时挑选关键部分拼接进模型的上下文窗口。它不能简单地“全记”因为很快窗口就该爆了。3.2 工具调用协议的设计让 LLM 以统一方式操作代码库模型层和编排层如果直接传递自由文本指令那工具调用的可靠性就很难保证。Kilo Code 的做法是给能力层的每个工具定义严格的 JSON Schema模型层只负责把模型输出的 JSON 解析成内部动作然后由编排层判断合法性再执行。下面是一个 search_files 工具的简化 Schema 示例{ name: search_files, description: 在仓库中按关键词或模式搜索文件返回匹配的文件路径列表, parameters: { type: object, properties: { query: {type: string, description: 要搜索的文本内容}, path: {type: string, description: 限定搜索的子目录默认是仓库根目录}, max_results: {type: integer, default: 20} }, required: [query] } }所有工具的返回结构也统一成如下的三明治格式{ success: true, output: [src/db/connection.py, src/db/models.py], error: null }这样设计的好处是编排层拿到任意工具的结果只需要检查 success 字段不需要为每个工具单独写解析逻辑。后面新增工具只要遵守这个协议就能无缝被编排层调用。3.3 上下文管理模块的“内存清理”机制聊到上下文管理我多说几句因为这是最容易让项目结构崩掉的地方。一开始我们设想得很天真上下文窗口有 128K整个仓库的核心文件加起来也就几百 KB全塞进去不就行了实际一跑模型每次推理的耗时飙到让人无法忍受而且大量无关代码会让模型“分心”反而降低生成质量。后来我给上下文管理模块定了一个优先级机制。可以这么理解它像一块只能展示一块屏幕内容的显示器你手上有很多文件片段但屏幕上同时只能显示十几个关键片段所以必须设计一个调度策略。具体分级是这样的P0当前正在处理的文件、用户原始指令、上一步工具的执行结果。必须完整保留。P1与当前任务强相关的文件片段比如要修改的函数所在文件。压缩到只保留关键函数和调用处。P2项目全局结构、依赖配置、readme 中的约束说明。用摘要形式保留。P3历史会话中已经退居次要的内容。直接丢弃只在需要回溯时从日志恢复。这个策略让 Kilo Code 在长任务中保持稳定不会出现“改到第 5 个文件时模型忘了需求目标”的问题。模块放的位置也讲究——它不属于任何一层而是作为编排层可依赖的公共服务这样入口层、能力层都不会和它产生耦合。3.4 流式输出的结构与事件类型编码工具的流畅感从一个地方体现你是不是能看见 AI 正在做什么。Kilo Code 的流式输出不是简单地把模型生成的字丢给用户而是定义了一套事件类型前端和 CLI 都能识别status状态变化比如“正在定位相关文件”“正在生成修改方案”。tool_call工具被调用的记录比如读了一个文件的第 100 到 180 行。text模型生成的普通文本比如解释改动的理由。diff最终生成的代码补丁。事件总线放在编排层所有内部模块产生的事件都统一发到总线上再由入口层决定怎么渲染。这种设计的价值在于将来如果想做一个有界面的 Web 版本只需要在入口层写一个事件渲染器核心逻辑一行不用改。4. 目录结构与扩展点接手一个 AI 编码项目该看什么4.1 目录结构设计原则按能力域分不按技术栈分一个项目的长期可维护性很大程度体现在目录结构上。我见过最痛苦的项目目录是按技术栈分的——单文件脚本放一起、数据库层放一起、工具函数放一起、UI 组件放一起。可一旦功能开始横切比如“AI 代码搜索”既要碰工具函数又要碰 UI你就不知道该去哪找代码。Kilo Code 的目录是严格按能力域组织的每个能力域自带完整的一套东西。下面是实际使用的目录骨架按照能力域组织kilo-code/ ├─ src/kilo/ │ ├─ core/ # 核心类型定义请求、任务、事件 │ ├─ orchestration/ # 编排层状态机、会话、任务规划 │ ├─ tools/ # 能力层搜索、读取、编辑、命令执行 │ ├─ llm/ # 模型层多个供应商适配器、token 计算 │ ├─ context/ # 上下文管理压缩、优先级调度 │ ├─ integration/ # 入口适配器CLI、IDE、HTTP │ └─ config/ # 配置加载与校验接手这类项目的经验是读到用户指令相关需求优先去 core 看类型定义发现问题定位不准去 orchestration 看状态机想加一个新的仓库操作能力直接去 tools 目录新增文件要切一个新模型供应商只需要修改 llm 目录下的适配器。4.2 配置体系四层默认值与运行时重载配置模块看着不起眼但几乎是每个项目最容易变成垃圾场的地方。Kilo Code 采用四层默认值叠加方案保证用户不需要忍受写死行为也不会被配置项淹没内置默认配置项目发布时候写死的 baseline。用户全局配置放在用户主目录下针对个人习惯的设置。项目级配置放在仓库根目录的.kilocode.yaml可以针对不同仓库设置规则。环境变量用于临时覆盖比如切换 API Key 或某个开关。配置加载的核心原则是“浅层覆盖深层”。我试过一开始把所有配置项塞在一个大字典里后来发现维护困难因为不同模块只关心自己那两三个 key。现在改成每个模块自己声明需要的配置项加载器负责把各层值合并后分发给对应模块。这样新增一个配置项时改动只局限在模块内部。4.3 插件与扩展点模型、工具、规则三个方向Kilo Code 的结构设计里留了三类扩展点这也是它被团队内部反复使用的核心原因。任何新需求你都可以先问自己它属于换模型、加工具、还是改规则模型扩展点最清晰就是实现一个 Provider 接口。不管是大模型、开源模型、还是本地部署的模型服务只要提供补全接口和流式接口两个方法就能接入系统class BaseProvider: async def complete(self, request) - CompletionResult: raise NotImplementedError async def stream(self, request) - AsyncIterator[CompletionChunk]: raise NotImplementedError工具扩展点就是新增一个 Tool 类只要能描述清楚它接收哪些参数、返回什么结构、需不需要管理员确认就可以注册进编排层被调用。规则扩展点则意味着项目团队可以写自定义的约束规则比如“禁止修改迁移文件”“所有新的数据库访问必须走仓储类”这些规则会在生成补丁时被重新评估作为额外的质量闸门。5. 结构设计里最容易踩的坑和我的取舍5.1 过度抽象最终演变成回调地狱早期 Kilo Code 的结构不是现在这么精简的。我一开始给所有工具都套了一层抽象基类每个工具还要挂装饰器、注册事件钩子、支持权限校验注解看起来非常“专业”。结果当工具数量超过二十个之后问题来了改一个工具的参数要在基类、装饰器、钩子函数三层地方同步改查一个问题调用链深到没法单步调试。后来我做了个激进的重构拍平抽象。抽象基类只保留一个 Tool 接口装饰器和事件钩子全部撤掉权限校验逻辑收进编排层的统一出口而不是散落在每个工具内部。事实证明大多数“灵活”的抽象在落地时根本用不上反而让代码变得不可读。现在新增一个工具平均只要二十分钟比起早期动辄半天快太多了。5.2 全局单例 vs 依赖注入的权衡上下文管理器最初被设计成全局单例因为当时觉得它只是一个内存中转站不需要实例化多份。后果是测试的时候只要前一个用例跑过一个大任务后一个用例的上下文缓存里还残留着上一个仓库的信息结果就是“串味”——模型莫名其妙地认为当前仓库里有上一个任务的旧文件。这个教训让我后来养成了一个习惯能用依赖注入传递实例的坚决不碰全局状态。现在每次请求编排层都会创建一个 Session 容器里面装着本请求专属的上下文管理器、配置快照和工具实例。这样做虽然增加了少量样板代码但换来了完全隔离的执行环境也让并发处理多个仓库任务成为可能。5.3 上下文管理器放错层引发的性能问题还有一个很隐蔽的结构决策失误一开始我把上下文管理逻辑直接写进了编排层的循环体里。粗看没问题但实际跑起来发现每次任务状态更新后都需要重新计算优先级、重新裁减上下文这部分开销会叠加在每一个子任务上任务越长性能越差。后来的做法是把上下文管理做成独立模块进程内作为服务常驻并且在处理流程里增加了增量更新机制——不是每次全量重建上下文而是只在关键节点比如执行完一次工具调用触发一次局部刷新。这样性能开销从“每个子任务都全算一遍”降到了“每步只梳理受影响的部分”实测整体耗时下降了大概三十个百分点。结构位置错了代价迟早会被注意到所以这个坑很值得分享出来。5.4 接下来可以扩展的方向现在这套结构跑得比较稳了但还留了一些可扩展的余地。比如多仓库并行任务处理当前状态机是单仓库粒度的如果想同时让 AI 在两个互相依赖的仓库间协同改动需要为会话层增加合并部署的能力。还有团队级规则引擎现在规则是项目配置里静态声明的如果能支持动态拉取团队远程规则对大型团队来说会更实用。这些方向都不需要推翻现有分层只需要在具体扩展点上增加能力这本身就是结构设计比较健康的一种体现。最后再分享一个我实际操作中的体会做结构设计时不要总想着面面俱到你永远无法预测所有未来需求但你至少可以做三件事——把依赖方向定死、把上下文和会话的状态管理单独拎出来、把每个扩展点设计得足够薄。Kilo Code 的整体结构是在不断踩坑之后稳定下来的后面再做同类项目我大概率会沿用这四层边界和那三个扩展点只是在细节上换一套更适合当下场景的处理方式。
返回列表