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_update;sleep_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( type=classify_user_or_environment(obs), tier=MemoryTier.SEMANTIC, key=extract_key(obs), content=validate_content(extract_content(obs)), source_session=session_key, importance=estimate_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和MemoryStore;test_memory_store_save.py验证 user/env 记忆分别写入对应 JSON;test_memory_reviewer.py验证 add、replace、remove 和多匹配错误。
这些测试说明,记忆系统不是一个 add/search 工具,而是覆盖安全、作用域、持久化、检索、复盘、遗忘和冲突治理的一组基础设施。
小结
Agent Memory 的核心不是“记住更多”,而是选择性地把未来仍有价值的信息变成可检索、可更新、可遗忘、可审计的结构。
短期状态留在WorkingMemory,任务片段进入 episode,稳定事实提升为 semantic memory,低频或旧记忆进入 archival。用户偏好和环境事实分开治理,写入前做安全扫描,检索时尊重作用域和上下文预算,冲突时记录版本关系,长期不用时归档或遗忘。
这样设计之后,Agent 不只是能在当前窗口里接上话,而是能在长期协作中积累经验,并且知道这些经验什么时候可靠、什么时候需要重新确认。
(全篇完)
本文为 echo-agent 设计笔记系列第 17 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《用 RAG 给 Agent 接入内部知识》,敬请期待。