前端转agent-【python】- 14 记忆系统优化:摘要与遗忘

前端转 agent -【python】- 14 记忆系统优化:摘要与遗忘p

一个没有记忆的 AI,只是一个瞬间的函数映射:输入 → 输出,然后遗忘。

上回我们用 LangGraph 搭建了多 Agent 协作系统,让不同的 Agent 各司其职、协同工作。但有一个问题被我们忽略了------记忆

写 Vue 3 的同学都知道,组件之间的状态管理是个大问题。你用 Pinia 还是 provide/inject?状态多了怎么办?是不是得做数据归一化、定期清理?

AI Agent 面临同样的问题------对话越来越长,上下文窗口撑爆了怎么办?用户上次说了什么,这次还记得吗?

今天我们来聊聊记忆系统的摘要与遗忘机制,用 Ollama + Qwen3:4b 来实现一个会"记住"也会"忘记"的智能对话系统。

为什么 Agent 需要"遗忘"?

上下文窗口和记忆是两码事。上下文窗口只是让模型一次看到更多对话,像是扩大了工作台。但记忆不同,它让 Agent 能保存、更新和选择性回忆信息。

现在大家都在追求更大的上下文窗口,从 8K 到 32K,再到 128K、1M。但这种暴力扩张有个问题:计算成本呈二次方增长。处理 100K tokens 的成本是 10K tokens 的百倍。

更关键的是------人类会遗忘,不是大脑容量不够,而是遗忘让我们更高效。想象一下,如果你记得生活中的每个细节,这些信息会淹没真正重要的记忆。

Agent 也需要这种机制:

  • 存储成本:每个用户的历史数据都完整保存,存储需求会爆炸式增长
  • 检索效率:在海量历史中找到相关信息越来越慢
  • 注意力分散:太多无关信息会干扰 Agent 的决策
  • 隐私风险:永久保存所有对话增加了数据泄露的风险

Vue 3 类比:就像你不会把所有的路由历史、组件状态、用户操作日志都塞进一个 Pinia store 里。你会做数据归一化、按需加载、定期清理。Agent 的记忆系统也是同样的道理。

记忆系统的分层架构

在开始写代码之前,我们先建立一个心智模型。AI Agent 的记忆系统可以分成几个层次:

层级 说明 类比 Vue
短期记忆(工作记忆) 当前对话的上下文,几 K 到几十 K tokens 组件的 reactive 数据
中期记忆(摘要记忆) 对历史对话的压缩摘要 Pinia store 中的持久化状态
长期记忆 跨会话的结构化知识、用户偏好 浏览器 localStorage

今天我们的重点是中期记忆------当对话太长时,如何自动把历史对话"摘要"成精简版本,而不是简单地丢掉。

环境准备

bash 复制代码
# 安装 Ollama(如果还没装)
# Mac: brew install ollama
# Linux: curl -fsSL https://ollama.com/install.sh | sh

# 拉取 Qwen3:4b 模型
ollama pull qwen3:4b

# 安装 Python 依赖
pip install ollama langchain langchain-community

第一步:最朴素的实现------全量记忆

先来看看最原始的做法:把所有的对话历史都存下来,每次请求都全量发给模型。

python 复制代码
# full_memory.py
import ollama

class FullMemoryChat:
    def __init__(self, model="qwen3:4b"):
        self.model = model
        self.messages = []  # 全部存下来,一个不漏
    
    def chat(self, user_input: str) -> str:
        self.messages.append({"role": "user", "content": user_input})
        
        response = ollama.chat(
            model=self.model,
            messages=self.messages
        )
        
        reply = response["message"]["content"]
        self.messages.append({"role": "assistant", "content": reply})
        return reply
    
    def get_history(self):
        return self.messages

# 使用示例
chat = FullMemoryChat()
chat.chat("你好,我叫小明")
chat.chat("我喜欢编程")
chat.chat("我最近在学 Python")
print(f"总消息数: {len(chat.get_history())}")
# 输出: 总消息数: 6

问题很明显 :对话越长,messages 数组越大,每次请求的 token 数不断膨胀,最终撑爆上下文窗口。

Vue 类比:就像你在一个组件里用了一个巨大的数组存储所有用户操作日志,从不清理。页面越用越卡,最后直接崩溃。

第二步:滑动窗口------最简单的"遗忘"

最粗暴的优化:只保留最近 N 轮对话,远的直接丢掉。

python 复制代码
# memory_demo/sliding_window.py
import ollama
from collections import deque

