
一个Agent项目在连续对话第17轮时报出了context is too large。第一反应是换更大的上下文窗口可真换成百万 token 后问题没有消失只是从第二轮推迟到了第二十轮。后来发现在每一轮里系统都把用户历史记录、实时搜索结果、候选工具列表、中间推理过程全部塞进了 Prompt。这其实是一个很典型的 Agent 记忆系统设计错误。Agent 记忆系统的核心问题不是“能不能装下更多上下文”而是“如何在有限上下文里保留真正重要的信息并把值得长期记住的东西沉淀到独立的存储层”。围绕这个主题LangChain、LangGraph以及 DeepAgent 这类 Agent 框架正在把记忆从“临时拼凑的历史记录”变成“可治理的长期状态”。1. 先看清Agent记忆问题的本质不是缓存而是状态治理1.1 从一次“Context长度超限”去理解Agent记忆瓶颈那个context is too large的报错看起来像是模型窗口不够大。搜索这类报错时你会看到很多人在问“400 this models maximum context length is 1048576 tokens. However”意思是模型上下文窗口已经给到 1048576 token还是超了。这说明什么问题说明很多 Agent 的 Prompt 不是“写出来的”而是“堆出来的”。每一轮调用模型时系统会把完整对话历史、检索回来的文档、工具返回结果、上一轮推理过程全部拼接进去。当 Agent 需要执行多步任务时这些中间结果会像滚雪球一样越滚越大。哪怕窗口给到 100 万 token也只是把“爆炸”的时间往后推并没有解决“信息结构混乱”和“重复冗余”这两个根本问题。从工程经验看Context 超限通常不是模型能力问题而是记忆管理策略问题。真正要回答的不是“我的窗口能装多少”而是“窗口里应该放什么、不该放什么、放到哪里去”。1.2 为什么“把上下文全部带上”不能解决记忆问题从直觉上很多人会觉得大模型是“全知”的只要把历史都给它它就能记得所有细节。但真实情况是上下文越长模型对关键信息的关注度越容易被稀释。首先是成本问题。每次请求的 token 数量直接决定延迟和费用。把 10 万字历史全部塞进去即使模型能处理响应时间也会明显变长。对交互型 Agent 来说体验基本不可接受。其次是噪声问题。历史里真正对当前任务有用的可能只有两三段内容其他都是寒暄、重复、中间失败尝试。模型在大量无关信息中找线索容易产生“幻觉式引用”——它可能把 A 轮说过的话安到 B 轮里。第三是更新问题。用户今天说“以后回复尽量简短”下周又说“这个项目想要详细文档”。如果只靠上下文中的历史记录两条互相矛盾的偏好会同时存在模型无法判断哪一条更接近用户当前意图。这时候需要的不是“记更多”而是“判断该信什么、该更新什么”。所以把上下文全部带上本质上是在用存储空间换逻辑简单但它只适用于一次性短对话。一旦 Agent 需要跨会话、跨任务、跨用户持续工作就必须把记忆系统拆开单独设计。1.3 三个层面的记忆Context、Working Memory、Long-term Me在实际架构里我会把 Agent 记忆分成三个层次层级英文概念生命周期典型内容工程落点短期上下文Context单次请求或单轮对话当前用户输入、当前工具的返回、当前 Prompt传给模型的那一段 Prompt工作记忆Working Memory一次多步任务执行过程Agent 的中间思考、计划、已执行步骤、临时变量LangGraph 的 State、Checkpoint长期记忆Long-term Me跨会话、长期积累用户偏好、知识图谱、历史决策、个人画像外部存储向量库、关系库、图数据库很多人讨论 Agent 记忆时只讨论前两层也就是“怎么把聊天历史传给模型”。但真正让 Agent 变得“像同一个持续服务的人”的是第三层 Long-term Me。它不是一个聊天记录存储桶而是一个会随用户交互不断演化的用户模型。短期 Context 解决的是“这一句话怎么看”Long-term Me 解决的是“这个人到底想要什么”。这三层不是替代关系而是漏斗关系。原始对话先进入短期 Context任务执行过程中部分信息沉淀到工作记忆任务结束后经过提炼、去重、更新写入长期记忆。后续任务再把长期记忆中的相关内容检索出来注入新的 Context。2. 用LangChain做记忆组件但不能只停在Memory接口上2.1 LangChain的Memory模块到底提供了什么LangChain 很早就提供了 Memory 相关组件名字有过调整但核心思路是延续的。最常见的几种模式ConversationBufferMemory把对话历史原样保留每次拼到 Prompt 里。适合短会话一旦历史变长就会失控。ConversationSummaryMemory对每一轮对话做摘要用摘要替代完整历史。能压缩信息但摘要本身可能丢细节。VectorStoreRetrieverMemory把对话内容切块、向量化存入向量数据库。需要时按相似度检索出相关片段再注入 Prompt。这三种模式恰好对应记忆系统演进的三步不加筛选的完整复制、粗粒度的信息压缩、语义级别的按需召回。如果你只是做一个 DemoConversationBufferMemory就能跑通。但一旦进入真实项目我建议直接奔着检索式记忆去设计。原因很简单Agent 的对话历史不是线性文本而是多话题、多轮次、可跳转的信息网络。用向量检索的方式才能在每次调用时只取最相关的片段。2.2 从Buffer到Retriever检索式记忆的正确打开方式检索式记忆的基本流程是用户每轮输入连同 Agent 的回复一起切成适当大小的文本块然后通过 Embedding 模型转成向量写入向量库新任务发起时用当前用户输入作为查询检索出最相似的若干条历史片段放到 Prompt 里。在 LangChain 生态里一个典型的处理思路是用ChatMessageHistory存储消息用向量库保存切分后的历史内容再用Retriever接口把它们接起来。具体代码会随版本变化这里只写一个概念结构# 这是一个抽象示例具体 API 以你使用的 LangChain 版本为准 from langchain.memory import VectorStoreRetrieverMemory from langchain_community.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings # 1. 准备向量存储和 embedding vectorstore Chroma( embedding_functionOpenAIEmbeddings(), collection_nameagent_memory ) # 2. 构造一个基于向量检索的记忆组件 retriever vectorstore.as_retriever( search_kwargs{k: 5} # 每次召回条数通常 3~10 条 ) memory VectorStoreRetrieverMemory( retrieverretriever, memory_keyrelevant_history, input_keyuser_input )这里最需要注意的不是 Memory 类怎么传参而是k值。召回太少了记忆没有意义召回太多又会把噪声带回来。更合理的做法是先设定一个相关性阈值只把相似度高于阈值的记录送回 Prompt而不是单纯取 Top-K。2.3 记忆内容不是原始的聊天记录而是经过提炼的结构化信息很多团队把聊天记录直接向量化就当作长期记忆这是一个典型的误区。原始聊天记录里有大量无关内容“好的”“嗯嗯”“稍等一下”“这个链接打不开”。这些内容如果进入长期记忆会让用户画像变得很脏。更合理的方式是增加一个“记忆提炼”环节。每一轮任务结束后不要急着把原始对话写入长期存储而是先让模型抽取出关键信息用户表达过哪些偏好这次任务解决了什么问题有没有新的知识点或决策用户的语气、风格、关注点有没有变化把这些信息整理成结构化的 JSON 或自然语言摘要再加上时间戳、来源会话 ID、置信度再写入存储。这样才能保证后续检索出来的内容是“有价值的记忆”而不是“噪音回放”。{ user_id: u_12345, timestamp: 2025-01-15T10:30:00Z, memory_type: preference, content: 用户偏好简洁回复但在技术方案评审时希望看到详细对比表格, confidence: 0.8, source_message_id: [msg_101, msg_102], status: active }结构化记忆的好处是后期可以做更新、合并、失效和审计。这一点会在第四部分展开。3. LangGraph真正改变的地方把记忆变成图的持久化状态3.1 LangGraph和LangChain的关系从链到图LangChain 本身提供了“链”的抽象也就是把 Prompt、模型、工具按顺序串起来。但链是线性的一旦遇到分支、循环、人工介入、多角色协作就会变得很别扭。LangGraph 的出现本质上是为了解决“复杂流程的状态管理”问题。它把 Agent 的执行过程定义成一个图节点是处理函数边是条件跳转状态是图中所有节点共享的数据结构。和 LangChain 相比LangGraph 更关注“执行的生命周期”每一步依赖什么状态、更新哪些状态、异常时怎么回退。对记忆系统来说LangGraph 提供的不是某个记忆类而是一个可以承载状态流转的基础设施。你可以在图中定义哪些状态字段是一次性的哪些需要持久化哪些要在不同节点间共享。比如一个典型 Agent 任务状态 Schema 里可以包含current_answer、messages、user_profile、retrieved_memories等字段其中user_profile是从长期记忆加载进来的messages是本次对话的临时历史。3.2 用LangGraph的State管理“这次对话”的中间状态在 LangGraph 里每个节点函数接收当前状态经过处理后再返回状态的部分更新。这种设计天然适合做工作记忆。一个简化示例from typing_extensions import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): messages: list user_input: str retrieved_memories: list current_plan: list async def retrieve_memory(state: AgentState): query state[user_input] memories await memory_service.search(query) return {retrieved_memories: memories} async def generate_answer(state: AgentState): memories state[retrieved_memories] # 拼入 prompt 并调用模型 return {messages: state[messages] [assistant_message]} graph StateGraph(AgentState) graph.add_node(retrieve_memory, retrieve_memory) graph.add_node(generate_answer, generate_answer) graph.set_entry_point(retrieve_memory) graph.add_edge(retrieve_memory, generate_answer) graph.add_edge(generate_answer, END)这个图虽然简单但已经体现了工作记忆的基本逻辑每个节点只依赖 State 里声明过的字段不依赖全局变量。这样测试单个节点时只需要构造一个符合 Schema 的 State 对象不需要真的启动整个 Agent。真正生产环境里你还会用到 LangGraph 的 Checkpointer。Checkpointer 的作用是保存每步执行后的状态快照这样 Agent 在执行到一半时如果崩溃或需要人工介入可以恢复到最近一个有效状态而不是全部重来。Checkpointer 本身也可以把工作记忆保存到外部存储比如文件系统、数据库或云存储。3.3 把“记忆维护”变成一个子图写入、更新、检索、归档如果你只是把记忆操作分散到主流程各个节点里一段时间后代码会变得难以维护。我更建议把记忆相关操作单独拆成一个“子图”或“记忆服务”内部包含几个专用节点write_memory接收任务输出提炼成结构化记忆。update_memory检查是否有旧记忆和新记忆冲突决定是合并还是替换。retrieve_memory根据当前输入生成查询返回相关记忆。forget_memory处理遗忘策略把过期或错误的记忆标记为失效。这样主流程会变得很干净。用户输入进来第一步调用retrieve_memory拿到历史记忆然后进入执行节点任务结束后调用write_memory把新信息写回去。记忆逻辑集中在子图里后续调整策略时不用改主流程。DeepAgent 这类 Agent 框架在我看来本质上就是在 LangGraph 这种“图编排”之上再做一层封装把记忆、规划、工具调用、反思这些通用能力打包成更上层的抽象。如果你使用的是团队内部自研框架也可以参考这种子图思路把记忆系统从主流程中彻底解耦。4. 从Context到Long-term Me长期记忆层要解决的是“我是谁”4.1 Long-term Me是什么不是历史记录是持续演化的用户模型很多团队搭建长期记忆时第一版就是把所有历史会话存进数据库然后通过关键词或向量检索。但做到后面会发现这只能叫“历史记录”不能叫“用户模型”。Long-term Me 应该是一个关于用户的动态知识体他关心什么、倾向于什么风格、有哪些长期目标、在什么场景下会改变要求。它并不需要保存用户说过的每一句话而是需要保存“从这些话里提炼出来的用户特征”。举个例子。用户第一次说“给我写一段 Python 脚本处理 CSV。”第二次说“能不能直接用 pandas别用 csv 模块。”如果把这两条原话都存进历史检索时可能只看到“处理 CSV”这个关键词。但如果提炼成“用户熟悉 Python 数据生态偏好 pandas”那后续遇到类似任务时系统可以直接给出更适合的方案而不是先踩一次坑。4.2 长期记忆的更新机制合并、冲突解决、衰减长期记忆不是只增不改。用户会改变偏好记忆会过期有些信息可能一开始就是错的。所以记忆系统必须设计更新策略。更新机制里最核心的是“冲突解决”。比如之前记忆里写着“用户喜欢简洁回复”但最近三次任务用户都要求“给出详细步骤”。这时候不应该简单把旧值删掉而是应该分析触发条件。有可能用户只是在这个项目里需要详细步骤其他场景仍然偏好简洁。更稳妥的做法是给记忆增加“上下文标签”和“置信度”{ preference: 回复风格, trigger_context: 技术方案评审, value: 详细对比表格, confidence: 0.9, last_updated: 2025-01-20T09:00:00Z }下一次检索到这条记忆时系统除了看内容本身还要看它是否匹配当前场景。如果当前场景不是技术方案评审这条记忆的权重可以降低。另外一个容易被忽略的是“衰减”。用户三个月前关注某个话题不代表今天仍然关注。比较简单的实现是给每条长期记忆设置一个时间窗口定期检查last_updated如果长时间没有被触发就把 confidence 调低最终归档或删除。衰减不一定要做成复杂的算法可以先从“超过 90 天未更新则标记为待确认”开始。4.3 隐私、权限和遗忘记忆治理的底线长期记忆越厚Agent 越像“懂用户的人”但风险也越大。如果系统保存了用户的偏好、工作习惯、项目敏感信息一旦泄露或使用不当会造成严重的信任问题。在工程治理层面至少要做到几件事用户可查看提供接口让用户看到 Agent 记住了什么。用户可删除允许用户删除某条记忆或清空全部长期记忆。分级权限不同角色、不同会话只允许访问对应权限级别的记忆。数据最小化不存与任务无关的敏感信息能存偏好的时候不存原始隐私内容。这里不是在讲合规法条而是从系统工程角度说如果一个记忆系统没有删除和审查能力那它就没有达到生产可用标准。因为记忆本身就是一种状态状态必须有生命周期管理。5. 工程治理把记忆系统放到生产环境要过的几道关5.1 记忆存储选型向量库、关系库、图数据库怎么组合长期记忆的数据形态不是单一的很难用一个存储全部解决。常见组合是存储类型适合内容典型工具理由向量数据库语义检索聊天摘要、知识点、偏好描述Chroma、Milvus、Weaviate、pgvector按语义相似度召回相关记忆关系数据库用户属性、偏好标签、置信度、时间戳PostgreSQL、MySQL方便做结构化查询、更新、删除图数据库实体关系用户-项目-任务-知识之间的关系Neo4j、NebulaGraph适合做多跳关联推理更稳妥的起步方案不是追求复杂存储而是先用 PostgreSQL 存结构化记忆用 pgvector 做向量检索。这样只需要维护一个数据库部署和运维成本都能接受。等真正遇到关系推理需求时再引入图数据库。5.2 记忆的写入管线从原始对话到可检索记忆长期记忆的质量取决于写入管线设计。建议把写入过程分成这几个阶段对话收集在 Agent 任务结束点拿到完整会话记录。内容清洗去掉无效消息、重复信息把长文本切块。信息抽取用模型提取偏好、目标、实体、决策等结构化字段。去重和冲突检测检查是否已有相同或矛盾记忆。写入存储给每条记忆加时间戳、来源、置信度和状态。这一步特别要注意“幂等性”。Agent 任务不一定只执行一次重试或并发可能导致同一信息被重复写入。给记忆写入接口设计一个基于(user_id, source_message_id, memory_type)的唯一键能避免大多数重复问题。5.3 记忆的读取策略什么时候检索、检索多少、如何和当前上下文融合读取记忆不是每次都要把所有记忆都捞出来。我的建议是只在任务开始时检索一次避免中间多次检索造成上下文混乱。用当前用户输入 最近一轮 Agent 回应作为查询提高召回相关度。召回条数控制在 3 到 10 条超过 10 条基本就是噪声。召回后先做一次重排把高置信度、高相关度的记忆放在最前。把记忆内容作为独立区块注入 Prompt并标注“以下是用户历史偏好供参考”。关于注入方式一个常见做法是在 System Prompt 里给出一段“记忆摘要”而不是把多条原始记忆直接平铺。模型需要的是“可参考的信息”不是“需要辩证分析的证据链”。5.4 异常与排查当记忆系统“越帮越忙”时先查哪几层实际运行时记忆系统会带来很多隐蔽问题。最常见的现象是模型回答看起来“不太对”但又说不出哪里错。这时可以按这个链路排查看输入当前轮用户输入是否完整有没有因为消息截断导致查询不准。看检索召回回来的记忆是否和当前问题相关相似度阈值是否太低。看状态LangGraph 的 State 在节点流转中是否被意外覆盖Checkpoint 恢复是否正确。看 Prompt注入记忆后Prompt 是否过长模型是否因为上下文拥挤而忽略关键信息。看写入新记忆是否覆盖了旧记忆有没有重复写入或错误写入。这里最容易踩坑的是“记忆检索命中错误内容”。比如用户之前问过 A 项目的数据库地址后来问 B 项目时向量检索把 A 的数据库地址召回出来导致模型直接引用旧信息。解决方案不是废弃向量检索而是给每条记忆增加“适用范围”字段检索时用当前任务元数据过滤不能只依赖语义相似度。注意任何记忆系统都需要支持“记忆失效”操作。用户明确说“之前说的这个方案不要了”这时候系统必须能删掉或覆盖对应记忆否则 Agent 会一直引用过时信息。6. 从“能用”到“好用”一套最小可落地的记忆系统框架6.1 一个通用分层架构把前面的讨论汇总成一个便于落地的分层架构层级职责关键技术交互层接收用户输入返回模型输出前端、API 服务编排层管理任务执行流程和工作记忆LangGraph StateGraph、Checkpointer记忆服务层提供记忆的写入、检索、更新、删除接口子图、MemoryService存储层持久化短期对话和长期记忆PostgreSQL pgvector或向量库 关系库在这个架构里编排层不直接依赖具体存储而是统一调用记忆服务层。这样后续替换存储、调整检索策略都不需要改动主流程。6.2 分阶段落地路径先跑通短期记忆再补长期记忆技术方案再完整也要一步一步落地。我的建议是由简到繁分四个阶段阶段一用 LangGraph State 保存单次会话状态。先不碰长期记忆只解决多步任务之间的状态传递。阶段二给 LangGraph 加 Checkpointer让任务中断后能恢复。这一阶段解决的是“执行到一半崩溃”的问题。阶段三用向量检索替代完整历史把聊天摘要和关键信息写入外部存储实现跨会话检索。阶段四增加长期记忆的提炼、更新、冲突解决和遗忘策略形成 Long-term Me。不要一上来就设计一个巨大的记忆系统。先把阶段一跑通你会更清楚 State 里哪些字段是稳定的哪些字段在真实业务里根本用不上。6.3 可以马上开始的检查清单如果你正在规划 Agent 记忆系统可以拿下面这张清单自我检查[ ] 是否定义了完整的状态 Schema而不是用一堆临时变量传递数据[ ] 是否使用了 Checkpointer 保存工作记忆[ ] 记忆写入接口是否幂等[ ] 长期记忆是否经过结构化提炼而不是原始聊天记录[ ] 是否有记忆检索的相关性阈值和召回条数限制[ ] 是否支持用户查看、删除记忆[ ] 是否设计了记忆更新和冲突解决策略[ ] 是否在记忆数据上打了时间戳和来源标识[ ] 有没有日志记录每次记忆的写入、检索和删除行为这些不是“加分项”而是生产环境的基本要求。6.4 回到主判断记忆系统不是“插件”而是Agent的“人格”养成系统很多人把记忆系统理解成“给 Agent 加一个存历史的地方”这个理解过于轻了。真正的 Agent 记忆系统决定的是 Agent 能不能随着使用过程不断变得更懂用户。从 Context 到 Long-term Me其实是一个从“一次性输入”到“持续状态”的演进过程。短期 Context 很重要但它只是记忆系统的入口。工作记忆让你能完成当前任务长期记忆让你能在未来任务里做得更好。LangChain 给了你记忆组件LangGraph 给了你状态编排能力DeepAgent 这类框架则试图把这些能力整合成更高级的 Agent 开发范式。但最终能否落地取决于你愿不愿意把记忆当作一个需要设计、治理、运维的系统而不是一句memory {}就结束的临时方案。如果你现在正要从零起步我建议先选一个真实场景比如“客服 Agent 记住用户偏好”或“代码助手记住项目约定”用 LangGraph 把状态流转搭好再用检索式记忆把长期信息接进来最后补上删除和更新机制。先跑通一条完整的链路远比反复研究理论更容易让你看清楚真正昂贵的从来不是模型窗口而是你如何管理窗口之外的那片记忆海洋。