第 30 期 | 全栈复盘与未来展望:进军更高阶的 AI 应用

⏱ 预计阅读 15 分钟 更新于 2026/5/7
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)

本期副标题:让客服机器人告别“七秒记忆”,打造懂上下文的贴心助理

各位开发者,欢迎回到《LangChain 全栈大师课》。我是你们的老朋友。

在前面的几期中,我们已经为「智能客服知识库 (Intelligent Support Copilot)」搭建了基础的问答骨架。很多同学在群里跟我反馈:“老师,我照着写完之后,机器人确实能回答问题了,但它好像是个‘智障’,哦不,是条‘金鱼’!”

为什么这么说?因为当你问它:“我的订单 12345 怎么还没发货?”它回答:“正在为您查询。” 紧接着你问:“那把它退了吧。” 它会一脸懵逼地反问:“请问您要退什么?”

大模型(LLM)本质上是无状态的(Stateless)。它就像一个没有记忆的超级大脑,每次你跟它说话,对它来说都是你们的“初次见面”。在真实的客服场景中,如果用户每说一句话都要重复一遍订单号和上下文,你的客服系统大概率会被用户砸了。

今天这节课,我们就来解决这个核心痛点。我将带你深入剖析 LangChain 的 Memory 机制,并用最现代的 LCEL(LangChain 表达式语言)架构,为我们的客服小助手装上“海马体”,让它成为一个真正懂上下文的贴心助理。


🎯 本期学习目标

  1. 洞悉“记忆”的本质:理解大模型无状态特性的破局之道,明白 Chat History 是如何工作的。
  2. 掌握现代 LangChain 记忆架构:抛弃老旧的 ConversationChain,掌握生产级 RunnableWithMessageHistory 的核心用法。
  3. 实战多轮客服对话:在我们的客服 Copilot 项目中,实现基于内存与模拟持久化的多轮工单处理。
  4. Token 成本优化策略:学会使用滑动窗口与摘要记忆,避免“记忆太长”导致系统 OOM 或破产。

📖 原理解析

在讲代码之前,我们先来建立架构思维。很多初学者以为“给大模型加上记忆”,是大模型内部有什么神奇的开关。错!大模型的记忆,全靠**“强行喂饭”**。

既然 LLM 没有记忆,我们就在每次提问时,把**过去的聊天记录(Chat History)当前的问题(Human Input)**打包在一起,一次性发给 LLM。LLM 看到这些上下文,就会“假装”自己记得。

在 LangChain 中,这个过程被抽象成了高度可复用的组件。我们来看下面这张架构图:

sequenceDiagram
    participant U as 🙎‍♂️ 用户 (User)
    participant R as 🤖 RunnableWithMessageHistory (LangChain)
    participant M as 🗄️ 记忆存储 (MessageHistory)
    participant P as 📝 提示词模板 (Prompt)
    participant L as 🧠 大模型 (LLM)

    U->>R: 1. "那把它退了吧" (Session ID: user_001)
    activate R
    R->>M: 2. 根据 Session ID 拉取历史对话
    M-->>R: 3. 返回: [User: "订单12345发货没?", AI: "还没"]
    R->>P: 4. 组装: 历史记录 + 系统设定 + 当前问题
    P-->>R: 5. 生成完整的 Prompt
    R->>L: 6. 提交给大模型请求
    L-->>R: 7. 返回: "好的,正在为您办理订单12345的退款。"
    R->>M: 8. 将本次的一问一答追加到存储中
    R-->>U: 9. 返回最终回答
    deactivate R

核心概念解析:

  1. Session ID (会话标识):在客服系统中,可能有成千上万个用户同时在线。我们必须通过 session_id 来区分这是张三的工单,还是李四的工单。
  2. MessageHistory (消息历史):它是存储介质的抽象。在开发测试阶段,我们存在内存里(ChatMessageHistory);在生产环境中,我们必须存在 Redis 或数据库里(如 RedisChatMessageHistory)。
  3. Prompt Injection (提示词注入):LangChain 使用 MessagesPlaceholder 这个占位符,在发给 LLM 之前,动态地将拉取到的历史消息插入到 System Prompt 和 Human Prompt 之间。

