第 10 课: 对话历史与消息管理

1 阅读4分钟

课程目标

掌握多轮对话中消息历史的存储、检索和自动注入机制:BaseChatMessageHistoryRunnableWithMessageHistory


10.1 多轮对话的核心挑战

LLM 是无状态的——每次调用都是独立的。要实现多轮对话,必须在每次调用时把历史消息一起发给模型。

挑战:

  1. 存储:对话历史存在哪里?内存?Redis?数据库?
  2. 检索:如何按会话 ID 获取正确的历史?
  3. 注入:如何自动将历史消息插入到 Prompt 中?
  4. 更新:用户新消息和 AI 回复如何自动追加到历史?
  5. 截断:上下文窗口有限,历史太长怎么办?

10.2 BaseChatMessageHistory — 存储抽象

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

export abstract class BaseChatMessageHistory extends Serializable {
  abstract getMessages(): Promise<BaseMessage[]>;
  abstract addMessage(message: BaseMessage): Promise<void>;
  abstract addUserMessage(message: string): Promise<void>;
  abstract addAIMessage(message: string): Promise<void>;
  abstract clear(): Promise<void>;

  // 批量添加(默认逐条,子类可优化)
  async addMessages(messages: BaseMessage[]): Promise<void> {
    for (const message of messages) {
      await this.addMessage(message);
    }
  }
}

10.2.1 BaseListChatMessageHistory

更简洁的变体,便利方法有默认实现:

export abstract class BaseListChatMessageHistory extends Serializable {
  abstract getMessages(): Promise<BaseMessage[]>;
  abstract addMessage(message: BaseMessage): Promise<void>;

  // 便利方法有默认实现
  addUserMessage(message: string) {
    return this.addMessage(new HumanMessage(message));
  }
  addAIMessage(message: string) {
    return this.addMessage(new AIMessage(message));
  }
}

10.2.2 内存实现示例

class InMemoryChatMessageHistory extends BaseListChatMessageHistory {
  lc_namespace = ["custom"];
  private messages: BaseMessage[] = [];

  async getMessages() {
    return this.messages;
  }

  async addMessage(message: BaseMessage) {
    this.messages.push(message);
  }

  async clear() {
    this.messages = [];
  }
}

10.3 RunnableWithMessageHistory — 自动注入历史

源码位置: libs/langchain-core/src/runnables/history.ts

RunnableWithMessageHistory 是一个包装器,自动完成"取历史 → 注入 → 执行 → 保存"的完整流程。

10.3.1 核心配置

interface RunnableWithMessageHistoryInputs {
  runnable: Runnable;                        // 被包装的链
  getMessageHistory: GetSessionHistoryCallable;  // 获取历史的函数
  inputMessagesKey?: string;     // 输入中代表用户消息的 key
  outputMessagesKey?: string;    // 输出中代表 AI 回复的 key
  historyMessagesKey?: string;   // 注入历史的 key(对应 MessagesPlaceholder)
}

10.3.2 使用方式

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";

// Step 1: 定义 Prompt(留出历史消息的位置)
const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant."],
  new MessagesPlaceholder("history"),   // ← 历史消息插入点
  ["human", "{question}"],
]);

// Step 2: 构建链
const chain = prompt.pipe(model).pipe(parser);

// Step 3: 准备历史存储(按 session ID 隔离)
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};

// Step 4: 包装
const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: (sessionId) => {
    if (!messageHistories[sessionId]) {
      messageHistories[sessionId] = new InMemoryChatMessageHistory();
    }
    return messageHistories[sessionId];
  },
  inputMessagesKey: "question",      // 输入中哪个 key 是用户消息
  historyMessagesKey: "history",     // 对应 MessagesPlaceholder 的变量名
});

// Step 5: 调用(通过 configurable.sessionId 指定会话)
const result1 = await chainWithHistory.invoke(
  { question: "My name is Alice" },
  { configurable: { sessionId: "session-001" } }
);

const result2 = await chainWithHistory.invoke(
  { question: "What's my name?" },
  { configurable: { sessionId: "session-001" } }
);
// AI 能回答 "Your name is Alice" — 因为历史自动注入了

// 不同 session 是隔离的
const result3 = await chainWithHistory.invoke(
  { question: "What's my name?" },
  { configurable: { sessionId: "session-002" } }
);
// AI 不知道名字 — 这是新会话

10.3.3 执行流程

1. 用户调用 chainWithHistory.invoke({ question: "xxx" }, { sessionId: "001" })
                                    │
2. getMessageHistory("001")         │ 获取历史存储
                                    │
