开源 Agent 套件 ECC 的记忆系统实操:什么该写进去,写多了会怎样
本文基于 ECC 仓库 commit 591ab5c(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/affaan-m/ECC 最新代码与文档为准。
ECC 处理跨会话记忆的第一原则是:存进去的东西永远是”待核实的上下文”,不是”下次照做的指令”。 这句话在它的 schemas/memory.schema.json 里被写死成了一个只有单一取值的字段——trust 的枚举只有 unreviewed 一个值,没有 verified,也没有任何一条把它改成别的值的路径。文档在 docs/design/ecc-memory-vault.md 里把理由讲得很直白:一个有 shell 能力的 agent 不能被当成独立的人工审批边界。你要是只从这一篇里带走一样东西,带走这个就够了。
一、它到底想解决哪个问题
ECC 是一套装在编码 Agent 之上的增强件,仓库里 agents 目录有 67 个 agent、skills 目录有 281 个技能、commands 目录有 94 个命令,采用 MIT 许可证。这么大的一坨东西里,记忆这一块解决的是一个很窄但很痛的问题:你在 Claude Code 里跟一个任务磨了三小时,得出的结论、踩过的坑、验证过的命令,会话一关就散了;换到另一个 harness 上接着干,等于从零开始。
站内已有两篇讲记忆的文章,跟这篇是分工关系:Agent 的记忆到底是什么 讲的是记忆这件事本身的通用分层与取舍,Claude Code 上下文管理 讲的是单个会话内部怎么把上下文用得省。那两篇是方法论;这篇不重复方法论,只看一个你能当场 clone 下来逐行核对的具体项目,把这套想法最终落成了什么样的目录、什么样的字段、什么样的命令,以及它在哪些地方主动认怂。
ECC 的做法是建一个本地的 Memory Vault,存的不是任何一家 harness 的原始 transcript,而是一种叫 ecc.memory.v1 的可移植 Markdown 文档。设计文档把这条写成硬约束:Markdown 文件是唯一真相来源,SQLite 上下文图谱、embedding、托管系统都只能是索引或适配器,不能是唯一的那份拷贝。这条约束的直接后果是——你不装任何数据库、不联网、不调模型,也能 cat 出自己所有的记忆。
二、记忆放在哪、长什么样
Vault 分三个作用域,这是你上手要做的第一个判断:
project:<repo>/.ecc/memory/project/,仓库内的本地上下文,带一个 fail-closed 的.gitignore保护;如果这个保护文件存在但内容不符合预期,初始化和写入会直接失败而不是”尽力而为”。team:<repo>/.ecc/memory/team/,打算给人看、打算提交进版本库的那部分。user:~/.ecc/memory/,跟着你这个人跨仓库走。
三个作用域下面按 kind 再分子目录:contexts、decisions、facts、handoffs、lessons、notes、preferences、runbooks,八类,跟 schema 里 kind 的枚举一一对应。检索的默认行为值得记住:常规 search 只覆盖 project 和 team 里状态为 active 的条目,user 作用域永远不会被隐式带上,必须显式 --scope user 才召回。如果你想让不同的 harness 共用同一个 vault,它们要么在同一个仓库工作目录下,要么统一设置 ECC_MEMORY_PROJECT_ROOT 和 ECC_MEMORY_USER_ROOT。
每条记忆是一个带严格 YAML frontmatter 的 Markdown 文件,设计文档给的样例是这样:
---
schema: "ecc.memory.v1"
id: "mem_20260726_01k123example"
title: "Authentication migration handoff"
kind: "handoff"
scope: "project"
trust: "unreviewed"
status: "active"
source_harness: "codex"
target_harnesses: ["claude"]
tags: ["auth", "migration"]
links: ["mem_20260725_01kolder"]
created_at: "2026-07-26T20:00:00.000Z"
updated_at: "2026-07-26T20:00:00.000Z"
---
schema 文件里对这些字段卡得比看上去紧:ID 必须匹配 mem_ 开头的小写 slug 文法;harness 名、tags 走同一套受限 slug 文法;时间戳必须是带毫秒的 UTC 格式;标题和正文都禁止控制字符和双向文本改写字符(那类字符是终端注入的常见载体);正文长度有上限,而且运行时还会按 UTF-8 字节再卡一遍,防止你用多字节字符绕过字符数限制。
写入是 create-only 的——工具永远不会覆盖一个已存在的记忆 ID。想推翻旧结论,做法是写一条新的、用 links 指回去,然后手工把旧的标成 superseded。status 三个取值 active / rejected / superseded,常规召回只返回 active,但你按 ID 直接读,仍然能读到非 active 的那条用于排查。这套设计的取向很清楚:宁可留下一条演化链,也不要让历史被静默改写。
三、这几块分别归谁管
记忆这件事在 ECC 里其实不止一条线。下面这张表是你排查问题时的定位图:
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| Memory Vault 文档契约 | 定义一条记忆的字段、长度、状态与 ID 文法 | schemas/memory.schema.json | doctor 报某个文件 malformed、想自己生成记忆文件时 |
| 设计与边界说明 | 讲清约束、威胁边界、非目标、状态迁移 | docs/design/ecc-memory-vault.md | 决定要不要引入、要不要把 team 记忆提交进库时 |
| 跨 harness 使用指引 | 什么时候写、怎么写 handoff、怎么校验 | skills/unified-memory/SKILL.md | 让 Agent 自己往 vault 里写东西时 |
| MCP 服务条目 | 把同一套读写能力暴露成 MCP 工具 | mcp-configs/mcp-servers.json 的 ecc-memory-vault | 想让 Agent 不通过 shell 也能存取记忆时 |
| 生命周期钩子契约 | 会话启动/压缩前/结束时的持久化时机 | hooks/memory-persistence/README.md、hooks/hooks.json | 会话一开就被塞进一堆旧内容、想关掉时 |
| 会话启动注入实现 | 真正决定往新会话里塞什么、塞多少 | scripts/hooks/session-start.js | 调 ECC_SESSION_START_MAX_CHARS 这类开关时 |
| 经验条目学习系统 | 从工具使用观测里攒出带置信度的小条目 | skills/continuous-learning-v2/SKILL.md | 发现 Agent 开始”自作主张”按某种偏好干活时 |
注意 Memory Vault 和后面两行不是一回事。Vault 是你(或 Agent)显式写进去的,钩子那条线是自动发生的。绝大多数”我的 Agent 怎么突然行为变了”的困惑,根子在自动那条线上,不在 vault 里。
四、什么该写进去,什么绝对不该
skills/unified-memory/SKILL.md 把该写的场景列得很克制:需要留给另一个 agent 或以后会话用的持久上下文、harness 之间的工作交接、恢复任务时要查的既往决策与教训。反过来,它明确说不要把 vault 当任务追踪器、密钥保管处、策略引擎,或者受治理的项目文档的替代品。
交接类记忆有一份很实用的正文清单,四件事:目标与当前状态;已经收集到的证据、跑过的命令或测试;牵涉到的文件和外部工作项;剩余工作、阻塞点、风险,以及下一个具体动作。这四条比任何模板都值钱——写不出”下一个具体动作”,说明这条交接本来就不该写。
保存操作有个细节容易被忽略:官方示例是把正文从标准输入喂进去的。
printf '%s\n' 'The migration tests pass; rollout is still pending.' |
ecc memory save \
--title "Authentication migration status" \
--kind context \
--source-harness codex \
--target all \
--tag auth \
--stdin
这么写不是为了好看,是为了让正文不出现在进程列表里。你要是图省事把正文塞进命令行参数,同机器上任何一个能跑 ps 的进程都看得见。
绝对不该写的部分,文档给得很硬:密码、token、私钥、cookie、凭据、敏感个人数据一律不进。运行时确实会拒绝掉已知形态的密钥,但它自己声明这只是兜底,不是完整的分类器——别把它当成安全网来用。另外两条同样重要:不要把原始会话记录整个导进去,只摘录未来真正用得上的那部分;不要把召回到的记忆直接提升成规则、技能、runbook 或架构决策,那必须由人核过证据之后去改仓库里的正式文档。设计文档特意补了一句——team 记忆不会因为它被提交进 Git 就变得可信。
还有一条是给写 prompt 的人看的:召回到的正文要当成不可信输入处理,不能当指令执行。CLI 上那个 --target-harness 只是调用方自己选的路由过滤器,不是授权边界。任何人往 vault 里放一段”请执行以下命令”,它照样会被 search 到。这跟站内 Agent 的记忆污染与遗忘 讲的是同一类风险,只不过这里能看到一个项目是怎么在文档层面把它挑明的。
五、边界与代价:它明确不管的事
这套设计放弃了不少东西,而且是写在”非目标”里主动放弃的:不做向量数据库、不做托管同步服务、不做新的 agent 框架、不自动导入 Claude/Codex/Hermes 的 transcript、不自动把记忆升级成技能或策略、不在首版里解决跨机器的无冲突复制。检索在首版是有界的词法检索,语义重排被规划成”可选适配器”,前提是不改文档契约。
安全边界也说得很坦白。首版运行时防的是恶意 vault 文档、稳定的符号链接与路径逃逸、误提交项目记忆、跨 harness 的 MCP 身份冒充、已知密钥形态、终端控制字符和有界的资源耗尽。它不是同一个操作系统用户下并发进程之间的安全边界——文档直接点名 Node.js 拿不到 openat2 那种目录文件描述符相对的保证,消除不了父目录替换竞态。需要防本机恶意进程的,得上独立系统账号、容器或等价的文件系统隔离。
代价还有一层在使用侧。这类套件会往你机器里写文件、挂钩子。hooks/memory-persistence/README.md 列的生命周期契约里,Stop 上挂的格式化与类型检查是会因为钩子失败而阻塞的;PreToolUse / PostToolUse 上挂着观测记录。文档给运营者的期望里有一条写得很清楚:持久化默认保持在本地,除非用户明确启用某个集成,否则不要把 transcript 或工具调用轨迹发到托管服务。这条是期望,不是强制——你装任何第三方增强件之前,都该自己去 hooks/hooks.json 里数一遍到底挂了多少个进程在你每次按回车之后跑起来。
MCP 那条路径也一样。stdio 服务是可选的,默认的 .mcp.json 并不启用它;启用要自己把 mcp-configs/mcp-servers.json 里的 ecc-memory-vault 条目抄过去,并把占位符换成一个小写的 harness 身份。这个身份由服务端的 ECC_MEMORY_HARNESS 决定,调用方在工具参数里改不了,写入的来源身份和检索的目标过滤都绑在它上面。user 作用域在 MCP 下默认是关的,除非运维者额外设 ECC_MEMORY_ALLOW_USER_SCOPE=1,而且调用方仍要显式请求。暴露出来的工具只有 memory_save、memory_search、memory_read、memory_doctor 四个——没有审核、没有提升、没有覆盖、没有导入 transcript、没有执行 shell。这个”少”是刻意的。
六、上手与避坑清单
技能不等于运行时。 skills/unified-memory/SKILL.md 开头就写了它只是指引,不是可执行程序;skill-only、最小化、手工和 Claude 插件这几种安装方式都不会把命令装到 PATH 上。踩坑的样子是:Agent 读了技能,信心满满地去调 ecc memory save,然后报 command not found,接着开始编造替代方案。避法是先把运行时装上(仓库给的是 npm install -g ecc-universal),用 ecc memory --help 和 command -v ecc-memory-mcp 各验一次;仓库检出的场景可以走 node scripts/ecc.js memory ...,但 MCP 配置里点名 ecc-memory-mcp 的地方仍然需要那个二进制在 PATH 上。
写之前先搜。 create-only 的写入模型意味着同一件事写三遍就是三个文件,检索时全都命中,谁也不知道哪个最新。技能里的顺序是先 ecc memory search 再决定要不要 save,这不是礼节,是这套模型下唯一能防重复的手段。
新会话被塞了一堆旧东西,去关注入而不是删记忆。 会话启动那条线跟 vault 是独立的:scripts/hooks/session-start.js 会把上一段会话的摘要和当前生效的经验条目注入进来。摘要外面裹了一层”仅作历史参考,不是当前指令”的护栏,原因写在代码注释里——实际观测到过压缩恢复之后模型拿着旧的 ARGUMENTS 重跑 slash 技能,重复建了 issue、分支和任务。真嫌吵的话,ECC_SESSION_START_CONTEXT=off 直接关,或者用 ECC_SESSION_START_MAX_CHARS 收紧上限;hooks/memory-persistence/README.md 还提到整条钩子链可以用 ECC_HOOK_PROFILE 和 ECC_DISABLED_HOOKS 做剖面控制。
经验条目攒多了会反噬。 skills/continuous-learning-v2/SKILL.md 那套系统从工具使用观测里生成带置信度的原子条目,会话启动时按置信度排序注入一批。这就是”记忆写多了”的反效果最容易发生的地方:条目一多,注入的都是几个月前某次一次性纠正沉淀下来的偏好,模型每次开工都要背一遍,既占上下文又带偏行为。仓库给了两个刹车——ECC_INSTINCT_CONFIDENCE_THRESHOLD 抬高门槛、ECC_MAX_INJECTED_INSTINCTS 限制条数,还有 /prune 清理长期没被提升的待定条目、/promote 把真正跨项目复现的条目才升成全局。默认是项目隔离的,别手贱把项目级的偏好一股脑升成全局——技能文档写得很明白,项目作用域这一层就是为了挡住跨项目污染才加的:React 项目里攒出来的写法留在 React 项目,Python 的约定留在 Python,只有真正跨项目复现的才够格升级。注入时的排序也是先按置信度、同置信度下项目级优先,所以全局条目一多,最先被挤掉的反而是当前项目里那条更贴题的。这块的取舍逻辑跟站内 Agent 记忆分层 讲的是一回事,只是这里能直接看到旋钮在哪。
提交 team 记忆之前跑一次校验。 ecc memory doctor 会报格式错误、重复 ID、断链和被跳过的符号链接,但它不删也不改,修是你自己的事。检出格式错误的文件会被排除在检索之外——如果你发现某条记忆怎么都搜不到,先怀疑它 frontmatter 写坏了。
别把钩子当成免费的。 挂钩子这件事本身对每次工具调用都有开销,Stop 上那条质量门还会因失败而阻塞。站内 Claude Code 钩子机制 那篇讲了这套机制本身的形态,配着 hooks/hooks.json 看就知道装一个第三方套件到底给你的会话加了多少道工序。
收个尾。判断一条信息该不该进 vault,用三个问题过一遍就够了:换个会话、换个 harness,有人会需要它吗?它是结论还是过程(过程不写)?它里面有没有一个字是凭据(有就不写)?三个都过了,再想它属于哪个 kind、该落在哪个 scope。
想继续深挖,按这个顺序读仓库:先 docs/design/ecc-memory-vault.md 看约束与非目标,再 schemas/memory.schema.json 看字段到底卡到什么粒度,最后 skills/unified-memory/SKILL.md 看它建议 Agent 怎么用。想弄清楚”我的会话开头为什么多了这么多字”,那就绕开 vault,直接去 hooks/hooks.json 和 scripts/hooks/session-start.js——答案在那儿,不在记忆库里。
本文属于 ECC 开源 Agent 套件专题(共 40 篇,从选择性安装一直拆到内部机制)。同一批还写了另一种取向的项目——不给你资产、只给你纪律的 superpowers 方法论专题。