课程目标
掌握多轮对话中消息历史的存储、检索和自动注入机制:BaseChatMessageHistory 和 RunnableWithMessageHistory。
10.1 多轮对话的核心挑战
LLM 是无状态的——每次调用都是独立的。要实现多轮对话,必须在每次调用时把历史消息一起发给模型。
挑战:
- 存储:对话历史存在哪里?内存?Redis?数据库?
- 检索:如何按会话 ID 获取正确的历史?
- 注入:如何自动将历史消息插入到 Prompt 中?
- 更新:用户新消息和 AI 回复如何自动追加到历史?
- 截断:上下文窗口有限,历史太长怎么办?
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?"),
],
});
RunnableWithMessageHistory 的 historyMessagesKey 对应 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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | langchain-core/src/chat_history.ts | BaseChatMessageHistory、BaseListChatMessageHistory 抽象接口 |
| P0 | langchain-core/src/runnables/history.ts | RunnableWithMessageHistory 完整实现 |
| P1 | langchain-core/src/runnables/history.ts 构造函数 | getMessageHistory、inputMessagesKey、historyMessagesKey 配置 |
| P2 | langchain-core/src/runnables/history.ts invoke 方法 | 历史注入、消息保存的完整流程 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解多轮对话需要手动管理历史;能用 RunnableWithMessageHistory 构建多轮对话链 |
| 🔵 中阶 | 掌握 BaseChatMessageHistory 接口;理解 inputMessagesKey / historyMessagesKey 配置 |
| 🟡 高阶 | 理解 RunnableWithMessageHistory 的完整执行流程(取历史→注入→执行→保存) |
| 🟠 资深 | 分析 session ID 路由机制;设计不同存储后端的选型策略 |
| 🔴 架构 | 设计对话历史的截断、摘要压缩和持久化方案 |
下一课预告
第 11 课讲 Language Models — 模型抽象层,理解 BaseChatModel._generate() 模板方法如何让 Provider 接入变得简单。