Agent Memory 设计笔记:从短期上下文到长期经验
echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot最初面向长期陪伴型个人智能体围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口服务用户超过 20 万、累计下载超过 50 万是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。项目地址https://github.com/fuyuxiang/echo-agent你让 Agent “继续修复昨天那个测试失败”。它打开仓库重新跑测试却忘了昨天已经确认过这个项目不能直接用npm test要先进子目录再用pnpm。它也忘了你说过“先给结论再展开原因”于是又输出了一大段过程分析。这不是单纯的上下文长度问题而是 Agent 没有把长期有用的信息从会话噪声里提取出来。记忆系统要解决的不是“把所有聊天记录塞回模型”而是让 Agent 知道什么值得记、记到哪里、何时召回、何时更新或忘记。问题入口如果只看传统文本型 Chatbot它的基本形态仍是用户输入文本模型返回文本。历史消息可以维持短期连贯性但窗口被截断或者新会话开始它就很容易回到“第一次见面”的状态。Agent 不一样。它会读文件、跑命令、修改代码、调用工具。它对用户偏好、项目事实、失败经验和环境约束的理解会直接影响后续行动。“这个项目使用 pytest”“用户偏好中文回答”“配置文件在config/default.yaml”“某次失败由路径策略阻断导致”这些不是普通聊天记录而是未来任务会反复用到的行为依据。会话历史记录的是发生过什么记忆记录的是以后还值得使用什么。所以记忆不是更长的上下文窗口也不是完整聊天归档。它是一套选择性持久化机制从大量交互中挑出少量稳定、有用、可归属的信息并让这些信息在未来恰当地影响 Agent 行为。概念边界记忆系统最容易混淆三件事session、compression 和 memory。机制解决的问题典型内容主要风险Session恢复会话发生过什么消息、工具调用、状态太长、太碎Compression保住当前任务重点摘要、目标、未完成事项压错重点Memory保存未来仍有价值的经验用户偏好、项目事实、调试教训写错后长期污染Session 像操作日志适合追溯。Compression 像会议纪要适合让当前对话继续推进。Memory 更像工程师的长期经验库适合在新任务、新会话、上下文压缩之后重新提供关键背景。如果把所有 session 都当 memory系统会被临时探索、错误尝试和过期状态污染。如果只依赖 compression长期偏好和稳定项目事实又会随着窗口滚动被丢掉。记忆分层为了不停留在抽象层面下面以 echo-agent 的实现为例。echo-agent 把记忆分成四个层级working、episodic、semantic、archival。WorkingMemory是当前任务的短期缓存只存在于进程内不持久化。它适合保存“正在修改第 12 章”“下一步要跑某条命令”“刚才测试失败在某个模块”这类临时状态。Episodic是一次会话或任务片段。它保留发生过的上下文但还不一定是稳定事实。Semantic是从 episode 中提取出的长期事实比如项目命令、用户偏好、流程约定。Archival则保存低频或旧记忆必要时进一步遗忘。echo-agent 还用MemoryType区分记忆描述谁类型说明例子User用户偏好、习惯、沟通风格用户偏好中文回答先给结论Environment项目事实、工具配置、流程规范、领域知识当前项目使用 pytest配置文件在某路径这个区分很重要。用户画像可以跨项目延续但更敏感环境事实通常绑定某个仓库、目录或机器跨项目复用反而危险。echo-agent 的MemoryEntry不只是key content还包含类型、层级、标签、来源会话、重要性、访问次数、最近访问时间、向量 ID、episode ID、版本关系和superseded_by。这些字段说明记忆不是一段文本而是带状态的行为依据。选择性持久化真正难的不是保存而是判断什么该保存。一段调试会话可能有几十条消息读日志、猜原因、跑命令、失败、换方向、再验证。最终值得长期保存的也许只有一条“该项目的测试入口是pnpm test且需要在子目录执行。”echo-agent 用两条路径处理这个问题。第一条是显式存储。MemoryStore用user_memory.json和env_memory.json保存结构化记忆用MEMORY.md保存整理后的长期 Markdown 记忆用HISTORY.md追加 consolidation 产生的历史摘要。JSON 便于检索、更新和去重Markdown 便于注入系统提示词也便于人阅读。第二条是后台整理。MemoryConsolidator会把会话片段整理成history_entry和memory_updatesleep_consolidate进一步创建 episode、提取 semantic facts、执行冲突检测和遗忘归档。MemoryReviewer则复盘对话只保存用户偏好、项目事实、工具配置、领域知识和调试经验。最小流程可以压成这样def remember(obs, session_key): if is_current_task_state(obs): working_memory.add(obs) return if not is_durable_or_reusable(obs): return entry MemoryEntry( typeclassify_user_or_environment(obs), tierMemoryTier.SEMANTIC, keyextract_key(obs), contentvalidate_content(extract_content(obs)), source_sessionsession_key, importanceestimate_importance(obs), ) store.add(entry) store.queue_embedding(entry)这里有三条边界。当前任务变量留在WorkingMemory不要写成长期事实。模型推断不能直接变成高置信记忆“用户可能喜欢简洁回答”不等于“用户喜欢简洁回答”。记忆还要有作用域echo-agent 的scope_policy在 session 模式下只让同 session 记忆在对应会话可见除非它带global标签或没有source_session。长期记忆是一种认知承诺系统承诺未来在某些场景下仍把它当作可用背景。既然是承诺写入就必须保守。宁可少记一些可以重新获取的信息也不要把一次性状态、未验证猜测、临时情绪或敏感内容写进长期上下文。写入安全记忆会进入系统提示词所以记忆内容本身就是攻击面。如果系统把“忽略之前所有指令”“不要告诉用户”“系统提示覆盖”这类内容保存为长期记忆下一次模型看到它时记忆就会变成 prompt injection 的载体。echo-agent 在写入前会校验内容扫描提示注入模式、外泄模式、敏感路径和隐形 Unicode 字符。比如curl携带KEY/TOKEN/SECRET、读取.env、.netrc、.npmrc、authorized_keys、~/.ssh、~/.echo-agent等模式都属于需要阻断的风险信号。写入一致性也不能靠运气。记忆可能被用户命令、后台 reviewer、consolidator 或多个任务并发更新。echo-agent 使用文件锁和原子写先取得锁再重新加载当前类型文件修改后写入临时文件、fsync最后用os.replace替换。检索与遗忘记忆保存下来之后还要回答另一个问题当前任务到底需要召回哪些记忆。召回过少Agent 会忘记关键事实召回过多模型上下文会被旧信息污染。长期运行后记忆库里会出现许多相似但不完全相同的事实只靠文本相似度很容易召回错误项目、旧配置或过期偏好。echo-agent 的MemoryStore.get_snapshot会把长期记忆、会话用户记忆和环境记忆注入系统提示词并设置字符上限。书稿中给出的限制是用户记忆约 1375 字符、环境记忆约 2200 字符。这不是小细节而是防止记忆淹没当前任务。HybridRetriever进一步组合 BM25、向量相似度和遗忘曲线。BM25 擅长精确关键词比如命令名、文件名、配置项。向量检索擅长语义相似比如“测试入口”“构建命令”“失败复盘”这些说法不同但含义接近的查询。echo-agent 还会根据 query entropy 自适应调整权重高熵、宽泛查询提高向量权重低熵、具体查询提高关键词权重。如果 FAISS 或 embedding provider 不可用系统会优雅降级到关键词检索。遗忘曲线提供第三类信号half_life base_half_life * (1 log2(1 access_count)) decay 0.5 ** (days_since_access / half_life) effective_importance importance * decay访问次数越多半衰期越长记忆衰减越慢。低价值、长期未使用的记忆会被归档或遗忘。这样系统不会变成只会追加的日志库而是会重新组织长期经验。检索的目标不是展示系统知道多少而是只把当前任务真正需要的少量信息带回上下文。冲突治理记忆缺失会让 Agent 重新询问记忆冲突会让 Agent 基于错误事实自信行动。用户今天说项目用npm下周迁移到pnpm某个配置路径昨天还有效今天已经重构。长期系统一定会遇到这些变化。直接覆盖会丢失历史不利于审计完全保留又会污染上下文。echo-agent 的ContradictionDetector会先用同 key 候选和向量相似度找可能冲突的记忆再用 LLM 或启发式规则检查。冲突会保存到memory_contradictions解决时可以把旧记忆标记为被新记忆supersede。这让记忆演化有历史。模型看到的不是两条互相打架的事实而应该是类似“旧记忆说使用 npm但当前证据显示已迁移到 pnpm以当前文件为准”的状态。生产可用性判断一个 Agent Memory 是否接近生产级不能只看“有没有记住用户偏好”。更硬的检查项应该是这些检查项可验证标准分层模型是否区分 working、episodic、semantic、archival类型隔离是否区分 user memory 和 environment memory写入标准是否判断稳定性、来源、作用域、隐私和冲突风险安全扫描是否阻断 prompt injection、敏感路径、凭证外泄和隐形字符并发一致性是否有文件锁、重新加载和原子写检索策略是否结合关键词、语义相似、重要性和时间衰减上下文上限snapshot 是否有字符预算冲突处理是否记录 supersede、contradiction 和版本关系遗忘能力是否支持归档、降权、删除和用户纠正回归测试是否覆盖 CRUD、作用域过滤、注入阻断、检索、reviewer 和冲突处理书稿里的测试也体现了这个方向test_memory_advanced.py覆盖MemoryEntry、Episode、Contradiction、ForgettingCurve、WorkingMemory、HybridRetriever、ContradictionDetector和MemoryStoretest_memory_store_save.py验证 user/env 记忆分别写入对应 JSONtest_memory_reviewer.py验证 add、replace、remove 和多匹配错误。这些测试说明记忆系统不是一个 add/search 工具而是覆盖安全、作用域、持久化、检索、复盘、遗忘和冲突治理的一组基础设施。小结Agent Memory 的核心不是“记住更多”而是选择性地把未来仍有价值的信息变成可检索、可更新、可遗忘、可审计的结构。短期状态留在WorkingMemory任务片段进入 episode稳定事实提升为 semantic memory低频或旧记忆进入 archival。用户偏好和环境事实分开治理写入前做安全扫描检索时尊重作用域和上下文预算冲突时记录版本关系长期不用时归档或遗忘。这样设计之后Agent 不只是能在当前窗口里接上话而是能在长期协作中积累经验并且知道这些经验什么时候可靠、什么时候需要重新确认。全篇完本文为 echo-agent 设计笔记系列第 17 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《用 RAG 给 Agent 接入内部知识》敬请期待。