ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent记忆机制详解:从上下文窗口到LangGraph落地实践

AI Agent记忆机制详解:从上下文窗口到LangGraph落地实践 先说个场景你肯定遇到过早上还在跟 Agent 讨论项目架构设计下午换了个窗口再问它完全不知道上午聊了什么从“高并发方案”到“用 Redis 做缓存”要重新解释一遍。我这两年在团队里推 AI Agent 落地几乎每周都被同事吐槽“这 Agent 怎么跟金鱼一样七秒记忆”。后来认真做了一轮记忆能力的改造才发现问题不全在大模型而是我们压根没给它设计记忆机制。这篇是“走进 AI Agent”系列的第三篇前两篇聊了 Agent 的基本构成和工具调用这篇专门讲怎么让 Agent 具备真正的记忆能力。重点会覆盖记忆的分类设计、几种可落地的存储方案、基于 LangGraph 的具体实现以及我在生产环境里踩过的坑。你如果是正在做 Agent 开发、想从 demo 往工程化推进这篇文章应该能帮你省不少弯路。1. 为什么你的 Agent 总像“金鱼记忆”很多人刚接触 AI Agent 时觉得奇怪ChatGPT 明明能记住前几轮对话啊为什么我们自己做出来的 Agent 一问三不知这个问题的根源在大模型的架构和调用方式上。我尽量用通俗的话讲清楚。1.1 底层原因无状态 API 与有限的上下文窗口不管是调 OpenAI 的接口、Claude 的接口还是国产大模型底层 API 本质上都是无状态的。所谓无状态就是模型本身不保存你和它之间的任何历史交互。它能“记住”上一句并不是真的记住了而是你把之前的所有对话记录重新打包发给它它基于这些文字片段做续写。这就像你每次去同一家餐厅都要把上次吃过的菜重新报一遍菜名才能延续上次的聊天话题。餐厅大厨再厉害也不记得你上周来过。另外还有一只手在拖后腿——上下文窗口的长度限制。即使你的上下文窗口有 200K token看起来能塞几十万字但只要对话持续多轮历史内容总会被顶出去。窗口从前面挤一点、后面挤一点中间丢了什么你根本不知道Agent 对用户的记忆自然就变得七零八碎。所以想让 Agent“记住你”我们需要的不是延长窗口而是设计一套独立于模型之外的记忆系统。它负责保存、检索、更新用户的长期信息在合适的时机把最相关的记忆注入到模型上下文里。1.2 无记忆带来的连锁问题没有记忆的 Agent不只是体验差还会引发一连串实际问题我列几个最常见的多轮任务断裂。用户告诉 Agent“帮我查三条美食街的营业时间”下一句说“第一条那家的推荐菜是什么”Agent 已经把第一条是哪个忘干净了回答完全对不上。个性化流于表面。用户已经明确说“我不吃辣”下一轮 Agent 又开始推荐麻辣香锅。不是模型不聪明是它压根不知道该按这个约束来。协作效率极低。跨会话协作时用户要把自己的背景、项目上下文、偏好从头讲一遍Agent 永远在问重复问题。业务无法沉淀。Agent 在对话里探索出的用户信息和需求没有沉淀到任何地方多轮下来等于白聊。这些问题的共同特征是问题不在单轮模型能力而在跨轮状态管理。所以记忆系统在 Agent 架构里的地位我倾向于把它放在和工具调用同等重要的位置。2. 记忆不是塞数据先搞清楚存什么跟团队里新人聊天最容易出现的误区是一上来就搞向量数据库不管三七二十一把所有对话都塞进去。结果检索出来的信息乱七八糟代码里还埋了一堆看不出用途的集合。其实记忆也要分层设计。你回忆一下自己跟某个熟人打交道脑子里存的也不是录像带式的完整回放而是几类信息拼起来的印象这人姓什么叫什么、做什么工作、喜欢吃什么、上个月聊到他要换房子。Agent 的记忆也应该这样分层、分类、按需存取。2.1 按时间维度拆分短期记忆与长期记忆短期记忆对应的是当前会话内的上下文。用户上一句说了什么、这轮任务做了一半的中间状态都属于短期记忆。它生命周期短、更新频繁通常直接放在上下文窗口里或用 Redis 这类带过期时间的存储来辅助管理。长期记忆对应的是跨会话的信息。比如用户的姓名、职业、偏好、KPI、历史项目结论等。它生命周期长、需要持久化并且在进行新的对话时要能被主动召回。向量数据库、关系型数据库、键值存储都可以充当长期记忆的载体。这里有个容易忽略的点短期记忆和长期记忆之间不是孤立关系。当会话结束或上下文快要塞满时需要把短期记忆里有价值的部分提炼出来沉淀为长期记忆。这个“提炼”动作其实就是摘要化或结构化存储。2.2 按内容类型拆分事实记忆、偏好记忆、行为记忆除了按时间拆我更习惯按内容类型拆因为这直接决定用什么存储结构。事实记忆。用户的硬信息比如名字、团队规模、技术栈、业务方向、联系方式。这部分适合用结构化字段存比如 JSON 或数据库表查询时精确匹配。偏好记忆。用户明确表达过的偏好比如“代码注释要中文”“回复控制在 300 字内”“咖啡不加糖”。这类信息也是结构化为主但可能会有更新需要支持覆盖。行为记忆。从用户的交互行为里隐式推断出来的信息比如“这个用户喜欢在晚上活跃”“他提问时经常附带代码片段”“他对性能优化的话题停留时间更长”。行为记忆通常要从历史交互中挖掘适合用标签或统计特征表示。事件记忆。用户曾经提到过的具体事件比如“我们下周三上线”“5月份做过一次压测”。这类信息有时效性单独存并绑定时间戳。这三类信息在工程实现上的重要度排序是事实记忆 偏好记忆 行为记忆。前两类直接决定了用户要不要继续用你的 Agent行为记忆更多用于锦上添花和用户画像分析。2.3 记忆的更新与遗忘机制记忆不是存进去就完事了。用户会换岗位、换技术栈、口味也会变。如果 Agent 一直记住旧信息反而会给出错误建议。我建议在记忆表里加两类字段更新时间戳和置信度。每次对话后发现冲突信息比如用户之前说不吃辣这次说“最近开始尝试微辣”Agent 应该用新的偏好覆盖旧偏好同时保留一条历史轨迹记录。置信度可以作为冲突时的判断依据明确表达的信息置信度高推断出来的信息置信度低。至于遗忘机制不必做很复杂。一个简单的策略是对事件记忆设置 TTL过期自动清理对偏好记忆连续 N 次出现相反信息时降权或删除对事实记忆以用户主动修正为准。这个机制说起来轻巧实际工程里是记忆系统最重要的部分之一后面讲实现时会详细说。3. 三套我实测可用的记忆落地方案现在到最实在的部分。我分别用过三种方案按复杂度从低到高排序你们可以按项目阶段选。3.1 纯上下文注入几分钟就能跑通的轻量方案最简单的一种就是把用户的历史对话或画像摘要直接塞到 System Prompt 里。比如启动对话时先拼 Promptsystem_prompt f 你是用户的专属助手。 以下是关于用户的长期记忆 {json.dumps(user_profile, ensure_asciiFalse, indent2)} 请基于这些信息提供个性化服务。 这个方案解决“0 到 1”非常快我早期做内部演示用的就是它。把用户画像存在一个小 JSON 文件或 Redis 里每次对话先加载再注入效果立竿见影。但它的局限性也很明显Prompt 会越塞越长挤占上下文空间。记忆多了之后模型真正该关注的任务反而被稀释回答质量下降。没有检索能力。所有记忆全量注入不管当前话题跟这些记忆相不相关浪费 token也容易让模型被不相关信息带偏。不适合细颗粒度的复杂场景。比如用户五百条历史对话里只有一条提到“公司下一轮融资在 7 月”全量注入太宽精确检索又做不了。所以这个方案适合产品原型阶段或者记忆量小、场景固定的内部工具。3.2 摘要记忆用 LLM 把对话“压”成可复用记忆如果对话轮次比较多四五个小时积累的原始对话可能就有几万 token。这时候把原始对话全塞进上下文不现实尝试用 LLM 做对话摘要按轮次或时间窗口压缩存储。核心思路是维护一个滚动摘要。每次对话结束把当前摘要和新一轮对话一起交给 LLM让它输出一轮更新后的摘要def update_summary(old_summary: str, new_messages: list[dict]) - str: prompt f 你是一个对话记忆管理器。 请将已有的摘要和最新对话合并生成更新后的摘要。 要求 1. 保留既有摘要中的重要信息 2. 提取最新对话中的关键信息包括用户的偏好、事件、明确要求 3. 删除已经过期、被推翻的旧信息 4. 摘要控制在400字以内 已有的摘要 {old_summary} 最新对话 {json.dumps(new_messages, ensure_asciiFalse)} 请直接输出更新后的摘要 response llm.invoke(prompt) return response.content summary while True: messages get_conversation_batch() summary update_summary(summary, messages)这个方案的优点是利用 LLM 的语义理解能力把历史对话里面的关键信息提炼出来还能自动做信息更新。摘要存储成本很低Redis 存字符串就行。缺点也可以预见到摘要会丢失细节特别是用户在某次对话中随口说的一个小偏好如果当时没被写入摘要后面就再也找不回来。所以摘要法适合信息密度不高的对话对强信息场景需要再配合下面第三种方案。3.3 向量记忆随时检索用户的历史信息向量记忆是当前生产级 Agent 的主流方案。做法是把用户的历史对话、文档、FAQ 等内容切块用 embedding 模型转成向量存入向量数据库检索时按相似度召回最相关的记忆片段注入上下文。我用的流程大概是把每次对话按段落或语义边界切块每块控制在几百个 token 内。调用 embedding 接口生成向量连同原文、用户 ID、时间戳、对话 ID 等元数据存进向量库。对话开始时拿到当前用户输入和可能的记忆需求embedding 之后做相似度查询取 top-k 个记忆片段。把检索到的记忆片段拼接进 System Prompt。代码示意from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore Chroma( collection_nameagent_memory, embedding_functionembeddings, persist_directory./memory_db ) # 写入记忆 vectorstore.add_texts( texts[2025-06-15 用户说项目当前使用 Python 3.11团队 8 人], metadatas[{user_id: u123, time: 2025-06-15, type: fact}] ) # 检索记忆 query 项目技术栈是什么 docs vectorstore.similarity_search(query, k5, filter{user_id: u123})向量记忆的好处是检索灵活能根据语义召回相关信息跨会话支持得很好。比如用户这次问“我想提升接口性能”向量检索能召回三个月前聊过一次的“压测发现数据库连接池偏小”这条记忆。它的问题也很需要重视按相似度召回不一定准。语义近但实际无关的内容很常见需要配合元数据过滤、时间衰减等策略。embedding 模型的质量直接决定记忆质量。我试过几款不同的 embedding有的在代码类文本上表现比较差出现专业术语语义反转的情况。存储有成本。每条记忆都要生成向量、写入数据库中大规模用户下要考虑性能和费用。3.4 三套方案的选择建议直接给个参考表格你们可以按自己的场景对号入座方案复杂度记忆精度适用场景纯上下文注入低中受上下文限制小规模、内部工具、原型验证摘要记忆中中高摘要质量依赖LLM对话密集、信息密度适中的场景向量记忆高高依赖Embedding与检索策略生产级 Agent、大记忆量、跨会话检索实际做生产系统时我通常不是只用其中一种而是混合使用。短期会话用原文会话结束生成摘要存长期存储同时把关键事实抽出来向量化。这样兼顾成本和准确性。4. 基于 LangGraph 把记忆装进 Agent 里方案选好之后就到了落地环节。我这边实际的工程实践是基于 LangGraph 搭的带记忆的 Agent。LangGraph 的好处是能清晰定义状态流天然适合“对话 → 记忆提取 → 存储”这种流程。下面是我的实现思路和关键代码。4.1 设计带记忆的 Agent 状态流首先定义记忆管理的基本循环核心包括四个阶段记忆加载对话开始前从存储中加载用户画像和与当前问题相关的记忆片段放入 System Prompt。对话处理Agent 执行对话、调用工具完成用户请求。记忆提取对话结束后LLM 从对话里提取值得记住的新信息。记忆写入把新信息更新到长期存储并同步更新用户画像。用 LangGraph 表达的话每个阶段作为一个节点节点之间用边连接。4.2 代码实现一个可直接运行的 Agent先定义状态from typing import TypedDict, Annotated import operator from langgraph.graph import StateGraph, END class AgentState(TypedDict): user_id: str current_input: str messages: list user_profile: dict retrieved_memories: list new_memories: list节点函数我拆成四个逻辑比较清晰。第一个是记忆加载节点def load_memory(state: AgentState) - AgentState: user_id state[user_id] profile get_user_profile(user_id) docs vectorstore.similarity_search( state[current_input], k5, filter{user_id: user_id} ) memories [doc.page_content for doc in docs] return { user_profile: profile, retrieved_memories: memories }第二个是对话处理节点。这里把用户画像和检索到的记忆注入 System Promptdef chat_node(state: AgentState) - AgentState: memory_text \n.join(state.get(retrieved_memories, [])) profile_text json.dumps(state.get(user_profile, {}), ensure_asciiFalse) system_prompt f 你是用户的专属AI助手。 【用户画像】 {profile_text} 【相关记忆】 {memory_text} 请根据以上信息回答用户问题。如果记忆与当前问题无关不要强行使用。 messages [{role: system, content: system_prompt}] messages.extend(state[messages]) response llm.invoke(messages) return {messages: state[messages] [response]}第三个是记忆提取节点。这一步我用了一个单独的结构化抽取 Prompt环境稳定性比直接让聊天大模型自由发挥好很多def extract_memory(state: AgentState) - AgentState: conversation json.dumps(state[messages], ensure_asciiFalse) prompt f 从以下对话中提取值得长期记忆的用户信息。 分类识别 1. facts用户明确说出的个人或业务事实 2. preferences用户的偏好、习惯、明确要求 3. events用户提到的事件记录时间和时间描述 输出格式 {{ facts: [...], preferences: [...], events: [...] }} 对话内容 {conversation} 只输出JSON不要有其他内容。 result llm.invoke(prompt) data json.loads(result.content) return {new_memories: data}第四个是记忆写入节点。提取出来的记忆分类写入向量库并更新用户画像def save_memory(state: AgentState) - AgentState: user_id state[user_id] new_memories state.get(new_memories, {}) for mtype, items in new_memories.items(): for item in items: vectorstore.add_texts( texts[f{datetime.now().isoformat()} 用户说{item}], metadatas[{user_id: user_id, type: mtype}] ) # 更新用户画像 profile state.get(user_profile, {}) for pref in new_memories.get(preferences, []): profile[pref] True # 简单示意实际工程会用更精细的覆盖策略 save_user_profile(user_id, profile) return {user_profile: profile}最后把它们串成图graph StateGraph(AgentState) graph.add_node(load_memory, load_memory) graph.add_node(chat, chat_node) graph.add_node(extract_memory, extract_memory) graph.add_node(save_memory, save_memory) graph.set_entry_point(load_memory) graph.add_edge(load_memory, chat) graph.add_edge(chat, extract_memory) graph.add_edge(extract_memory, save_memory) graph.add_edge(save_memory, END) app graph.compile()这个图跑起来之后Agent 已经具备了“记一次用很多次”的基本能力。4.3 多 Agent 协作时的记忆隔离与共享如果你用的是 Spring AI Multi Agent 这类框架做多 Agent 架构记忆的隔离和共享就变成了一个必须处理的问题。我的原则是会话级 Agent 之间的记忆要隔离。比如一个 Agent 处理售前咨询另一个 Agent 处理售后工单不能互相污染用户画像。针对同一用户的记忆需要共享。售前 Agent 记录的用户行业信息售后 Agent 应该能读到但要有只读和可写的权限边界。每个 Agent 可以有自己的记忆作用域。比如客服 Agent 只能读用户的心愿单不能读写支付信息。在 LangGraph 里实现隔离很简单user_id 天然隔离即可。跨 Agent 共享则需要一个统一的记忆服务所有 Agent 通过 API 存取而不是各自维护一套本地库。这块后续如果大家有兴趣可以单独写一篇讲多 Agent 记忆协作的文章。5. 生产环境里那些容易踩的坑方案落地、代码跑通只是开始。真正上生产之后会遇到一堆意想不到的问题我把印象最深的几个整理出来希望能帮你提前规避。5.1 Token 悄悄涨全量注入的后遗症最开始的版本图省事把所有记忆不管相关的还是不相关的都拼进 System Prompt。一开始用户量小感觉还行。后来用户会话多了Prompt 动辄五六千 token每次请求都这么烧一个月下来 API 费用涨了快一倍响应时间也从几百毫秒涨到三秒开外。解决的思路是“少而精”。只把用户画像里和当前会话相关度最高的字段以及向量检索 top-k 的记忆片段注入。ControlNet 里有个做法我借用了过来叫“条件注入”本质就是控制注入内容的范围信息量要小而精准。5.2 摘要丢失细节一次压测数据引发的教训有次用户在做性能优化对话里提到过“压测时 QPS 卡在 600 上不去后面排查出来是数据库连接池配置问题”。这个信息在当时的上下文窗口中是有的但一周后来问“为什么数据库连接池不建议开太大”Agent 一点印象都没有。我去查了日志发现摘要生成的时候这句话被 LLM 归为“次要执行细节”摘要里只保留了“用户做过性能优化”关键的排查过程和结论全丢了。这个问题的根源在于LLM 做摘要时并不知道哪些信息对未来的检索是重要的。后来我做了两个改进摘要只负责提炼“用户说了什么”不负责判断“信息重不重要”。对技术细节类的内容单独走向量库存储原文切块摘要和原文两套并行。效果好很多至少在技术类对话场景里丢细节的概率大幅下降。5.3 向量检索召回“看起来像实际无关”的噪声向量检索最大的问题是它能找到语义相近的信息但语义相近不等于逻辑相关。用户问“怎么优化数据库”向量检索可能召回“用户上次聊到 MongoDB 和 MySQL 的区别”因为“数据库”这个词距离近但整条信息跟“优化”没有任何关系。调优的办法有几个过滤元数据。检索时强制加 user_id、type、time 范围等过滤条件缩小召回空间。提升 embedding 模型的针对性。通用 embedding 对技术术语的理解往往不够深可以选择在代码或领域语料上微调过的模型。top-k 别贪多。我一般设为 3 到 5召回多了噪声也多了效果反而下降。加一轮相关性重排序。对召回的候选记忆用 LLM 或交叉编码器判断是否与当前问题相关把无关的过滤掉。5.4 记忆更新时机避免“过早记住”或“永远不更新”记忆的更新时机很微妙。用户可能只是随口吐槽“这个方案真的好吗”并不代表他决定放弃这个方案。如果 Agent 太敏感对类似的试探性表达也写入长期记忆很快用户画像里就会充满噪声。我的处理策略是明确表达式信息才写入试探性、疑问式表达不进长期记忆。记忆写入前加置信度判断。LLM 在提取阶段同时输出置信度低于阈值的丢弃。定期进行记忆整理。比如每周对用户画像做一次合并、去重、冲突消解避免长期积累导致画像臃肿。5.5 隐私与安全记忆功能做不好容易变成“烫手山芋”说到记忆必然要面对用户隐私和数据安全的问题。Agent 记住了用户的偏好、业务数据如果存储不当或者被越权读取后果会很严重。这块我在工程上是这样做的机密信息默认不写入长期记忆除非用户明确授权。记忆存储加密访问走鉴权用户维度隔离。提供“查看记忆”和“清除记忆”的接口用户可以随时了解 Agent 记住了自己什么。敏感字段打标签在检索展示时脱敏。这个点在产品层面也值得注意。记忆能力越强用户的信任建立和隐私顾虑是并行的处理不好记忆功能反而会让用户流失。6. 回到开头当 Agent 真正记住你之后我自己在实际操作中的体会是记忆系统不是 Agent 的“附加功能”而是 Agent 从“demo 玩具”走向“生产工具”的分水岭。没有记忆的 Agent 每次对话都在“重新认识你”有了记忆它能像一位可靠的同事知道你项目的来龙去脉理解你的偏好还能在你忘记的时候提醒你“这个思路你上个月试过效果不太好”。最后再分享一个小技巧如果你只是想让现有 Agent 快速具备基础的记忆能力别急着上向量库先用“用户画像 JSON Redis 缓存”的方式跑起来把记忆的提取和更新逻辑调顺再逐步演进到向量检索。记忆系统最难的永远不是存储技术而是“记什么、忘什么、什么时候信”这套策略问题。这套策略想清楚了工具反而是顺手就能换的事。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进