📌 核心结论(TL;DR)
-
大模型无状态:LLM 本身没有记忆,每次请求独立。多轮对话的本质是应用层在每次请求时,将历史消息和最新问题一同发送给大模型。
-
上下文裁剪 :受限于 Token 上限,需使用
trim_messages根据策略(保留最近、保留系统提示)清理历史消息,防止超限并控制成本。 -
自动记忆管理 :通过
RunnableWithMessageHistory结合session_id,可按用户会话自动存取历史消息,实现"记忆隔离"。
🔍 一、 原理图解与核心概念
1. 为什么大模型是个"七秒记忆的金鱼"?
如果你直接调用大模型 API,你会发现它根本记不住你刚才说了什么。这是因为大模型的 HTTP API 是**无状态(Stateless)**的。
那网页版 ChatGPT 是怎么记住你的?
因为前端代码在每次发送新问题时,偷偷把你之前的聊天记录全都打包带上了一起发过去。模型真实收到的输入结构是这样的:
html
[System: 你是一个助手]
[Human: 我叫小明]
[AI: 你好,小明]
[Human: 我是谁?] <-- 你当前问的话
2. 为什么不能无限把历史记录带上?
大模型有"上下文窗口(Context Window)"限制。如果你聊了几百句,全都带上,不仅每次请求要花很多钱 ,还会因为超过长度限制直接报错。
-
解决方案 :使用
trim_messages进行智能裁剪。 -
多用户隔离 :用
session_id区分不同用户,防止 A 用户看到 B 用户的记忆。
💻 二、 精选核心代码片段
(注:DeepSeek 的 API 完全兼容 OpenAI 协议,因此使用 ChatOpenAI 类并修改 base_url 即可无缝调用)
1. 智能上下文裁剪(trim_messages)
当历史记录过长时,保留最近的对话并维护系统提示词。
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, trim_messages
# 加载环境变量
load_dotenv()
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 假设 messages 是一个很长的历史记录列表
messages = [
SystemMessage(content="你是一个严格的班主任。"),
HumanMessage(content="hi! I'm bob"),
AIMessage(content="hi!"),
# ... 中间省略大量对话 ...
HumanMessage(content="What's my name?"), # 最新问题
]
# 定义裁剪器
trimmer = trim_messages(
max_tokens=8, # 关键:这里代表最多保留 8 条消息
strategy="last", # 策略:保留最新的
token_counter=len, # 按消息条数计算,绕开 Token 统计报错
include_system=True, # 永远保留最开头的 SystemMessage
allow_partial=False, # 不允许把一句话切成两半
start_on="human", # 裁剪后的第一条必须是 HumanMessage
)
# 结合 LCEL 链:先裁剪再传给模型
chain = trimmer | model
print(chain.invoke(messages).content)
2. 多轮对话记忆隔离(RunnableWithMessageHistory)
通过 session_id 自动隔离不同用户的对话历史。
python
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_core.messages import HumanMessage
# 存储字典
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# 包装模型,赋予其自动存取记忆的能力
with_message_history = RunnableWithMessageHistory(model, get_session_history)
# 绑定唯一的 session_id
config = {"configurable": {"session_id": "user_1"}}
# 第一轮对话
with_message_history.invoke([HumanMessage(content="我是小明")], config=config)
# 第二轮对话,模型会自动读取上一轮历史
with_message_history.invoke([HumanMessage(content="我是谁?")], config=config)
(完整的包含本地 Ollama 测试的代码已上传至 Gitee 仓库)
⚠️ 三、 踩坑排查排雷
-
报错
NotImplementedError: get_num_tokens_from_messages() is not presently implemented for model cl100k_base:-
原因 :在使用非 OpenAI 模型(如 DeepSeek)时,直接传入
token_counter=model会报错,因为底层没有适配该模型的精确 Token 统计方法。 -
解决方案 :将参数改为
token_counter=len,按消息条数进行裁剪,简单且绝对不会报错。
-
-
多用户数据串台(生产级事故) :在 Web 应用中忘记为不同用户分配独立的
session_id,导致 A 的聊天记录被 B 看到。排查:确保前端传入了唯一的 UUID 作为 session_id。 -
本地 Ollama 连接失败(WinError 10061) :如果使用本地模型,提示目标计算机积极拒绝。排查:检查 Ollama 软件是否已在后台启动,且拉取了对应模型。

