喜欢对技术刨根问底,却总是被打退,晕,再上,屡败屡战

注定要与程序打交道,毫无疑问我喜欢编程,而且适合

导航

本文端到端追踪一个用户 Prompt 在 kimi-code 中的完整生命周期:从用户在终端敲下第一行文字,到引擎层层处理、LLM 生成回复、最后将结果流式渲染回终端。理解这条数据流是掌握 kimi-code 内部机制的关键。

1. 用户输入阶段

一切从用户敲下回车开始。kimi-code 提供了三种主要的交互入口:​CLI 非交互模式​(kimi -p "prompt")、​TUI 交互模式​(kimi 直接启动),以及 ​SDK 程序化调用​。无论哪个入口,数据流的起点都是同一个——一个 ContentPart[] 数组,其中每个元素可以是文本、图片 URL 或视频 URL。

1.1 pi-tui 如何处理用户输入

pi-tui 是 kimi-code 的终端 UI 层(包名 @moonshot-ai/pi-tui),基于 Node.js 的 raw mode terminal 实现。其核心架构包含以下几个关键组件:

  • tui.ts — 顶层 TUI 管理器,持有事件循环、渲染逻辑和组件树
  • terminal.ts — 底层终端抽象,处理 raw mode 切换、光标定位和 ANSI 转义序列
  • editor-component.ts — 多行编辑器组件,负责文本输入、光标移动和历史记录
  • keybindings.ts — 全局快捷键绑定(Ctrl+C 取消、Esc 返回等)

当用户在 TUI 中输入文本并按下回车,事件链如下:

raw stdin byte stream
  → terminal.ts 读取输入字符
    → editor-component.ts 维护编辑状态(字符数组、光标位置)
      → 用户按回车触发 submit
        → tui.ts 将文本封装为 ContentPart[]
          → 通过 RPC 发送给 node-sdk

同时,pi-tui 通过差分渲染(delta rendering)来更新终端显示:只重绘发生变化的行,避免全屏刷新。当新的流式内容到达时,增量追加到输出区域,确保在低带宽场景下也能保持流畅。

1.2 CLI 路径

在 CLI 非交互模式(kimi -p)下,路径更短:

$ kimi -p "帮我写一个快速排序函数"

  → CLI 入口解析参数
    → 将字符串转换为 ContentPart[]: [{ type: "text", text: "帮我写一个快速排序函数" }]
      → 调用 SDK: session.prompt(...)

2. SDK 层

node-sdk(包名 @moonshot-ai/node-sdk)是面向外部消费者的编程接口层。它提供类型安全的 TypeScript API,同时负责认证、会话生命周期管理和 RPC 通信。其核心架构如下:

┌─────────────────────────────────────────────────┐
│                   node-sdk                        │
│  ┌───────────┐  ┌─────────┐  ┌───────────────┐  │
│  │ KimiHarness│→ │ Session │→ │ SDKRpcClient  │  │
│  │  (编排器)  │  │ (会话)   │  │  (RPC 客户端)  │  │
│  └───────────┘  └─────────┘  └───────┬───────┘  │
│                                       │          │
└───────────────────────────────────────┼──────────┘
                                        │ JSON-RPC 2.0
                               ┌────────▼────────┐
                               │ agent-core 引擎 │
                               └─────────────────┘

2.1 KimiHarness 编排器

KimiHarness 是 SDK 的顶层协调者。它负责:

  • 认证管理​:OAuth 登录、Token 刷新、凭证持久化
  • 会话创建与恢复​:新建 Session 或从磁盘恢复已有会话
  • 遥测初始化​:配置遥测客户端,上报使用数据
  • 配置加载​:读取 .kimi/config 配置,包括模型选择、权限模式等

典型调用流程:

const harness = new KimiHarness({ workDir: './my-project' });
const session = await harness.createSession();

// 发送第一条 prompt
await session.prompt('帮我重构 UserService 类');

// 接收流式响应
session.onEvent((event) => {
  if (event.type === 'assistant.delta') {
    process.stdout.write(event.delta);
  }
});

2.2 Session.prompt() 方法

Session 是用户操作的主要入口。其 prompt() 方法(源码位置:node-sdk/src/session.ts:107)做三件事:

  1. 输入规范化​:将字符串或 PromptInput 统一转换为 ContentPart[],包含空值和类型校验
  2. 会话活性检查​:确保 Session 未被 close
  3. RPC 转发​:调用 this.rpc.prompt({ sessionId, input }) 将请求通过 JSON-RPC 发送给引擎

