第 5 课 / 共 7 课15 分钟2 个闯关

对话记忆:它其实没有魔法

MessagesPlaceholder、session_id 隔离、上下文撑爆了怎么办。

记忆的真相

第一课说过:模型无状态,每次调用都是全新的。所谓"记忆",就是你自己维护一份消息列表,每次请求都带上

看懂下面这段,你就懂了所有记忆机制:

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 社区包提供了现成的 RedisChatMessageHistorySQLChatMessageHistory)。

上下文会撑爆

对话越长,每次请求带的历史越多,于是:越来越贵、越来越慢、终将超出模型上下文上限直接报错。

三种应对,按复杂度递增:

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。

广告位(未配置 AdSense,上线后在此展示)

动手闯关

读懂了不算会,写出来才算

0/2 已通过

闯关和判题完全免费,交错了不扣任何东西。卡住随时点「问 AI」,它能看到你正在写的代码。

关卡 01选择+10 点 · +15 XP

session_id 写死会怎样

如果把 `session_id` 固定成 "default",线上会发生什么?

  • 可能是多选,选错不扣点数,随便试。
登录后闯关拿点数
关卡 02选择+10 点 · +15 XP

什么不该放进对话记忆

用户的订单信息、账户余额这类结构化事实,应该怎么处理?

  • 可能是多选,选错不扣点数,随便试。
登录后闯关拿点数

学完了?标记一下

标记完成会记进度、涨 20 XP,还会点亮连续学习天数。