课程目标
精读 @langchain/openai 的 ChatOpenAI 实现,理解 Provider 层如何实现 BaseChatModel 的核心抽象:消息格式转换、API 调用、流式响应处理、工具绑定。
18.1 Provider 的角色
在第 11 课中我们学习了 BaseChatModel 的抽象设计:Provider 只需实现 _generate() 方法。现在我们看 OpenAI 如何"填充"这个抽象。
BaseChatModel (抽象)
└── BaseChatOpenAI<CallOptions> (OpenAI 实现基类)
└── ChatOpenAI (对外暴露的具体类)
源码位置: libs/providers/langchain-openai/src/chat_models/base.ts
18.2 类结构与配置
export abstract class BaseChatOpenAI<
CallOptions extends BaseChatOpenAICallOptions,
> extends BaseChatModel<CallOptions, AIMessageChunk>
implements Partial<OpenAIChatInput>
{
temperature?: number;
topP?: number;
frequencyPenalty?: number;
presencePenalty?: number;
model = "gpt-3.5-turbo";
streaming = false;
streamUsage = true;
maxTokens?: number;
apiKey?: OpenAIApiKey;
client: OpenAIClient; // OpenAI SDK 客户端实例
clientConfig: ClientOptions; // 客户端配置
supportsStrictToolCalling?: boolean; // 是否支持 strict 工具调用模式
reasoning?: OpenAIClient.Reasoning; // 推理模型选项
}
关键观察:
BaseChatOpenAI本身是泛型抽象类,CallOptions参数允许子类扩展调用选项- 配置字段直接映射 OpenAI API 参数,降低心智负担
client是原生openaiSDK 的实例,Provider 层是 SDK 的适配器
18.2.1 序列化支持
lc_serializable = true;
get lc_secrets(): { [key: string]: string } | undefined {
return {
apiKey: "OPENAI_API_KEY",
organization: "OPENAI_ORGANIZATION",
};
}
lc_secrets 声明哪些字段是敏感信息,序列化时会用环境变量名替代真实值。
18.3 消息格式转换
Provider 层的核心职责之一是将 LangChain 的统一消息格式转换为 OpenAI API 要求的格式。
18.3.1 LangChain 消息 -> OpenAI 格式
LangChain 的 BaseMessage 需要转换为 OpenAI 的 ChatCompletionMessageParam:
| LangChain 消息类型 | OpenAI role | 特殊处理 |
|---|---|---|
SystemMessage | system | 直接映射 |
HumanMessage | user | 支持多模态 content |
AIMessage | assistant | 携带 tool_calls |
ToolMessage | tool | 需要 tool_call_id |
FunctionMessage | function | 遗留格式 |
18.3.2 角色映射
// libs/providers/langchain-openai/src/utils/misc.ts
export function messageToOpenAIRole(message: BaseMessage): string {
// HumanMessage -> "user"
// AIMessage -> "assistant"
// SystemMessage -> "system"
// ToolMessage -> "tool"
// FunctionMessage -> "function"
}
18.3.3 多模态内容
当 HumanMessage.content 是数组时(包含文本和图片),会被转换为 OpenAI 的 content parts 格式:
// LangChain 统一格式
new HumanMessage({
content: [
{ type: "text", text: "这张图片是什么?" },
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
],
});
// 转换后的 OpenAI 格式
{
role: "user",
content: [
{ type: "text", text: "这张图片是什么?" },
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
],
}
18.4 bindTools -- 工具绑定
源码位置: libs/providers/langchain-openai/src/chat_models/base.ts:662
override bindTools(
tools: ChatOpenAIToolType[],
kwargs?: Partial<CallOptions>
): Runnable<BaseLanguageModelInput, AIMessageChunk, CallOptions> {
let strict: boolean | undefined;
if (kwargs?.strict !== undefined) {
strict = kwargs.strict;
} else if (this.supportsStrictToolCalling !== undefined) {
strict = this.supportsStrictToolCalling;
}
return this.withConfig({
tools: tools.map((tool) => {
if (isBuiltInTool(tool) || isCustomTool(tool)) {
return tool; // 内置工具直接透传
}
if (hasProviderToolDefinition(tool)) {
return tool.extras.providerToolDefinition; // Provider 专属定义
}
return this._convertChatOpenAIToolToCompletionsTool(tool, { strict });
}),
...kwargs,
} as Partial<CallOptions>);
}
设计要点:
bindTools返回一个新的RunnableBinding,不修改原始模型实例(不可变性)- 工具转换分三种路径:内置工具、Provider 专属工具、标准 LangChain 工具
strict模式确保模型输出严格匹配 JSON Schema
18.4.1 工具格式转换
LangChain 的 StructuredTool(含 Zod schema)会被转换为 OpenAI 的 function calling 格式:
// LangChain StructuredTool
{
name: "get_weather",
description: "获取天气",
schema: z.object({ city: z.string() })
}
// 转换后的 OpenAI 格式
{
type: "function",
function: {
name: "get_weather",
description: "获取天气",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}
}
18.5 _generate -- 核心生成方法
BaseChatOpenAI 的 _generate 在继承链中由 BaseChatModel 的模板方法驱动。实际的 API 调用通过 OpenAI SDK 的 client.chat.completions.create() 完成。
18.5.1 调用流程
invoke(messages)
→ BaseChatModel._generate(messages, options)
→ 构造 OpenAI API 请求参数
→ client.chat.completions.create(params)
→ 将 OpenAI 响应转换为 ChatResult
18.5.2 响应映射
OpenAI API 返回的 ChatCompletion 被映射为 LangChain 的 ChatResult:
// OpenAI 响应
{
choices: [{
message: {
role: "assistant",
content: "Hello!",
tool_calls: [{ id: "tc_1", function: { name: "get_weather", arguments: '{"city":"北京"}' } }]
}
}],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
}
// 映射为 LangChain ChatResult
{
generations: [{
message: AIMessage {
content: "Hello!",
tool_calls: [{ name: "get_weather", args: { city: "北京" }, id: "tc_1" }]
},
text: "Hello!"
}],
llmOutput: { tokenUsage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 } }
}
18.6 _streamResponseChunks -- 流式实现
流式调用使用 OpenAI 的 Server-Sent Events (SSE) 协议:
18.6.1 流式工作原理
client.chat.completions.create({ stream: true })
→ 返回 AsyncIterable<ChatCompletionChunk>
→ 每个 chunk 转换为 ChatGenerationChunk
→ yield 给上层
18.6.2 关键处理
- 首个 chunk:可能包含 role 信息但不含 content
- tool_calls 流式:函数名和参数分多个 chunk 逐步到达,需要累积拼接
- usage 信息:当
streamUsage: true时,最后一个 chunk 携带 token 统计
// 流式 chunk 示例
{ choices: [{ delta: { role: "assistant" } }] }
{ choices: [{ delta: { content: "Hello" } }] }
{ choices: [{ delta: { content: " World" } }] }
{ choices: [{ delta: {} }], usage: { prompt_tokens: 5, completion_tokens: 2 } }
每个 delta 被包装为 AIMessageChunk,由 BaseChatModel._generateUncached() 自动合并。
18.7 Token 统计
_combineLLMOutput(...llmOutputs: OpenAILLMOutput[]): OpenAILLMOutput {
return llmOutputs.reduce(
(acc, llmOutput) => {
if (llmOutput?.tokenUsage) {
acc.tokenUsage.completionTokens += llmOutput.tokenUsage.completionTokens ?? 0;
acc.tokenUsage.promptTokens += llmOutput.tokenUsage.promptTokens ?? 0;
acc.tokenUsage.totalTokens += llmOutput.tokenUsage.totalTokens ?? 0;
}
return acc;
},
{ tokenUsage: { completionTokens: 0, promptTokens: 0, totalTokens: 0 } }
);
}
_combineLLMOutput 在 batch 调用时合并多次调用的 token 统计。
18.8 推理模型支持
OpenAI 的推理模型(如 o1、o3 系列)有特殊要求:
// CallOptions 中的推理选项
reasoning?: OpenAIClient.Reasoning; // { effort: "low" | "medium" | "high" }
// 推理模型检测
import { isReasoningModel } from "../utils/misc.js";
推理模型可能不支持 temperature、top_p 等采样参数,Provider 层会自动处理这些兼容性问题。
18.9 错误处理
import { wrapOpenAIClientError } from "../utils/client.js";
Provider 层会捕获 OpenAI SDK 抛出的底层错误,包装为更有意义的 LangChain 错误(如 ContextOverflowError、认证错误等),让上层代码能够统一处理。
18.10 实战练习
练习 1:阅读 _generate 源码
打开 libs/providers/langchain-openai/src/chat_models/base.ts,找到 _generate() 方法,跟踪以下流程:
- BaseMessage 数组如何转为 OpenAI API 的
messages参数 - 工具定义如何转为
tools参数 - API 返回值如何映射为
ChatResult
练习 2:对比有无工具的调用
import { FakeChatModel } from "@langchain/core/utils/testing";
import { tool } from "@langchain/core/tools";
import { z } from "zod/v3";
const weatherTool = tool(
async ({ city }) => `${city}: 25°C`,
{ name: "weather", description: "Get weather", schema: z.object({ city: z.string() }) }
);
const model = new FakeChatModel({});
// 无工具调用
const result1 = await model.invoke("Hello");
console.log(result1.tool_calls); // []
// 有工具绑定(观察 bindTools 返回的类型)
const modelWithTools = model.bindTools([weatherTool]);
console.log(modelWithTools.constructor.name); // RunnableBinding
练习 3:流式输出观察
const stream = await model.stream("Tell me a story");
for await (const chunk of stream) {
console.log("chunk type:", chunk.constructor.name);
console.log("chunk content:", chunk.content);
}
18.11 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | langchain-openai/src/chat_models/base.ts:244-400 | BaseChatOpenAI 类定义、配置字段 |
| P0 | langchain-openai/src/chat_models/base.ts:662-694 | bindTools() 实现 |
| P1 | langchain-openai/src/utils/tools.ts | _convertToOpenAITool()、工具格式转换 |
| P1 | langchain-openai/src/utils/misc.ts | messageToOpenAIRole()、推理模型检测 |
| P2 | langchain-openai/src/utils/output.ts | 结构化输出、withStructuredOutput() |
| P2 | langchain-openai/src/chat_models/profiles.ts | 模型 Profile(能力声明) |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解 Provider 包的结构:继承 BaseChatModel,实现 _generate() |
| 🔵 中阶 | 掌握消息格式转换的完整流程:LangChain Message -> OpenAI API 格式 -> ChatResult |
| 🟡 高阶 | 理解流式实现:SSE chunk 的逐步到达和 AIMessageChunk 的累积合并 |
| 🟠 资深 | 分析 bindTools() 的不可变设计;理解工具格式转换的三种路径 |
| 🔴 架构 | 评估 Provider 层的职责边界:纯适配 vs 增强(token 统计、推理模型兼容) |
下一课预告
第 19 课对比 Anthropic Provider 的实现,看同一抽象在不同 API 设计下的差异化处理。