3. history.getMessages()            │ 读取历史消息
                                    ▼
4. 将历史注入输入:  { question: "xxx", history: [msg1, msg2, ...] }
                                    │
5. chain.invoke(enrichedInput)      │ 执行原始链
                                    │
6. history.addUserMessage("xxx")    │ 保存用户消息
   history.addAIMessage(result)     │ 保存 AI 回复
                                    ▼
7. 返回结果

10.4 Session ID 路由

RunnableWithMessageHistory 通过 configurable.sessionId 实现多会话隔离:

// 同一条链,不同 session 独立管理历史
await chain.invoke(input, { configurable: { sessionId: "user-alice-001" } });
await chain.invoke(input, { configurable: { sessionId: "user-bob-001" } });

getMessageHistory 回调函数接收 session ID,返回对应的历史存储实例:

getMessageHistory: (sessionId: string) => {
  // 可以返回任何 BaseChatMessageHistory 实现
  // 内存、Redis、MongoDB、PostgreSQL...
  return new RedisChatMessageHistory({ sessionId, client: redisClient });
}

10.5 MessagesPlaceholder 配合

MessagesPlaceholder 是 Prompt 模板中用于动态插入消息列表的占位符:

import { MessagesPlaceholder } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are helpful."],
  new MessagesPlaceholder("chat_history"),  // 插入历史消息列表
  ["human", "{input}"],
]);

// 调用时传入消息数组
await prompt.invoke({
  input: "Hello",
  chat_history: [
    new HumanMessage("Hi"),
    new AIMessage("Hello! How can I help?"),
  ],
});

RunnableWithMessageHistoryhistoryMessagesKey 对应 MessagesPlaceholder 的变量名。


10.6 存储后端选择

后端适合场景
内存自定义开发/测试
Redis@langchain/community高性能、TTL 支持
MongoDB@langchain/mongodb持久化、查询灵活
PostgreSQL自定义与业务数据库统一
文件系统自定义简单场景

10.7 实战练习

完整多轮对话示例

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { FakeChatModel } from "@langchain/core/utils/testing";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";

// 简单内存历史实现
class MemoryHistory extends BaseListChatMessageHistory {
  lc_namespace = ["test"];
  messages: BaseMessage[] = [];
  async getMessages() { return this.messages; }
  async addMessage(msg: BaseMessage) { this.messages.push(msg); }
  async clear() { this.messages = []; }
}

const histories = new Map<string, MemoryHistory>();

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You remember everything the user tells you."],
  new MessagesPlaceholder("history"),
  ["human", "{input}"],
]);

const chain = prompt.pipe(new FakeChatModel({})).pipe(new StringOutputParser());

const withHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: (sessionId) => {
    if (!histories.has(sessionId)) {
      histories.set(sessionId, new MemoryHistory());
    }
    return histories.get(sessionId)!;
  },
  inputMessagesKey: "input",
  historyMessagesKey: "history",
});

// 第一轮
await withHistory.invoke(
  { input: "I live in Shanghai" },
  { configurable: { sessionId: "demo" } }
);

// 第二轮 — AI 能访问到上一轮的对话
await withHistory.invoke(
  { input: "Where do I live?" },
  { configurable: { sessionId: "demo" } }
);

// 检查存储的历史
const stored = await histories.get("demo")!.getMessages();
console.log(stored.length);  // 4 条消息(2 轮 × 2 条)

10.8 源码精读路线

优先级文件关注点
P0langchain-core/src/chat_history.tsBaseChatMessageHistoryBaseListChatMessageHistory 抽象接口
P0langchain-core/src/runnables/history.tsRunnableWithMessageHistory 完整实现
P1langchain-core/src/runnables/history.ts 构造函数getMessageHistoryinputMessagesKeyhistoryMessagesKey 配置
P2langchain-core/src/runnables/history.ts invoke 方法历史注入、消息保存的完整流程

本课收获总结

级别你应该掌握的
🟢 基础理解多轮对话需要手动管理历史;能用 RunnableWithMessageHistory 构建多轮对话链
🔵 中阶掌握 BaseChatMessageHistory 接口;理解 inputMessagesKey / historyMessagesKey 配置
🟡 高阶理解 RunnableWithMessageHistory 的完整执行流程(取历史→注入→执行→保存)
🟠 资深分析 session ID 路由机制;设计不同存储后端的选型策略
🔴 架构设计对话历史的截断、摘要压缩和持久化方案

下一课预告

第 11 课讲 Language Models — 模型抽象层,理解 BaseChatModel._generate() 模板方法如何让 Provider 接入变得简单。