AI Agent白手起家57: 上下文记忆系统——基于 Redis 的长短期记忆实现

纲要

  • 记忆系统的核心挑战
    • 多轮对话与上下文窗口限制
    • 多用户会话隔离
  • 技术选型:为什么选择 Redis
  • 架构设计
    • 基础组件:ConversationBufferMemory + RedisChatMessageHistory
    • 智能总结:自动摘要链防止上下文溢出
    • 会话隔离:基于 session_id 的 Key 管理
  • 完整实现
    • 项目结构
    • 记忆管理器 MemoryManager
    • 总结链设计:保持人格统一
    • 读写流程与自动触发逻辑
  • 可运行示例
  • 总结与相关度说明

记忆系统要解决什么问题

任何聊天机器人都面临两个核心问题:

  1. 上下文窗口有限:大模型一次能处理的 token 数量有上限。对话轮次增多,历史记录不断累积,迟早会撑爆窗口。
  2. 多用户隔离:张三和李四的对话不能混在一起,否则智能体会"串台",把别人的事当成你的事。

一个健壮的记忆系统必须在"记住足够上下文"和"不超出窗口限制"之间找到平衡,同时保证每个用户的记忆是独立的。

技术选型:为什么用 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()

运行说明

  1. 确保本地 Redis 服务已启动(默认 localhost:6379)。
  2. .env 中填入 OPENAI_API_KEY
  3. 执行 python main.py,观察两个用户的记忆完全独立,互不干扰。

关键设计:总结链中的人格保持

在构造总结提示词时,刻意将原始的 system_prompt 拼接在最前面。这样总结出来的摘要仍然以"我是小浪助手"的第一人称视角叙述,避免了摘要内容变成冷冰冰的第三方记述,从而在后续对话中保持人格统一。许多开发者容易忽略这一点,导致压缩后的记忆与原始角色设定产生割裂。

总结

本文实现了一个基于 Redis 的智能体记忆系统,核心特性包括多用户会话隔离、自动触发摘要压缩、以及人格统一的第一人称总结。这套方案可直接嵌入任何 LangChain 单智能体项目,有效解决长对话上下文溢出的痛点。

本文覆盖了 Redis 记忆存储、会话隔离、总结链设计、自动压缩触发逻辑等全部技术细节,并提供了示例代码。

相关推荐
Leo.yuan1 小时前
给数据团队的AI智能体选型参考:2026年8款AI数据分析产品深度测评
人工智能
寥落半伤感1 小时前
codex接入deepseek+VLM视觉语言模型教程
人工智能·语言模型·自然语言处理·codex·deepseek
FlyWIHTSKY1 小时前
idea中集成claude功能
java·ide·人工智能·intellij-idea·cloudera
IT_陈寒1 小时前
SpringBoot自动配置坑了我三天,原来漏了这个注解
前端·人工智能·后端
zandy10111 小时前
主流 AI Agent搜索Skill推荐及高阶选型指南
大数据·人工智能
Greg_Zhong1 小时前
微信小程序 + 腾讯云人体分析:从 0 到 1 实现 AI 抠图打卡合照(细节待更新~)
人工智能·微信小程序·腾讯云·ai抠图-ai打卡拍照
xingyuzhisuan1 小时前
团队实践:引入 Vera 1.1 之后,视频内容团队工作流重构
人工智能·重构·音视频
晴天161 小时前
Cordis 框架代码核心解析:一个可逆插件系统的实现-Day18
人工智能·ai·架构