课程目标
理解 BaseChatModel 的核心设计:模板方法模式如何让 Provider 只需实现 _generate() 就能接入框架。
11.1 继承体系
Runnable<BaseLanguageModelInput, AIMessage>
└── BaseLanguageModel
└── BaseChatModel<CallOptions, OutputMessageType>
├── ChatOpenAI (Provider 实现)
├── ChatAnthropic (Provider 实现)
├── ChatGoogleGenerativeAI (Provider 实现)
├── FakeChatModel (测试用)
└── ...
源码位置: libs/langchain-core/src/language_models/chat_models.ts
11.2 核心设计:模板方法模式
BaseChatModel 的核心设计思想是模板方法模式:
┌─── BaseChatModel(框架控制) ─────────────────────┐
│ │
│ invoke(input) │
│ ├── 1. 转换输入为 PromptValue │
│ ├── 2. 启动 callbacks (handleLLMStart) │
│ ├── 3. 检查缓存 │
│ ├── 4. 调用 _generate() ◄── Provider 实现 │
│ ├── 5. 触发 callbacks (handleLLMEnd) │
│ └── 6. 返回 AIMessage │
│ │
│ stream(input) │
│ ├── 1. 转换输入 │
│ ├── 2. 启动 callbacks │
│ ├── 3. 调用 _streamResponseChunks() ◄── Provider │
│ ├── 4. 逐 chunk 触发 handleLLMNewToken │
│ └── 5. yield AIMessageChunk │
│ │
└────────────────────────────────────────────────────┘
Provider 只需实现标记了 ◄── 的方法。
11.3 Provider 必须实现的方法
11.3.1 _generate() — 核心方法
abstract _generate(
messages: BaseMessage[],
options: this["ParsedCallOptions"],
runManager?: CallbackManagerForLLMRun
): Promise<ChatResult>;
职责:接收标准化的 BaseMessage 数组,调用 Provider API,返回 ChatResult。
interface ChatResult {
generations: ChatGeneration[]; // 生成结果(可能多个,如 n > 1)
llmOutput?: Record<string, any>; // Provider 特定的元数据
}
interface ChatGeneration {
text: string; // 文本内容
message: BaseMessage; // 完整的消息对象(通常是 AIMessage)
generationInfo?: Record<string, any>;
}
11.3.2 _streamResponseChunks() — 可选的流式方法
async *_streamResponseChunks(
messages: BaseMessage[],
options: this["ParsedCallOptions"],
runManager?: CallbackManagerForLLMRun
): AsyncGenerator<ChatGenerationChunk> {
// 默认:抛出 "Not implemented"
throw new Error("Not implemented.");
}
如果 Provider 实现了这个方法,stream() 就会使用它实现真正的逐 token 流式。否则 stream() 回退到调用 invoke() 并一次性 yield 整个结果。
11.4 invoke 的完整流程
源码位置: chat_models.ts:283
async invoke(input, options) {
// 1. 将各种输入形式统一为 PromptValue
const promptValue = BaseChatModel._convertInputToPromptValue(input);
// 2. 调用 generatePrompt(内部处理 callbacks、缓存等)
const result = await this.generatePrompt([promptValue], options, options?.callbacks);
// 3. 提取 AIMessage
const chatGeneration = result.generations[0][0];
return chatGeneration.message;
}
_convertInputToPromptValue 支持多种输入形式:
// 以下都是合法的输入
await model.invoke("Hello"); // 字符串 → HumanMessage
await model.invoke([new HumanMessage("Hello")]); // 消息数组
await model.invoke([["human", "Hello"]]); // 元组数组
await model.invoke(chatPromptValue); // PromptValue 对象
11.5 bindTools — 绑定工具
bindTools?(
tools: BindToolsInput[],
kwargs?: Partial<CallOptions>
): Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>;
bindTools() 不是抽象方法——它是可选的。Provider 选择性实现。
使用方式:
const tools = [weatherTool, calculatorTool];
const modelWithTools = model.bindTools(tools);
// modelWithTools 是一个新的 Runnable
// 调用时,模型可以决定调用工具
const result = await modelWithTools.invoke("What's the weather?");
// result.tool_calls: [{ name: "get_weather", args: {...} }]
内部机制:bindTools() 通常返回一个 RunnableBinding,将工具定义绑定到 CallOptions 中。Provider 在 _generate() 中读取这些工具定义,转换为 Provider 特定的格式传给 API。
11.6 withStructuredOutput — 结构化输出
const structuredModel = model.withStructuredOutput(
z.object({
answer: z.string(),
confidence: z.number(),
sources: z.array(z.string()),
})
);
const result = await structuredModel.invoke("What is TypeScript?");
// result: { answer: "TypeScript is...", confidence: 0.95, sources: ["..."] }
内部机制(chat_models.ts:1124):
withStructuredOutput(schema, config) {
// 1. 将 schema 转为 tool 定义
const tools = [convertSchemaToTool(schema)];
// 2. 用 bindTools 绑定
const llm = this.bindTools(tools);
// 3. 添加输出解析器(从 tool_calls 提取结构化数据)
return llm.pipe(outputParser);
}
本质上是 bindTools + OutputParser 的语法糖。
11.7 Token 管理
// BaseLanguageModel 提供的能力
abstract class BaseLanguageModel {
async getNumTokens(text: string): Promise<number>;
getModelContextSize?(modelName: string): number;
}
Provider 可以实现精确的 token 计算(如使用 tiktoken),也可以使用默认的近似计算。
11.8 FakeChatModel — 测试利器
import { FakeChatModel, FakeListChatModel } from "@langchain/core/utils/testing";
// FakeChatModel:回显输入
const fake = new FakeChatModel({});
const result = await fake.invoke("Hello");
// result.content = "Hello"(回显最后一条 HumanMessage)
// FakeListChatModel:预设响应列表
const fakeList = new FakeListChatModel({
responses: ["Response 1", "Response 2", "Response 3"],
});
await fakeList.invoke("Q1"); // "Response 1"
await fakeList.invoke("Q2"); // "Response 2"
await fakeList.invoke("Q3"); // "Response 3"
await fakeList.invoke("Q4"); // "Response 1"(循环)
FakeModel 的价值:
- 单元测试不需要 API key
- 可以预设返回值,验证链的行为
- 是理解
BaseChatModel最小实现的最佳示例
11.9 实战练习
练习 1:用 FakeListChatModel 测试链
import { FakeListChatModel } from "@langchain/core/utils/testing";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const model = new FakeListChatModel({
responses: ["TypeScript is a typed superset of JavaScript."],
});
const chain = ChatPromptTemplate.fromMessages([
["human", "Explain {topic} in one sentence"],
]).pipe(model).pipe(new StringOutputParser());
const result = await chain.invoke({ topic: "TypeScript" });
console.log(result); // "TypeScript is a typed superset of JavaScript."
练习 2:理解模板方法
// 最小化的 ChatModel 实现
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
class MyChatModel extends BaseChatModel {
_llmType() { return "my-model"; }
async _generate(messages, options, runManager) {
// 这是 Provider 唯一需要实现的核心方法
const lastMessage = messages[messages.length - 1];
return {
generations: [{
text: `Echo: ${lastMessage.content}`,
message: new AIMessage(`Echo: ${lastMessage.content}`),
}],
};
}
}
11.10 源码精读路线
| 优先级 | 位置 | 关注点 |
|---|---|---|
| P0 | language_models/chat_models.ts:283-296 | invoke() 的完整流程 |
| P0 | language_models/chat_models.ts:1058 | _generate() 抽象方法定义 |
| P1 | language_models/chat_models.ts:298-305 | _streamResponseChunks() 默认实现 |
| P1 | language_models/chat_models.ts:272-275 | bindTools() 可选方法 |
| P2 | language_models/chat_models.ts:1124+ | withStructuredOutput() 实现 |
| P2 | language_models/base.ts | BaseLanguageModel 基类 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解 BaseChatModel 的调用方式:invoke 返回 AIMessage |
| 🔵 中阶 | 掌握 _generate() 模板方法:Provider 只需实现这一个方法 |
| 🟡 高阶 | 理解 bindTools() 和 withStructuredOutput() 的关系和默认实现 |
| 🟠 资深 | 分析 invoke 中的缓存检查、callback 处理等横切关注点 |
| 🔴 架构 | 评估"最小实现接口"模式如何降低 Provider 接入门槛 |
下一课预告
第 12 课讲 Prompts — 提示词模板系统,理解 ChatPromptTemplate 如何将变量和消息历史组合为结构化的提示词。