Skip to content

从零到进阶做 Agent(五)会话记忆

🕒 Published at:

第二篇里,我们靠「手动维护一个 messages 列表」实现了多轮对话。能用,但要自己管历史、自己控制长度、多用户还得自己分隔会话。这一篇用 LangGraph 的持久化机制来做记忆——这是 LangChain 官方目前推荐的方式(老的 RunnableWithMessageHistory 已被标记弃用)。顶部可切换 Python / Node.js。

为什么是 LangGraph?LangChain 1.x 把「状态、记忆、循环」这些能力下沉到了 LangGraph。你可以把 LangGraph 理解成「带记忆和流程控制的 LangChain」。后面做 Agent 也是它,所以这一篇正好打个基础。

装依赖

bash
pip install -U langgraph langchain-openai
bash
npm install @langchain/langgraph @langchain/openai @langchain/core

三个核心概念

用一句话先建立直觉:我们要画一张流程图(Graph),图里有节点(Node)负责干活(比如调用模型),图有一份状态(State)在节点间流动,再挂一个检查点存储器(Checkpointer),它会按 thread_id(会话 ID)自动把每轮的消息存下来、下轮再取出来。记忆,就是「同一个 thread_id 的状态被持久化了」。

STARTmodel 节点调用大模型ENDCheckpointer 检查点按 thread_id 存取历史写回读取同一个 thread_id ⇒ 状态被持久化 ⇒ 记忆

最常用的状态类型是内置的 MessagesState(Python)/ MessagesAnnotation(Node),它专门用来累积一个消息列表——每个节点返回的新消息会自动追加进去,而不是覆盖。这正是对话历史需要的行为。

搭一个有记忆的对话

python
import os
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import MemorySaver

model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# 节点:拿到当前所有历史消息,调用模型,返回模型的回复
def call_model(state: MessagesState):
    response = model.invoke(state["messages"])
    return {"messages": response}   # 返回的消息会被自动追加进历史

# 画图:START -> model
builder = StateGraph(MessagesState)
builder.add_node("model", call_model)
builder.add_edge(START, "model")

# 挂上「内存检查点」,编译成可执行的图
graph = builder.compile(checkpointer=MemorySaver())

# 用 thread_id 区分会话;同一个 id 就是同一段对话
config = {"configurable": {"thread_id": "user-1"}}

def chat(text: str) -> str:
    result = graph.invoke({"messages": [{"role": "user", "content": text}]}, config)
    return result["messages"][-1].content   # 最后一条就是模型本轮回复

print(chat("我叫小明,在学 Python。"))
print(chat("我叫什么名字?"))   # 它记得——因为 thread_id 相同
javascript
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, MessagesAnnotation, START, MemorySaver } from "@langchain/langgraph";

const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: process.env.DASHSCOPE_API_KEY,
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
});

// 节点:拿到当前所有历史消息,调用模型,返回模型的回复
const callModel = async (state) => {
  const response = await model.invoke(state.messages);
  return { messages: response };   // 返回的消息会被自动追加进历史
};

// 画图:START -> model
const graph = new StateGraph(MessagesAnnotation)
  .addNode("model", callModel)
  .addEdge(START, "model")
  .compile({ checkpointer: new MemorySaver() });   // 挂上「内存检查点」

// 用 thread_id 区分会话;同一个 id 就是同一段对话
const config = { configurable: { thread_id: "user-1" } };

async function chat(text) {
  const result = await graph.invoke(
    { messages: [{ role: "user", content: text }] },
    config
  );
  return result.messages[result.messages.length - 1].content;
}

console.log(await chat("我叫小明,在学 JavaScript。"));
console.log(await chat("我叫什么名字?"));   // 它记得——因为 thread_id 相同

运行后你会看到,第二问「我叫什么名字?」被正确回答了。而我们完全没有手动拼历史——MessagesState + MemorySaver 把「取出旧消息、追加新消息、按会话存档」全包了。

多用户天然隔离

换一个 thread_id,就是一段全新的、互不干扰的对话。这让「一套代码服务多个用户」变得很自然——每个用户一个 thread_id 即可。

python
config_a = {"configurable": {"thread_id": "alice"}}
config_b = {"configurable": {"thread_id": "bob"}}

graph.invoke({"messages": [{"role": "user", "content": "记住:我最喜欢的数字是 7"}]}, config_a)
# bob 的会话里问同样的问题,它并不知道 alice 说过什么
r = graph.invoke({"messages": [{"role": "user", "content": "我最喜欢的数字是几?"}]}, config_b)
print(r["messages"][-1].content)   # bob 这边无从得知
javascript
const configA = { configurable: { thread_id: "alice" } };
const configB = { configurable: { thread_id: "bob" } };

await graph.invoke({ messages: [{ role: "user", content: "记住:我最喜欢的数字是 7" }] }, configA);
// bob 的会话里问同样的问题,它并不知道 alice 说过什么
const r = await graph.invoke({ messages: [{ role: "user", content: "我最喜欢的数字是几?" }] }, configB);
console.log(r.messages[r.messages.length - 1].content);   // bob 这边无从得知

关于「长期记忆」与长度控制

MemorySaver内存版检查点,进程重启就没了,适合开发调试。要真正的长期记忆(重启仍在、跨机器共享),把检查点换成持久化实现即可,比如官方的 SQLite / Postgres 检查点(langgraph-checkpoint-sqlitelanggraph-checkpoint-postgres),图的写法完全不用改——这正是「把存储和逻辑解耦」的好处。

另外,历史会越来越长、越来越费 token。生产里常在 call_model 节点里对 state["messages"] 做裁剪(只保留最近若干条)或先摘要再喂给模型。因为节点就是普通函数,这些策略你可以自由实现。

小结与预告

这一篇你用官方推荐的 LangGraph 方式做出了带记忆的对话:图 + 节点 + MessagesState + Checkpointer,靠 thread_id 区分会话、自动存取历史。顺便,你已经摸到了 LangGraph 的门——这对后面做 Agent 是关键铺垫。

到这里,模型能聊、能记。但它还只会「凭脑子答」,答不了你的私有资料。下一篇进入 RAG 的世界:先讲怎么把文档加载、切分、向量化,并存进向量库、按语义检索——这是让模型「带着你的资料回答」的第一步。