💻 实战代码演练

废话不多说,Show me the code。 我们将使用最新的 LangChain Core 接口(LCEL)来重构我们的客服 Copilot。

环境准备: 请确保你安装了最新的库:pip install langchain-core langchain-openai

第一步:构建带记忆的客服 Chain

在这个 Demo 中,我们模拟一个处理售后退款的场景。

import os
from typing import Dict
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory

# 确保配置了你的 API KEY
# os.environ["OPENAI_API_KEY"] = "your-api-key"

# 1. 模拟一个生产环境的内存数据库,用于存储不同用户的会话
# 字典的 Key 是 session_id,Value 是历史记录对象
store: Dict[str, BaseChatMessageHistory] = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    """
    这是一个核心回调函数。
    当用户发来消息时,LangChain 会调用这个函数来获取对应的历史记录。
    如果 session_id 不存在,就创建一个新的。
    """
    if session_id not in store:
        # 在实际生产中,这里应该是从 Redis 或 MySQL 中读取历史记录
        store[session_id] = ChatMessageHistory()
        print(f"[系统日志] 为会话 {session_id} 创建了新的记忆库。")
    return store[session_id]

# 2. 定义客服 Copilot 的系统提示词 (System Prompt)
# 注意这里的 MessagesPlaceholder,它就是记忆注入的“插槽”
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一名金牌电商客服助理,名字叫'小智'。你的态度必须非常礼貌、专业。"
               "如果用户提到退款,你需要先确认他们的订单号。"),
    MessagesPlaceholder(variable_name="chat_history"), # 🌟 核心:历史消息占位符
    ("human", "{question}") # 当前用户的提问
])

# 3. 实例化大模型
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)

# 4. 使用 LCEL 组装基础 Chain
chain = prompt | llm

# 5. 🌟 核心魔法:用 RunnableWithMessageHistory 包装基础 Chain
# 它会自动拦截输入,拉取记忆,注入 prompt,并将输出写回记忆
copilot_with_memory = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="question",      # 告诉模型,用户的输入在字典的哪个 key 里
    history_messages_key="chat_history" # 告诉模型,历史记录要塞到哪个变量里
)

# ==========================================
# 🎬 模拟真实客服场景运行
# ==========================================

print("=== 👩‍🦰 用户 A (Session: user_A_101) 接入 ===")
response1 = copilot_with_memory.invoke(
    {"question": "你好,我买的那个机械键盘坏了,我想退货。"},
    config={"configurable": {"session_id": "user_A_101"}} # 传入会话 ID
)
print(f"🤖 小智: {response1.content}\n")
# 预期输出:询问订单号

print("=== 👩‍🦰 用户 A 继续回复 ===")
# 注意:我们这里没有再提“退货”和“键盘”,直接发了订单号
response2 = copilot_with_memory.invoke(
    {"question": "订单号是 KB20231024。"},
    config={"configurable": {"session_id": "user_A_101"}}
)
print(f"🤖 小智: {response2.content}\n")
# 预期输出:小智根据上下文,知道这个订单号是用来退机械键盘的

print("=== 👨‍🦱 用户 B (Session: user_B_999) 突然接入 ===")
# 测试会话隔离:用户 B 不应该知道用户 A 的事情
response3 = copilot_with_memory.invoke(
    {"question": "刚刚那个订单号记错啦,是 KB20231025!"},
    config={"configurable": {"session_id": "user_B_999"}}
)
print(f"🤖 小智: {response3.content}\n")
# 预期输出:小智会一脸懵,因为在 user_B_999 的记忆里,根本没有前面的对话

运行原理解剖:

在这个代码中,最精妙的设计是 get_session_historyRunnableWithMessageHistory 的配合。 作为架构师,我们要明白解耦的重要性。LangChain 把“如何处理 LLM”与“如何存储记忆”完全分开了。今天你可以用内存字典 store = {} 来测试,明天系统上线,你只需要把 get_session_history 里面的逻辑换成连接 Redis 的代码(例如使用 RedisChatMessageHistory),其他核心逻辑一行都不用改!这就是高级架构的美感。