2.3 SDKRpcClient — RPC 通道

SDKRpcClient 是 SDK 与 agent-core 引擎之间的桥梁。它基于 JSON-RPC 2.0 协议,提供了类型安全的请求/响应模型:

  • 请求路径​:SDK → RPC Client → agent-core(同步或异步)
  • 事件路径​:agent-core → RPC Client → Event Emitter → SDK Session.onEvent() 回调
  • 双向通信​:SDK 可以同时发送多个请求,引擎通过 turnId 将响应路由回正确的会话

RPC 消息本身是纯 JSON 对象,不包含二进制数据。图片、视频等媒体通过 URL 引用传递,实际数据传输由底层 HTTP 处理。

3. Agent 核心引擎

agent-core(包名 @moonshot-ai/agent-core)是整个系统的智能核心。它接收 SDK 层传入的请求,管理对话上下文、编排多轮对话循环、协调工具调用,并最终产生回复。引擎设计遵循 依赖注入(DI) 模式,所有外部依赖(日志、遥测、存储)通过构造函数注入,核心逻辑保持纯函数式、可测试。

引擎内部由三个关键层次构成:​Session 层​(生命周期管理)、​Agent 层​(子系统组合)、​TurnFlow 层​(对话循环执行)。

3.1 Session 层

Session 是 agent-core 侧的最外层容器。它拥有一个或多个 Agent 实例,并负责:

  • 生命周期管理​:创建、挂起、恢复、销毁
  • 生命周期钩子​:提供 SessionStartSessionEndUserPromptSubmitStopPostToolUse 等钩子,允许插件和外部系统介入关键节点
  • 持久化​:将会话元数据存储到磁盘(通过 AgentRecordPersistence),支持跨进程恢复
  • Provider 管理​:管理 LLM 模型提供商的配置和认证(通过 ModelProvider

Session 通过 session.prompt() 接收用户输入,内部路由到主 Agent 的 Agent.rpcMethods.prompt(),从而进入 Agent 层的编排逻辑。

3.2 Agent 类

Agent(源码位置:agent-core/src/agent/index.ts)是整个引擎的核心编排器。它通过 组合模式 组装了 15+ 个子系统,每个子系统各司其职:

子系统 职责
ContextMemory 对话历史管理与 token 计数
ConfigState 运行时配置(模型、权限、思考模式等)
TurnFlow 多轮对话循环控制
ToolManager 工具注册、发现、工具声明生成
PermissionManager 权限模式管理(yolo / manual / auto)
PlanMode 计划模式:先生成执行计划再执行
SwarmMode Swarm 并行 Agent 模式
GoalMode 自主目标驱动模式
BackgroundManager 后台任务管理(长期运行的子进程)
CronManager 定时任务调度
SkillManager Skill 加载与激活
Compaction 对话压缩(Full / Micro)
InjectionManager 上下文注入(插件提醒、Skill 提示等)
UsageRecorder Token 使用量统计
AgentRecords 事件日志记录到 wire.jsonl

Agent 的 rpcMethods 是一个方法路由表,将外部 RPC 调用映射到内部操作。其中最核心的两条路径是:

// prompt: 启动新对话轮次
prompt: (payload) => { this.turn.prompt(payload.input); },

// cancel: 取消当前正在执行的轮次
cancel: (payload) => { this.turn.cancel(payload.turnId); },

Agent 还持有 generate 的封装函数。该封装在调用底层 kosong.generate 之前,自动注入认证信息、记录请求日志和遥测数据(LLM 请求追踪):

get generate(): typeof generate {
  return async (provider, systemPrompt, tools, history, callbacks, options) => {
    // 记录请求日志
    this.llmRequestLogger.logRequest({ provider, systemPrompt, tools, messages: history });
    // 记录遥测
    this.llmRequestRecorder.record({ provider, systemPrompt, tools, messages: history });
    // 注入认证后调用底层 generate
    return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, requestOptions);
  };
}

3.3 TurnFlow — 对话循环(核心)

TurnFlow 是整个 kimi-code 数据流中最核心的组件。 它在 agent-core/src/agent/turn/index.ts 中实现,负责将一次用户输入转化为多步 Agent-LLM 交互循环。理解 TurnFlow 就理解了 kimi-code 的"心脏"是如何跳动的。

主循环概览

TurnFlow_主循环流程图.png

