kimi-code 深度掌握系列文章-LLM 抽象层:Kosong(十一)
1. Kosong 的定位:供应商中立的抽象层
“Kosong” 在马来语/印尼语中意为"空"或"无",暗示这个包不绑定任何特定供应商。它是 agent-core 和具体 LLM SDK 之间的统一接口层——Agent 引擎通过 Kosong 与 Kimi、OpenAI、Anthropic、Google GenAI 等后端通信,而无需在意底层是谁。
┌──────────────────────────────────────────────────────────────┐
│ agent-core │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ModelRequester ─── request(systemPrompt, tools, │ │
│ │ messages, params) │ │
│ └───────────────────────┬──────────────────────────────┘ │
├──────────────────────────┼──────────────────────────────────┤
│ kosong │ (LLM 抽象层) │
│ ┌───────────────────────┴──────────────────────────────┐ │
│ │ ChatProvider (interface) │ │
│ │ ├── KimiChatProvider ──→ Moonshot API │ │
│ │ ├── AnthropicChatProvider ──→ Claude API │ │
│ │ ├── OpenAILegacyChatProvider ──→ Chat Completions │ │
│ │ ├── OpenAIResponsesChatProvider ──→ Responses API │ │
│ │ └── GoogleGenAIChatProvider ──→ Gemini / VertexAI │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Kosong 承担了三个核心职责:
- 类型归一化:将每个供应商特有的请求/响应类型转换为一套统一的
Message、ContentPart、ToolCall抽象 - 流式编排:管理 SSE / NDJSON 流的逐 token 消费,拼接完整响应
- 错误映射:将各 SDK 的异构错误对象分类为统一的
ChatProviderError家族,驱动上游的重试和恢复逻辑
设计哲学上,Kosong 遵循"薄适配"原则:不做 prompt 工程,不做策略决策。它只负责把 agent-core 的意图忠实地翻译为每个供应商的 API 调用,策略(重试、降级、模型切换)留给上层。
2. ChatProvider 接口设计
ChatProvider 是 Kosong 的核心接口,定义在 packages/kosong/src/provider.ts。它是所有供应商适配器的契约——任何接入 Kosong 的新供应商只需要实现这个接口。
export interface ChatProvider {
/** 供应商标识,如 "kimi"、"anthropic"、"openai" */
readonly name: string;
/** 模型名称,如 "moonshot-v1-auto" */
readonly modelName: string;
/** 当前思考模式 */
readonly thinkingEffort: ThinkingEffort | null;
/** 生效的 max completion tokens */
readonly maxCompletionTokens?: number;
/** 核心方法:发送请求并返回流式响应 */
generate(
systemPrompt: string,
tools: Tool[],
history: Message[],
options?: GenerateOptions,
): Promise<StreamedMessage>;
/** 不可变克隆:设置思考力度 */
withThinking(effort: ThinkingEffort): ChatProvider;
/** 不可变克隆:限制最大输出 token */
withMaxCompletionTokens?(
maxCompletionTokens: number,
options?: MaxCompletionTokensOptions,
): ChatProvider;
/** 视频上传(可选) */
uploadVideo?(
input: string | VideoUploadInput,
options?: GenerateOptions,
): Promise<VideoURLPart>;
}
2.1 readonly 属性
| 属性 | 类型 | 说明 |
|---|---|---|
name |
string |
供应商标识符,用于日志、错误追踪和 Provider 选择逻辑。例如 "kimi"、"anthropic"、"google-genai" |
modelName |
string |
实际传给上游 API 的模型名。这是经过 Catalog 解析后的标准化名称,而非用户的原始输入 |
thinkingEffort |
ThinkingEffort | null |
思考模式配置。'off' 禁用思考,'on' 开启(布尔模型),其他字符串值(如 "low"、"high"、"max")为模型声明的 effort 级别 |
maxCompletionTokens |
number | undefined |
最终发送给 API 的 max_tokens 值,经过窗口约束和 transport 上限的 clamping。由上层读取以记录实际的请求参数 |
2.2 ThinkingEffort 的设计
ThinkingEffort 类型定义为 'off' | 'on' | (string & {})。前两个是保留值:'off' 禁用思考,'on' 是布尔模型的开关信号。其余为模型声明的 effort 级别,以开放 string 形式传递——提供者适配器接收已经解析好的 effort 值,直接透传给上游 API。
这种设计避免了在 Kosong 层维护一个不断膨胀的 effort 枚举。当一个新模型声明了 "extreme" 级别的思考能力时,Kosong 不需要修改一行代码。
2.3 withXxx 模式的契约
withThinking(effort) 和 withMaxCompletionTokens(n, options?) 都返回 浅拷贝的新实例。原实例不受影响。这个不可变模式的实现细节:
- 克隆后的实例必须共享底层 HTTP 连接池和 SDK 客户端——不允许重新创建 transport
withMaxCompletionTokens是可选的(?),因为并非所有后端都需要客户端计算的 token 上限- 当提供
MaxCompletionTokensOptions时,实现可以根据上下文窗口(maxContextTokens - usedContextTokens)进一步收束上限
3. 多供应商适配器
Kosong 内置 6 种 ProviderType:
export type ProviderType =
| 'kimi' // Moonshot AI 原生 API
| 'anthropic' // Claude 模型
| 'openai' // Chat Completions API(Legacy)
| 'openai_responses' // Responses API(新一代)
| 'google-genai' // Gemini
| 'vertexai'; // Google Cloud Vertex AI
每个适配器都实现 ChatProvider 接口,负责将统一的 Message / Tool 类型转换为供应商的原生格式:
3.1 KimiChatProvider
直接对接 Moonshot AI 的 API。相比其他适配器,Kimi 适配器有独特的处理:
- JSON Schema 规范化:Moonshot 的工具校验对 JSON Schema 兼容性要求更严格。Kimi 适配器包含
normalizeKimiToolSchema()函数,自动补全缺失的type字段、展开$ref引用、修复类型矛盾的 schema - 动态工具加载:Kimi 是唯一支持
messages[].tools的协议——允许在对话中途动态装载工具定义而不破坏 prompt cache
3.2 AnthropicChatProvider
对接 Claude 模型家族。关键差异包括:
- 思考模式映射:Anthropic 有 budget 和 adaptive 两种思考模式,通过
parseAnthropicModelVersion()从模型名推断版本,匹配AnthropicModelProfile(effort 列表、是否支持 effort 参数、是否可禁用思考) - 系统 prompt 处理:Anthropic 将 system prompt 作为独立的
system参数而非消息数组中的一条消息 - tool_use / tool_result 配对校验:Anthropic 严格要求请求中的工具调用和结果必须相邻配对,否则返回 400
3.3 OpenAILegacyChatProvider
使用 Chat Completions API(/v1/chat/completions)。处理包括:
- message 转换:将 kosong 的
ContentPart[]转为 OpenAI 的 content 格式(单字符串或数组) - tool message 处理:Chat Completions 的
tool消息只接受文本内容,多模态工具结果需通过ToolMessageConversion策略处理 - reasoning 往返:支持
reasoning_content等多种 reasoning 字段方言,自动检测并回传 - 并行工具调用路由:通过 streaming
index字段将交错的 argument delta 正确路由到对应的 tool call
3.4 OpenAIResponsesChatProvider
使用 OpenAI 新一代的 Responses API。这是一个"托管循环"式 API——OpenAI 服务端可以自主执行多轮工具调用后返回最终结果。适配器需要将其"自动循环"行为映射回 Kosong 的单轮 generate() 语义。
3.5 GoogleGenAIChatProvider
同时服务 google-genai 和 vertexai 两种配置。特点:
- 函数声明格式:使用 Gemini 的
functionDeclarations格式 - finishReason 映射:Gemini 在工具调用完成后报告
STOP而非特殊的 tool_calls 信号
4. generate() 的协调逻辑
generate() 函数(packages/kosong/src/generate.ts)是整个 LLM 交互的核心编排器。其职责远不止"调用 provider 然后返回结果"。
4.1 调用签名
export async function generate(
provider: ChatProvider,
systemPrompt: string,
tools: Tool[],
history: Message[],
callbacks?: GenerateCallbacks,
options?: GenerateOptions,
): Promise<GenerateResult>
4.2 协调流程图
请求前处理
检查 AbortSignal 是否已取消(避免无效的网络请求);过滤 deferred 工具(这些工具通过 message-level tools 声明,不应出现在顶层 tools[] 中);触发 onRequestStart 钩子。
调用 Provider
调用 provider.generate(systemPrompt, wireTools, history, options) 获取 StreamedMessage。立即捕获 traceId(从响应头 x-trace-id 获取),以便即使流被中断也能关联到服务端请求。
流式消费与 Part 合并
通过 for await (const part of stream) 逐 chunk 消费。使用 mergeInPlace(pendingPart, part) 合并兼容的连续 parts:
TextPart + TextPart→ 拼接 textThinkPart + ThinkPart→ 拼接 thinkToolCall + ToolCallPart→ 追加 arguments
并行工具调用路由
当 OpenAI API 并行返回多个 tool call 时,argument delta 可能交错到达(例如 tc0-header → tc1-header → tc0-args → tc1-args)。generate() 通过 toolCallIndexMap(由 provider streaming index 映射到 message.toolCalls 数组位置)将每个 delta 路由到正确的 tool call。
解码阶段计时
将 streaming 窗口拆分为 serverDecodeMs(等待下一 part 的耗时)和 clientConsumeMs(处理每个 part 的耗时)。这种拆分允许上层将延迟归因于服务端/网络 vs 客户端处理。
结果验证与组装
flush 最后一个 pending part → 检查是否空响应(无 content 且无 toolCalls)→ 检查是否纯 think 响应 → 触发 onToolCall 回调 → 组装 GenerateResult。
4.3 并行工具调用:一个关键设计
OpenAI 的 Chat Completions API 在返回多个并行 tool_call 时,streaming 的 argument delta 可能交错到达——这是因为多个 tool call 共享同一个 SSE 流。generate() 通过
toolCallIndexMap和 index-based 路由,确保每个 tool call 的参数被正确组装,而非依赖于流的顺序。
具体的实现位于 flushPart() 函数。当一个新的 ToolCall header 到达时,如果当前 pending 的是另一个 ToolCall(merge 会失败),pending 的 tool call 被 flush 到 message.toolCalls 数组,新 tool call 成为新的 pending。如果后续的 ToolCallPart 带有 index 指向已 flush 的 tool call,则通过 toolCallIndexMap 直接追加到正确位置。
5. 错误分类与重试机制
Kosong 定义了一套完整的错误类型层次,位于 packages/kosong/src/errors.ts:
ChatProviderError // 基础错误
├── APIConnectionError // 网络连接失败 → 可重试
├── APITimeoutError // 请求超时 → 可重试
├── APIStatusError // HTTP 状态错误
│ ├── APIProviderRateLimitError // 429 限流 → 可重试
│ ├── APIProviderQuotaExhaustedError // 429 额度耗尽 → 不可重试
│ ├── APIContextOverflowError // 上下文溢出 → 触发 compaction
│ └── APIRequestTooLargeError // 请求体过大 → 触发 media strip
└── APIEmptyResponseError // 空响应 → 可重试
5.1 可恢复 vs 不可恢复
| 类别 | 错误类型 | 处理策略 |
|---|---|---|
| 可重试 | APIConnectionError |
指数退避重试 |
APITimeoutError |
指数退避重试 | |
APIProviderRateLimitError (429) |
服从 Retry-After 头,否则指数退避 |
|
| 408, 409, 5xx, 529 | 指数退避重试 | |
APIEmptyResponseError |
重试(流中断等瞬态问题) | |
| 不可重试 | APIProviderQuotaExhaustedError |
立即失败——账户余额不足,重试无用 |
APIContextOverflowError |
触发上下文压缩(compaction),不是重试 | |
APIRequestTooLargeError |
触发 media-stripped resend | |
| 400 (认证/参数错误)、401、403、404、422 | 立即失败 |
5.2 判断逻辑
isRetryableGenerateError(error) 函数实现了精确的重试判定:
export function isRetryableGenerateError(error: unknown): boolean {
// 网络层错误:总是可重试
if (error instanceof APIConnectionError ||
error instanceof APITimeoutError) return true;
// 空响应:可能是流中断,可重试
if (error instanceof APIEmptyResponseError) return true;
if (error instanceof APIStatusError) {
// 额度耗尽:429 但不可重试
if (error instanceof APIProviderQuotaExhaustedError) return false;
// HTTP 状态码白名单
return [408, 409, 429, 500, 502, 503, 504, 529]
.includes(error.statusCode);
}
// 未分类的 ChatProviderError:安全重试
return error instanceof ChatProviderError
&& !isImageFormatError(error);
}
注意 APIProviderQuotaExhaustedError 虽然 HTTP 状态码也是 429,但它是 APIStatusError 的独立子类而非 APIProviderRateLimitError 的子类。设计理由是:rate limit 会随时间自动解除(重试有意义),而配额耗尽在账户充值前是确定性的(重试不可能成功)。
5.3 Abort 支持
cancel 是 Kosong 的一等公民。generate() 在多个检查点调用 throwIfAborted(options?.signal, stream):
- 请求前:signal 已 abort → 不发起请求,直接抛出
AbortError - 每轮遍历后:每个 stream part 之后检查
- 流结束后:处理完所有 part 后检查
当 abort 发生时,generate() 会尝试调用流的 cancel()(如果支持),确保底层 HTTP 连接被正确关闭。
6. ModelCapability 能力矩阵
不同模型支持的能力差别很大——有的支持图片输入,有的不支持工具调用,有的支持视频。Kosong 通过 ModelCapability 接口声明每个模型的能力,让上层在发起请求前就能做出判断。
6.1 接口定义
export 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; // 最大输入 token(可能小于窗口)
readonly dynamically_loaded_tools?: boolean; // 动态工具加载
}
6.2 能力查询
getModelCapability(wire, modelName) 根据提供商和模型名返回能力矩阵:
export function getModelCapability(
wire: ProviderType,
modelName: string,
): ModelCapability {
switch (wire) {
case 'anthropic':
return getAnthropicModelCapability(modelName);
case 'openai':
return getOpenAILegacyModelCapability(modelName);
case 'openai_responses':
return getOpenAIResponsesModelCapability(modelName);
case 'google-genai':
case 'vertexai':
return getGoogleGenAIModelCapability(modelName);
case 'kimi':
// Kimi 的能力由宿主 catalog/config 提供,此处返回 UNKNOWN
return UNKNOWN_CAPABILITY;
}
}
对于 'kimi' wire,能力信息来自用户配置的 catalog 而非模型名称匹配——因为 Kimi 平台的模型列表是通过 API 动态获取的,Kosong 不硬编码其能力。
6.3 UNKNOWN_CAPABILITY 的设计
当查询一个未编目的模型时,返回 UNKNOWN_CAPABILITY——一个所有字段都为 false/0 且被 Object.freeze() 的只读常量。这不是一个错误状态,而是"我不知道"的诚实表态:上层可以选择忽略(不阻塞请求)或保守处理。
6.4 Catalog 驱动的能力推断
在实际使用中,模型能力通过 models.dev 格式的 catalog 文件驱动。以 Anthropic 模型为例,catalogModelToCapability() 从 catalog entry 推断:
model.limit.context→max_context_tokensmodel.modalities.input包含"image"→image_in: truemodel.reasoning_options的 effort 列表 →thinking: truemodel.tool_call→tool_use(默认为 true)
7. 不可变配置模式
Kosong 的 ChatProvider 采用不可变配置模式——所有 withXxx 方法返回新实例,原实例保持不变。
7.1 为什么用不可变模式?
在 Agent 引擎中,同一个 Provider 配置可能被多个并发请求共享。如果直接修改实例的属性,一个请求的 thinking 设置可能意外影响另一个请求。不可变克隆确保了配置隔离。
// 基础配置:所有请求共享
const baseProvider = createProvider({
type: 'anthropic',
model: 'claude-sonnet-4-5',
apiKey: process.env.ANTHROPIC_API_KEY,
});
// 请求 1:需要思考模式
const thinkingProvider = baseProvider
.withThinking('high')
.withMaxCompletionTokens(8000);
// 请求 2:快速响应,不需要思考
const quickProvider = baseProvider
.withThinking('off')
.withMaxCompletionTokens(1024);
// baseProvider 不变,两个变体独立
7.2 配置的 compose 和 override
每次 withXxx 调用只是浅拷贝配置对象并合并新值:
- withThinking 覆盖
thinkingEffort,保留其他所有字段 - withMaxCompletionTokens 覆盖 max_tokens 相关配置,并可选地根据上下文窗口收束
- 多个
withXxx可以链式调用,右侧覆盖左侧
7.3 共享 Transport 的契约
一个容易被忽视的细节:克隆后的 Provider 实例必须共享底层的 HTTP 客户端和连接池。如果每次 withXxx 都创建新的 SDK 客户端,会耗尽 socket 连接、丢失连接复用,并导致性能大幅下降。
Kosong 的源码注释明确声明了这一契约(见 KimiChatProvider._clone()):
Implementations MUST NOT mutate or replace internal HTTP clients on the returned clone — the clone is expected to share transport state with the original.
7.4 ModelRequester 中的缓存
在 agent-core-v2 中,ModelRequesterImpl 为每个 Model 缓存一个 ChatProvider 实例:
private cachedChatProvider: ChatProvider | undefined;
private resolveChatProvider(): ChatProvider {
if (this.cachedChatProvider !== undefined)
return this.cachedChatProvider;
this.cachedChatProvider =
this.protocolRegistry.createChatProvider({ ... });
return this.cachedChatProvider;
}
每次请求时,通过 ModelRequestParams 携带 per-turn 配置(thinking effort、max tokens、cache key 等),而不是每次都创建新的 Provider。这既保证了配置隔离,又避免了重复创建 transport 的开销。
总结
Kosong 是 kimi-code 中大语言模型交互的统一入口。它通过 ChatProvider 接口 + 多供应商适配器 + generate() 协调器 三层架构,将 Agent 引擎与具体 LLM SDK 解耦:
- ChatProvider 定义了清晰的契约——供应商只需实现约 7 个方法就能接入
- 5 种供应商适配器 各自处理协议差异(Kimi 的 schema 规范化、Anthropic 的 tool_use 配对、OpenAI 的 content 格式转换)
- generate() 管理全生命周期——从 abort 检查、流式消费、并行 tool call 路由到结果验证
- 错误分类系统 精确区分可重试/不可重试错误,驱动上层的重试和恢复策略
- ModelCapability 让上游在请求前就能判断模型能力,避免无效请求
- 不可变配置 保证了并发安全,同时通过共享 transport 避免了重复连接的开销
理解 Kosong 的设计对于扩展新的 LLM 供应商至关重要——你需要做的只是实现 ChatProvider 接口,然后通过 createProvider() 注册即可。整个 Agent 引擎无需任何修改就能使用新模型。
更多推荐



所有评论(0)