[LangChian] 18. 自动维护聊天记录
上一节我们体验了“手动维护聊天记录”,每次都要:
- 把用户发言添加到
history - 把模型输出添加到
history - 每轮都手动调用
getMessages()构造上下文
await history.addMessage(new HumanMessage(input));
await history.addMessage(fullRes);
虽然原理简单,但实际开发中太过繁琐。尤其在构建多轮对话 Agent 时,这种手动维护非常不优雅。所以这一节,我们引入 LangChain.js 提供的“自动加记忆”工具 —— RunnableWithMessageHistory。
快速上手
前面我们有介绍过 Runnable 相关的接口,例如
- RunnableLambda
- RunnableMap
- RunnableSequence
- RunnablePassthrough
这里的 RunnableWithMessageHistory 也属于 Runnable 家族的一员。在实例化的时候,接收一个配置对象:
new RunnableWithMessageHistory({
runnable: baseChain, // 原始链
getMessageHistory: (sessionId) => chatHistory, // 指定聊天记录
inputMessagesKey: "input", // 用户输入字段名
historyMessagesKey: "chat_history", // Prompt 中历史记录占位符
});
配置对象通常需要配置这几个参数:
- runnable:需要被包裹的 chain,可以是任意 chain
- getMessageHistory(sessionId):传入会话 ID,返回一个
BaseChatMessageHistory实例 - inputMessagesKey:本轮用户消息在哪个 key,调用后会被追加进历史。
- historyMessagesKey:历史注入到输入对象这个 key,下游用
new MessagesPlaceholder("<同名>")接住。 - outputMessagesKey::当链输出是对象时,指定对象里哪一个字段是消息(否则默认把顶层输出当消息)。
课堂演示
快速上手示例
import { ChatMessageHistory } from "@langchain/classic/memory";
import { StringOutputParser } from "@langchain/core/output_parsers";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatOllama } from "@langchain/ollama";
const pt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个健谈的中文 AI 助手,请结合上下文尽可能详细地使用中文回答用户问题。"
),
new MessagesPlaceholder("history"),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]);
const model = new ChatOllama({
model: "llama3",
temperature: 0.7,
});
const parser = new StringOutputParser();
const chain = pt.pipe(model).pipe(parser);
const store = new Map();
const withHistoryChain = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: (sessionId) => {
if (!store.has(sessionId)) {
store.set(sessionId, new ChatMessageHistory());
}
return store.get(sessionId);
},
inputMessagesKey: "input",
historyMessagesKey: "history",
});
const config = {
configurable: {
sessionId: "1234567890",
},
};
await withHistoryChain.invoke(
{
input: "Hello, what is Rustlang?",
},
config
);
const res = await withHistoryChain.invoke(
{
input: "what did we just talk about?",
},
config
);
console.log(res);
stream() 方法,方法签名如下:
stream(
input: Input,
options?: RunnableConfig
): AsyncGenerator<StreamEvent<Output>>
1. 输入参数 (input)
类型:Input
与 invoke() 方法保持一致:
- 如果是 LLM:可以传字符串、
BaseMessage、BaseMessage[] - 如果是 Chain / Runnable:则是该 Chain 约定的输入对象(例如
{ input: "..." }) - 如果是 Embeddings:通常是字符串或字符串数组
换句话说,input 的类型由具体的 Runnable 实例 决定。
2. 配置参数
类型:RunnableConfig(可选)
常见字段包括:
configurable:运行时传入的上下文配置(例如用户 ID、对话 ID,用于内存/持久化关联)。tags:给运行标记,用于调试、Tracing。metadata:附加元信息,方便日志或监控。callbacks:传入回调函数(如handleLLMNewToken等),可用于实时处理 token。maxConcurrency:并发控制。timeout:超时设置。
import { ChatMessageHistory } from "@langchain/classic/memory";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
import { ChatOllama } from "@langchain/ollama";
import readline from "readline-sync";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
// 1. 模型
const model = new ChatOllama({
model: "llama3",
temperature: 0.7,
});
// 2. 提示词
const pt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个健谈的中文 AI 助手,请结合上下文尽可能详细地使用中文回答用户问题。"
), // 系统提示词
new MessagesPlaceholder("history"), // 会话的历史记录,一开始是一个占位符
HumanMessagePromptTemplate.fromTemplate("{input}"), // 用户输入的内容
]);
// 3. 存储会话历史
const history = new ChatMessageHistory();
// 4. 创建一个chain
const chain = pt.pipe(model);
async function chatLoop() {
console.log("开始会话,输入内容后回车;输入 /clear 清空历史,/exit 退出。");
while (true) {
const input = readline.question("用户:").trim();
if (!input) continue;
if (input === "/exit") {
console.log("拜拜");
break;
}
if (input === "/clear") {
await history.clear();
console.log("历史记录已清空");
continue;
}
let fullRes = ""; // 记录完整的信息
try {
const values = {
input, // 用户本次的输入
history: await history.getMessages(), // 获取之前会话记录
};
const stream = chain.streamEvents(values, { version: "v2" });
process.stdout.write("助理:");
for await (const event of stream) {
if (event.event === "on_chat_model_stream") {
process.stdout.write(event.data?.chunk?.content || "");
}
}
console.log("\n");
} catch (err) {
console.error("调用大模型失败☹️", err);
}
// 将本轮会话记录到历史里面
await history.addMessage(new HumanMessage(input));
await history.addMessage(new AIMessage(fullRes));
}
}
chatLoop();
实战案例
把上节课的对话练习改为 RunnableWithMessageHistory
import { ChatMessageHistory } from "@langchain/classic/memory";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
import { ChatOllama } from "@langchain/ollama";
import readline from "readline-sync";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
// 1. 模型
const model = new ChatOllama({
model: "llama3",
temperature: 0.7,
});
// 2. 提示词
const pt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个健谈的中文 AI 助手,请结合上下文尽可能详细地使用中文回答用户问题。"
), // 系统提示词
new MessagesPlaceholder("history"), // 会话的历史记录,一开始是一个占位符
HumanMessagePromptTemplate.fromTemplate("{input}"), // 用户输入的内容
]);
// 3. 存储会话历史
const history = new ChatMessageHistory();
const parser = new StringOutputParser();
// 4. 创建一个chain
const chain = pt.pipe(model).pipe(parser);
const store = new Map();
const withHistoryChain = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: (sessionId) => {
if (!store.has(sessionId)) {
store.set(sessionId, new ChatMessageHistory());
}
return store.get(sessionId);
},
inputMessagesKey: "input",
historyMessagesKey: "history",
});
const config = {
configurable: {
sessionId: "1234567890",
},
};
async function chatLoop() {
console.log("开始会话,输入内容后回车;输入 /clear 清空历史,/exit 退出。");
while (true) {
const input = readline.question("用户:").trim();
if (!input) continue;
if (input === "/exit") {
console.log("拜拜");
break;
}
if (input === "/clear") {
const { sessionId } = config.configurable;
store.set(sessionId, new ChatMessageHistory());
console.log("历史记录已清空");
continue;
}
try {
const stream = await withHistoryChain.stream({ input }, config);
process.stdout.write("助理:");
for await (const chunk of stream) {
process.stdout.write(chunk);
}
console.log("\n");
} catch (err) {
console.error("调用大模型失败☹️", err);
}
}
}
chatLoop();
-EOF-

浙公网安备 33010602011771号