课程目标
精读 @langchain/anthropic 的 ChatAnthropic 实现,与第 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 有两条路径:
- 非流式路径:
_generateNonStreaming()直接调用messages.create() - 流式路径:调用
_streamResponseChunks()然后合并所有 chunk
这种设计让 streaming: true 的实例即使通过 invoke() 调用也走流式路径,可以获得更早的超时检测。
19.6 差异四:流式响应处理
OpenAI
- 使用
Chat CompletionsAPI 的 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 特有功能
| 功能 | Anthropic | OpenAI |
|---|---|---|
| Thinking(推理过程) | thinking 参数,显式控制 | reasoning 参数 |
| Prompt Caching | 顶层 cache_control 参数 | promptCacheKey |
| Context Management | context_management.edits 支持自动压缩 | 无 |
| MCP Servers | mcp_servers 参数,原生 MCP 支持 | 无 |
| Content Blocks | 支持 text/image/tool_use/thinking 等多种 block | 支持 text/image |
| Inference Geo | inferenceGeo 参数控制推理地域 | 无 |
OpenAI 特有功能
| 功能 | OpenAI | Anthropic |
|---|---|---|
| Responses API | 新一代 API,支持 built-in tools | 无 |
| Predicted Output | prediction 参数优化延迟 | 无 |
| Audio | modalities: ["audio"] | 无 |
| Logprobs | logprobs / topLogprobs | 无 |
| Service Tier | service_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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | langchain-anthropic/src/chat_models.ts:1418-1458 | _generate() 双路径设计 |
| P0 | langchain-anthropic/src/chat_models.ts:1267-1330 | _streamResponseChunks() 流式事件处理 |
| P1 | langchain-anthropic/src/chat_models.ts:1146-1154 | bindTools() 实现 |
| P1 | langchain-anthropic/src/utils/message_inputs.ts | _convertMessagesToAnthropicPayload()、system 消息拆分 |
| P1 | langchain-anthropic/src/utils/message_outputs.ts | anthropicResponseToChatMessages()、响应映射 |
| P2 | langchain-anthropic/src/chat_models.ts:1159-1247 | invocationParams()、参数构造 |
| P2 | langchain-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 接入的最小要求和标准测试验证流程。