Skip to content

第4章 Provider 抽象层

"模型供应商的 API 是最不可靠的依赖之一——它的变更不遵循语义版本,它的故障不提前通知,它的限速策略随时可能收紧。你的架构必须假设任何模型都可能在下一秒消失。"

本章要点

  • 理解多模型适配架构:如何用统一接口屏蔽 OpenAI、Anthropic、Google 等供应商差异
  • 掌握模型路由与降级策略:Auth Profile 轮转、冷却期、自动故障切换
  • 深入 Token 计量与成本控制:预算感知的上下文管理
  • 解析流式响应处理的工程挑战与解决方案

上一章,我们剖析了 Gateway 如何管理系统的生命周期与配置。Gateway 是控制中心,但它本身不做 AI 推理——推理的重担,交给了 Provider 抽象层。

2025 年 3 月 14 日,Anthropic 的 API 在亚太区域出现了长达 47 分钟的服务降级。如果你的 Agent 系统只接入了 Claude,那 47 分钟里,你的所有用户——无论身在 Telegram、Discord 还是 WhatsApp——都在面对一个沉默的 Agent。47 分钟的沉默,在用户体验上等于 47 年。

但如果你运行的是 OpenClaw,你的用户甚至不会察觉这次故障。

OpenClaw 的 Provider 抽象层在检测到 Claude 返回 429 的瞬间,便以毫秒级速度切换到 GPT-4 作为降级模型,同时完整保留对话上下文。当 Claude 恢复后,系统在冷却期结束后自动切回主模型——整个过程,对用户完全透明

这不是魔法,这是架构设计的力量。

🔥 深度洞察:供应商关系的本质

Provider 抽象层的设计哲学,与国际贸易中的供应链多元化策略如出一辙。一家成熟的制造企业不会把全部原材料押注在单一供应商身上——不是因为那个供应商不好,而是因为任何单一依赖都是系统性风险。芯片短缺时,同时与台积电、三星和英特尔合作的公司活了下来;只依赖单一代工厂的公司停产了。AI 模型市场正在经历同样的演变:今天最好的模型可能是 Claude,明天可能是 GPT-6,后天可能是一个开源模型。Provider 抽象层保证的不是"永远选到最好的模型",而是"无论最好的模型是谁,你都能在秒级切换过去"。这才是真正的战略优势。

要实现这种丝滑的模型切换,系统必须攻克三道难关:如何抹平不同供应商的 API 差异?如何在模型之间智能路由和降级?如何统一计量来自不同供应商的 Token 消耗?三个问题,环环相扣,层层递进。本章将逐一剖析 OpenClaw 给出的答案。

4.1 多模型适配架构

下图展示了 Provider 抽象层的核心类型层次结构,所有类名和字段均来自源码实现:

图 4-1:Provider 核心类型层次图

图 4-1:Provider 核心类型层次图

该图基于 src/agents/model-selection.tssrc/agents/model-catalog.tssrc/agents/provider-capabilities.tssrc/agents/model-fallback.types.tssrc/agents/usage.tssrc/agents/context-window-guard.ts 中的类型定义绘制。所有字段名与源码一致。

4.1.1 Provider 标识与规范化

OpenClaw 将每个模型供应商抽象为一个 Provider。Provider 的核心标识是一个字符串 ID,经过规范化处理后存储和匹配。

规范化逻辑位于 src/agents/provider-id.ts

typescript
export function normalizeProviderId(provider: string): string {
  const normalized = provider.trim().toLowerCase();
  if (normalized === "z.ai" || normalized === "z-ai") {
    return "zai";
  }
  if (normalized === "opencode-zen") {
    return "opencode";
  }
  if (normalized === "bedrock" || normalized === "aws-bedrock") {
    return "amazon-bedrock";
  }
  if (normalized === "bytedance" || normalized === "doubao") {
    return "volcengine";
  }
  return normalized;
}

这个函数的设计思想值得注意:它通过一系列 if 分支将历史遗留名称、厂商别名统一映射到规范形式。例如,"bytedance""doubao" 统一映射为 "volcengine"(火山引擎),"bedrock""aws-bedrock" 统一为 "amazon-bedrock"

这种规范化机制允许用户在配置中使用各种惯用名称,而系统内部始终使用统一标识。

关键概念:Auth Profile(认证配置) Auth Profile 是 OpenClaw 管理 API 密钥的核心抽象。每个 Auth Profile 包含一个或多个 API Key,支持轮转(同一供应商多个 Key 交替使用)和冷却期(Key 被限速后暂时停用)。Provider 层通过 Auth Profile 而非直接持有密钥来访问 LLM API,实现密钥管理与模型调用的解耦。

4.1.2 模型引用与选择

模型的引用采用 provider/model 的复合键形式。核心类型定义在 src/agents/model-selection.ts

typescript
export type ModelRef = {
  provider: string;
  model: string;
};

export type ThinkLevel = "off" | "minimal" | "low" | "medium"
  | "high" | "xhigh" | "adaptive";

模型键的构建逻辑简洁而巧妙:

