智能体的记忆系统:为什么你的Agent总是“忘了上回说了什么“

一个真实场景

上周测试一个同行的产品——某公司做的"智能客服Agent"。我故意在对话中先说"我想看看你们的A产品",Agent详细介绍了A产品。然后我又说"帮我换成蓝色的"。

它回我:"请问您想更换什么产品的颜色呢?"

它忘了"刚才"在聊A产品。

这不是个别现象。你如果用过大模型的原生API,你就知道为什么——每次调用都是一个独立的HTTP请求,模型没有任何"记忆"。所谓的上下文窗口,不过是你把之前的对话历史拼成一个巨大的字符串塞进prompt里。

对话一长,要么超出上下文窗口被截断,要么关键信息被淹没在几万字的对话历史里。

这就是为什么你的Agent"记不住事"。

不是模型笨,是你没有给它设计记忆系统。


记忆系统的三层架构

人类记忆有短期记忆、长期记忆、工作记忆。智能体也一样。

记忆类型作用持续时间容量技术方案
短期记忆当前对话上下文单次会话受上下文窗口限制消息历史管理
长期记忆跨会话的知识/偏好永久理论上无限向量数据库 + RAG
工作记忆当前任务的中间状态任务执行期间结构化存储状态机 / 数据库

下面一层一层拆开讲。


第一层:短期记忆——管好你的上下文窗口

问题本质

大模型的上下文窗口是有限的。GPT-4o是128K token,Claude 3.5是200K token,国产模型大多在32K-128K之间。

听起来很大?但你算一笔账:

  • 系统Prompt:500-2000 token
  • 工具定义(function calling schema):500-3000 token
  • RAG检索回来的上下文:1000-5000 token
  • 对话历史:每轮约200-500 token

如果你的对话超过50轮,光对话历史就可能吃掉2万-2.5万token。 再加上系统Prompt、工具定义和RAG结果,很容易逼近上限。

而且,上下文窗口越大,推理成本越高、延迟越大。不是窗口能装下就万事大吉——装得下不代表装得有效率。

解决方案:滑动窗口 + 摘要压缩

最基础的策略是滑动窗口——只保留最近N轮对话,更早的直接丢掉。

