Agent 三层记忆机制:短期记忆、情景记忆与语义记忆
Agent 三层记忆机制:短期记忆、情景记忆与语义记忆
目录
三层记忆模型
LLM 每次调用都是无状态的,如果消息历史没有落库,服务器一停,所有对话历史都会消失。要让 Agent 跨会话"记住"用户,光靠上下文窗口不够,需要一套记忆系统。
Agent 的记忆可以分成三层,每一层解决不同的问题:

| 层级 | 存什么 | 生命周期 | 访问方式 |
|---|---|---|---|
| 短期记忆 | 当前会话的完整消息 | 会话结束即销毁 | 直接读 messages 数组 |
| 情景记忆 | 历史会话的对话片段 | 持久化,跨会话保留 | RAG 检索 |
| 语义记忆 | 从对话中提炼的事实 | 持久化,持续更新 | 直接读取用户画像 |
第一层记忆:短期记忆
短期记忆就是我们熟悉的上下文窗口,Agent 当前会话中能看到的所有消息。这部分在之前的博客里已经详细讨论过,这里只做一个简单的回顾。
# 短期记忆:就是这个 messages 数组
messages = [{"role": "user", "content": "帮我排查连接池问题"}]
while True:
response = call_llm(messages)
messages.append(response)
if not response.has_tool_use():
break
result = execute_tool(response)
messages.append(result)
短期记忆的特点是无成本、高保真。不需要额外的存储和检索,直接读就行。但它有两个限制:
- 窗口有上限。 对话太长时,早期消息会被压缩或丢弃。
- 会话结束就没了。 下次打开新会话,一切从零开始。
所以短期记忆只够处理"当前正在做的事",跨会话的持久化需要另外两层来支撑。
第二层记忆:情景记忆
情景记忆解决的是"以前聊过什么"的问题。它会把历史会话的对话片段存下来,下次需要时通过 RAG 检索回来。
存储
每次会话结束后,Agent 把对话内容做一个总结,存到向量数据库里:
import chromadb
client = chromadb.PersistentClient(path="./agent_memory")
collection = client.get_or_create_collection("episodic_memory")
def save_episodic_memory(session_id: str, messages: list):
"""会话结束后,把对话摘要存入向量数据库"""
# 用 LLM 生成对话摘要
summary = summarize_conversation(messages)
# 计算摘要的 embedding,存入向量数据库
collection.add(
documents=[summary],
ids=[session_id],
metadatas=[{
"timestamp": get_current_time(),
"topic": extract_topic(messages),
}]
)
注意这里存的是摘要,不是原始对话。原因很简单:原始对话可能有几十上百条消息,全部存下来既浪费空间,检索时噪声也大。摘要保留了关键信息,体积小得多。
检索
新会话开始时,Agent 根据当前话题去向量数据库里检索相关的历史会话:
def retrieve_episodic_memory(query: str, top_k: int = 3) -> list[str]:
"""根据当前话题,检索相关的历史会话"""
results = collection.query(
query_texts=[query],
n_results=top_k,
)
return results["documents"]
检索回来的历史摘要会被注入到当前会话的上下文中。这样 Agent 就知道"上周三你问过某某问题了"。
写入时机
情景记忆不是每次对话都值得存。写入情景记忆的时机通常是:
- 会话结束时:自动保存本次对话的摘要
- 用户主动要求时:“记住我们刚才讨论的方案”
- 对话中出现关键决策时:Agent 判断当前讨论有长期价值,主动存储
一个实际的写入判断:
def should_save(messages: list) -> bool:
"""判断当前对话是否值得存入情景记忆"""
# 对话太短,没什么信息量
if len(messages) < 4:
return False
# 用 LLM 判断:这段对话有没有长期价值?
judgment = llm_judge(
prompt="这段对话是否包含值得长期记住的信息?"
"比如技术方案、架构决策、问题排查过程。"
"回答 yes 或 no。",
context=messages
)
return judgment == "yes"
检索的挑战
向量检索当情景记忆积累到几百条之后,会出现几个实际问题。
语义漂移。 用户问"怎么优化 SQL",向量检索可能返回一段关于"SQL 注入防护"的历史对话。两者都和 SQL 相关,但完全不是一回事。解决办法是给每条记忆打上结构化标签(时间、话题、涉及的技术),检索时同时用语义相似度和标签过滤。
时效性衰减。 三个月前讨论的方案可能已经被推翻了。可以给每条记忆加一个权重,越近期的权重越高,检索时按相似度和时间加权排序。
存储膨胀。 每天都用 Agent,几个月下来可能有上千条记忆。全量检索既慢又不准。实际做法通常是设定一个上限,比如只保留最近 200 条,或者按重要性评分淘汰低分条目。
第三层记忆:语义记忆
语义记忆解决的是"关于这个用户我知道什么"的问题。它不存具体的对话内容,而是从对话中提炼出结构化的事实和偏好。
和情景记忆的区别
| 情景记忆 | 语义记忆 | |
|---|---|---|
| 存什么 | 完整对话片段(摘要形式) | 提炼后的事实/偏好 |
| 形态 | 原始记录,近似原文 | 结构化条目,高度压缩 |
| 举例 | “2026年7月1日用户问过连接池配置” | “用户使用 HikariCP + SpringBoot” |
| 量 | 大,每次会话都可能存 | 小,只有值得沉淀的才存 |
| 检索方式 | 按话题相似度 | 按实体、关系、类别 |
可以把情景记忆理解成你的日记本,记录了每天发生的事;语义记忆是你脑子里对一个人的印象——他喜欢用什么框架、技术水平怎么样、沟通风格是什么。你不会把每次和他聊天的记录都背下来,但你会记住关于他的关键信息。
实现
语义记忆的核心是一个用户画像,通常用 JSON 或 KV 结构存储:
import json
USER_PROFILE_PATH = "./user_profile.json"
def load_user_profile() -> dict:
"""加载用户画像"""
try:
with open(USER_PROFILE_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {"facts": [], "preferences": {}, "tech_stack": []}
def save_user_profile(profile: dict):
"""保存用户画像"""
with open(USER_PROFILE_PATH, "w", encoding="utf-8") as f:
json.dump(profile, f, ensure_ascii=False, indent=2)
画像的结构可以是这样:
{
"facts": [
"使用 SpringBoot 2.7 + Java 17",
"数据库用 MySQL 8.0",
"连接池用 HikariCP"
],
"preferences": {
"code_style": "简洁,不喜欢过多抽象",
"explanation_depth": "中等,喜欢类比但不要啰嗦"
},
"tech_stack": ["SpringBoot", "MySQL", "Redis", "HikariCP"]
}
提炼过程
语义记忆需要一个提炼过程。每轮对话结束后,Agent 检查有没有新的事实值得加入画像:
def extract_facts(messages: list, current_profile: dict) -> dict:
"""从对话中提取新的事实,更新用户画像"""
prompt = f"""
当前用户画像:
{json.dumps(current_profile, ensure_ascii=False, indent=2)}
最近的对话:
{format_messages(messages)}
请从对话中提取新的事实或偏好,更新用户画像。
只输出新增或修改的部分,不需要重复已有的信息。
如果没有新的发现,返回空对象 {{}}。
"""
new_facts = call_llm(prompt)
return merge_profile(current_profile, new_facts)
注意两个细节:
- 对比已有画像再提取。 把当前画像传给 LLM,让它只提取新增的部分,避免重复存储。
- 提炼频率低于情景记忆。 不是每轮对话都要提炼,可能几轮对话才值得更新一次画像。
信息冲突
语义记忆有一个绕不开的问题:新信息和旧信息冲突了怎么办。
比如用户上个月说"我用 MySQL",这个月说"我们迁到 PostgreSQL 了"。画像里如果同时存在两条,Agent 就不知道该信哪个。
处理方式取决于信息的类型:
- 可覆盖型信息(技术栈、配置、版本号):新信息直接覆盖旧信息。"数据库从 MySQL 迁到 PostgreSQL"意味着 PostgreSQL 是当前状态,MySQL 变成历史记录。
- 累积型信息(技能水平、项目经验):新信息追加到旧信息后面。"会 Java"和"最近在学 Go"不冲突,两条都留着。
实现上,可以在提取事实时让 LLM 判断每条信息属于哪种类型,然后决定是覆盖还是追加。
三层怎么协作
这三层拆开来看都不复杂,放在一起怎么配合才是重点。来看一个完整的流程:

整个过程对用户是透明的。他只看到 Agent "记住了"昨天的事,背后的检索和组装他感知不到。
数据流
会话结束
│
├─→ 摘要 → 存入情景记忆(向量数据库)
│
└─→ 提炼 → 更新语义记忆(用户画像)
新会话开始
│
├─→ 读取语义记忆 → 注入 system prompt
│
└─→ 检索情景记忆 → 注入 system prompt
│
▼
Agent 开始推理(同时拥有三层记忆)
小结
Agent 记忆机制的核心是三层分工:短期记忆管当前会话,情景记忆管历史回溯,语义记忆管用户画像。三层各司其职,通过 RAG 检索和 system prompt 注入把信息送到 Agent 眼前。这个模型可以让Agent越用越懂你,模型本身其实没有变化,但它的记忆层在持续积累关于你的信息。
更多推荐
所有评论(0)