1. 记忆缺失的Agent,做得再花哨也是个玩具
做过AI Agent的人应该都有过这种感受:记忆这个东西,平时不觉得它重要,一旦Agent把你忘得干干净净,你就知道它有多关键。上一轮对话里用户明明说了"我叫陈默,目前在做一个跨境电商独立站",结果换个会话打开,Agent开口还是"你好,我是你的AI助手,很高兴为你服务"。这种失忆式体验,几乎是判断一个Agent是demo还是产品的分水岭。
我在这个系列的前两篇分别聊了Agent的基础架构和工具调用,这篇专门把"记忆"这件事拆开讲清楚。标题叫"让Agent记住你",听起来不复杂,真正落地时牵扯到短期记忆、长期记忆、记忆迁移、向量检索、上下文组装一整套问题。最近社区里总有人在问opencode怎么没有记忆功能、workbuddy的历史对话记录怎么迁移到本地、hindsight记忆库怎么用,这些问题的本质,都是没把Agent记忆系统当作一个完整的工程模块来设计。
1.1 上下文窗口再大,也只是"桌面"不是"硬盘"
很多人有个经典误区:大模型上下文窗口已经做到200K甚至更大了,把所有聊天记录全塞进去不就行了?这个想法我初学阶段也有过。但实际跑下来你会发现,上下文窗口更像是一张临时桌面——上面能摆很多东西,但你一旦收摊(结束会话、切换线程),桌面就清空了。更麻烦的是,窗口越大,检索噪声越大,模型在几千条历史消息里找一条关键信息,效果反而不如精挑细选出来的几条记忆。
另一个问题是成本。200K上下文的输入token,每次请求都要完整过一遍,价格和延迟肉眼可见地涨。我亲眼见过一个团队把整个知识库塞进system prompt,单次请求成本翻了十几倍,响应慢到用户直接弃用。所以无论如何,把记忆做成独立的存储和检索系统,而不是无脑堆上下文,是唯一靠谱的路线。桌面再大,也替代不了硬盘。
1.2 没有记忆的Agent,翻车是必然的
我把没有记忆的Agent的翻车现场归成三类,你可以对号入座。
第一类是信息无限重复。用户每次都要重新自我介绍,每轮都要重申约束条件。程序员用AI编程助手时特别典型——上一轮明明说了"这个模块不要引入第三方依赖",下一轮它又给你装了一个库进来。做过代码Agent的朋友对opencode相关讨论应该有印象,很多人吐槽"opencode没有记忆功能",本质上就是工具在会话结束后把之前的修改意图、代码约定全丢了,重启一下,之前为什么这样改完全对不上。
第二类是任务连续性断裂。Agent帮用户做一个跨多天的项目,比如写一本书、搭一套系统,第一天聊清楚的需求第二天就忘光。用户得从头解释一遍,这种Agent根本没法承担真正的长期任务。
第三类是Agent无法进化。一个合格的助手应该越用越懂你:你的偏好、你的工作习惯、你项目的来龙去脉。没有记忆的Agent永远像个新来的实习生,每次见面都是第一次,永远在重复问同样的问题。这三类翻车,根源都是记忆系统缺位。
2. Agent记忆的分层模型:工作记忆、短期记忆与长期记忆
要设计好记忆系统,第一步是分清层次。业界的分类方法很多,但本质上是按生命周期和作用范围来划分的。我习惯把Agent记忆分成三层:工作记忆、短期记忆、长期记忆。想清楚这三个层次,后面所有方案都顺了。
2.1 工作记忆:Agent当前这一步在做什么
工作记忆对应的是Agent在执行当前任务过程中产生的临时信息——上一步工具返回了什么结果、现在正在处理哪段代码、下一步计划是什么。这一层的特点是生命周期极短,可能只有几十秒,但它决定了Agent能不能正常运转。在LangGraph这类框架里,它通常体现为State对象里的临时字段,任务结束就应该清空,不该落库。
这个层次的实现没什么玄学,就是一个运行时的变量空间。但有一个设计原则要记住:不要把工作记忆和短期记忆混在一起。我见过有人把工具调用的中间结果直接塞进消息历史,结果对话一长,消息列表里全是乱七八糟的函数返回值,模型反而迷失了重点。工作记忆该清就清,该走State就走State,不要污染对话流。
2.2 短期记忆:线程内的会话上下文
短期记忆就是通常说的对话历史,它的作用范围被限定在一个会话线程内。用户和Agent你来我往的消息序列就属于这一层。短期记忆的核心问题是"怎么截断":对话变长之后不能全部塞进上下文,需要做滑动窗口、摘要压缩或者关键信息抽取。
LangGraph的Checkpointer就是为这个设计的:它把每个线程的消息历史持久化下来,并允许你用RemoveMessage删掉过旧的消息,保留最近N轮。这样一来,即使Agent进程重启,同一个线程内的对话上下文也能恢复。我实际用下来,SqliteSaver做本地持久化是最省心的方案,一条连接串搞定,不用额外部署Redis。
2.3 长期记忆:跨会话的"用户档案"
长期记忆是这篇文章的主角。它要回答的是:用户是谁、用户偏好什么、正在做什么项目、哪些事情是被明确禁止的。长期记忆必须跨线程、跨会话存在,而且应该是结构化提炼后的条目,而不是一堆原始聊天记录。
举个例子,用户说过"我是做跨境电商的"和"我不喜欢长回复",这两条都应该作为独立记忆条目存下来。按记忆类型拆分可以这么看:
| 记忆类型 | 示例内容 | 存储形态 | 生命周期 |
|---|---|---|---|
| 用户事实 | 姓名、职业、团队规模 | 结构化字段 | 长期 |
| 用户偏好 | 回复风格、代码规范、禁用的依赖 | 键值/标签 | 长期 |
| 进行中的任务 | 正在开发的模块、当前优先级 | 任务列表 | 中期 |
| 会话摘要 | 上一轮聊了哪些重要内容 | 压缩文本 | 短期偏中 |
长期记忆设计的重点在两块:一是"提炼"质量,能不能从流水账对话里抽出真正有用的信息;二是"召回"准确度,能不能在合适的时候把合适的记忆拿出来。这两个点单独展开都够写一篇,后面我详细讲。
2.4 双网络记忆模型在做什么
最近"双网络记忆模型"这个词出现频率很高,很多教程都在提。它其实就是把记忆拆成两条通路:一条是快速通道,负责实时的、和当前上下文强相关的记忆读写,类似于人类的"工作记忆加情景记忆"——当前对话里提到的细节立刻就能用上;另一条是慢速通道,负责异步地、批量地把重要信息沉淀到长期记忆库。
这套设计的好处是兼顾实时性和持久性。快速通道保证当前对话流畅,慢速通道保证知识不流失。实现上并不复杂:快速通道就是上下文加短时缓存,慢速通道就是一个后台任务,定期对对话做摘要、抽取事实、写入向量库。我后面给的LangGraph例子,本质上就是双网络模型的一个最小落地。
3. 记忆流水线:采集、提炼、存储、召回、遗忘
记忆系统不是"一个存储",而是一条流水线。我把它拆成五个环节:采集、提炼、存储、召回、遗忘。每个环节都有人踩坑,我一个一个说清楚。
3.1 采集:不是所有对话都值得记住
采集环节最容易犯的错是"全存"。对话里绝大部分内容是寒暄、临时指令、重复确认,全存下来除了浪费存储,还会在召回时引入大量噪声。正确的做法是在采集口做一层过滤,只保留那些包含实体信息、用户明确偏好、任务状态变化的对话片段。
我建议用规则做初筛:用户主动陈述的事实消息优先级最高,Agent自己的长篇输出基本不记,只有用户确认过的结论才值得进记忆库。关键词触发也挺管用,包含"我喜欢""我不喜欢""我习惯""我在做""我是""记住""以后都"这类字眼的消息,直接打高权重。规则能处理掉的坚决不调用LLM,这一条能帮你省下大量成本。
3.2 提炼:把对话变成结构化条目
过滤之后,需要把有价值的对话片段变成结构化条目,这是LLM最擅长的活。我常用的方案是"定时批量提炼":每积攒一定数量的消息,或者出现明确的偏好信号时,调一次LLM,给它一个固定的抽取模板。
比如这样一个提取Prompt:
从下面的对话内容中提取值得长期记忆的信息,只输出JSON,不要解释。 对话内容: {conversation} 输出格式: { "facts": [{"subject": "用户", "predicate": "职业", "object": "独立开发者"}], "preferences": [{"topic": "代码风格", "preference": "优先使用TypeScript"}], "forbidden": ["不要使用闭源SDK"] }这里的关键是输出格式固定,便于后续合并去重。我实测下来,用JSON Schema约束配合结构化输出模式(比如LangChain的with_structured_output)会更稳,能省掉大量解析容错代码。提炼频率也要控制,不要每轮对话都跑LLM,我一般是消息累积到20条左右,或者出现高强度偏好信号才触发一次。
3.3 存储选型:向量库不是唯一答案
存储选型是很多人纠结的问题。实际上答案取决于记忆类型:
- 结构化事实和偏好,适合放关系型数据库或键值库,字段明确,支持精确查询和更新。
- 自由文本类记忆,比如"用户上次提到他在做一个AI教育项目,核心痛点是获客",适合向量化之后放向量库,便于语义检索。
- 混合场景,最常用的就是"向量库+关系库"双写,向量库管语义检索,关系库管精确事实。
向量库本身的选择也有讲究。小项目用Chroma或FAISS就够了,别一上来就上Milvus这类分布式系统。数据量到了几十万条再考虑扩展不迟。我自己在生产里更常用pgvector,因为直接复用现有Postgres,少一套基础设施,事务和备份都省心。存记忆这件事,稳定性比花哨重要得多。
3.4 召回:相关度、新鲜度、重要度缺一不可
召回决定了Agent能不能在需要的时候"想起来"。最简单的做法是embedding相似度TopK,但纯向量召回在真实场景下远远不够。我建议做加权打分:
def memory_score(item, query_embedding): relevance = cosine_similarity(item["embedding"], query_embedding) freshness = math.exp(-item["age_days"] / 30.0) # 30天半衰期 importance = item["importance"] # 0~1,提炼时打标 return 0.5 * relevance + 0.3 * freshness + 0.2 * importance新鲜度分按时间衰减,记忆条目越新分越高。重要度分来自提炼阶段给每条记忆打的标签——用户主动强调的、和当前任务直接相关的,重要度就高。不同场景下权重可以动态调,比如用户问"我之前说过的饮食偏好"时,偏好类记忆的权重就该放大。
另外,召回结果一定要"少而精"。我见过一个Agent每次召回20条记忆全部拼进prompt,结果模型被无关记忆干扰得答非所问。一般控制在3到8条比较合适,每条记忆要附上来源时间和概要,让模型能判断该怎么用。
3.5 遗忘与更新:比存储更难的工程问题
遗忘听起来反直觉,但它是记忆系统里最容易被忽视的一环。没有遗忘机制的记忆库最后一定会变成垃圾场:过期的偏好、错误的事实、互相矛盾的条目堆积在一起,召回时模型根本不知道该信谁。
我目前的做法有四条。第一,每条记忆都带timestamp和来源消息ID,方便溯源。第二,定期跑一致性检查,发现冲突条目(比如"用户不用微信"和"用户的微信号是xxx"同时存在),标记出来交给LLM裁定或者人工删除。第三,对偏好类记忆做TTL过期,比如三个月未确认的偏好自动降权。第四,允许用户显式删除和修正记忆,Agent必须提供接口让用户看到"你记住了什么"。
4. 动手实践:用LangGraph搭一套双通道记忆Agent
理论讲了一堆,来点能直接抄作业的。我用LangGraph给你搭一个带短期+长期记忆的Agent骨架。设计思路不复杂:Checkpointer管线程内的短期记忆,外面挂一个LongTermMemory模块管跨会话的长期记忆,两条通道互不干扰。
4.1 整体架构:Checkpointer管短期,向量库管长期
先看依赖和整体结构:
from langgraph.graph import StateGraph, MessagesState, START, END from langgraph.checkpoint.sqlite import SqliteSaver from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, RemoveMessage # 模拟长期记忆存储,实际生产可换 pgvector / Chroma memory_store = [] # 每条记忆: {"text": str, "embedding": list, "ts": float, "importance": float}整个Agent的State用标准的MessagesState。短期记忆靠SqliteSaver把每条消息持久化到本地SQLite文件,Agent进程重启后同一个session还能接着聊。长期记忆单独维护一个memory_store(生产环境换成向量库),每次对话结束触发记忆提炼逻辑。
4.2 记忆写入节点:对话结束后做提炼
在LangGraph里写一个节点,挂在LLM调用节点之后。最简单的做法是每轮对话结束后,把最后几轮消息交给提炼函数,返回新的记忆条目写进存储。
def memorize_node(state: MessagesState): recent = state["messages"][-4:] # 最近4条消息 if len(recent) < 2: return {"messages": []} facts = extract_memory(recent) # 调用LLM做结构化抽取 for f in facts: memory_store.append(normalize_memory(f)) return {"messages": []}生产环境我不会每轮都跑LLM提炼,成本太高。一般是消息积攒到一定数量,或者出现明显的"用户陈述偏好"信号才触发。触发规则可以很朴素:包含"我喜欢""我不喜欢""我习惯""我在做""我是"这类字眼的消息,权重直接拉高。
4.3 记忆召回节点:组装上下文时注入
召回节点在LLM调用前执行,把长期记忆检索结果拼进SystemMessage:
def recall_node(state: MessagesState): query = state["messages"][-1].content related = retrieve_from_memory(query, top_k=5) memory_context = "\n".join( [f"- {item['text']} (时间:{item['ts']})" for item in related] ) system_msg = SystemMessage( content=( "你是用户的长期AI助手。以下是你对用户的长期记忆," "回答时优先参考,但不要生硬复述:\n" + memory_context ) ) return {"messages": [system_msg]}这里有个特别容易踩的细节:SystemMessage每次动态生成,千万不要把它放在持久化的消息列表里,否则它会跟着Checkpointer一起存进短期记忆,造成重复注入。正确的做法是组装好messages列表后直接传给model,而不是先塞进state。
另外注意token预算。召回的记忆条数要受system prompt剩余空间的约束,我通常把记忆上下文控制在总上下文窗口的15%以内,主对话留白给推理。
4.4 完整图结构和运行效果
把节点串起来:
graph_builder = StateGraph(MessagesState) graph_builder.add_node("recall", recall_node) graph_builder.add_node("call_llm", call_llm_node) graph_builder.add_node("memorize", memorize_node) graph_builder.add_edge(START, "recall") graph_builder.add_edge("recall", "call_llm") graph_builder.add_edge("call_llm", "memorize") graph_builder.add_edge("memorize", END) checkpointer = SqliteSaver.from_conn_string("agent_memory.db") graph = graph_builder.compile(checkpointer=checkpointer)运行效果就是:同一个user_id,第一次对话说"记住,我平时喜欢简短回复",第二次开新线程问"你喜欢什么风格回复",Agent能答上来。这就是长期记忆和短期记忆配合的最小闭环。跑通这个小例子之后,你再往里面加向量库、加记忆面板,方向都不会歪。
5. 从WorkBuddy、Opencode到生产环境:记忆迁移与避坑实录
最后聊点工程实战里的经验。最近workbuddy的历史对话记录本地记忆迁移、opencode的永久记忆和代码修改召回,都是社区里问得非常多的问题。这些工具本质上就是把上面这套记忆流水线产品化了,但实际使用中,记忆迁移和记忆质量是两大难题。
5.1 本地记忆迁移:格式、版本、隐私
做记忆迁移,核心是三点:导出格式要标准化、迁移过程要可回溯、明文数据要降敏。
WorkBuddy这类工具如果支持导出历史对话记录,导出文件一般会包含对话原文、时间戳、消息ID。但把导出文件原样导入另一个Agent,并不能实现"无缝记忆"——不同Agent的记忆schema不一样,它要的是提炼过的用户偏好,而不仅仅是聊天记录。所以迁移的正确姿势是:先把导出文件做一次LLM提炼,转成目标系统的结构化记忆格式,再执行写入。我踩过直接导入的坑,结果就是对方系统读了一堆原始文本,完全没法按结构化字段检索。
Opencode能把代码修改情况通过记忆召回出来,本质上是把代码变更事件当成一种特殊记忆写入索引。你要复刻这个能力,思路是一致的:每次代码提交或者文件修改时,把diff摘要和修改意图写入记忆库,查询时按项目路径过滤召回。记住,代码记忆的关键不是存diff原文,而是存"为什么这么改"的意图说明。
隐私方面提醒一句:记忆库里经常有用户隐私。无论做产品还是自用,本地明文存储都要谨慎,敏感字段至少做脱敏处理。我在自己的工具里把所有记忆条目的渲染层做了脱敏,只暴露摘要,详情需要用户主动展开。
5.2 回归测试:改记忆逻辑别把Agent改傻了
记忆系统是最容易"这里动一下、那里坏一片"的模块。给团队定的规矩是每个Agent项目必须有一套记忆回归测试:给一个固定的用户画像,跑一组标准对话,断言"用户职业有没有被正确记住""偏好有没有串线""重开线程后能不能正确召回"。没有这套测试,谁都不敢改记忆逻辑。
我遇到过最典型的回归事故是调整提炼Prompt后,把用户的临时指令误存成了长期偏好,结果Agent之后每次回答都带着那条已经失效的限制条件,用户体验直接崩掉。有了回归测试,这类问题能在上线前被拦住。
5.3 成本与效果的平衡
记忆系统是典型的"效果靠LLM提炼,成本也烧在LLM提炼"的模块。控制成本最有效的方法是分级触发:规则能处理的(比如提取时间、地点)绝不调用LLM;必须LLM的场景,优先用小模型做抽取,把抽取结果丢给大模型做最终召回判断;批量提炼任务的频率拉长,容忍一点延迟。
从我实际项目的量化数据来看:加入记忆系统之后,用户主动重复描述需求的比例下降了一半左右,任务连续中断率明显下降,单次会话的token消耗确实涨了15%到20%,但整体体验收益远大于成本。关键还是别一股脑全存全提炼,把提炼频率和召回数量都压到最低可用值。
最后再分享一个我多次重构记忆模块后的体会:记忆系统设计得再好,如果用户感知不到"你记住了",价值就是零。所以我在每个Agent里都会加一个"记忆面板"——告诉用户你记住了哪些内容、这些内容是你什么时候说的、要如何删除。透明、可控、可撤回,这才是让人放心把记忆交给Agent的基础。后面我打算再写一篇关于"记忆评估指标"的文章,聊聊怎么量化一套记忆系统的真实表现,到时候见。