class SlidingWindowChat:
    def __init__(self, model="qwen3:4b", max_turns=5):
        self.model = model
        self.max_turns = max_turns
        # 用 deque 自动管理容量
        self.messages = deque(maxlen=max_turns * 2)  # 每条消息算一个
    
    def chat(self, user_input: str) -> str:
        self.messages.append({"role": "user", "content": user_input})
        
        # 转成 list 给 ollama
        messages_list = list(self.messages)
        response = ollama.chat(
            model=self.model,
            messages=messages_list
        )
        
        reply = response["message"]["content"]
        self.messages.append({"role": "assistant", "content": reply})
        return reply
    
    def get_history(self):
        return list(self.messages)

# 使用示例
chat = SlidingWindowChat(max_turns=2)
chat.chat("我叫小明")
chat.chat("我喜欢编程")
chat.chat("我最近在学 Python")
chat.chat("你觉得我适合做什么工作?")
print(f"保留的消息数: {len(chat.get_history())}")
# 输出: 保留的消息数: 4(只保留了最近2轮对话)

优点 :实现简单,成本低。缺点:粗暴截断,丢失了重要的上下文信息。用户半小时前说的"我是后端开发",现在问"推荐什么技术栈",模型已经忘了。

Vue 类比 :就像你用 keep-alive 缓存组件,但设置了 max=3,最早的组件被销毁,状态丢失。用户切回来发现之前填的表单数据全没了。

第三步:摘要记忆------聪明的"压缩"

滑动窗口的问题是"遗忘"太彻底。更好的做法是:把旧对话压缩成一段摘要,和新对话一起发给模型

这就是 ConversationSummaryBufferMemory 的核心思想------结合了缓冲区和摘要两种策略。

python 复制代码
# summary_memory.py
import ollama
from typing import List, Dict

