[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理

📌 核心结论(TL;DR)

  1. 大模型无状态:LLM 本身没有记忆,每次请求独立。多轮对话的本质是应用层在每次请求时,将历史消息和最新问题一同发送给大模型。

  2. 上下文裁剪 :受限于 Token 上限,需使用 trim_messages 根据策略(保留最近、保留系统提示)清理历史消息,防止超限并控制成本。

  3. 自动记忆管理 :通过 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 仓库)

⚠️ 三、 踩坑排查排雷

  1. 报错 NotImplementedError: get_num_tokens_from_messages() is not presently implemented for model cl100k_base:

    • 原因 :在使用非 OpenAI 模型(如 DeepSeek)时,直接传入 token_counter=model 会报错,因为底层没有适配该模型的精确 Token 统计方法。

    • 解决方案 :将参数改为 token_counter=len,按消息条数进行裁剪,简单且绝对不会报错。

  2. 多用户数据串台(生产级事故) :在 Web 应用中忘记为不同用户分配独立的 session_id,导致 A 的聊天记录被 B 看到。排查:确保前端传入了唯一的 UUID 作为 session_id。

  3. 本地 Ollama 连接失败(WinError 10061) :如果使用本地模型,提示目标计算机积极拒绝。排查:检查 Ollama 软件是否已在后台启动,且拉取了对应模型。

相关推荐
文丑颜不良啊19 分钟前
LeetCode2061~2080题目解析
java·leetcode
景煜拾年23 分钟前
InnoAI SQL 助手
运维·数据库·笔记·sql
旖旎夜光24 分钟前
【C++ 基础入门】类和对象(下):初始化列表、static 成员、友元与内部类
c++·笔记·学习
LONGZHIQIN44 分钟前
STM32学习笔记
笔记·stm32·学习·arm
官方认定好文1 小时前
第八章 中间件2——内置中间件
langchain
叼烟扛炮1 小时前
[LangChain] 学习笔记(04):手搓 ChatGPT 打字机——流式输出(SSE)与模型初始化选型
人工智能·笔记·chatgpt·langchain
特立独行的猫A1 小时前
13MB 单文件、一条命令变服务:把仓颉写的 cjh 内嵌进你的应用,当它的 AI 大脑
java·后端·python
Brookty1 小时前
Java博客项目实战|工程环境与构建:工程目录、PowerShell命令调用与JDK/IDEA/Maven工作机制
java·项目实战
redtank20001 小时前
纯血鸿蒙小工具三件套实测:弹幕手持、爱记录与仓库管理软件
学习·华为·智能手机·harmonyos