TurnFlow 通过 Agent.llm getter 创建 KosongLLM 实例,将 provider、systemPrompt、completion budget 和上下文 token 计数封装为一个统一的 LLM 调用接口。这个设计使得 loop 层(agent-core/src/loop/)完全无状态,只关心"给定 input 返回 output"。

主循环详细分解

以下按时间顺序逐步拆解一次 Turn 的完整执行过程:

Turn 启动

当 Agent 收到 RPC prompt 调用后,TurnFlow.prompt() 被触发。首先执行图片格式门控(gateImageFormatParts),将不被支持的图片格式(如 AVIF、HEIC)转换为文本提示,防止无效数据污染会话历史。然后分配一个单调递增的 turnId,创建 AbortController 开始异步执行 turnWorker

如果此时 Agent 有活跃的 Goal,turnWorker 会委托给 driveGoal,在一个循环中依次执行多个 continuation turn。否则,只执行一次 runOneTurn。每个 turn 的元数据通过 turn.started 事件通知 SDK 层。

Step 循环入口​(runStepLoop):在进入 LLM 调用循环前,执行以下初始化操作:

  • 等待 MCP 连接管理器完成初始加载(mcp.waitForInitialLoad(signal)
  • Goal 注入​:如果有激活的 Goal,将目标提醒注入到对话上下文中
  • Tools Diff 注入​:如果可加载工具集发生变化,注入变化通知
  • 创建 mediaStripSnapshot,用于构建剥离媒体数据的消息(媒体降级恢复用)

每步预处理(beforeStep 钩子)

在每次 LLM 调用之前,loop 触发 beforeStep 钩子:

  • Micro Compaction 检测​:检查是否需要进行微压缩(大于 token 阈值)
  • Full Compaction 检测​:检查是否触发自动全量压缩
  • Steer Buffer 刷新​:将缓冲的 steer 消息(来自后台任务通知、cron 触发等)追加到上下文
  • 上下文注入​:通过 InjectionManager.inject() 注入插件提醒、Skill 提示等系统消息

消息构建与 LLM 调用

loop 层通过 buildMessages 回调获取当前上下文的 Message[] 数组。ContextMemory.messages getter 在返回前执行 投影(projection) 管道:

  1. Micro Compaction​:压缩最近的消息以控制 token 数量
  2. Dynamic Tool 变形​:如果启用了 toolSelect,对动态工具 schema 消息进行变形处理
  3. Projector 矫正​:修复孤儿 tool result、合并连续 assistant 消息、去除空消息等
  4. Media Strip​(恢复路径):如果需要,构建剥离媒体的替代消息列表

然后调用 LLM.generate()(即 KosongLLM),内部将 system prompt、tools 数组和消息历史传给 this.agent.generate,最终进入 kosong 的 generate() 函数。

响应解析与工具调用

LLM 返回的 assistant 消息包含 ​文本内容​(content: ContentPart[])和 ​工具调用列表​(toolCalls: ToolCall[])。loop 层通过 dispatchEvent 回调逐个处理:

  • text.delta → 流式文本增量,最终映射为 assistant.delta 事件推送到 UI
  • thinking.delta → 思维链增量,映射为 thinking.delta 事件
  • tool.call → 每个工具调用启动事件,映射为 tool.call.started
  • tool.result → 每个工具执行结果事件,映射为 tool.result

工具调用去重​:ToolCallDeduplicator 在每个 step 内检测同名、同参数的重复调用,避免模型陷入调用循环时重复执行相同操作。同一步内检测到重复时直接返回缓存结果(syntheticResult),跨步重复上报遥测事件但正常执行。

工具调度​:tool-scheduler.ts 负责有状态的工具执行调度。具有冲突资源访问的工具调用被序列化执行,而无冲突的调用可以并行执行,遵循 provider 顺序边界。

权限检查

工具调用在执行前必须通过 PermissionManager.beforeToolCall() 的权限检查。三种权限模式下的行为:

模式 行为
yolo 自动批准所有工具调用,无需用户干预
manual 每个工具调用都需要用户明确批准(通过 SDK 的 ApprovalHandler 回调)
auto 自动批准"安全"的工具调用(如读取文件),敏感操作(如执行 shell 命令)仍需确认

如果权限检查不通过,工具调用被标记为 denied,引擎向模型注入一条说明(如"Tool call was blocked by the user"),然后继续下一轮循环。

结果处理与循环继续

工具执行完成后:

  • 结果预算​(budgetToolResultForModel):对过大的工具输出进行截断,防止超出模型上下文限制
  • PostToolUse 钩子​:触发插件回调,允许外部系统检视或修改工具结果
  • Goal 状态更新​:如果工具调用是 UpdateGoal 且状态为 complete/blocked,标记 Goal 已终结
  • 结果追加​​:通过 ContextMemory.appendLoopEvent 将 tool result 追加到对话历史

afterStep 钩子在每个 step 结束后执行:记录 token 使用量、触发 Full Compaction afterStep 检查、结束去重周期。如果 Goal 预算超限,返回 { stopTurn: true } 终止当前 turn。

循环终止

Step 循环在以下条件之一满足时终止:

  • 模型不再调用工具​:assistant 消息中 toolCalls.length === 0
  • 达到最大步数限制​:maxStepsPerTurn(默认无限制,可通过 KIMI_LOOP_MAX_STEPS_PER_TURN 环境变量配置)
  • 用户主动中断​:AbortController.abort() 被触发(Esc 键或 SDK cancel 调用)
  • Provider 过滤​:LLM 返回 finishReason === 'filtered'(内容安全拦截)
  • 上下文溢出恢复​:发生 context overflow 错误后自动压缩重试;如果压缩无效则终止

循环终止后,shouldContinueAfterStop 钩子检查以下条件是否允许继续:

  1. 是否有新的 steer 消息注入(steer 消息优先于 Stop 钩子)
  2. print 模式下是否还有后台 subagent 在运行(drain 等待其完成)
  3. UpdateGoal 标记为 terminal 后是否需要最后一条用户可见消息
  4. Stop 钩子是否请求一次额外 continuation

如果以上条件均不满足,turn 结束,发出 turn.ended 事件。

错误恢复

TurnFlow 内置了多层错误恢复机制:

  • Context Overflow 恢复​:捕获 APIContextOverflowError,触发 FullCompaction 压缩历史后重试
  • Step 重试​:每次 LLM 调用最多重试 maxRetriesPerStep 次(通过 KIMI_LOOP_MAX_RETRIES_PER_STEP 配置),重试期间发射 step.retrying 事件
  • Strict Messages 回退​:当 LLC 提供者拒绝正常消息格式时,使用 strictMessages 投影重建完全合规的请求
  • Media Degrade 回退​:413 错误时构建剥离媒体的降级消息列表重试
  • Turn 级别失败处理​:未恢复的错误导致 turn.ended with reason failed

4. LLM 抽象层(kosong)

kosong(包名 @moonshot-ai/kosong,印度尼西亚语"零/空")是 kimi-code 的统一 LLM 抽象层。它将 Kimi、OpenAI、Anthropic 等不同提供商的 API 差异封装在 ChatProvider 接口之后,为上层提供一致的调用体验。

4.1 ChatProvider 接口

interface ChatProvider {
  readonly name: string;
  readonly modelName: string;
  generate(
    systemPrompt: string,
    tools: Tool[],
    history: Message[],
    options?: GenerateOptions
  ): Promise<StreamedMessage>;
}

每个 provider 实现内部处理:认证头组装、URL 构造、请求体格式转换(OpenAI chat/completions vs Anthropic messages vs Kimi 自有格式)、响应流解析。上游调用方只需传入统一的 systemPrompttoolshistory

4.2 generate() 函数

generate()(源码:kosong/src/generate.ts)是实际执行 LLM 调用的函数,其核心职责:

  1. Tool 过滤​:从 tools 数组中剥离 deferred: true 的工具(这些工具通过 message-level declarations 发送,以保持 prompt caching 稳定性)
  2. Pre-flight abort 检查​:如果 signal 已 abort,直接抛出而不发起网络请求
  3. 流式消费​:通过 for await (const part of stream) 逐个处理流式消息分段,处理并行工具调用的交叉 delta(通过 toolCallIndexMap 路由到正确的 tool call)
  4. Part 合并​:连续的同类型 part 被合并(如两个 TextPart 拼接),并行工具调用的 interleaved delta 路由正确
  5. 时间统计​:分别记录 server decode time 和 client consume time,为性能分析提供数据
  6. 空响应检测​:如果响应既无文本也无工具调用,抛出 APIEmptyResponseError

4.3 取消支持

取消通过 AbortController 信号实现。当用户按下 Esc 或 SDK 调用 cancel(),信号被 abort。generate 在每个异步边界检查 signal.aborted,一旦检测到中止就调用 stream.cancel() 关闭底层 HTTP 连接,然后抛出 AbortError。TurnFlow 的 runOneTurn 捕获该错误,将 turn 标记为 cancelled

5. 响应输出

LLM 生成的响应需要一路传回用户的终端。这一过程通过多级事件转发完成:

kosong.stream → loop.dispatchEvent → Agent.emitEvent → RPC → SDK.onEvent → pi-tui.render

5.1 事件路由链

loop → agent-core​:loop 层通过 createLoopEventDispatcher 回调记录 transcript 事件(appendTranscriptRecord)和发射实时事件(emitLiveEvent)。mapLoopEvent 函数(turn/index.ts:1285)将内部 loop 事件类型映射为外部 Agent 事件类型:

Loop 事件 Agent 事件
text.delta assistant.delta
thinking.delta thinking.delta
tool.call tool.call.started
tool.result tool.result
step.begin turn.step.started
step.end turn.step.completed

agent-core → SDK​:Agent 通过 this.rpc?.emitEvent?.(event) 将事件推送到 RPC 通道。如果 Agent.records.restoring 为 true(正在重放历史记录),事件被静默跳过,防止重放污染实时 UI。

SDK → pi-tui​:Session.onEvent() 的回调在 SDK 端接收事件。pi-tui 注册了事件监听器,对不同类型的响应做不同处理:文本 delta 被追 �� 到终端输出,工具调用事件触发权限审批 UI。

5.2 pi-tui 差分渲染

pi-tui 采用 ​差分渲染​(delta rendering)策略更新终端显示:

  1. 维护一个 ​虚拟屏幕缓冲区​(字符串矩阵)
  2. 接收新文本内容后,与当前缓冲区对比,只生成 变更区域 的 ANSI 转义序列
  3. 通过 process.stdout.write() 写入仅更新实际变化的行,避免全屏刷新

这种方式在低带宽(如 SSH 连接)和高延迟场景下至关重要,确保流式输出在人眼感知上是流畅的。

6. 持久化

kimi-code 的持久化层确保会话状态在进程重启后可以完整恢复。核心组件是 AgentRecords 和 ​Transcript 系统​。

6.1 AgentRecords — wire.jsonl

AgentRecordsagent-core/src/agent/records/)负责将所有 Agent 事件以 追加写入 的方式持久化到 <sessionDir>/wire.jsonl。每条记录是一个 JSON 对象,包含事件类型和完整数据:

