第 19 课: Provider 对比 -- Anthropic 实现与差异分析

4 阅读6分钟

课程目标

精读 @langchain/anthropicChatAnthropic 实现,与第 18 课的 ChatOpenAI 逐方法对比,理解统一抽象层如何屏蔽底层 API 差异。


19.1 统一抽象的价值

对于上层应用代码,切换 Provider 只需一行:

// 从 OpenAI 切换到 Anthropic
// import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";

const model = new ChatAnthropic({ model: "claude-sonnet-4-6" });
// 后续的 invoke / stream / bindTools 完全一致

但在 Provider 内部,两者的实现差异非常大。本课逐点分析这些差异。


19.2 类继承结构对比

// OpenAI
BaseChatModel  BaseChatOpenAI<CallOptions>  ChatOpenAI

// Anthropic
BaseChatModel  ChatAnthropicMessages<CallOptions>  ChatAnthropic

源码位置

  • OpenAI: libs/providers/langchain-openai/src/chat_models/base.ts
  • Anthropic: libs/providers/langchain-anthropic/src/chat_models.ts

19.3 差异一:System 消息处理

OpenAI

System 消息作为普通消息发送,role 为 "system"

{ role: "system", content: "你是一个有用的助手" }

Anthropic

Anthropic API 将 system 消息作为顶层参数,而非消息数组的一部分:

// Anthropic API 格式
{
  system: "你是一个有用的助手",   // 顶层参数
  messages: [                     // 只包含 user/assistant 消息
    { role: "user", content: "你好" }
  ]
}

源码位置: libs/providers/langchain-anthropic/src/utils/message_inputs.ts

_convertMessagesToAnthropicPayload() 函数负责将 LangChain 消息数组拆分为 system 参数和 messages 数组。


19.4 差异二:工具调用格式

OpenAI: function calling

// OpenAI 工具定义
{
  type: "function",
  function: {
    name: "get_weather",
    description: "获取天气",
    parameters: { type: "object", properties: { city: { type: "string" } } }
  }
}

// OpenAI 工具调用响应
{
  role: "assistant",
  tool_calls: [{
    id: "call_abc123",
    type: "function",
    function: { name: "get_weather", arguments: '{"city":"北京"}' }
  }]
}

Anthropic: tool_use content block

// Anthropic 工具定义
{
  name: "get_weather",
  description: "获取天气",
  input_schema: { type: "object", properties: { city: { type: "string" } } }
}

// Anthropic 工具调用响应 -- 是 content block 的一部分
{
  role: "assistant",
  content: [
    { type: "text", text: "让我查一下天气" },
    { type: "tool_use", id: "tu_abc123", name: "get_weather", input: { city: "北京" } }
  ]
}

关键差异

  • OpenAI 的 tool_calls 是消息的独立字段,arguments 是 JSON 字符串
  • Anthropic 的 tool_use 是 content block,input 是已解析的对象
  • LangChain 将两者统一映射为 AIMessage.tool_calls

19.4.1 bindTools 对比

// OpenAI: 转换为 function calling 格式
override bindTools(tools, kwargs) {
  return this.withConfig({
    tools: tools.map(tool => this._convertChatOpenAIToolToCompletionsTool(tool, { strict })),
    ...kwargs,
  });
}

// Anthropic: 转换为 Anthropic 工具格式
override bindTools(tools, kwargs) {
  return this.withConfig({
    tools: this.formatStructuredToolToAnthropic(tools),
    ...kwargs,
  });
}

两者都使用 withConfig 返回新的 RunnableBinding,保持不可变性。但格式转换逻辑完全不同。


19.5 差异三:_generate 实现策略

OpenAI

OpenAI 的 _generate 直接调用 client.chat.completions.create(),同步等待完整响应。

Anthropic

源码位置: libs/providers/langchain-anthropic/src/chat_models.ts:1418

async _generate(
  messages: BaseMessage[],
  options: this["ParsedCallOptions"],
  runManager?: CallbackManagerForLLMRun
): Promise<ChatResult> {
  options.signal?.throwIfAborted();

  const params = this.invocationParams(options);
  if (params.stream) {
    // 即使是非流式调用,如果 streaming=true,也走流式路径再合并
    let finalChunk: ChatGenerationChunk | undefined;
    const stream = this._streamResponseChunks(messages, options, runManager);
    for await (const chunk of stream) {
      if (finalChunk === undefined) {
        finalChunk = chunk;
      } else {
        finalChunk = finalChunk.concat(chunk);
      }
    }
    return { generations: [{ text: finalChunk.text, message: finalChunk.message }] };
  } else {
    return this._generateNonStreaming(messages, params, { signal: options.signal });
  }
}

设计差异:Anthropic 的 _generate 有两条路径:

  1. 非流式路径_generateNonStreaming() 直接调用 messages.create()
  2. 流式路径:调用 _streamResponseChunks() 然后合并所有 chunk

这种设计让 streaming: true 的实例即使通过 invoke() 调用也走流式路径,可以获得更早的超时检测。


19.6 差异四:流式响应处理

OpenAI

  • 使用 Chat Completions API 的 SSE 流
  • 每个 chunk 有 choices[0].delta 结构
  • 增量内容在 delta.content

Anthropic

源码位置: libs/providers/langchain-anthropic/src/chat_models.ts:1267

async *_streamResponseChunks(
  messages: BaseMessage[],
  options: this["ParsedCallOptions"],
  runManager?: CallbackManagerForLLMRun
): AsyncGenerator<ChatGenerationChunk> {
  const params = this.invocationParams(options);
  const formattedMessages = _convertMessagesToAnthropicPayload(messages);
  const stream = await this.createStreamWithRetry({
    ...params, ...formattedMessages, stream: true,
  });
  // 遍历 Anthropic SSE 事件
  for await (const event of stream) {
    // 将 Anthropic 事件转换为 ChatGenerationChunk
  }
}