🚧 坑与避坑指南

在我的职业生涯中,见过太多因为“记忆”处理不当导致的生产级事故。客服机器人的记忆不是越长越好,以下是三个必须规避的致命坑点:

坑一:无限增长的记忆导致 Token 破产 (OOM)

症状:客服机器人刚开始聊得很好,聊到第 20 回合时,突然报错 context_length_exceeded,或者月底你的 OpenAI 账单爆炸。 病因ChatMessageHistory 默认是无限追加的。前面说过,记忆是“强行喂饭”,聊得越多,每次发给大模型的文本就越长,消耗的 Token 呈指数级上升。 高阶解法:在生产环境中,绝对不能使用无限长度的记忆。你需要引入“滑动窗口”或“记忆摘要”。

  • 滑动窗口 (Window Memory):只保留最近的 N 轮对话。客服场景通常保留最近 5-10 轮即可。
  • 摘要记忆 (Summary Memory):后台跑一个小的 LLM 任务,定期把前 20 轮对话压缩成一段摘要(例如:“用户购买了键盘,正在申请退货,订单号已确认”),然后把摘要作为上下文发给大模型。

(注:在 LCEL 架构中,可以通过在 get_session_history 返回之前,对 messages 列表进行切片 messages[-10:] 来轻松实现滑动窗口。)

坑二:内存泄漏与无状态部署冲突

症状:在本地跑得好好的,一部署到 K8s 或 Serverless 平台上,机器人偶尔会失忆,或者串线。 病因:新手喜欢像 Demo 里那样用一个全局变量 store = {} 存记忆。但生产环境通常是多节点、多进程的(比如 Gunicorn 起了 4 个 Worker)。用户的上一句话打到了 Worker A 存进内存,下一句话被负载均衡分发到了 Worker B,Worker B 的内存里根本没有这个 session_id高阶解法计算与存储分离。永远不要在应用容器的内存里存状态。务必使用 Redis、PostgreSQL 或 MongoDB 来持久化 ChatMessageHistory

坑三:记忆太长导致“系统设定 (System Prompt) 遗忘”

症状:客服机器人原本被设定为“绝对不能骂人”。但在用户连续发了 30 条脏话后,机器人突然破防,开始和用户对骂。 病因:大模型的注意力机制(Attention)是有偏好的,它通常对 Prompt 的“开头”和“结尾”印象最深。如果中间插入了超长的历史对话,最顶部的 System Prompt 权重会被稀释,导致大模型“忘了自己是谁”。 高阶解法

  1. 像我们代码中那样,严格保证 System Prompt 在最前面。
  2. 采用 System Prompt Reminder 策略:在最后一条 Human Message 之前,再插入一条简短的 System Message 提醒(例如:“[系统提示:请保持客服的礼貌态度]”),强制拉回模型的注意力。

📝 本期小结

今天这节课,我们深入探讨了让大模型拥有记忆的艺术。

我们明确了“记忆即上下文注入”的本质,抛弃了容易产生技术债的老旧代码,使用最符合现代 LangChain 哲学的 RunnableWithMessageHistory 为我们的智能客服 Copilot 注入了灵魂。同时,我们站在架构师的视角,审视了 Token 消耗、分布式存储和注意力稀释这三大生产级深坑。

现在,你的客服小智已经不再是那只七秒记忆的金鱼了。它可以从容地应对用户的多轮追问,处理复杂的退换货上下文。

但是,仅仅有记忆就够了吗? 如果用户问:“你们最新的双十一退换货政策是什么?” 小智虽然记得用户是谁,但它的大脑里并没有你们公司最新的内部文档,它要么胡说八道(幻觉),要么只能抱歉。

如何让客服机器人拥有“专业知识”?如何让它能够读取你们公司的 PDF、Word 和内部 Wiki? 下一期,我们将进入 LangChain 最激动人心的篇章——RAG(检索增强生成)与向量数据库的碰撞

我们下期再见!保持热爱,继续 Coding!