让一套配置自己变好:开源 Agent 套件 ECC 的持续学习机制拆解
本文基于 ECC 仓库 commit 591ab5c(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/affaan-m/ECC 最新代码与文档为准。
这套机制里最值钱的一步不是”自动学习”,而是它逼你把「学到了什么」写成一个有触发条件、有置信度、有证据、可以被删掉的最小单元。 至于自动那部分——后台起一个进程去读你的操作日志、猜你的习惯——它做得挺工整,但恰恰是最容易滑向自嗨的地方。先说清楚这个判断,后面的拆解才有落点。
ECC(https://github.com/affaan-m/ECC ,MIT 许可证)是一套装在编码 Agent 之上的增强件:agents 目录 67 个 agent、skills 目录 281 个技能、commands 目录 94 个命令。本文只挑其中一块讲——skills/continuous-learning-v2/。它试图回答一个所有天天用 AI 写代码的人都想过的问题:这一天下来我纠正了它十几次,这些纠正能不能沉淀下来,明天不用再纠正一遍。
一、它要解决的是「会话一关,经验就散了」
这个项目里同时躺着两代实现,对照着看,设计意图最清楚。
老的那版在 skills/continuous-learning/SKILL.md,文件头上就标了 DEPRECATED。它的做法是挂一个 Stop 钩子,会话结束时评估整段对话,把可复用的模式抽出来写成一个技能文件,扔进 ~/.claude/skills/learned/。思路直白,问题也直白:抽出来的东西是”技能”,而技能要不要在下一次被激活,是模型自己判断的。仓库里对这一点的原话是:
“v1 relied on skills to observe. Skills are probabilistic — they fire ~50-80% of the time based on Claude’s judgment.”
一件事只有五成到八成的概率发生,你就没法基于它做工程决策。v2 的应对是把观察这一环从”模型判断”挪到”钩子触发”:改用 PreToolUse 和 PostToolUse,每一次工具调用都记一笔,不依赖任何判断。钩子这层机制本身是宿主提供的,如果你对触发时机和数据格式没概念,可以先看钩子机制那篇。
同时被换掉的还有分析的位置。v1 在主会话里做分析,v2 挪到一个后台进程,用小模型跑。这一改动的后果比看上去大:它意味着”学习”这件事不再占用你正在干活的那个会话的上下文,代价是它变成了一笔你看不见的额外调用。这笔账后面单独算。
二、「学到了什么」被定义成 instinct
这是整套设计里我认为最该被抄走的部分。
v1 存的是技能——一段散文,讲某个坑怎么绕过去。v2 存的是 instinct,一个更小、更硬的东西。仓库给的样例是这样:
---
id: prefer-functional-style
trigger: "when writing new functions"
confidence: 0.7
domain: "code-style"
source: "session-observation"
scope: project
project_id: "a1b2c3d4e5f6"
project_name: "my-react-app"
---
# Prefer Functional Style
## Action
Use functional patterns over classes when appropriate.
## Evidence
- Observed 5 instances of functional pattern preference
- User corrected class-based approach to functional on 2025-01-15
拆开看这几个字段各自扛着什么:
trigger 强制你写出触发条件。没有触发条件的经验,本质上是感想,你没法判断它该在什么时候起作用,也没法事后追问它到底有没有起作用。
confidence 是一个 0.3 到 0.9 的数。SKILL.md 里给了四档语义:0.3 试探性、0.5 中等、0.7 强、0.9 接近确定。有了这个数,一条经验才可能被降权、被遗忘,而不是永远躺在那里。
domain 是分类标签。观察者的提示词里把可选值写死成六个:code-style、testing、git、debugging、workflow、file-patterns。窄枚举比自由文本好得多——自由文本的分类第二天就会分裂成十几种写法。
Evidence 段落要求写清楚这条 instinct 是凭什么建立的。这一节的存在让每条 instinct 都可以被质问。
scope 是第二代实现后来才补上的一栏,只有 project 和 global 两个值。这解决的是一个很具体的痛点:你在 React 项目里养出来的习惯,跑到 Python 项目里就成了噪音。默认落到项目,只有真正通用的东西才升到全局。
把这五个字段放在一起,你会发现它其实定义了一种可证伪的经验格式。这跟站内讲的通用记忆分层思路是一路的——Agent 记忆怎么分层那篇讲的是原则,这里是一个具体项目把原则落成了 YAML frontmatter。
三、这条链路实际是怎么跑起来的
从一次工具调用到磁盘上多出一个 instinct 文件,中间经过这么几站。至于这个文件怎么再被读回会话,放到下一节讲,因为那一环恰恰是判断这套东西有没有用的关键。
写观察。 hooks/observe.sh 在每次工具调用前后各跑一次,把工具名、输入、输出、会话 ID、项目 ID 拼成一行 JSON,追加到 observations.jsonl。输入输出各截断到 5000 字符。写盘之前过一遍正则脱敏,命中 api_key、token、secret、password、authorization、credentials、auth 这些词后面跟着的一串值,替换成 REDACTED。
认项目。 scripts/detect-project.sh 负责判断你现在在哪个项目,优先级四级:CLAUDE_PROJECT_DIR 环境变量、git remote get-url origin 的哈希、git rev-parse --show-toplevel 的路径哈希、都拿不到就落到全局。用 remote URL 哈希的好处是同一个仓库在不同机器上得到同一个 ID,用路径哈希则是机器相关的。每个项目拿到一个 12 位哈希。
存哪儿。 不在 ~/.claude 下面。SKILL.md 里给的理由是宿主的敏感路径守卫会拦住后台写入,所以数据目录按 CLV2_HOMUNCULUS_DIR、$XDG_DATA_HOME/ecc-homunculus、$HOME/.local/share/ecc-homunculus 的顺序解析。老用户有一个 scripts/migrate-homunculus.sh 做一次性迁移。
后台分析。 agents/observer-loop.sh 是一个常驻循环,被定时或者信号唤醒。唤醒后它不会把整个观察文件塞给模型,而是 tail 最后若干行(默认 500,可用 ECC_OBSERVER_MAX_ANALYSIS_LINES 调),写到临时文件,然后起一个非交互的 claude 子进程去读它。子进程默认用 haiku(ECC_OBSERVER_MODEL 可改),并且带着 ECC_SKIP_OBSERVE=1 和 ECC_HOOK_PROFILE=minimal——不然观察者自己的操作又会被观察一遍,套娃。
别把它吵死。 observe.sh 里有一个计数器,每积累 20 次观察才给观察者进程发一次 SIGUSR1(ECC_OBSERVER_SIGNAL_EVERY_N)。agents/session-guardian.sh 再加一道闸:活跃时段默认 8 点到 23 点,加冷却和空闲判断。这些细节说明作者在真实环境里被后台进程反复触发咬过。
判定口径写在哪。 agents/observer.md 是给观察者看的规则文件,列了四类模式:用户纠正、错误修复、重复工作流、工具偏好。它反复强调保守——只有出现三次以上的清晰模式才建 instinct,触发条件要窄,绝不写入真实代码片段只写模式描述,拿不准就用项目作用域。
人来收口。 一组斜杠命令,背后都是同一个 scripts/instinct-cli.py:
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 观察钩子 | 每次工具调用写一条脱敏后的观察记录 | skills/continuous-learning-v2/hooks/observe.sh | 装上就一直在跑,你平时看不见它 |
| 项目识别 | 把当前仓库映射成 12 位项目哈希 | skills/continuous-learning-v2/scripts/detect-project.sh | 换 remote、换目录后 instinct 认不出来时 |
| 观察者规则 | 定义识别哪几类模式、instinct 长什么样 | skills/continuous-learning-v2/agents/observer.md | 想改判定口径、觉得它学歪了 |
| 后台循环 | 定时唤醒、采样、起子进程分析 | skills/continuous-learning-v2/agents/observer-loop.sh | 排查”为什么一条 instinct 都没生成” |
| 命令行工具 | status / evolve / promote / prune / 导入导出 | skills/continuous-learning-v2/scripts/instinct-cli.py | 所有斜杠命令背后都是它 |
| 开关配置 | 观察者是否启用、间隔、起分析的最低观察数 | skills/continuous-learning-v2/config.json | 第一次装完,以及怀疑它没在工作时 |
| 回读注入 | 开会话时挑高置信度 instinct 拼成一段附加上下文 | scripts/hooks/session-start.js | 想知道”学到的东西到底有没有生效” |
命令那一层:/instinct-status 看当前学到了什么,/evolve 把相关的 instinct 聚成技能、命令或者 agent,/promote 把项目级的提到全局,/projects 列所有已知项目,/prune 清理过期的待定项,/instinct-export 和 /instinct-import 做导出导入。另外还有一个 /learn,是手动的——你在会话中途觉得刚才这个问题解得漂亮,可以当场让它把可复用的模式抽成候选技能。注意它不走 instinct-cli.py 这条线,产物形态也不是 instinct,别把两者混在一起看。
顺带一个观察:docs/continuous-learning-v2-spec.md 只有十几行,写的是”实现主要在 skills/continuous-learning-v2/ 和 scripts/hooks/,把这个文件当作稳定的引用路径”。这类快速迭代项目的文档普遍如此,权威在代码里不在文档里。你要核对行为,去读脚本。
四、这件事最容易滑向的自嗨
现在说开头那个判断的另一半。
第一处,回路是接上了,但接得很细。 回读这一环不在 continuous-learning-v2 这个目录里,而在仓库根上的 scripts/hooks/session-start.js——它注册在 SessionStart 上,开新会话时把项目级和全局的 instinct 目录都扫一遍,按 id 去重(同名时项目级压过全局),按置信度排序,然后拼成一段以 Active instincts: 开头的附加上下文塞进会话。
关键在于它塞进去的是什么。每条 instinct 只取 Action 段落的第一行,前面缀上作用域和百分制置信度,一条就一行。而且有两道闸:置信度低于阈值的直接丢弃(默认阈值写在 DEFAULT_INSTINCT_CONFIDENCE_THRESHOLD,可用 ECC_INSTINCT_CONFIDENCE_THRESHOLD 覆盖),排序后还要截断到 ECC_MAX_INJECTED_INSTINCTS 指定的条数上限。再往上还有一层总的注入开关和字符预算,关掉或设成零就整段不注入。
所以真实情况是:你辛辛苦苦攒出来的一堆 instinct,最后能开口说话的只有排在前面的那几条,而且每条只有一句话。剩下的躺在磁盘上,/instinct-status 会把它们算进统计,但它们从来没进过任何一次会话。至于 /evolve --generate 生成的技能、命令、agent 文件,落在数据目录的 evolved/ 下面——我把仓库里所有引用 evolved 的文件挑出来看过,只有 instinct-cli.py 和它的测试在读写这个目录,没有任何加载环节会去碰它。也就是说这批产物是纯离线的,你不手动搬走,它就一直是死文件。
这带来一个很具体的失败模式:跑上两周,/instinct-status 打出一屏漂亮的置信度进度条,你觉得它学了很多,实际生效的只是开头几行摘要。判断有没有真在工作,别看统计数字,去看会话开头有没有出现那段 Active instincts:。
第二处,聚类比你想的笨。 /evolve 的聚类逻辑在 instinct-cli.py 里,做法是把 trigger 转成小写,然后把 when、creating、writing、adding、implementing、testing 这几个词从字符串里删掉,剩下的当作聚类的 key,同 key 的归成一簇,两条以上算技能候选,三条以上且平均置信度达标算 agent 候选。生成命令名的时候也是类似的字符串替换加截断。这是纯字面处理,不是语义聚类。后果很直接:instinct 的 trigger 怎么写,直接决定了 evolve 能不能聚出有意义的东西。你写”when writing new functions”和”when creating a function”,在它眼里是两簇。
第三处,也是最要命的——置信度衡量的是频次,不是效果。 观察者的提示词里把映射写死了:出现 3 到 5 次给 0.5,6 到 10 次给 0.7,11 次以上给 0.85。SKILL.md 里补充的调整规则也是同一路数:重复观察到就加,用户明确纠正就减,长期没观察到就衰减。
请留意这里发生了什么替换:“你反复这么干”被当成了”这么干是对的”。 可你反复干的事里,有相当一部分是你反复犯的错、反复绕的弯路、反复用错的工具。Evidence 字段记的是”观察到 N 次”,没有任何一栏记录”这条 instinct 被应用之后,结果是不是更好”。也就是说,这套系统定义的”学到了什么”,严格讲是”重复出现的行为”,不是”被验证有效的行为”。
分清这两者,是判断这类工具值不值得开的分水岭。想做到后者,你得有独立的验证环节而不是自我确认——站内让 Agent 自查自纠的边界和给 Agent 配一个对手来验证讲的是通用方法,Agent 记忆该怎么建讲的是原则层面该怎么想。本篇不重复那些方法论,只做一件事:把 ECC 这个具体项目怎么把它落到磁盘上的每一步拆开,让你能拿它对照自己的方案。
五、边界与代价:它明确不管的那些事
不管效果验证。 上一节说完了。它给你频次和纠正信号,不给你有效性证明。
不管你的隐私偏好,只给你一层尽力而为的脱敏。 观察数据全部留在本机,导出时只能导 instinct 不能导原始观察,这两条是明写的。但请如实理解它写盘的是什么:每次工具调用的输入和输出,各截断 5000 字符,过一遍正则脱敏。正则能挡住形如”api_key: xxx”的常见写法,挡不住你粘进对话里的一段配置文件、一段客户数据、一个没有关键词前缀的凭据。这类套件往你机器上写文件、挂钩子、起后台进程,装之前先问清楚你的代码库允不允许这种落盘。
不管额外调用的成本,那是你的账单。 后台分析是真的又起了一个模型进程在跑,虽然默认用的是小模型、只喂尾部若干行、还加了节流和活跃时段限制,但它确实在消耗。除此之外还有一笔容易被忽略的:回读注入会在每个新会话开头往上下文里加一段,条数虽有上限,但它是每次都加。各家服务商的计费与限制规则不同且会调整,以官方最新说明为准;你要做的是在开之前知道有这两笔支出,而不是月底才发现。
Windows 上默认不产出。 observer-loop.sh 里有一段明确的判断:检测到 Windows 就跳过分析,理由是非交互模式已知会挂死,除非你把 ECC_OBSERVER_ALLOW_WINDOWS 设成 true 覆盖掉。这意味着 Windows 用户默认只会攒观察记录,一条 instinct 都不会自动生成。这不是 bug,是作者主动关掉了一条会卡住的路径,但你得知道。
不在 git 仓库里就没有项目作用域。 项目识别依赖 git,散落在仓库外的脚本目录里干活,观察会落到全局。
默认是关的。 config.json 里 observer.enabled 是 false。装上不等于在学。
导入导出没有合并语义。 export 和 import 是文件级的,团队里两个人各学各的、想合并到一起,冲突要你自己解。
六、上手与避坑清单
先确认观察者是否真的启用了。 会踩是因为整套东西的观察部分(钩子写 jsonl)和分析部分(后台起进程)是分开的,钩子一装就在跑,分析默认关着。结果就是观察文件天天涨,instinct 目录始终空的,你以为它坏了。怎么避:装完先跑 /instinct-status,它会告诉你当前的数据路径和数量,然后去数据目录看 observations.jsonl 和 instincts/personal/ 各自有没有东西——只有前者有,就是分析没开或者没跑起来。
Windows 上先去看日志再怀疑人生。 会踩是因为跳过分析这件事只写在日志里,界面上没有任何提示。怎么避:先读 observer 的日志文件确认是不是被那条 Windows 判断挡了,再决定要不要开覆盖变量——同时明白它默认关掉是为了绕开一个已知的挂死问题,你开了就得自己承担。
别去 ~/.claude 底下找数据。 会踩是因为很多教程和旧版本都在那个位置,而 v2 把数据挪到了 XDG 目录以避开宿主的敏感路径守卫。怎么避:以 /instinct-status 报出的路径为准;有老数据就跑一次仓库里那个迁移脚本,不要手动拷。
用插件方式安装就别再往设置文件里手抄一份钩子。 会踩是因为手动安装的文档段落写着要往 settings.json 里加 PreToolUse / PostToolUse 块,插件方式其实已经自动注册过了。SKILL.md 里明确写了后果:重复执行,而且 ${CLAUDE_PLUGIN_ROOT} 这个变量只在插件管理的钩子配置里可用,抄到用户设置里会解析失败。怎么避:确认自己是哪种安装方式,二选一,别叠加。
trigger 一定要写窄。 会踩是因为 evolve 的聚类是字面处理,宽泛的触发条件既聚不起来也没法判断命中。怎么避:手写或修正 instinct 时,触发条件写到”在这个项目里改 X 类文件时”这种粒度,别写”写代码时”。
别急着往全局提。 会踩是因为看到一条 instinct 挺通用就手动提到 global,然后它开始在所有项目里发言。怎么避:守住自动提升的门槛——同一条在两个以上项目里出现过、平均置信度达到 0.8 才算候选;不确定就留在项目作用域,/promote 支持先 dry-run 看一眼。
定期清待定项。 会踩是因为自动生成的候选会一直堆着,堆多了 /instinct-status 就没法看了。怎么避:跑 /prune,默认清 30 天以上没被处理的,也支持先 dry-run。
别把观察文件当审计日志。 会踩是因为它看起来像一份完整的操作记录,实际上 observe.sh 里写了:超过 10MB 就归档,归档文件超过 30 天自动删。怎么避:真要留痕,另做一套,别指望这个。
最后
把这套东西读完,我留下的自检清单是三个问题,可以拿去问任何一个声称能持续学习的方案,不限于 ECC:
一,这条学来的经验能被证伪吗?如果它没有触发条件、没有证据来源,那它就只是一段感想,你连它有没有生效都判断不了。
二,它上一次真正影响行为是什么时候?这个问题必须能指到一个具体位置——某个会话开头的某段注入、某个被真正加载的文件。指不到,它的产出就还是一份报表,不是一套配置。
三,删掉它,会变差吗?敢做这个实验,说明你的置信度是测出来的;不敢,说明它是频次凑出来的。
要接着往下读,顺序建议这样:先 skills/continuous-learning-v2/SKILL.md,把存储结构和作用域规则过一遍;再 agents/observer.md,那是判定口径,你觉得它学歪了就该改这里;最后 hooks/observe.sh,看清楚它到底往你磁盘上写了什么。这三个文件看完,你就有足够依据决定要不要让它在你机器上常驻。
本文属于 ECC 开源 Agent 套件专题(共 40 篇,从选择性安装一直拆到内部机制)。同一批还写了另一种取向的项目——不给你资产、只给你纪律的 superpowers 方法论专题。