【AI实战日记-手搓聊天机器人】Day 4:告别金鱼记忆!LangChain 记忆原理与 Token 成本优化实战

Day 3 我们完成了代码重构。今天是 Day 4,我们将攻克 LLM 应用开发中最基础也最重要的功能——Memory(记忆)。为了解决原生 API 的“无状态”问题,我引入了 LangChain 框架。本文将首先揭示 LangChain 记忆管理的底层原理(4步闭环),随后以架构师的视角指出“全量记忆”带来的 Token 爆炸 隐患,并最终采用 ConversationSummaryBufferMemory(混合摘要记忆) 策略,实现了一个既能记住用户,又能自动压缩历史记录的低成本、高可用记忆系统。

一、 项目进度:Day 4 启动

根据15天路线图,今天是 Phase 2 的第一天。
我们正式引入 LangChain,目标是解决“连贯对话”与“成本控制”的双重挑战。


二、 核心原理:为什么引入 LangChain 及其记忆机制

在动手写代码前,我们需要先理解架构设计的初衷。为什么不直接用 Python 列表存对话?LangChain 到底在后台帮我们做了什么?

1. 为什么要引入 LangChain?

在 Day 3 之前,我们是直接调用 openai 原生库。对于单轮对话这没问题,但一旦涉及到多轮对话(记忆),如果我们继续“裸写”代码,会面临两大痛点:

  • 痛点 A:繁琐的字符串拼接
    我们需要手动维护一个 history_list,每次都要写代码去遍历它,把它格式化成字符串,再拼接到 System Prompt 后面。代码不仅丑陋,而且容易出错。

  • 痛点 B:难以扩展
    今天我们把记忆存在内存的 List 里,明天如果想存到 Redis 数据库里怎么办?后天如果想用“摘要记忆”怎么办?如果裸写代码,每次都要重写底层逻辑。

LangChain 的出现,就是为了解决这些问题。 它主要负责:

  1. 标准化:统一封装不同模型(DeepSeek, OpenAI)的调用接口。

  2. 自动化:自动完成“读取历史 -> 拼装 Prompt -> 自动保存”的繁琐闭环。

2. 底层揭秘:LangChain 是如何管理记忆的?

在代码中,我们将使用 ConversationChain。你可以把它想象成一个 “极其负责任的秘书”。当你调用 chain.predict(input="我叫什么?") 时,LangChain 实际上在后台按顺序执行了 4 个隐藏步骤

  • 🟢 阶段一:读取 (Load)

    • 秘书(Chain)先不去打扰大模型,而是先翻开“记事本”(Memory组件)。

    • 它会查阅之前所有的对话记录:[User: 我叫小明, AI: 你好小明...]。

  • 🟡 阶段二:注入 (Inject)

    • 秘书拿着这些记录,自动把它们格式化,并填入 Prompt Template 中的 MessagesPlaceholder(记忆插槽)位置。

    • 此时,发给 AI 的实际 Prompt 变成了:

      System: 你是傲娇助手...
      History: [User: 我叫小明, AI: 你好小明]  <-- LangChain 自动插入的
      User: 我叫什么名字?
  • 🔴 阶段三:执行 (Execution)

    • 秘书把这个拼装好的长文本发送给 DeepSeek / 通义千问。

    • AI 看到历史记录,于是回答:“你叫小明。”

  • 🔵 阶段四:保存 (Save)

    • 拿到 AI 的回复后,秘书并不会下班。

    • 它反手把 你的新问题 和 AI 的新回答,再次写入“记事本”(Memory)中,供下一次使用。


三、 架构思考:从“能用”到“好用”的优化

虽然 LangChain 的基础组件 ConversationBufferMemory 能实现全量记忆,但作为架构师,我看到了一个巨大的隐患:Token 爆炸

1. 什么是 Token 爆炸?

如果用户和 AI 聊了 1000 轮,全量记忆意味着每次都要把这 1000 轮对话发给 API。

  • 后果 A(烧钱):费用指数级上升。

  • 后果 B(崩溃):超出模型的最大上下文限制(Context Window),程序直接报错。

2. 优化方案:LangChain 记忆策略

