第 9 课: Messages — 对话数据模型

0 阅读5分钟

课程目标

掌握 LangChain.js 的消息数据模型:BaseMessage 继承体系、多模态内容、流式消息分片。


9.1 为什么需要统一消息模型?

不同 LLM Provider 的消息格式差异很大:

  • OpenAI 用 { role: "user", content: "..." }
  • Anthropic 用 { role: "user", content: [{ type: "text", text: "..." }] }
  • Google 用完全不同的结构

LangChain.js 用 BaseMessage 体系统一它们,Provider 层负责格式转换。


9.2 消息类型继承体系

Serializable
  └── BaseMessage
        ├── HumanMessage      // 用户消息 (type: "human")
        ├── AIMessage          // AI 回复 (type: "ai")
        ├── SystemMessage      // 系统指令 (type: "system")
        ├── ToolMessage        // 工具返回值 (type: "tool")
        ├── FunctionMessage    // (已弃用,兼容旧版)
        ├── ChatMessage        // 自定义 role
        └── RemoveMessage      // 标记删除消息

9.2.1 BaseMessage 核心字段

源码位置: libs/langchain-core/src/messages/base.ts

export type MessageContent = string | Array<ContentBlock>;

export interface BaseMessageFields {
  content?: MessageContent;               // 消息内容(文本或多模态块数组)
  contentBlocks?: Array<ContentBlock>;     // 标准化内容块
  id?: string;                            // 消息 ID
  name?: string;                          // 发送者名称
  additional_kwargs?: Record<string, any>; // 额外参数
  response_metadata?: Record<string, any>; // 响应元数据
}

content 的两种形态

  1. 纯文本string — 最常见的情况
  2. 多模态块数组ContentBlock[] — 包含文本、图片、文件等混合内容

9.2.2 各消息类型详解

类型type 字段用途典型内容
HumanMessage"human"用户输入文本、图片 URL
AIMessage"ai"模型回复文本、tool_calls
SystemMessage"system"系统指令角色设定、规则
ToolMessage"tool"工具执行结果返回值文本
ChatMessage自定义自定义角色任意

9.3 AIMessage — 最复杂的消息类型

源码位置: libs/langchain-core/src/messages/ai.ts

export class AIMessage extends BaseMessage {
  readonly type = "ai";

  tool_calls?: ToolCall[] = [];           // 结构化工具调用
  invalid_tool_calls?: InvalidToolCall[];  // 解析失败的工具调用
  usage_metadata?: UsageMetadata;          // token 使用量
}

9.3.1 tool_calls

当 LLM 决定调用工具时,AIMessage.tool_calls 包含结构化的调用信息:

interface ToolCall {
  name: string;             // 工具名称
  args: Record<string, any>; // 参数(已解析的 JSON)
  id?: string;              // 调用 ID(用于匹配 ToolMessage)
  type: "tool_call";
}

示例:

const aiMsg = new AIMessage({
  content: "",
  tool_calls: [
    {
      name: "get_weather",
      args: { city: "Beijing", unit: "celsius" },
      id: "call_123",
      type: "tool_call",
    },
  ],
});

9.3.2 contentBlocks

contentBlocks 提供标准化的内容块表示,懒解析 Provider 特定格式:

// Provider 原始格式可能不同(Anthropic 风格、OpenAI 风格等)
// contentBlocks 将它们统一为标准 ContentBlock 格式
const blocks = aiMessage.contentBlocks;
// [
//   { type: "text", text: "Based on..." },
//   { type: "tool_use", name: "get_weather", input: {...} },
// ]

9.3.3 usage_metadata

interface UsageMetadata {
  input_tokens: number;     // 输入 token 数
  output_tokens: number;    // 输出 token 数
  total_tokens: number;     // 总 token 数
}

9.4 ToolMessage — 工具结果

import { ToolMessage } from "@langchain/core/messages";

const toolResult = new ToolMessage({
  content: "Beijing: 25°C, Sunny",
  tool_call_id: "call_123",  // 匹配 AIMessage.tool_calls[].id
  name: "get_weather",
});

tool_call_id 是关键——它将 ToolMessage 和触发它的 AIMessage.tool_call 关联起来。


9.5 多模态内容

9.5.1 ContentBlock 类型

type ContentBlock =
  | { type: "text"; text: string }                           // 文本
  | { type: "image_url"; image_url: { url: string } }       // 图片 URL
  | { type: "input_audio"; input_audio: { data: string } }  // 音频
  | { type: "file"; file: { url: string } }                 // 文件
  // ... 更多类型

9.5.2 发送图片消息

const imageMsg = new HumanMessage({
  content: [
    { type: "text", text: "What's in this image?" },
    {
      type: "image_url",
      image_url: { url: "https://example.com/cat.jpg" },
    },
  ],
});

