一个真实场景
上周测试一个同行的产品——某公司做的"智能客服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
工程实践中的几个建议
-
不是所有Agent都需要三层记忆。 简单的FAQ机器人只需要短期记忆;个人助手类Agent需要长期记忆;复杂任务型Agent才需要工作记忆。按需选择,不要过度设计。
-
向量数据库选型: 小规模(<10万条)用Chroma(纯Python,零部署);中规模用Qdrant(性能好,支持过滤);大规模用Milvus(分布式,企业级)。不要一上来就搞Milvus,你的Agent可能连1000条记忆都没有。
-
记忆提取的Prompt是关键。 提取什么、不提取什么、提取的粒度多大——这决定了长期记忆的质量。建议花时间调这个Prompt,比调模型选型更重要。
-
短期记忆的摘要压缩可以异步做。 不要等对话结束后再压缩,而是在每次对话轮次达到阈值时自动触发。这样用户下次来的时候,历史已经被压缩好了。
-
工作记忆要有超时机制。 用户可能3天才回来继续任务,工作记忆里的数据不能永远留着。建议设置TTL(比如7天过期),过期后提示用户"你之前的任务还在,要继续吗?"
结语
你的Agent"记不住事",不是模型的锅,是你没给它设计记忆系统。
短期记忆管当前对话,长期记忆管跨会话知识,工作记忆管任务状态。三层各司其职,组合起来就是一个"有记忆"的智能体。
大部分Agent demo只做了第一层(而且是最粗暴的直接截断),就出来接单了。这就是为什么你遇到的"智能客服"总是问你"请问您说的是哪个"——不是它傻,是它的记忆系统就是摆设。
把记忆系统做好,你的Agent就能从"每条对话都是第一次见面",进化到"记得你说过的每一句重要的话"。
这才是用户真正期待的"智能"。
*栈上月明(ZSoftYM)是一家专注于AI智能体开发与工程化落地的西安技术团队。