为了解决这个问题,LangChain 提供了多种记忆管理策略。我整理了一份对比表,帮助大家理解不同方案的适用场景。

记忆类型  原理描述 优点  缺点  适用场景

ConversationBufferMemory

(全量记忆)

简单粗暴:将所有历史对话原封不动地放入 Prompt。

1. 信息最完整,无任何丢失。

2. 调试简单直观。

1. 烧钱:Token 消耗呈指数级增长。

2. 易崩:很快会超出模型最大长度限制。

仅适合短轮次的测试或Demo。

ConversationBufferWindowMemory

(滑动窗口记忆)

健忘:只保留最近的K轮对话(例如最近10句),旧的直接丢弃。

1. Token 消耗恒定且可控。

2. 永远不会撑爆模型。

1. 丢失关键信息:以前提到的名字、喜好,聊久了就忘了。 适合不需要长期记忆的闲聊工具。

ConversationSummaryMemory

(摘要记忆)

概括:每次对话后,调用 LLM 把历史记录总结成一段摘要。

1. 极其节省 Token(几千字变几十字)。

2. 能保留长期关键信息。

1. 丢失细节:语气的微小差别会被抹去。

2. 延迟高:每次都要额外调用 LLM 生成摘要。

适合长篇大论的文档分析或会议总结。
ConversationSummaryBufferMemory
(混合摘要记忆)
智能:保留最近 N个 Token 的原话 + 更早历史的摘要。

1. 兼顾细节与长久:近期对话鲜活,远期记忆不丢。

2. 成本可控:自动压缩旧数据。

1. 配置稍复杂。2. 生成摘要时仍需消耗少量算力。 最适合复杂的 AI 智能体/伴侣应用。

2) 架构决策:为什么选择“混合摘要记忆”?

最终选择 ConversationSummaryBufferMemory作为优化策略,理由如下:

  1. 保持“人设”的鲜活感
    我们的机器人是“傲娇酱”,她需要根据用户上一句话的语气做出反应。如果使用纯摘要,具体的语气词可能会丢失;而混合记忆保留了最近的几轮原话,保证了回复的“味道”不对冲。

  2. 长期记住用户是谁
    用户可能会在第一句介绍“我叫阿强”,然后聊了 500 轮别的。如果是滑动窗口,“阿强”这个名字早丢了;而混合记忆会把它压缩进摘要里(例如:“User is named A-Qiang...”),永远不会忘。

  3. 成本与体验的平衡
    我们设定一个阈值(如 2000 Token)。只有当对话真的长到一定程度时,才触发总结。这意味着在短对话中,它响应极快;在长对话中,它又足够稳健。

结论:这是目前实现“类人记忆”性价比最高的工程化方案。


四、 实战:代码实现

理论讲完了,现在开始代码进行“手术”,植入 LangChain 核心。

⏳ 第一步:安装“全家桶”依赖

LangChain 现已拆分为多个包,我们需要安装核心库和 OpenAI 适配器。

pip install langchain-core langchain-openai langchain-community

🛠️ 第二步:改造底层适配器 (src/core/llm.py)

我们需要让 LLMClient 返回一个 LangChain 兼容的对象,而不是原生的 OpenAI 客户端。

# 引入 LangChain 的 OpenAI 封装器
from langchain_openai import ChatOpenAI
from src.config.settings import settings
from src.utils.logger import logger

class LLMClient:
    def __init__(self):
        # 实例化 LangChain 的 ChatOpenAI 对象
        # 这里的参数非常关键,决定了能不能连上国内的大模型
        self.llm = ChatOpenAI(
            model=settings.MODEL_NAME,         # 例如: qwen-plus / deepseek-chat
            openai_api_key=settings.API_KEY,   # 从 settings 读取 Key
            openai_api_base=settings.BASE_URL, # 【关键】这里指向国内厂商的 API 地址
            temperature=0.7,                   # 创造性 (0-1)
            streaming=False                    # 暂时关闭流式输出,方便调试
        )
        logger.info(f"✅ LangChain LLM 初始化完成: {settings.MODEL_NAME}")

    def get_client(self):
        """返回 LangChain 的 LLM 实例"""
        return self.llm