class SummaryMemoryChat:
    def __init__(self, model="qwen3:4b", max_tokens=500):
        self.model = model
        self.max_tokens = max_tokens  # 缓冲区最大 token 数
        self.buffer: List[Dict[str, str]] = []  # 最近的对话
        self.summary: str = ""  # 历史摘要
    
    def _count_tokens(self, messages: List[Dict[str, str]]) -> int:
        """粗略估算 token 数(实际生产用 tiktoken)"""
        return sum(len(m["content"]) // 2 for m in messages)
    
    def _generate_summary(self, messages: List[Dict[str, str]]) -> str:
        """用 LLM 生成摘要"""
        # 把要摘要的消息转成文本
        history_text = "\n".join([
            f"{m['role']}: {m['content']}" for m in messages
        ])
        
        prompt = f"""请将以下对话历史压缩成一段简短的摘要(50字以内),保留关键信息(如用户身份、偏好、已讨论的话题等):

{history_text}

摘要:"""
        
        response = ollama.generate(
            model=self.model,
            prompt=prompt
        )
        return response["response"].strip()
    
    def chat(self, user_input: str) -> str:
        # 1. 把用户消息加入缓冲区
        self.buffer.append({"role": "user", "content": user_input})
        
        # 2. 构建完整的上下文:摘要 + 缓冲区
        context_messages = []
        
        # 如果有摘要,先加进去(作为 system 消息)
        if self.summary:
            context_messages.append({
                "role": "system",
                "content": f"【历史对话摘要】{self.summary}"
            })
        
        # 再加缓冲区里的消息
        context_messages.extend(self.buffer)
        
        # 3. 调用模型
        response = ollama.chat(
            model=self.model,
            messages=context_messages
        )
        
        reply = response["message"]["content"]
        self.buffer.append({"role": "assistant", "content": reply})
        
        # 4. 检查缓冲区是否超限,超限则触发摘要
        if self._count_tokens(self.buffer) > self.max_tokens:
            self._compress()
        
        return reply
    
    def _compress(self):
        """压缩缓冲区:把早期对话变成摘要"""
        if len(self.buffer) <= 2:
            return
        
        # 保留最新的 2 条消息,其余全部拿去生成摘要
        keep = 2
        to_summarize = self.buffer[:-keep]
        self.buffer = self.buffer[-keep:]
        
        # 生成新摘要(合并旧摘要)
        if self.summary:
            # 如果已有摘要,把旧摘要也放进去一起压缩
            combined = [{"role": "system", "content": f"之前的摘要:{self.summary}"}] + to_summarize
            new_summary = self._generate_summary(combined)
        else:
            new_summary = self._generate_summary(to_summarize)
        
        self.summary = new_summary
        print(f"[摘要触发] 已压缩 {len(to_summarize)} 条消息,当前摘要:{self.summary}")

# 使用示例
chat = SummaryMemoryChat(max_tokens=200)  # 设置小一点方便触发摘要

for i in range(10):
    chat.chat(f"这是第{i+1}条消息,请记住我说的话")
    print(f"第{i+1}轮结束,缓冲区大小: {len(chat.buffer)}")
    if chat.summary:
        print(f"当前摘要: {chat.summary[:50]}...")

核心逻辑

  1. 维护一个 缓冲区(存最近的几条消息)
  2. 维护一个 摘要(存历史对话的压缩版本)
  3. 每次请求 = 摘要 + 缓冲区 一起发给模型
  4. 缓冲区超限时,把早期消息"摘要化",并入摘要中

Vue 类比:就像你用 Pinia 管理状态时,不会把所有操作日志都存下来。你会定期做"数据聚合"------把过去一周的点击事件聚合成"用户最常点击的模块 Top 5",然后清空原始日志。下次需要分析时,看聚合数据就够了。

第四步:加入遗忘机制------让记忆"自然衰减"

上面的摘要机制虽然聪明,但还有个问题:摘要永远不会被遗忘。随着对话越来越多,摘要会越来越长,最终还是会撑爆上下文。

真正的记忆系统需要遗忘机制------让旧信息随时间自然衰减。

DeepSeek 的做法很有启发:近期对话用高分辨率保存,一周前的对话降到中等分辨率,久远的记忆压缩到最小。

我们来实现一个带"衰减"的摘要系统:

python 复制代码
# memory_demo/decay_memory.py
import ollama
import time
from typing import List, Dict, Optional
from dataclasses import dataclass

@dataclass
class MemoryItem:
    content: str
    timestamp: float
    importance: float  # 0-1,重要性分数

class DecayMemoryChat:
    def __init__(self, model="qwen3:4b", max_buffer_tokens=300, decay_rate=0.01):
        self.model = model
        self.max_buffer_tokens = max_buffer_tokens
        self.decay_rate = decay_rate  # 衰减速率
        
        self.buffer: List[Dict[str, str]] = []  # 近期对话
        self.long_term: List[MemoryItem] = []   # 长期记忆(带时间戳)
        self.summary: str = ""
    
    def _get_decay_factor(self, item: MemoryItem) -> float:
        """计算衰减因子:时间越久,值越小"""
        age = time.time() - item.timestamp
        # 指数衰减:e^(-decay_rate * age)
        return max(0.1, min(1.0, 2.71828 ** (-self.decay_rate * age / 60)))
    
    def _should_forget(self, item: MemoryItem) -> bool:
        """判断是否应该遗忘"""
        decay = self._get_decay_factor(item)
        # 重要性 * 衰减因子 < 阈值 => 遗忘
        return item.importance * decay < 0.1
    
    def _extract_importance(self, message: Dict[str, str]) -> float:
        """用 LLM 判断消息的重要性"""
        prompt = f"""判断以下对话内容的重要性(0-1之间),只输出一个数字:
- 1.0: 非常关键(用户身份、核心偏好、重大决策)
- 0.5: 一般重要(普通讨论)
- 0.1: 不重要(闲聊、寒暄)

内容:{message['content']}

重要性分数:"""
        
        response = ollama.generate(
            model=self.model,
            prompt=prompt
        )
        try:
            return float(response["response"].strip())
        except:
            return 0.5
    
    def chat(self, user_input: str) -> str:
        # 1. 加入缓冲区
        self.buffer.append({"role": "user", "content": user_input})
        
        # 2. 构建上下文
        context = []
        if self.summary:
            context.append({"role": "system", "content": f"【历史摘要】{self.summary}"})
        
        # 加入长期记忆中"存活"的项目
        alive_items = [
            item for item in self.long_term 
            if not self._should_forget(item)
        ]
        if alive_items:
            memory_text = "\n".join([
                f"- {item.content}" for item in alive_items[-5:]  # 只取最近的5条
            ])
            context.append({"role": "system", "content": f"【长期记忆】\n{memory_text}"})
        
        context.extend(self.buffer)
        
        # 3. 调用模型
        response = ollama.chat(
            model=self.model,
            messages=context
        )
        reply = response["message"]["content"]
        self.buffer.append({"role": "assistant", "content": reply})
        
        # 4. 提取重要信息存入长期记忆
        for msg in self.buffer[-2:]:  # 只处理最新的两条
            if msg["role"] == "user":
                importance = self._extract_importance(msg)
                if importance > 0.6:  # 只有足够重要的才存入长期记忆
                    self.long_term.append(MemoryItem(
                        content=msg["content"],
                        timestamp=time.time(),
                        importance=importance
                    ))
                    print(f"[记忆存储] 重要性={importance:.2f}: {msg['content'][:30]}...")
        
        # 5. 清理被遗忘的项目
        forgotten = [item for item in self.long_term if self._should_forget(item)]
        if forgotten:
            self.long_term = [item for item in self.long_term if not self._should_forget(item)]
            print(f"[遗忘触发] 遗忘了 {len(forgotten)} 条记忆")
        
        # 6. 缓冲区压缩
        if sum(len(m["content"]) // 2 for m in self.buffer) > self.max_buffer_tokens:
            self._compress()
        
        return reply
    
    def _compress(self):
        """压缩缓冲区到摘要"""
        if len(self.buffer) <= 2:
            return
        
        keep = 2
        to_summarize = self.buffer[:-keep]
        self.buffer = self.buffer[-keep:]
        
        history_text = "\n".join([
            f"{m['role']}: {m['content']}" for m in to_summarize
        ])
        prompt = f"将以下对话压缩为一句摘要(30字内):\n{history_text}\n摘要:"
        response = ollama.generate(model=self.model, prompt=prompt)
        
        if self.summary:
            self.summary = f"{self.summary};{response['response'].strip()}"
        else:
            self.summary = response["response"].strip()
        
        # 摘要太长时,再次压缩
        if len(self.summary) > 200:
            prompt = f"将以下摘要进一步压缩为20字以内:\n{self.summary}\n压缩后:"
            response = ollama.generate(model=self.model, prompt=prompt)
            self.summary = response["response"].strip()
        
        print(f"[摘要更新] {self.summary}")

# 使用示例
chat = DecayMemoryChat(decay_rate=0.02)

# 模拟多轮对话
chat.chat("我叫小明,是一名后端开发,主要用 Python")
chat.chat("我最近在研究 AI Agent")
chat.chat("今天天气不错")
chat.chat("我想做一个智能客服系统")

# 查看长期记忆状态
print(f"\n长期记忆条数: {len(chat.long_term)}")
for item in chat.long_term:
    decay = chat._get_decay_factor(item)
    print(f"  - {item.content[:30]}... (重要性={item.importance:.2f}, 衰减={decay:.2f})")

这个设计模仿了人类的记忆机制

  • 重要性判断:重要的信息(用户身份、核心偏好)被标记为高重要性
  • 时间衰减:再重要的信息,时间久了也会慢慢淡化
  • 主动遗忘:重要性 × 衰减因子 < 阈值时,信息被主动遗忘

Vue 类比:就像你在做前端监控时,不会把所有日志都存下来。你会给日志打上"严重程度"标签,Error 级别的永久保留,Info 级别的 7 天自动清理。这就是"重要性 + 时间衰减"的遗忘机制。

总结

今天我们聊了 AI Agent 记忆系统的三个层次:

方案 优点 缺点 适用场景
全量记忆 信息完整 token 爆炸 短对话
滑动窗口 简单高效 信息丢失严重 无关紧要的闲聊
摘要记忆 保留关键信息 摘要可能膨胀 中等长度对话
衰减记忆 模拟人类遗忘 实现复杂 长期、大规模对话系统

核心 takeaways

  1. 记忆不是存得越多越好------遗忘是一种过滤机制,帮我们保留有价值的信息
  2. 摘要 + 缓冲区是性价比最高的方案------既保留了上下文,又控制了 token 成本
  3. 重要性 + 时间衰减是更高级的遗忘策略------让记忆系统像人一样"自然老化"
  4. 记忆系统不是"让模型变聪明",而是让智能体"不会重复犯同样的错"

记忆系统的关键在于:不是存储,而是组织和衰减。对话历史是日志,向量检索只是工具,真正的记忆需要结构化、冲突解决和定期维护。

相关推荐
Bigfish_coding1 小时前
前端转agent-【python】-13 Ollama Python流式输出教程:stream=True 与 async 实践
人工智能
字节跳动数据库4 小时前
文章分享——相似函数处理方法
人工智能·后端·程序员
Bigfish_coding4 小时前
前端转agent-【python】-12 LangChain 入门实战:RAG + LCEL 链式调用
人工智能
程序员cxuan4 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
饼干哥哥4 小时前
扣子3.0测评:我让 Codex 和 Claude Code 住同一个桌面,结果它们打架了!
人工智能·开源·代码规范
Token炼金师5 小时前
IP-Adapter:解耦交叉注意力如何让扩散模型看见图像
人工智能
Bigfish_coding5 小时前
前端转agent-【python】-11 LangGraph 高级特性:时间旅行与人工介入
人工智能
Token炼金师5 小时前
从safetensors到像素:ComfyUI Checkpoint加载机制的底层拆解
人工智能
AI闲人5 小时前
AI 写代码越来越快,为什么 Code Review 反而更慢了?
人工智能·code review·ai 编程