Anthropic 的流式事件类型更丰富:

Anthropic 事件含义
message_start消息开始,包含元信息
content_block_start内容块开始(text/tool_use)
content_block_delta内容块增量
content_block_stop内容块结束
message_delta消息级增量(stop_reason、usage)
message_stop消息结束

对比 OpenAI 只有简单的 delta 结构,Anthropic 的事件模型更结构化但也更复杂。


19.7 差异五:maxTokens 处理

OpenAI

max_tokens 是可选参数,不传则由模型决定。

Anthropic

max_tokens必填参数ChatAnthropic 通过模型名前缀匹配来设置默认值:

const MODEL_DEFAULT_MAX_OUTPUT_TOKENS: Partial<Record<Anthropic.Model, number>> = {
  "claude-opus-4-6": 16384,
  "claude-sonnet-4-6": 16384,
  "claude-3-5-sonnet": 8192,
  "claude-3-opus": 4096,
  // ...
};

function defaultMaxOutputTokensForModel(model?: Anthropic.Model): number {
  const maxTokens = Object.entries(MODEL_DEFAULT_MAX_OUTPUT_TOKENS)
    .find(([key]) => model.startsWith(key))?.[1];
  return maxTokens ?? 4096;  // fallback
}

19.8 差异六:高级功能

Anthropic 特有功能

功能AnthropicOpenAI
Thinking(推理过程)thinking 参数,显式控制reasoning 参数
Prompt Caching顶层 cache_control 参数promptCacheKey
Context Managementcontext_management.edits 支持自动压缩
MCP Serversmcp_servers 参数,原生 MCP 支持
Content Blocks支持 text/image/tool_use/thinking 等多种 block支持 text/image
Inference GeoinferenceGeo 参数控制推理地域

OpenAI 特有功能

功能OpenAIAnthropic
Responses API新一代 API,支持 built-in tools
Predicted Outputprediction 参数优化延迟
Audiomodalities: ["audio"]
Logprobslogprobs / topLogprobs
Service Tierservice_tier 优先级控制

19.9 统一抽象的边界

尽管 LangChain 的抽象层屏蔽了大部分差异,有些功能无法完全统一:

// Anthropic 特有的调用选项
interface ChatAnthropicCallOptions extends BaseChatModelCallOptions {
  thinking?: AnthropicThinkingConfigParam;  // Anthropic 特有
  mcp_servers?: AnthropicMCPServerURLDefinition[];  // Anthropic 特有
  cache_control?: AnthropicCacheControl;    // Anthropic 特有
}

// OpenAI 特有的调用选项
interface OpenAICallOptions extends BaseChatModelCallOptions {
  parallel_tool_calls?: boolean;  // OpenAI 特有
  prediction?: OpenAIClient.ChatCompletionPredictionContent;  // OpenAI 特有
  modalities?: Array<OpenAIClient.Chat.ChatCompletionModality>;  // OpenAI 特有
}

设计启示:统一抽象覆盖公共能力(消息、工具、流式),Provider 特有功能通过 CallOptions 泛型扩展暴露。


19.10 实战练习

编写一段同时支持 OpenAI 和 Anthropic 的代码:

import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { HumanMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

// 通过配置选择 Provider
function createModel(provider: "openai" | "anthropic"): BaseChatModel {
  switch (provider) {
    case "openai":
      return new ChatOpenAI({ model: "gpt-4o" });
    case "anthropic":
      return new ChatAnthropic({ model: "claude-sonnet-4-6" });
  }
}

// 定义工具 -- 与 Provider 无关
const weatherTool = tool(
  (input) => `${input.city} 的天气是晴天,25度`,
  {
    name: "get_weather",
    description: "获取指定城市的天气",
    schema: z.object({ city: z.string().describe("城市名") }),
  }
);

// 使用 -- 两个 Provider 的调用方式完全一致
const model = createModel("anthropic");
const modelWithTools = model.bindTools([weatherTool]);
const result = await modelWithTools.invoke([
  new HumanMessage("北京今天天气怎么样?"),
]);

19.11 源码精读路线

优先级文件关注点
P0langchain-anthropic/src/chat_models.ts:1418-1458_generate() 双路径设计
P0langchain-anthropic/src/chat_models.ts:1267-1330_streamResponseChunks() 流式事件处理
P1langchain-anthropic/src/chat_models.ts:1146-1154bindTools() 实现
P1langchain-anthropic/src/utils/message_inputs.ts_convertMessagesToAnthropicPayload()、system 消息拆分
P1langchain-anthropic/src/utils/message_outputs.tsanthropicResponseToChatMessages()、响应映射
P2langchain-anthropic/src/chat_models.ts:1159-1247invocationParams()、参数构造
P2langchain-anthropic/src/utils/tools.ts工具格式转换、tool_choice 处理

本课收获总结

级别你应该掌握的
🟢 基础学会切换 Provider:从 OpenAI 切到 Anthropic 只需更换构造函数
🔵 中阶理解消息格式差异:OpenAI 的 system role vs Anthropic 的顶层 system 参数
🟡 高阶对比两个 Provider 的工具调用格式:function calling vs tool_use content block
🟠 资深分析 Anthropic _generate 的双路径设计(流式/非流式)及 maxTokens 必填处理
🔴 架构提炼 Provider 适配器模式:统一抽象覆盖公共能力,特有功能通过泛型 CallOptions 扩展

下一课预告

第 20 课动手实践:从零实现一个自定义 Provider,理解 Provider 接入的最小要求和标准测试验证流程。