尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

kimi-code 深度掌握系列文章-关键架构决策(三)

kimi-code 深度掌握系列文章-关键架构决策(三) 一个大型 AI Agent 项目的架构不是凭空产生的——它是对约束的回应。kimi-code 在三年多的迭代中沉淀了一系列关键架构决策这些决策今天仍然在塑造着每一行代码。本文逐个分析五个影响最深远的架构选择它们面临什么问题、有哪些替代方案、以及为什么最终选择了这条路。1. DI 容器模式借鉴 VS Code面临的问题kimi-code 不是一个小脚本。它有数十个服务配置管理、LLM 请求、工具执行、权限控制、会话状态……这些服务之间存在复杂的依赖关系。如果每个模块都手动import依赖并new实例替换一个服务实现需要改数十处import测试时需要侵入式 mock 模块系统不同作用域应用 / 会话 / 智能体的实例无法隔离生命周期管理分散在各处容易泄漏核心矛盾需要一种既支持服务组合、又支持作用域隔离、同时不引入第三方依赖的模式。可选方案方案优点缺点直接 import零学习成本紧耦合不可替换无法隔离Service Locator全局单例简单解耦隐式依赖难追踪无法作用域化tsyringe / inversify成熟的 DI 框架引入第三方依赖签名不匹配 VS Code 习惯VS Code 风格 DI选定零依赖团队熟悉实例隔离需要手动实现容器逻辑最终选择自研 VS Code 风格 DI项目选择了借鉴 VS Code 的 DI 容器设计。DI README 中明确写道「Two reasons:Zero runtime dependencies— the container is ~600 LoC of plain TypeScript;Familiar shape— most kimi-code contributors have seen VSCode’s service pattern.」核心设计细节createDecorator 模式createDecorator是 DI 容器的入口。调用它传递一个字符串名称得到一个兼具三种身份的值// 1. 服务标识符ServiceIdentifier作为 Map 的 key // 2. 参数装饰器Parameter Decorator用于构造函数注入 // 3. 诊断名称toString() 返回构造函数名 const ILogger createDecoratorILogger(logger); // 使用作为参数装饰器声明依赖 class SomeService { constructor( ILogger private readonly logger: ILogger, IClock private readonly clock: IClock, ) {} }关键实现细节​singleton-per-name​——两个createDecorator(logger)返回完全相同的标识符引用。这确保了跨模块引用的一致性。IInstantiationService作用域化服务解析IInstantiationService是运行时的服务容器契约interface IInstantiationService { readonly _serviceBrand: undefined; invokeFunctionR(fn: (accessor: ServicesAccessor) R): R; createInstanceT(descriptor: SyncDescriptor0T): T; createInstanceCtor(ctor: Ctor, ...args: GetLeadingNonServiceArgs): InstanceTypeCtor; createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService; dispose(): void; }核心行为​singleton-per-container​同一个容器内多次get返回同一实例​createChild​创建子容器子容器继承父容器的注册但可以覆盖​auto-injection​通过 TypeScript 参数装饰器元数据自动解析​cycle detection​基于 Graph 子树遍历在构造函数执行前发现循环依赖延迟实例化 (Proxy)当服务标记supportsDelayedInstantiation: true时容器返回一个Proxy真正的构造函数在第一次非事件属性访问时才执行const proxy ix.invokeFunction((a) a.get(IFoo)); // Foo 的构造函数尚未执行 proxy.ping(); // 构造函数在 ping() 访问时首次执行后续调用命中缓存延迟实例化还支持早期监听器early-listener contract在 Proxy 上订阅onDidChange事件会暂存监听器当真实实例创建后自动重绑定。DisposableStore生命周期管理与 VS Code 的 dispose 模式一致项目实现了完整的生命周期体系// 基础契约 interface IDisposable { dispose(): void; } // 持有子 disposables 的基础类 abstract class Disposable implements IDisposable { protected readonly _store new DisposableStore(); protected _registerT extends IDisposable(d: T): T { return this._store.add(d); } dispose(): void { this._store.dispose(); } } // 可变 disposables动态替换 class MutableDisposableT extends IDisposable implements IDisposable { get value(): T | undefined { ... } set value(value: T | undefined) { ... } // 自动 dispose 旧值 }容器 dispose 时按 LIFO 顺序反向构造顺序销毁所有服务子容器先于父容器销毁。循环依赖在构造时就被 Graph 算法拦截dispose 时不会出现死锁。在 agent-core 和 agent-core-v2 中的应用V1 使用registerSingleton在模块加载时注册所有服务描述符启动时通过getSingletonServiceDescriptors()收集并构建根容器。V2 则在此基础上引入了registerScopedService机制按LifecycleScopeApp / Session / Agent三层分级注册。V2 中 DI x Scope 架构的演进V2 的Scope类是 DI 容器和作用域的融合enum LifecycleScope { App 0, // 应用级配置、遥测、持久化引擎 Session 1, // 会话级LLM Provider、会话上下文 Agent 2, // 智能体级循环、工具注册表、提示词 } // Scope 树 // App Scope // ├── Session Scope (session-1) // │ ├── Agent Scope (agent-1) // │ └── Agent Scope (agent-2) // └── Session Scope (session-2)关键特性​子 Scope 只能向更深层级创建​kind this.kind时抛出异常防止架构退化为扁平化​Dispose 级联​父 Scope dispose 自动销毁所有子 Scope​服务注册按 Scope 隔离​Agent 级服务只对当前智能体可见这个设计使 kimi-code 从单体 Agent 演进为可以同时运行多个隔离会话、每个会话又能运行多个子智能体的多租户系统——而容器隔离是这一切的基石。2. LLM Provider 抽象设计kosong面临的问题kimi-code 需要支持多种 LLM 后端Kimi 自家的 API、OpenAIChat Completions 和 Responses 两种风格、AnthropicMessages API、Google GenAI 等。直接调用各 SDK 会导致每个提供商的 API 语义差异工具调用格式、流式事件、错误码泄漏到业务层更换模型需要修改业务代码无法统一处理重试、取消、流水线核心设计统一的 ChatProvider 接口interface ChatProvider { readonly name: string; // 后端标识如 kimi, anthropic readonly modelName: string; // 上游 API 使用的模型名 readonly thinkingEffort: ThinkingEffort | null; readonly maxCompletionTokens?: number; generate( systemPrompt: string, tools: Tool[], history: Message[], options?: GenerateOptions, ): PromiseStreamedMessage; withThinking(effort: ThinkingEffort): ChatProvider; withMaxCompletionTokens?(max: number, opts?: MaxCompletionTokensOptions): ChatProvider; }不可变配置模式withThinking()和withMaxCompletionTokens()不修改当前实例而是返回一个​浅拷贝​ 新实例。这个设计的意图是​请求安全​创建 provider 实例的基配置是一个稳定的 baseline每次调用衍生一个临时配置不会污染下一条请求​共享传输层​拷贝实例共享 HTTP client实现中明确禁止替换或变更避免重复建立连接​immutable-like​在异步并发的场景下避免了状态竞争generate() 函数的编排逻辑generate()不是一个简单的方法调用而是一个精细的编排器​Pre-flight abort 检查​在发出请求前验证 signal 是否已中止​Deferred tools 剥离​标记为延迟的工具不在请求的tools[]中发送只通过消息级tools声明传递——保持 prompt caching 稳定性​流式合并​consecutive compatible parts 使用mergeInPlace合并在内存中避免内存膨胀​并行工具调用索引路由​通过_streamIndextoolCallIndexMap正确路由交错到达的工具调用参数片段​解码阶段记账​精确分开serverDecodeMs等待下一个 part 的时间和clientConsumeMs处理每个 part 的时间让性能诊断有据可循​Tail abort 检查​流结束时再次检查中止信号多供应商适配矩阵providers 目录下每个文件对应一个后端文件后端适配要点kimi.tsKimi / KFC原生 Kimi 协议x-trace-id 链路追踪openai-legacy.tsOpenAI Chat CompletionsSSE 流式解析tool_choice 映射openai-responses.tsOpenAI Responses API新协议tool_calls finish reason 变为 completedanthropic.tsAnthropic Messagesrequired max_tokenspause_turn 语义google-genai.tsGoogle GenAIREST 直接调用tool_calls finish reason 变为 completed每个适配器负责将统一的Message/Tool类型转换为上游 API 的 wire format再将流式响应映射回统一的StreamedMessagePart。FinishReason 归一化不同提供商的停止原因被归一化为统一的枚举type FinishReason | completed // 正常完成 | tool_calls // 暂停等待工具调派Chat Completions / Anthropic 专有 | truncated // token 预算耗尽 | filtered // 内容过滤器拦截 | paused // Anthropic pause_turn | other; // 其他已知但未归类的信号ModelCapability 声明矩阵每个模型的能力通过ModelCapability字典预先声明interface ModelCapability { readonly image_in: boolean; readonly video_in: boolean; readonly audio_in: boolean; readonly thinking: boolean; readonly tool_use: boolean; readonly max_context_tokens: number; readonly max_input_tokens?: number; readonly dynamically_loaded_tools?: boolean; }业务代码在构建请求前通过getModelCapability(provider, model)查询能力矩阵对不支持图像或音频的模型提前拦截而不是把请求发出去再等上游报错。未知模型返回冻结的UNKNOWN_CAPABILITY哨兵值全falsemax_context_tokens: 0。错误分类体系错误不是简单的Error——它们有恢复策略的含义ChatProviderError // 基础类型 ├── APIConnectionError // 网络故障 → 可重试退避后 ├── APITimeoutError // 超时 → 可重试 ├── APIStatusError // HTTP 错误 │ ├── APIContextOverflowError // 413 → 需要 compact 后重试 │ ├── APIRequestTooLargeError // 413 → 不可重试需要缩小媒体 │ └── APIProviderRateLimitError // 429 → 等待 Retry-After 后重试 └── APIEmptyResponseError // 空响应 → 不可重试每个错误都携带retryAfterMs从 HTTPRetry-After头解析和traceId从 Kimi/KFC 的x-trace-id头解析重试循环可以基于错误类型和退避时间做出智能决策。kosong 让 kimi-code 的 Agent 逻辑与具体 LLM 提供商完全解耦。无论是内部 Kimi 模型还是第三方 APIAgent 看到的都是同一个ChatProvider.generate()契约。3. Transcript 四层数据模型面临的问题智能体的对话过程不是简单的「用户问了什么、AI 回了什么」。一个真实的 Agent 对话包含多轮 turn每轮有多个 step每个 step 包含多个 frame工具调用的增量文本流逐字到达的 bash 输出后台任务、审批交互、计划状态、todo 列表多个订阅者需要不同粒度的更新UI 需要「逐字增量」日志需要「完整 turn」如果用一个 JSON 对象序列化整个对话状态每次更新都需要全量传输和渲染性能无法接受。四层架构┌─────────────────────────────────────────────────────┐ │ L4 Views — 渲染器注册表框架无关 │ │ ViewRegistry: toolRenderers, inputRenderers, │ │ markerRenderers — key → component lookup │ ├─────────────────────────────────────────────────────┤ │ L3 Subscriptions — 粒度过滤器 │ │ off / turn / block / delta — 按需订阅 │ ├─────────────────────────────────────────────────────┤ │ L2 Ops — 传输词汇 (idempotent upsert append) │ │ turn.upsert / step.upsert / frame.upsert / │ │ append / marker.upsert / interaction.upsert / … │ ├────────────────────────────────────────────────────┤ │ L1 Store — 内存状态容器AgentTranscript │ │ items[] tasks/meta/todos/prompts 映射表 │ └─────────────────────────────────────────────────────┘L1 StoreAgentTranscriptAgentTranscript是单智能体粒度的内存状态容器持有items[]有序的时间线条目turn / marker / taskReftasksTaskId → TranscriptTask 映射interactionsInteractionId → TranscriptInteraction 映射attachments / todos / prompts / meta其他实体集合状态是​复制即写copy-on-write​的每次apply()产生新的 state 引用已返回的 snapshot 不被后续操作污染。L2 Ops幂等操作词汇这是传输层的词汇表。关键设计原则​**唯一非幂等操作是 ​append**​必须携带offset累积位置​所有 upsert 都是幂等的​重复接收不会产生副作用​按 producer 因果顺序执行​单代理的序列化通道保证收敛type TranscriptOperation | ResetOp // op: reset — 全量快照首次加载或 gap 修复 | TurnUpsertOp // op: turn.upsert — turn 头信息 | StepUpsertOp // op: step.upsert — step 头信息 | FrameUpsertOp // op: frame.upsert — 框架工具调用/结果 | AppendOp // op: append — 增量文本块唯一的 non-idempotent | MarkerUpsertOp // op: marker.upsert | TaskUpsertOp // op: task.upsert | InteractionUpsertOp | AttachmentUpsertOp | TodoUpsertOp | PromptUpsertOp | TaskRefUpsertOp;wire.jsonl 作为单一真相源所有 L2 ops 以 append-only JSONL 格式写入磁盘。这是可审计、可重放、可跨会话恢复的持久化层。L3 Subscriptions按粒度过滤订阅者可以选择关注的粒度级别off不订阅turn仅在 turn 级别变化时通知block文本块级别delta逐字增量用于 UI 流式渲染L4 Views框架无关的渲染器注册表ViewRegistryC是一个 key-based 的渲染器查找器完全不依赖任何 UI 框架。C 是泛型参数可以是 Vue 组件、React 组件或 Ink 渲染器class ViewRegistryC { registerTool(key: string, renderer: C): this; registerInput(originKind: string, renderer: C): this; registerMarker(marker: string, renderer: C): this; resolveTool(frame: ToolCallFrame): C | undefined; // frame.view ?? frame.name resolveInput(origin: TurnOrigin): C | undefined; // origin.kind resolveMarker(marker: string): C | undefined; // marker key }操作批处理序列合约ops 信道需要保证单调递增的seq值。消费者通过since_seq发起 catch-up 请求获取错过的 ops。这是服务端推送和客户端拉取的边界——服务端负责维护 seq 递增客户端负责检测 gap 并发起重置。历史重建groupTurns foldFacts从 L2 ops 重建可读的对话历史需要两个步骤​groupTurns​将 flat ops 序列重组为 turn → step → frame 的层级结构​foldFacts​将分散的工具调用和工具结果折叠为可渲染的对话块Transcript 的四层模型使 kimi-code 能够在服务端以 ops 粒度推送更新、在客户端按需重建视图——既不丢失流式渲染的实时性又保持了数据的完整性和可审计性。4. 执行环境抽象kaos面临的问题kimi-code Agent 需要在多种环境中运行本地开发机、SSH 远程服务器、容器通过 ACP 协议连接。每个环境有不同的文件系统语义、进程管理方式和路径规范。如果 Agent 代码直接调用fs.readFileSync或child_process.exec​不可切换​本地写死的调用无法重定向到远程环境​不可测试​单元测试需要真实的文件系统​不可组合​无法在同一进程中同时操作多个环境核心设计Kaos 接口KaosKimi Agent Operating System是一套统一的文件系统和进程操作抽象interface Kaos { readonly name: string; // 环境标识local, ssh:host readonly osEnv: Environment; // OS/Shell 探测结果 // 路径操作 pathClass(): posix | win32; normpath(path: string): string; gethome(): string; getcwd(): string; // 文件操作 readBytes(path: string, n?: number): PromiseBuffer; readText(path: string, opts?: { encoding?, errors? }): Promisestring; writeBytes(path: string, data: Buffer): Promisenumber; writeText(path: string, data: string, opts?): Promisenumber; // 目录操作 stat(path: string, opts?): PromiseStatResult; iterdir(path: string): AsyncGeneratorstring; mkdir(path: string, opts?): Promisevoid // 进程执行 exec(...args: string[]): PromiseKaosProcess; execWithEnv(args: string[], env?: Recordstring, string): PromiseKaosProcess; // 不可变派生 withCwd(cwd: string): Kaos; withEnv(env: Recordstring, string): Kaos; }三种实现实现环境底层机制LocalKaos本地开发机Node.jsfschild_processSSHKaosSSH 远程服务器SSH2 客户端 SFTP 协议ACPKaos (acp-adapter)ACP 客户端环境Agent Client Protocol通过 kaos-acp.tsAsyncLocalStorage 上下文传递这是实现「同一份 Agent 代码在不同环境可运行」的关键// 底层AsyncLocalStorage 绑定当前 kaos 实例 const kaosStorage new AsyncLocalStorageKaos(); export function getCurrentKaos(): Kaos { const store kaosStorage.getStore(); if (store undefined) throw new KaosError(No Kaos is bound...); return store; } export function runWithKaosT(kaos: Kaos, fn: () T): T { return kaosStorage.run(kaos, fn); } // 上层模块级的便捷函数自动从当前上下文获取 kaos export function readText(path: string, opts?): Promisestring { return getCurrentKaos().readText(path, opts); } export function exec(...args: string[]): PromiseKaosProcess { return getCurrentKaos().exec(...args); }Agent 代码调用import { readText, exec } from moonshot-ai/kaos——完全不需要知道当前是在本地还是远程。环境切换通过外层runWithKaos(localKaos, () agent.run())或runWithKaos(sshKaos, () agent.run())完成。同一进程可以有多个并发上下文每个使用不同的 Kaos 实例互相隔离。不可变派生模式类似 kosong providerwithCwd()和withEnv()返回新实例。这让工具的沙箱化变得简单给某个工具调用创建一个/tmp/workspace子目录的 kaos 视图它只在这个请求的上下文中有效。Kaos 抽象让 kimi-code 的 Agent 代码实现了「编写一次处处运行」——本地开发、SSH 远程、ACP 客户端三种环境共用同一套 Agent 逻辑只在初始化时注入不同的 Kaos 实例。5. V1 → V2 架构演进V1 的问题agent-core V1 的根基是一个巨大的Agent类——它是所有逻辑的中心集成了配置、LLM 调用、工具执行、权重管理、会话状态、对话循环、context 构建等数十种职责。这导致了几个根本性问题​紧耦合​Agent 直接import所有依赖模块的实例替换任何一个实现都需要侵入 Agent 内部​难以测试​单元测试需要构造完整的 Agent 实例Mock 依赖需要 mock 模块导入​难以多租户​同时运行两个 Agent 实例时全局单例会互相干扰​用户代码侵入​面向用户的 CLI 代码和核心 Agent 逻辑混杂在同一个类中V2 的目标V2 重新设计了整个架构核心原则是​DI x Scope​所有服务通过 DI 容器接入按生命周期 Scope 隔离​服务化​每个职责成为一个独立服务通过接口契约定义边界​可测试​通过容器 stub 可以在单元测试中注入 mock 依赖​多租户隔离​Session 和 Agent 级别的 Scope 天然隔离不同实例的状态V2 的域划分V2 按业务域组织代码每个域有清晰的职责边界域路径职责示例服务_base_base/基础设施DI 容器、日志InstantiationService, LogServiceagentagent/智能体核心行为LoopService, ToolExecutor, ContextMemoryappapp/应用层配置、遥测、插件、authConfigService, PluginService, TelemetryServicesessionsession/会话管理生命周期、状态AgentLifecycleService, SessionMcpServicekosongkosong/LLM 供应商抽象ProviderService, ModelService, Generateosos/操作系统接口HostEnvironmentService, HostFsServicepersistencepersistence/持久化层FileStorageService, AppendLogStoretooltool/工具注册和执行ToolRegistryService, ToolContributionswirewire/记录和回放WireService, WireMigrationkap-serverV2 引擎的对外接口kap-server是 V2 架构的 HTTP/WebSocket 网关。它启动 V2 Scope 树将外部请求映射到 V2 服务调用REST API → SessionLifecycleService创建/恢复会话WebSocket → SessionScope维持会话级别的状态和订阅ACP Bridge → 通过 kaos ACP 适配器在远程环境执行klient传输无关的类型化切面klient提供客户端侧的类型安全 API。它抽象了底层传输层HTTP / WebSocket只暴露领域概念Session, Message, AgentRun 等。上层 UI如 kimi-web、vscode 插件、终端 TUI全部依赖 klient 而不是直接打 HTTP 请求。演进策略双引擎共存V1 和 V2 不是一夜之间切换的。项目通过以下策略平滑过渡​双引擎共存​V1 和 V2 的 Agent 在同一个进程中并行运行通过路由层分流​逐步迁移​工具注册、权限管理、会话管理等功能逐个迁移到 V2 域​兼容层​migration-legacy包提供 V1 ↔ V2 的数据格式转换​Session Legacy Service​V2 可以读取和恢复 V1 创建的历史会话V1 → V2 不仅是代码重构更是一次架构哲学上的演变从「一个大类包办一切」变为「通过 DI 容器组合服务通过 Scope 隔离实例通过接口契约定义边界」。这个转变让 kimi-code 从单一 CLI 工具成长为可以支撑 Web、VSCode 插件、SSH 远程等多形态产品的平台级引擎。总结这五个架构决策不是孤立存在的——它们形成了一个相互支撑的网络┌──────────────┐ 注入 ┌──────────────┐ │ DI 容器 │ ◄───────── │ Kaos 抽象 │ │ Scope 树 │ │ (执行环境) │ └──────┬───────┘ └──────────────┘ │ │ 管理生命周期 提供执行能力 │ │ ┌──────────┴──────────┐ ┌─────────┴──────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ LLM Provider │ │ Agent V2 │ │ Transcript │ │ (kosong) │ │ 引擎 │ │ 四层模型 │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ 统一 LLM 服务化编排 状态快照 供应商接口 多租户隔离 增量同步DI 容器提供了服务组合和生命周期管理的骨架kosong让 Agent 逻辑与具体 LLM 提供商解耦​Transcript​解决了对话状态的流式同步和历史重建问题Kaos让同一份 Agent 代码在不同执行环境中无缝运行V2 架构将所有组件整合为一个可测试、可扩展、可多租户的平台理解这些架构决策就理解了 kimi-code 如何从一个实验性的 AI CLI 工具演化为支撑 Kimi 全线产品的 Agent 引擎平台。下一篇将深入 Agent 类的内部——这个引擎的编排者如何协调所有服务完成一轮对话。
返回列表