9.5.3 Base64 图片

const base64Msg = new HumanMessage({
  content: [
    { type: "text", text: "Describe this" },
    {
      type: "image_url",
      image_url: { url: `data:image/png;base64,${base64Data}` },
    },
  ],
});

9.6 AIMessageChunk — 流式消息

流式输出时,模型不是一次返回完整的 AIMessage,而是逐步返回 AIMessageChunk:

class AIMessageChunk extends BaseMessageChunk {
  type = "AIMessageChunk";
  tool_call_chunks?: ToolCallChunk[];  // 工具调用的流式分片
}

chunk 合并:多个 chunk 可以用 concat() 合并:

import { concat } from "@langchain/core/utils/stream";

let fullMessage: AIMessageChunk | undefined;
for await (const chunk of model.stream("Hello")) {
  fullMessage = fullMessage ? concat(fullMessage, chunk) : chunk;
  console.log(chunk.content);  // 逐 token 输出
}
// fullMessage 现在包含完整的消息

tool_call_chunks 的合并

// 流式中工具调用也是分片到达:
// chunk1: { tool_call_chunks: [{ name: "get", args: '{"ci' }] }
// chunk2: { tool_call_chunks: [{ args: 'ty":"Be' }] }
// chunk3: { tool_call_chunks: [{ args: 'ijing"}' }] }
// 合并后: tool_calls: [{ name: "get_weather", args: {city: "Beijing"} }]

9.7 消息创建的便捷方式

// 方式 1: 构造函数
const msg = new HumanMessage("Hello");

// 方式 2: 字符串(纯文本)
const msg = new HumanMessage("Hello");

// 方式 3: 元组形式(在 ChatPromptTemplate 中使用)
["human", "Hello {name}"]       // → HumanMessage
["ai", "I am an assistant"]     // → AIMessage
["system", "You are helpful"]   // → SystemMessage

// 方式 4: BaseMessageLike 类型
type BaseMessageLike =
  | BaseMessage
  | [type: string, content: MessageContent]  // 元组
  | string;  // 纯文本 → HumanMessage

9.8 消息序列化与反序列化

消息可以序列化为 JSON 存储,也可以从 JSON 恢复:

// 序列化
const stored: StoredMessage = {
  type: "human",
  data: {
    content: "Hello",
    role: "user",
    name: undefined,
    tool_call_id: undefined,
  },
};

// 反序列化
import { mapStoredMessageToChatMessage } from "@langchain/core/messages";
const restored = mapStoredMessageToChatMessage(stored);

9.9 实战练习

练习 1:构造完整对话

import {
  HumanMessage, AIMessage, SystemMessage, ToolMessage
} from "@langchain/core/messages";

const conversation = [
  new SystemMessage("You are a helpful assistant with access to weather tools."),
  new HumanMessage("What's the weather in Beijing?"),
  new AIMessage({
    content: "",
    tool_calls: [{
      name: "get_weather",
      args: { city: "Beijing" },
      id: "call_001",
      type: "tool_call",
    }],
  }),
  new ToolMessage({
    content: "Beijing: 25°C, Sunny",
    tool_call_id: "call_001",
    name: "get_weather",
  }),
  new AIMessage("The weather in Beijing is 25°C and sunny!"),
];

练习 2:多模态消息

const multimodalMsg = new HumanMessage({
  content: [
    { type: "text", text: "Compare these two images:" },
    { type: "image_url", image_url: { url: "https://example.com/img1.jpg" } },
    { type: "image_url", image_url: { url: "https://example.com/img2.jpg" } },
  ],
});

9.10 源码精读路线

优先级文件关注点
P0messages/base.tsBaseMessage, MessageContent, BaseMessageChunk
P0messages/ai.tsAIMessage, tool_calls, AIMessageChunk
P1messages/human.tsHumanMessage
P1messages/system.tsSystemMessage
P1messages/tool.tsToolMessage, ToolCall, ToolCallChunk
P2messages/content/ContentBlock 类型定义
P2messages/block_translators/Provider 格式转换器

本课收获总结

级别你应该掌握的
🟢 基础掌握五种消息类型及其用途;能构造包含工具调用的完整对话
🔵 中阶理解 AIMessage.tool_calls 的结构和 ToolMessage 的 tool_call_id 匹配机制
🟡 高阶掌握多模态内容的 ContentBlock 表示;理解 contentBlocks 的懒解析
🟠 资深分析 AIMessageChunk 的流式合并策略,特别是 tool_call_chunks 的合并
🔴 架构评估消息模型的扩展性:如何支持新的内容类型和 Provider 格式

下一课预告

第 10 课讲对话历史与消息管理——BaseChatMessageHistoryRunnableWithMessageHistory,解决多轮对话的上下文管理问题。