Agent 三层记忆机制:短期记忆、情景记忆与语义记忆 Agent 三层记忆机制短期记忆、情景记忆与语义记忆目录三层记忆模型第一层记忆短期记忆第二层记忆情景记忆第三层记忆语义记忆三层怎么协作小结三层记忆模型LLM 每次调用都是无状态的如果消息历史没有落库服务器一停所有对话历史都会消失。要让 Agent 跨会话记住用户光靠上下文窗口不够需要一套记忆系统。Agent 的记忆可以分成三层每一层解决不同的问题层级存什么生命周期访问方式短期记忆当前会话的完整消息会话结束即销毁直接读 messages 数组情景记忆历史会话的对话片段持久化跨会话保留RAG 检索语义记忆从对话中提炼的事实持久化持续更新直接读取用户画像第一层记忆短期记忆短期记忆就是我们熟悉的上下文窗口Agent 当前会话中能看到的所有消息。这部分在之前的博客里已经详细讨论过这里只做一个简单的回顾。# 短期记忆就是这个 messages 数组messages[{role:user,content:帮我排查连接池问题}]whileTrue:responsecall_llm(messages)messages.append(response)ifnotresponse.has_tool_use():breakresultexecute_tool(response)messages.append(result)短期记忆的特点是无成本、高保真。不需要额外的存储和检索直接读就行。但它有两个限制窗口有上限。对话太长时早期消息会被压缩或丢弃。会话结束就没了。下次打开新会话一切从零开始。所以短期记忆只够处理当前正在做的事跨会话的持久化需要另外两层来支撑。第二层记忆情景记忆情景记忆解决的是以前聊过什么的问题。它会把历史会话的对话片段存下来下次需要时通过 RAG 检索回来。存储每次会话结束后Agent 把对话内容做一个总结存到向量数据库里importchromadb clientchromadb.PersistentClient(path./agent_memory)collectionclient.get_or_create_collection(episodic_memory)defsave_episodic_memory(session_id:str,messages:list):会话结束后把对话摘要存入向量数据库# 用 LLM 生成对话摘要summarysummarize_conversation(messages)# 计算摘要的 embedding存入向量数据库collection.add(documents[summary],ids[session_id],metadatas[{timestamp:get_current_time(),topic:extract_topic(messages),}])注意这里存的是摘要不是原始对话。原因很简单原始对话可能有几十上百条消息全部存下来既浪费空间检索时噪声也大。摘要保留了关键信息体积小得多。检索新会话开始时Agent 根据当前话题去向量数据库里检索相关的历史会话defretrieve_episodic_memory(query:str,top_k:int3)-list[str]:根据当前话题检索相关的历史会话resultscollection.query(query_texts[query],n_resultstop_k,)returnresults[documents]检索回来的历史摘要会被注入到当前会话的上下文中。这样 Agent 就知道上周三你问过某某问题了。写入时机情景记忆不是每次对话都值得存。写入情景记忆的时机通常是会话结束时自动保存本次对话的摘要用户主动要求时“记住我们刚才讨论的方案”对话中出现关键决策时Agent 判断当前讨论有长期价值主动存储一个实际的写入判断defshould_save(messages:list)-bool:判断当前对话是否值得存入情景记忆# 对话太短没什么信息量iflen(messages)4:returnFalse# 用 LLM 判断这段对话有没有长期价值judgmentllm_judge(prompt这段对话是否包含值得长期记住的信息比如技术方案、架构决策、问题排查过程。回答 yes 或 no。,contextmessages)returnjudgmentyes检索的挑战向量检索当情景记忆积累到几百条之后会出现几个实际问题。语义漂移。用户问怎么优化 SQL向量检索可能返回一段关于SQL 注入防护的历史对话。两者都和 SQL 相关但完全不是一回事。解决办法是给每条记忆打上结构化标签时间、话题、涉及的技术检索时同时用语义相似度和标签过滤。时效性衰减。三个月前讨论的方案可能已经被推翻了。可以给每条记忆加一个权重越近期的权重越高检索时按相似度和时间加权排序。存储膨胀。每天都用 Agent几个月下来可能有上千条记忆。全量检索既慢又不准。实际做法通常是设定一个上限比如只保留最近 200 条或者按重要性评分淘汰低分条目。第三层记忆语义记忆语义记忆解决的是关于这个用户我知道什么的问题。它不存具体的对话内容而是从对话中提炼出结构化的事实和偏好。和情景记忆的区别情景记忆语义记忆存什么完整对话片段摘要形式提炼后的事实/偏好形态原始记录近似原文结构化条目高度压缩举例“2026年7月1日用户问过连接池配置”“用户使用 HikariCP SpringBoot”量大每次会话都可能存小只有值得沉淀的才存检索方式按话题相似度按实体、关系、类别可以把情景记忆理解成你的日记本记录了每天发生的事语义记忆是你脑子里对一个人的印象——他喜欢用什么框架、技术水平怎么样、沟通风格是什么。你不会把每次和他聊天的记录都背下来但你会记住关于他的关键信息。实现语义记忆的核心是一个用户画像通常用 JSON 或 KV 结构存储importjson USER_PROFILE_PATH./user_profile.jsondefload_user_profile()-dict:加载用户画像try:withopen(USER_PROFILE_PATH,r,encodingutf-8)asf:returnjson.load(f)exceptFileNotFoundError:return{facts:[],preferences:{},tech_stack:[]}defsave_user_profile(profile:dict):保存用户画像withopen(USER_PROFILE_PATH,w,encodingutf-8)asf:json.dump(profile,f,ensure_asciiFalse,indent2)画像的结构可以是这样{facts:[使用 SpringBoot 2.7 Java 17,数据库用 MySQL 8.0,连接池用 HikariCP],preferences:{code_style:简洁不喜欢过多抽象,explanation_depth:中等喜欢类比但不要啰嗦},tech_stack:[SpringBoot,MySQL,Redis,HikariCP]}提炼过程语义记忆需要一个提炼过程。每轮对话结束后Agent 检查有没有新的事实值得加入画像defextract_facts(messages:list,current_profile:dict)-dict:从对话中提取新的事实更新用户画像promptf 当前用户画像{json.dumps(current_profile,ensure_asciiFalse,indent2)}最近的对话{format_messages(messages)}请从对话中提取新的事实或偏好更新用户画像。 只输出新增或修改的部分不需要重复已有的信息。 如果没有新的发现返回空对象 {{}}。 new_factscall_llm(prompt)returnmerge_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越用越懂你模型本身其实没有变化但它的记忆层在持续积累关于你的信息。