class ShortTermMemory:
    """短期记忆:滑动窗口管理"""
    
    def __init__(self, max_turns: int = 20):
        self.max_turns = max_turns
        self.messages = []
    
    def add(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        self._trim()
    
    def get_context(self) -> list[dict]:
        """返回当前上下文"""
        return self.messages
    
    def _trim(self):
        """超过窗口大小时,保留最近的对话"""
        if len(self.messages) > self.max_turns * 2:  # user+assistant算2条
            # 保留system prompt(如果有)+ 最近N轮
            system_msgs = [m for m in self.messages if m["role"] == "system"]
            recent = self.messages[-(self.max_turns * 2):]
            self.messages = system_msgs + recent

问题: 直接丢弃早期对话,意味着Agent会"失忆"——用户开头说的重要信息(比如"我是做医疗行业的")可能被截掉。

升级方案:摘要压缩

不直接丢弃,而是用LLM把早期对话压缩成一段摘要,保留在系统Prompt里:

class SummarizedShortTermMemory(ShortTermMemory):
    """带摘要压缩的短期记忆"""
    
    def __init__(self, max_turns: int = 20, llm_client=None):
        super().__init__(max_turns)
        self.llm = llm_client
        self.summary = ""
    
    async def _trim(self):
        if len(self.messages) > self.max_turns * 2:
            # 把要丢弃的对话提取出来
            to_discard = self.messages[1:-(self.max_turns * 2)]  # 跳过system
            
            if to_discard:
                # 用LLM生成摘要
                new_summary = await self.llm.generate(
                    "请将以下对话历史压缩成一段简洁的摘要,保留关键信息和用户偏好:\n\n"
                    + "\n".join(f"{m['role']}: {m['content']}" for m in to_discard)
                    + f"\n\n已有摘要:{self.summary}"
                )
                self.summary = new_summary
            
            # 保留system prompt + 摘要 + 最近对话
            system_msgs = [m for m in self.messages if m["role"] == "system"]
            recent = self.messages[-(self.max_turns * 2):]
            
            # 把摘要注入system prompt
            system_msgs[0]["content"] += f"\n\n[历史对话摘要] {self.summary}"
            self.messages = system_msgs + recent

效果: 对话100轮,Agent仍然记得用户第5轮提到的行业背景——因为那条信息已经被压缩进了摘要里。

工程取舍

方案信息保留Token成本实现复杂度
直接截断差(早期信息丢失)极低
滑动窗口+摘要好(关键信息保留)中(多一次LLM调用)
全量保留最好高(成本高、延迟大)

经验法则: 如果你的场景对话超过20轮,用"滑动窗口+摘要"。如果超过100轮,考虑加长期记忆。


第二层:长期记忆——让Agent"记住"跨会话的信息

问题场景

  • 用户周一说:"我们公司做医疗器械,有200个员工。"
  • 用户周三来了:"帮我做个方案。"
  • Agent:"请问您是做什么行业的?公司规模多大?"

用户直接炸了。Agent没有跨会话的记忆能力。

解决方案:向量数据库 + RAG

长期记忆的核心思路:把对话中的关键信息提取出来,存进向量数据库。下次对话时,根据当前话题检索相关记忆。

[用户输入] → 向量检索 → 召回相关记忆 → 注入上下文 → 模型推理

存储层设计

from datetime import datetime

class LongTermMemory:
    """长期记忆:基于向量数据库的跨会话记忆"""
    
    def __init__(self, vector_store, embedding_model, llm_client):
        self.vector_store = vector_store  # 向量数据库(如Milvus/Qdrant/Chroma)
        self.embedding = embedding_model
        self.llm = llm_client
    
    async def store(self, user_id: str, conversation: list[dict]):
        """从对话中提取关键信息并存储"""
        
        # Step 1: 用LLM提取关键事实
        facts = await self._extract_facts(conversation)
        
        # Step 2: 向量化并存入数据库
        for fact in facts:
            embedding = await self.embedding.encode(fact["content"])
            self.vector_store.insert({
                "user_id": user_id,
                "content": fact["content"],
                "category": fact["category"],  # 行业/偏好/需求/背景
                "embedding": embedding,
                "timestamp": datetime.now().isoformat(),
                "source_session": fact.get("session_id")
            })
    
    async def recall(self, user_id: str, query: str, top_k: int = 5) -> list[dict]:
        """根据当前查询,召回相关记忆"""
        
        query_embedding = await self.embedding.encode(query)
        
        results = self.vector_store.search(
            embedding=query_embedding,
            filter={"user_id": user_id},
            top_k=top_k
        )
        
        return results
    
    async def _extract_facts(self, conversation: list[dict]) -> list[dict]:
        """用LLM从对话中提取值得长期记忆的事实"""
        
        prompt = """从以下对话中提取需要长期记忆的关键事实。
        
只提取以下类型的信息:
- 用户的身份信息(姓名、公司、行业、职位)
- 用户的明确偏好(喜欢的风格、预算范围)
- 用户的具体需求(想做什么产品、目标是什么)
- 重要的业务背景(公司规模、现有系统、技术栈)

不要提取:寒暄、确认语、一次性指令。

输出JSON数组格式:
[{"content": "用户是一家200人医疗器械公司的CTO", "category": "身份背景"},
 {"content": "用户预算在10-20万之间", "category": "偏好"}]

对话内容:
"""
        conversation_text = "\n".join(
            f"{m['role']}: {m['content']}" for m in conversation
        )
        
        result = await self.llm.generate(prompt + conversation_text)
        return json.loads(result)

检索与注入

class AgentWithLongTermMemory:
    """带长期记忆的Agent"""
    
    def __init__(self, llm_client, long_term_memory: LongTermMemory):
        self.llm = llm_client
        self.memory = long_term_memory
    
    async def chat(self, user_id: str, user_message: str, session_history: list):
        # Step 1: 从长期记忆中检索相关信息
        relevant_memories = await self.memory.recall(user_id, user_message, top_k=5)
        
        # Step 2: 将记忆注入系统Prompt
        memory_context = ""
        if relevant_memories:
            memory_context = "\n\n[用户历史记忆]\n" + "\n".join(
                f"- {m['content']}(记录于{m['timestamp'][:10]})" 
                for m in relevant_memories
            )
        
        # Step 3: 组装完整上下文
        messages = [
            {"role": "system", "content": f"你是一个智能助手。{memory_context}"},
            *session_history,
            {"role": "user", "content": user_message}
        ]
        
        # Step 4: 调用LLM
        response = await self.llm.chat(messages)
        
        # Step 5: 异步更新长期记忆(不阻塞回复)
        asyncio.create_task(
            self.memory.store(user_id, session_history + [
                {"role": "user", "content": user_message},
                {"role": "assistant", "content": response}
            ])
        )
        
        return response

工程取舍

坑1:记忆提取的质量

LLM提取的事实可能有误——用户说"我预算大概10万吧",被提取成"用户预算10万元",丢了"大概"这个模糊限定。

解法: 提取时保留原始表述,不要做二次加工。或者在提取后加一步"事实校验"——让用户确认"我记得你说过预算大概10万,对吗?"

坑2:记忆检索的相关性

向量检索是按语义相似度来的,但语义相近不等于逻辑相关。用户问"帮我做个方案",可能召回的是上次聊的"方案A"而不是最近的"方案B"。

解法: 检索结果加上时间权重——近期的记忆权重更高。

def time_weighted_score(similarity: float, days_ago: float) -> float:
    """时间衰减权重:越近的记忆分数越高"""
    decay = 0.95 ** days_ago  # 每天衰减5%
    return similarity * max(decay, 0.3)  # 最低保留30%的原始分数

坑3:记忆膨胀

用户聊了100次,向量库里存了500条记忆。每次检索回来5条,可能都是过时的(比如用户三个月前说的需求已经变了)。

解法: 加记忆淘汰机制——定期清理过期记忆,或者用LLM对记忆做"合并更新"(新记忆覆盖旧记忆)。


第三层:工作记忆——让Agent"记住"当前任务做到哪了

问题场景

用户说:"帮我做一个项目计划。"Agent开始一步步收集信息:项目名称、截止时间、团队成员……

收集到第5步时,用户突然问:"你们公司周末上班吗?"

Agent回答了这个问题。然后用户说:"继续做项目计划。"

Agent:"好的,请问您的项目名称是什么?"

它忘了项目计划已经收集了4个字段了。

解决方案:显式状态管理

工作记忆的核心不是"记对话",而是记状态——当前任务进行到哪一步了,哪些信息已经收集到了,哪些还没有。

from enum import Enum
from typing import Optional

class TaskState(Enum):
    COLLECTING_INFO = "collecting_info"  # 收集信息中
    PLANNING = "planning"                # 制定计划中
    WAITING_CONFIRM = "waiting_confirm"  # 等待确认
    COMPLETED = "completed"             # 已完成

class WorkingMemory:
    """工作记忆:当前任务的状态和中间结果"""
    
    def __init__(self):
        self.current_task: Optional[str] = None
        self.state: TaskState = TaskState.COLLECTING_INFO
        self.collected_data: dict = {}
        self.pending_fields: list[str] = []
        self.step_history: list[dict] = []
    
    def start_task(self, task_name: str, required_fields: list[str]):
        self.current_task = task_name
        self.state = TaskState.COLLECTING_INFO
        self.collected_data = {}
        self.pending_fields = required_fields
    
    def update_field(self, field: str, value: str):
        self.collected_data[field] = value
        if field in self.pending_fields:
            self.pending_fields.remove(field)
        self.step_history.append({
            "action": "update_field",
            "field": field,
            "value": value,
            "timestamp": datetime.now().isoformat()
        })
    
    def is_complete(self) -> bool:
        return len(self.pending_fields) == 0
    
    def get_status(self) -> dict:
        return {
            "task": self.current_task,
            "state": self.state.value,
            "collected": self.collected_data,
            "remaining": self.pending_fields,
            "progress": f"{len(self.collected_data)}/"
                       f"{len(self.collected_data) + len(self.pending_fields)}"
        }

与短期记忆的配合

工作记忆和短期记忆是独立的两个系统

  • 短期记忆存的是"对话历史"(原始消息)
  • 工作记忆存的是"任务状态"(结构化数据)

当用户打断对话(问了一个无关问题)再回来时:

class AgentWithWorkingMemory:
    """带工作记忆的Agent"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
        self.short_term = ShortTermMemory()
        self.working = WorkingMemory()
    
    async def chat(self, user_message: str):
        # Step 1: 判断用户意图——是继续当前任务,还是打断
        intent = await self._classify_intent(user_message)
        
        if intent == "continue_task" and self.working.current_task:
            # 继续任务:从工作记忆中恢复状态
            return await self._continue_task(user_message)
        
        elif intent == "new_question":
            # 打断:临时回答,但不影响工作记忆
            answer = await self._handle_interruption(user_message)
            return answer
        
        elif intent == "new_task":
            # 新任务:保存当前任务状态,开始新任务
            self._pause_current_task()
            return await self._start_new_task(user_message)
    
    async def _continue_task(self, user_message: str) -> str:
        """继续当前任务,带着工作记忆的状态"""
        
        status = self.working.get_status()
        
        prompt = f"""你正在帮用户完成一个任务:{status['task']}
当前进度:{status['progress']}
已收集信息:{json.dumps(status['collected'], ensure_ascii=False)}
还需要的信息:{', '.join(status['remaining'])}

用户刚刚说:{user_message}

请根据用户的回复,提取信息并继续推进任务。如果所有信息都收集完毕,直接生成结果。"""
        
        response = await self.llm.generate(prompt)
        
        # 更新工作记忆
        new_data = await self._extract_new_data(user_message, response)
        for field, value in new_data.items():
            self.working.update_field(field, value)
        
        return response

效果: 用户打断后回来,Agent说:"好的,继续项目计划。你之前告诉了我项目名称(XX产品上线)、截止时间(9月底)、团队规模(5人),还需要确认一下预算范围。"

而不是从头开始问。


三层记忆的统一架构

把三层记忆组合在一起:

                    ┌─────────────────┐
                    │   用户输入       │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  意图分类       │ ← 决定走哪条路径
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼──────┐ ┌────▼─────┐ ┌──────▼──────┐
     │  短期记忆     │ │ 工作记忆 │ │  长期记忆    │
     │ (对话历史)    │ │ (任务状态)│ │ (向量数据库) │
     └────────┬──────┘ └────┬─────┘ └──────┬──────┘
              │              │              │
              └──────────────┼──────────────┘
                             │
                    ┌────────▼────────┐
                    │  上下文组装     │ ← 三层记忆合并注入Prompt
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  LLM推理       │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  记忆更新       │ ← 异步更新短期+长期记忆
                    └─────────────────┘
class UnifiedAgentMemory:
    """统一记忆系统的Agent"""
    
    def __init__(self, llm_client, vector_store, embedding_model):
        self.llm = llm_client
        self.short_term = SummarizedShortTermMemory(max_turns=20, llm_client=llm_client)
        self.working = WorkingMemory()
        self.long_term = LongTermMemory(vector_store, embedding_model, llm_client)
    
    async def chat(self, user_id: str, user_message: str) -> str:
        # 1. 短期记忆:获取最近对话
        recent_context = self.short_term.get_context()
        
        # 2. 工作记忆:获取当前任务状态
        task_context = ""
        if self.working.current_task:
            task_context = f"\n[当前任务] {json.dumps(self.working.get_status(), ensure_ascii=False)}"
        
        # 3. 长期记忆:检索相关历史
        long_term_memories = await self.long_term.recall(user_id, user_message, top_k=5)
        memory_context = ""
        if long_term_memories:
            memory_context = "\n[用户历史记忆]\n" + "\n".join(
                f"- {m['content']}" for m in long_term_memories
            )
        
        # 4. 组装完整上下文
        system_prompt = f"你是一个智能助手。{task_context}{memory_context}"
        
        messages = [
            {"role": "system", "content": system_prompt},
            *recent_context,
            {"role": "user", "content": user_message}
        ]
        
        # 5. 推理
        response = await self.llm.chat(messages)
        
        # 6. 异步更新记忆
        self.short_term.add("user", user_message)
        self.short_term.add("assistant", response)
        asyncio.create_task(
            self.long_term.store(user_id, [
                {"role": "user", "content": user_message},
                {"role": "assistant", "content": response}
            ])
        )
        
        return response

工程实践中的几个建议

  1. 不是所有Agent都需要三层记忆。 简单的FAQ机器人只需要短期记忆;个人助手类Agent需要长期记忆;复杂任务型Agent才需要工作记忆。按需选择,不要过度设计。

  2. 向量数据库选型: 小规模(<10万条)用Chroma(纯Python,零部署);中规模用Qdrant(性能好,支持过滤);大规模用Milvus(分布式,企业级)。不要一上来就搞Milvus,你的Agent可能连1000条记忆都没有。

  3. 记忆提取的Prompt是关键。 提取什么、不提取什么、提取的粒度多大——这决定了长期记忆的质量。建议花时间调这个Prompt,比调模型选型更重要。

  4. 短期记忆的摘要压缩可以异步做。 不要等对话结束后再压缩,而是在每次对话轮次达到阈值时自动触发。这样用户下次来的时候,历史已经被压缩好了。

  5. 工作记忆要有超时机制。 用户可能3天才回来继续任务,工作记忆里的数据不能永远留着。建议设置TTL(比如7天过期),过期后提示用户"你之前的任务还在,要继续吗?"


结语

你的Agent"记不住事",不是模型的锅,是你没给它设计记忆系统。

短期记忆管当前对话,长期记忆管跨会话知识,工作记忆管任务状态。三层各司其职,组合起来就是一个"有记忆"的智能体。

大部分Agent demo只做了第一层(而且是最粗暴的直接截断),就出来接单了。这就是为什么你遇到的"智能客服"总是问你"请问您说的是哪个"——不是它傻,是它的记忆系统就是摆设。

把记忆系统做好,你的Agent就能从"每条对话都是第一次见面",进化到"记得你说过的每一句重要的话"。

这才是用户真正期待的"智能"。


*栈上月明(ZSoftYM)是一家专注于AI智能体开发与工程化落地的西安技术团队。