typescript
export function modelKey(provider: string, model: string) {
  const providerId = provider.trim();
  const modelId = model.trim();
  if (!providerId) return modelId;
  if (!modelId) return providerId;
  return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)
    ? modelId
    : `${providerId}/${modelId}`;
}

如果模型 ID 本身已经包含了 Provider 前缀(例如 "anthropic/claude-opus-4-6"),函数会避免重复拼接。这种防御性设计在多层调用栈中非常重要。

4.1.3 默认模型与上下文窗口

系统的默认配置定义在 src/agents/defaults.ts

typescript
export const DEFAULT_PROVIDER = "anthropic";
export const DEFAULT_MODEL = "claude-opus-4-6";
export const DEFAULT_CONTEXT_TOKENS = 200_000;

这里透露了 OpenClaw 的设计偏好:以 Anthropic 的 Claude 系列作为默认模型,200K 的上下文窗口作为保守回退值。

4.1.4 Provider 能力声明

不同 Provider 对工具调用、消息格式等有不同的支持方式。src/agents/provider-capabilities.ts 定义了能力声明机制:

typescript
export type ProviderCapabilities = {
  anthropicToolSchemaMode: "native" | "openai-functions";
  anthropicToolChoiceMode: "native" | "openai-string-modes";
  providerFamily: "default" | "openai" | "anthropic";
  preserveAnthropicThinkingSignatures: boolean;
  openAiCompatTurnValidation: boolean;
  geminiThoughtSignatureSanitization: boolean;
  transcriptToolCallIdMode: "default" | "strict9";
  // ... 更多能力声明
};

能力解析采用三层叠加策略:

typescript
export function resolveProviderCapabilities(
  provider?: string | null,
  options?: ProviderCapabilityLookupOptions,
): ProviderCapabilities {
  const normalized = normalizeProviderId(provider ?? "");
  const pluginCapabilities = normalized
    ? resolveProviderCapabilitiesWithPlugin({ ... })
    : undefined;
  return {
    ...DEFAULT_PROVIDER_CAPABILITIES,     // 第1层:全局默认值
    ...CORE_PROVIDER_CAPABILITIES[normalized],  // 第2层:内核预设
    ...(pluginCapabilities ?? PLUGIN_CAPABILITIES_FALLBACKS[normalized]),  // 第3层:插件声明
  };
}

这种叠加模式遵循"约定优于配置"原则:大多数 Provider 使用默认值即可工作;核心 Provider(如 anthropic-vertexamazon-bedrock)有内核级预设;第三方 Provider 通过插件注入能力声明。

下图展示了 Provider 的注册与发现机制——从配置文件到运行时可用模型列表的完整流转:

图 4-2:Provider 注册与模型发现机制

图 4-2:Provider 注册与模型发现机制

此图基于 src/agents/models-config.tsensureOpenClawModelsJson()src/agents/model-catalog.tsloadModelCatalog()src/agents/provider-capabilities.tsresolveProviderCapabilities() 绘制。writeModelsFileAtomic 的原子写入和 withModelsJsonWriteLock 的写锁机制保证了并发安全。

4.2 模型路由与降级策略

4.2.1 模型目录

模型目录(Model Catalog)是 Provider 抽象层的动态注册表。src/agents/model-catalog.ts 定义了目录的核心数据结构:

typescript
export type ModelCatalogEntry = {
  id: string;
  name: string;
  provider: string;
  contextWindow?: number;
  reasoning?: boolean;
  input?: ModelInputType[];
};

目录的构建是一个懒加载过程:

typescript
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;

export async function loadModelCatalog(params?: {
  config?: OpenClawConfig;
  useCache?: boolean;
}): Promise<ModelCatalogEntry[]> {
  if (params?.useCache === false) {
    modelCatalogPromise = null;
  }
  // ... 从 Pi SDK 和配置中发现模型
}

通过模块级的 Promise 变量实现单例式缓存——首次调用触发发现流程,后续调用复用结果。useCache: false 可强制刷新。

4.2.2 模型配置生成

src/agents/models-config.ts 负责将用户配置转化为运行时模型文件。核心流程:

  1. 读取 OpenClaw 配置(openclaw.yaml / 环境变量)
  2. 发现可用的 Provider 及其模型
  3. 生成 models.json 到 Agent 目录
typescript
export async function ensureOpenClawModelsJson(
  config?: OpenClawConfig,
  agentDirOverride?: string,
): Promise<{ agentDir: string; wrote: boolean }> {
  const resolved = resolveModelsConfigInput(config);
  const agentDir = agentDirOverride ?? resolveOpenClawAgentDir();
  const targetPath = path.join(agentDir, "models.json");

  return await withModelsJsonWriteLock(targetPath, async () => {
    const plan = await planOpenClawModelsJson({ ... });
    if (plan.action === "skip" || plan.action === "noop") {
      return { agentDir, wrote: false };
    }
    await writeModelsFileAtomic(targetPath, plan.contents);
    return { agentDir, wrote: true };
  });
}

基于 VitePress 构建