Appearance
第二篇里,我们靠「手动维护一个 messages 列表」实现了多轮对话。能用,但要自己管历史、自己控制长度、多用户还得自己分隔会话。这一篇用 LangGraph 的持久化机制来做记忆——这是 LangChain 官方目前推荐的方式(老的 RunnableWithMessageHistory 已被标记弃用)。顶部可切换 Python / Node.js。
为什么是 LangGraph?LangChain 1.x 把「状态、记忆、循环」这些能力下沉到了 LangGraph。你可以把 LangGraph 理解成「带记忆和流程控制的 LangChain」。后面做 Agent 也是它,所以这一篇正好打个基础。
装依赖
bash
pip install -U langgraph langchain-openaibash
npm install @langchain/langgraph @langchain/openai @langchain/core三个核心概念
用一句话先建立直觉:我们要画一张流程图(Graph),图里有节点(Node)负责干活(比如调用模型),图有一份状态(State)在节点间流动,再挂一个检查点存储器(Checkpointer),它会按 thread_id(会话 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-sqlite、langgraph-checkpoint-postgres),图的写法完全不用改——这正是「把存储和逻辑解耦」的好处。
另外,历史会越来越长、越来越费 token。生产里常在 call_model 节点里对 state["messages"] 做裁剪(只保留最近若干条)或先摘要再喂给模型。因为节点就是普通函数,这些策略你可以自由实现。
小结与预告
这一篇你用官方推荐的 LangGraph 方式做出了带记忆的对话:图 + 节点 + MessagesState + Checkpointer,靠 thread_id 区分会话、自动存取历史。顺便,你已经摸到了 LangGraph 的门——这对后面做 Agent 是关键铺垫。
到这里,模型能聊、能记。但它还只会「凭脑子答」,答不了你的私有资料。下一篇进入 RAG 的世界:先讲怎么把文档加载、切分、向量化,并存进向量库、按语义检索——这是让模型「带着你的资料回答」的第一步。