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 承担了三个核心职责:

  1. 类型归一化​:将每个供应商特有的请求/响应类型转换为一套统一的 MessageContentPartToolCall 抽象
  2. 流式编排​:管理 SSE / NDJSON 流的逐 token 消费,拼接完整响应
  3. 错误映射​:将各 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-genaivertexai 两种配置。特点:

  • 函数声明格式​:使用 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 → 拼接 text
  • ThinkPart + ThinkPart → 拼接 think
  • ToolCall + 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.contextmax_context_tokens
  • model.modalities.input 包含 "image"image_in: true
  • model.reasoning_options 的 effort 列表 → thinking: true
  • model.tool_calltool_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 引擎无需任何修改就能使用新模型。

更多推荐