纲要
- 记忆系统的核心挑战
- 多轮对话与上下文窗口限制
- 多用户会话隔离
- 技术选型:为什么选择
Redis - 架构设计
- 基础组件:
ConversationBufferMemory+RedisChatMessageHistory - 智能总结:自动摘要链防止上下文溢出
- 会话隔离:基于
session_id的 Key 管理
- 基础组件:
- 完整实现
- 项目结构
- 记忆管理器
MemoryManager类 - 总结链设计:保持人格统一
- 读写流程与自动触发逻辑
- 可运行示例
- 总结与相关度说明
记忆系统要解决什么问题
任何聊天机器人都面临两个核心问题:
- 上下文窗口有限:大模型一次能处理的 token 数量有上限。对话轮次增多,历史记录不断累积,迟早会撑爆窗口。
- 多用户隔离:张三和李四的对话不能混在一起,否则智能体会"串台",把别人的事当成你的事。
一个健壮的记忆系统必须在"记住足够上下文"和"不超出窗口限制"之间找到平衡,同时保证每个用户的记忆是独立的。
技术选型:为什么用 Redis
记忆存储有很多选择,但 Redis 在对话场景下有天然优势:
- 读写极快:内存存储,适合高频率的聊天消息存取。
- 数据结构匹配 :对话记录是一条一条追加的,与
Redis的 List 结构天然吻合。 - 键值隔离:通过不同的 Key 可以轻松实现多用户会话隔离。
LangChain 官方提供了 RedisChatMessageHistory 组件,可以将对话消息以标准格式直接存入 Redis,省去了手动序列化和解析的麻烦。
架构设计
#mermaid-svg-9udWuiqxhIfCrC3e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9udWuiqxhIfCrC3e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9udWuiqxhIfCrC3e .error-icon{fill:#552222;}#mermaid-svg-9udWuiqxhIfCrC3e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9udWuiqxhIfCrC3e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9udWuiqxhIfCrC3e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9udWuiqxhIfCrC3e .marker.cross{stroke:#333333;}#mermaid-svg-9udWuiqxhIfCrC3e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9udWuiqxhIfCrC3e p{margin:0;}#mermaid-svg-9udWuiqxhIfCrC3e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9udWuiqxhIfCrC3e .cluster-label text{fill:#333;}#mermaid-svg-9udWuiqxhIfCrC3e .cluster-label span{color:#333;}#mermaid-svg-9udWuiqxhIfCrC3e .cluster-label span p{background-color:transparent;}#mermaid-svg-9udWuiqxhIfCrC3e .label text,#mermaid-svg-9udWuiqxhIfCrC3e span{fill:#333;color:#333;}#mermaid-svg-9udWuiqxhIfCrC3e .node rect,#mermaid-svg-9udWuiqxhIfCrC3e .node circle,#mermaid-svg-9udWuiqxhIfCrC3e .node ellipse,#mermaid-svg-9udWuiqxhIfCrC3e .node polygon,#mermaid-svg-9udWuiqxhIfCrC3e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9udWuiqxhIfCrC3e .rough-node .label text,#mermaid-svg-9udWuiqxhIfCrC3e .node .label text,#mermaid-svg-9udWuiqxhIfCrC3e .image-shape .label,#mermaid-svg-9udWuiqxhIfCrC3e .icon-shape .label{text-anchor:middle;}#mermaid-svg-9udWuiqxhIfCrC3e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9udWuiqxhIfCrC3e .rough-node .label,#mermaid-svg-9udWuiqxhIfCrC3e .node .label,#mermaid-svg-9udWuiqxhIfCrC3e .image-shape .label,#mermaid-svg-9udWuiqxhIfCrC3e .icon-shape .label{text-align:center;}#mermaid-svg-9udWuiqxhIfCrC3e .node.clickable{cursor:pointer;}#mermaid-svg-9udWuiqxhIfCrC3e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9udWuiqxhIfCrC3e .arrowheadPath{fill:#333333;}#mermaid-svg-9udWuiqxhIfCrC3e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9udWuiqxhIfCrC3e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9udWuiqxhIfCrC3e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9udWuiqxhIfCrC3e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9udWuiqxhIfCrC3e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9udWuiqxhIfCrC3e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9udWuiqxhIfCrC3e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9udWuiqxhIfCrC3e .cluster text{fill:#333;}#mermaid-svg-9udWuiqxhIfCrC3e .cluster span{color:#333;}#mermaid-svg-9udWuiqxhIfCrC3e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9udWuiqxhIfCrC3e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9udWuiqxhIfCrC3e rect.text{fill:none;stroke-width:0;}#mermaid-svg-9udWuiqxhIfCrC3e .icon-shape,#mermaid-svg-9udWuiqxhIfCrC3e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9udWuiqxhIfCrC3e .icon-shape p,#mermaid-svg-9udWuiqxhIfCrC3e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9udWuiqxhIfCrC3e .icon-shape .label rect,#mermaid-svg-9udWuiqxhIfCrC3e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9udWuiqxhIfCrC3e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9udWuiqxhIfCrC3e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9udWuiqxhIfCrC3e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 历史记录
否
是
用户发送消息
Agent 执行器
get_memory 读取历史
Redis
条数 > 80?
返回原始历史
调用总结链压缩
清除旧记录
保存摘要到 Redis
LLM 生成回复
set_memory 存储新消息
核心思路:每次读取时检查当前会话的消息条数。若超过阈值(例如 80 条),触发总结链将历史压缩为一段摘要,清空旧记录后只保留摘要。这样上下文窗口永远不会无限膨胀,同时保留了关键信息。
完整实现
项目结构
dir
memory_demo/
├── config.py
├── memory_manager.py
├── main.py
└── .env
依赖安装
bash
pip install langchain langchain-openai langchain-community redis python-dotenv
配置文件 config.py
python
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
MAX_HISTORY_LENGTH = int(os.getenv("MAX_HISTORY_LENGTH", 80))
记忆管理器 memory_manager.py
这是记忆系统的核心,封装了读取、写入和自动总结的全部逻辑。
python
# memory_manager.py
from typing import List
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from config import Config
class MemoryManager:
"""带自动摘要的对话记忆管理器"""
def __init__(self, system_prompt: str = ""):
self.memory_key = "chat_history"
self.llm = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0,
openai_api_key=Config.OPENAI_API_KEY,
base_url=Config.OPENAI_BASE_URL,
)
# 构建总结链
summary_template = (
system_prompt + "\n\n"
"以下是你和用户之间的对话记录,请用第一人称「我」对其进行摘要,"
"提取关键信息,保留用户的核心诉求和你的处理结果。\n"
"对话记录:\n{chat_history}\n\n摘要:"
)
self.summary_prompt = ChatPromptTemplate.from_template(summary_template)
self.summary_chain = self.summary_prompt | self.llm | StrOutputParser()
def _get_history(self, session_id: str) -> RedisChatMessageHistory:
"""获取指定会话的 Redis 历史记录对象"""
return RedisChatMessageHistory(
session_id=session_id,
url=Config.REDIS_URL
)
def get_memory(self, session_id: str) -> ConversationBufferMemory:
"""读取会话记忆,超过阈值时自动压缩"""
history = self._get_history(session_id)
messages = history.messages
# 如果消息数量超过阈值,触发总结
if len(messages) > Config.MAX_HISTORY_LENGTH:
# 将所有消息格式化为文本
chat_text = ""
for msg in messages:
role = "用户" if msg.type == "human" else "助手"
chat_text += f"{role}: {msg.content}\n"
# 调用总结链生成摘要
summary = self.summary_chain.invoke({"chat_history": chat_text})
# 清空 Redis 中的旧记录
history.clear()
# 将摘要作为新的系统级消息存入
history.add_user_message("(以下为历史对话摘要)")
history.add_ai_message(summary)
# 返回标准的 ConversationBufferMemory
return ConversationBufferMemory(
memory_key=self.memory_key,
chat_memory=history,
return_messages=True,
)
def save_memory(self, session_id: str, user_input: str,
ai_output: str) -> None:
"""保存一轮对话到记忆"""
history = self._get_history(session_id)
history.add_user_message(user_input)
history.add_ai_message(ai_output)
演示主程序 main.py
模拟多轮对话,观察记忆读取和自动总结的行为。
python
# main.py
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools import tool
from config import Config
from memory_manager import MemoryManager
# 定义一个简单的工具供测试
@tool
def get_weather(city: str) -> str:
"""查询城市天气"""
return f"{city}今天晴天,25°C"
tools = [get_weather]
def main():
system_prompt = "你是小浪助手,友好且专业地回答用户问题。"
memory_manager = MemoryManager(system_prompt=system_prompt)
llm = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0,
openai_api_key=Config.OPENAI_API_KEY,
base_url=Config.OPENAI_BASE_URL,
)
# 模拟两个不同用户的会话
for user_id in ["user_001", "user_002"]:
print(f"\n=== 会话 {user_id} ===")
memory = memory_manager.get_memory(user_id)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
agent=agent, tools=tools, memory=memory,
verbose=False, handle_parsing_errors=True
)
# 第一轮
res = executor.invoke({"input": f"你好,我叫{user_id},请记住我的名字。"})
print("助手:", res["output"])
memory_manager.save_memory(user_id, f"你好,我叫{user_id},请记住我的名字。", res["output"])
# 第二轮
res = executor.invoke({"input": "我叫什么名字?"})
print("助手:", res["output"])
memory_manager.save_memory(user_id, "我叫什么名字?", res["output"])
# 检查记忆是否隔离
print(f"--- {user_id} 的 Redis 消息数: ---")
history = memory_manager._get_history(user_id)
print(len(history.messages))
if __name__ == "__main__":
main()
运行说明
- 确保本地
Redis服务已启动(默认localhost:6379)。 - 在
.env中填入OPENAI_API_KEY。 - 执行
python main.py,观察两个用户的记忆完全独立,互不干扰。
关键设计:总结链中的人格保持
在构造总结提示词时,刻意将原始的 system_prompt 拼接在最前面。这样总结出来的摘要仍然以"我是小浪助手"的第一人称视角叙述,避免了摘要内容变成冷冰冰的第三方记述,从而在后续对话中保持人格统一。许多开发者容易忽略这一点,导致压缩后的记忆与原始角色设定产生割裂。
总结
本文实现了一个基于 Redis 的智能体记忆系统,核心特性包括多用户会话隔离、自动触发摘要压缩、以及人格统一的第一人称总结。这套方案可直接嵌入任何 LangChain 单智能体项目,有效解决长对话上下文溢出的痛点。
本文覆盖了 Redis 记忆存储、会话隔离、总结链设计、自动压缩触发逻辑等全部技术细节,并提供了示例代码。