🔗 第三步:组装“记忆链” (main.py)

这是今天的重头戏。我们将在这里组装 LLM + Prompt + 混合记忆

# ==========================================
# Day 4: Memory Management (LCEL 新版架构)
# ==========================================

# 1. 引入 LCEL 核心组件
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import trim_messages # 新版 Token 管理工具

from src.core.llm import LLMClient
from src.core.prompts import PROMPTS
from src.utils.logger import logger

# --- 全局变量:用于模拟数据库存储 Session ---
# 在 Day 6 我们会把它换成 Redis
store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    """
    根据 session_id 获取对应的聊天记录
    """
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

def main():
    logger.info("--- Project Echo: Day 4 (LCEL Version) ---")
    
    # 1. 获取 LLM
    client = LLMClient()
    llm = client.get_client()
    
    # 2. 定义 Token 管理策略 (替代旧的 SummaryBuffer)
    # 策略:保留最后 20 条消息 (约等于之前的 WindowMemory)
    # LCEL 的 trimmer 更加灵活,这里先用简单的保留策略
    trimmer = trim_messages(
        max_tokens=2000,
        strategy="last",
        token_counter=llm,
        include_system=True,
    )

    # 3. 构建 Prompt (LCEL 风格)
    # 加载傲娇酱人设
    sys_prompt = PROMPTS["tsundere"]
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", sys_prompt),
        MessagesPlaceholder(variable_name="history"), # 历史记录插槽
        ("human", "{input}")
    ])

    # 4. 组装链条 (The Chain)
    # 逻辑:Prompt -> LLM
    chain = prompt | llm

    # 5. 挂载记忆系统
    # 这一步把 Chain 变成了带记忆的 Chain
    with_message_history = RunnableWithMessageHistory(
        chain,
        get_session_history, # 传入获取历史记录的函数
        input_messages_key="input",
        history_messages_key="history",
    )

    print("\n💡 Tip: 输入 'quit' 退出\n")
    
    # 模拟一个 Session ID (比如用户的 ID)
    session_id = "user_001"

    while True:
        user_input = input("You: ")
        if user_input.lower() in ["quit", "exit"]:
            break
            
        if user_input.strip():
            try:
                # 调用 invoke,必须传入 session_id
                response = with_message_history.invoke(
                    {"input": user_input},
                    config={"configurable": {"session_id": session_id}}
                )
                
                # LCEL 返回的是 AIMessage 对象,取 content
                print(f"Bot: {response.content}\n")
                
            except Exception as e:
                logger.error(f"调用失败: {e}")

if __name__ == "__main__":
    main()

五、 运行与验证

运行 python main.py,因为开启了 verbose=True,我们可以通过控制台日志验证优化效果。

1. 验证“记住我”

You: 我叫阿强。
Bot: 哼,阿强... 这种土名字本小姐记住了。
You: 我叫什么?
Bot: 刚才不是说了吗?阿强!你是不是金鱼脑子?

2. 验证“摘要优化” (模拟长对话后)
当对话量巨大,超过 2000 Token 时,你会发现日志中的 History 发生了变化:

  • Before: [User:你好, AI:你好, User:吃了吗, AI:吃了, ...] (几十条记录)

  • After:

    • System: (摘要) "The user is named A-Qiang. They discussed weather and lunch."

    • Human: "最近怎么样?" (只保留最近一条)

  • 结论:以前的废话被压缩了,但关键信息(名字)依然保留。这就是混合记忆的威力。


六、 总结与预告

今天我们通过引入 LangChain 和 SummaryBufferMemory,完美解决了 AI 的记忆问题和成本问题。这是一个真正具备工程思维的方案。

  • 能力上:AI 拥有了无限的短期记忆。

  • 成本上:通过“摘要机制”,把 Token 消耗控制在了一个固定范围内。

明日预告 (Day 5)
有了记忆,机器人还是个直男怎么办?明天 Day 5,我们将给它装上“情商”,利用 LLM 进行情绪识别,让傲娇酱在你难过的时候,也会变得温柔起来。

posted @ 2026-04-24 01:02  FBshark  阅读(13)  评论(0)    收藏  举报