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 的出现,就是为了解决这些问题。 它主要负责:
-
标准化:统一封装不同模型(DeepSeek, OpenAI)的调用接口。
-
自动化:自动完成“读取历史 -> 拼装 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作为优化策略,理由如下:
-
保持“人设”的鲜活感:
我们的机器人是“傲娇酱”,她需要根据用户上一句话的语气做出反应。如果使用纯摘要,具体的语气词可能会丢失;而混合记忆保留了最近的几轮原话,保证了回复的“味道”不对冲。 -
长期记住用户是谁:
用户可能会在第一句介绍“我叫阿强”,然后聊了 500 轮别的。如果是滑动窗口,“阿强”这个名字早丢了;而混合记忆会把它压缩进摘要里(例如:“User is named A-Qiang...”),永远不会忘。 -
成本与体验的平衡:
我们设定一个阈值(如 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 loggerclass LLMClient:def __init__(self):# 实例化 LangChain 的 ChatOpenAI 对象# 这里的参数非常关键,决定了能不能连上国内的大模型self.llm = ChatOpenAI(model=settings.MODEL_NAME, # 例如: qwen-plus / deepseek-chatopenai_api_key=settings.API_KEY, # 从 settings 读取 Keyopenai_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. 获取 LLMclient = 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 -> LLMchain = prompt | llm# 5. 挂载记忆系统# 这一步把 Chain 变成了带记忆的 Chainwith_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"]:breakif user_input.strip():try:# 调用 invoke,必须传入 session_idresponse = with_message_history.invoke({"input": user_input},config={"configurable": {"session_id": session_id}})# LCEL 返回的是 AIMessage 对象,取 contentprint(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 进行情绪识别,让傲娇酱在你难过的时候,也会变得温柔起来。
