[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:可以传字符串、BaseMessageBaseMessage[]
  • 如果是 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-

posted @ 2025-11-14 14:57  Zhentiw  阅读(2)  评论(0)    收藏  举报