记忆的真相
第一课说过:模型无状态,每次调用都是全新的。所谓"记忆",就是你自己维护一份消息列表,每次请求都带上。
看懂下面这段,你就懂了所有记忆机制:
history = []
def chat(user_input):
history.append({"role": "user", "content": user_input})
resp = llm.invoke(history)
history.append({"role": "assistant", "content": resp.content})
return resp.content
chat("我叫小明")
print(chat("我叫什么?")) # 小明
剩下的全是工程细节:存哪里、多用户怎么隔离、太长了怎么办。
LangChain 的标准做法
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个耐心的编程老师,回答简洁。"),
MessagesPlaceholder(variable_name="history"), # 历史消息插在这里
("user", "{input}"),
])
chain = prompt | llm | StrOutputParser()
store = {}
def get_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chat_with_memory = RunnableWithMessageHistory(
chain,
get_history,
input_messages_key="input",
history_messages_key="history",
)
cfg = {"configurable": {"session_id": "user-42"}}
chat_with_memory.invoke({"input": "我叫小明"}, config=cfg)
print(chat_with_memory.invoke({"input": "我叫什么?"}, config=cfg))
三个要点:
MessagesPlaceholder是历史消息的插槽,位置很重要——必须在 system 之后、当前 user 之前。session_id是多用户隔离的钥匙。真实产品里它应该是用户 ID 或会话 ID,绝不能写死。写死了就是所有用户共用一份记忆,这是严重的隐私事故。InMemoryChatMessageHistory顾名思义存在内存里,进程一重启就没了。生产要换成数据库或 Redis 实现(LangChain 社区包提供了现成的RedisChatMessageHistory、SQLChatMessageHistory)。
上下文会撑爆
对话越长,每次请求带的历史越多,于是:越来越贵、越来越慢、终将超出模型上下文上限直接报错。
三种应对,按复杂度递增:
1. 滑动窗口——只保留最近 N 轮。 最简单,绝大多数场景够用。
from langchain_core.messages import trim_messages
trimmer = trim_messages(max_tokens=2000, strategy="last", token_counter=llm)
2. 摘要——把早期对话压缩成一段话。 省 token 但会丢细节,且每次摘要本身也要花一次模型调用。
3. 窗口 + 摘要混合。 最近 N 轮保留原文,更早的压成摘要。效果最好,复杂度也最高。
默认选 1。 只有当用户明确抱怨"你忘了我前面说的"时,再上 3。不要一开始就做复杂方案。
别把记忆当数据库
新手常犯的错:把"用户的偏好设置"、"订单信息"这类结构化事实塞进对话历史,指望模型记住。
不要这么做。 对话历史会被裁剪、会被摘要、模型也可能读漏。结构化事实应该存数据库,每次需要时查出来拼进 system 提示词。
记忆只负责"聊天的上下文连贯",不负责"数据正确性"。这条边界划清楚,能避开一大类诡异的线上问题。
10 秒自测
如果把 session_id 写成固定值 "default",会发生什么?
所有用户共用同一份对话历史——A 用户能看到 B 用户的聊天内容。这是隐私事故,不是 bug。