第 11 课: Language Models — 模型抽象层

2 阅读4分钟

课程目标

理解 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 源码精读路线

优先级位置关注点
P0language_models/chat_models.ts:283-296invoke() 的完整流程
P0language_models/chat_models.ts:1058_generate() 抽象方法定义
P1language_models/chat_models.ts:298-305_streamResponseChunks() 默认实现
P1language_models/chat_models.ts:272-275bindTools() 可选方法
P2language_models/chat_models.ts:1124+withStructuredOutput() 实现
P2language_models/base.tsBaseLanguageModel 基类

本课收获总结

级别你应该掌握的
🟢 基础理解 BaseChatModel 的调用方式:invoke 返回 AIMessage
🔵 中阶掌握 _generate() 模板方法:Provider 只需实现这一个方法
🟡 高阶理解 bindTools()withStructuredOutput() 的关系和默认实现
🟠 资深分析 invoke 中的缓存检查、callback 处理等横切关注点
🔴 架构评估"最小实现接口"模式如何降低 Provider 接入门槛

下一课预告

第 12 课讲 Prompts — 提示词模板系统,理解 ChatPromptTemplate 如何将变量和消息历史组合为结构化的提示词。