{"type":"turn.prompt","input":[{"type":"text","text":"帮我写一个快速排序"}],"origin":{"kind":"user"}}
{"type":"turn.started","turnId":0,"origin":{"kind":"user"}}
{"type":"context.append_loop_event","event":{"type":"step.begin","uuid":"abc123",...}}
...

写入路径:

  • Turn 级事件​:turn.promptturn.cancelcontext.clear 等由 TurnFlow 直接记录
  • Step 级事件​:step.beginstep.endtool.calltool.result 等通过 ContextMemory.appendLoopEvent() 记录
  • 压缩事件​:context.apply_compaction 记录压缩前后的 token 计数和保留消息统计

持久化写入是​同步追加​​(appendFileSync),不在关键路径上引入异步开销。写入失败时通过 error 事件上报,但不会阻塞 Agent 的正常运行。

6.2 Transcript 系统

Agent.resume() 通过 AgentRecords.replay() 读取 wire.jsonl 并重建完整的 Agent 状态:

  • Context 重建​:重放 context.append_loop_eventcontext.append_message 记录,逐步填充 ContextMemory._history
  • 工具状态恢复​:turn.prompt 记录触发的 turn 被标记为 resuming,后续通过 observeRestoredTurnId 提升 turn 计数器
  • Goal 恢复​:agent-core/src/agent/goal/index.ts 在 resume 后调用 normalizeAfterReplay() 重建 Goal 状态
  • Background 恢复​:BackgroundManager.loadFromDisk() 读取 <sessionDir>/tasks/ 下的任务文件
  • Cron 恢复​:CronManager.loadFromDisk() 恢复定时任务

Transcript 系统的设计原则是:​wire.jsonl 是最终的真实来源​(source of truth)。内存中的任何状态都可以从 wire.jsonl 重新推导出来。这使得进程崩溃后的恢复不丢失任何对话历史。

至此,我们完成了一条 Prompt 从用户指尖到 LLM 再到终端显示的完整旅程。理解这条数据通路是深入 kimi-code 内部机制的基础。下一篇将探讨 kimi-code 的关键架构决策——为什么选择某些模式而放弃另一些,以及这些决策如何塑造了整个系统的形态。