字数:约3400字 | 阅读时间:8分钟
“人可读,不等于人可直改——这是am项目用一次事故、一份被打回三轮的提案、一天三阶段落地换来的设计翻转。”


一、从一个字段说起:事故是怎么发生的

给AI建的”第二大脑”(am项目)有一条铁律:记忆正本是明文Markdown加Git,谁都能打开看。这个设计让审计变得极其简单——git log 一翻,哪条记忆谁写的、什么时候改的,清清楚楚。

但”人人可看”慢慢滑向了”人人可改”。

事故锚点是一次提交 74b004f:一条 rel-amr-delivers-context 的记忆链接记录里,混进了一个schema之外的字段。入库没人拦,落盘没人报,唯一能看到异常的地方——解析器的 parseErrors 统计——因为字段不合法,记录解析直接失败,错误永远不可见。

一条记忆静静躺在库里,看起来完好,实际上永远读不出来。更麻烦的是没人知道还有多少条这样的记录。

事后复盘时发现了更根本的问题:这条记录是人工直接编辑进库的。而整个系统里,人工写入完全绕过归属检查——.ownership.json 里写着这条记录属于哪个 agent,但人工提交根本不看它。

二、提案:关掉人工直写,听起来很简单

方案听起来一句话就能说完:graph/records 目录关闭人工直写,所有修正走专门的意见通道,由 agent 程序化消化入库。

这就是决策#23「vault 程序化只写」。核心原则一句话:人可读 ≠ 人可直改。正本对人是只读的,人想改,写意见进 human-feedback/ 暂存目录,管道提取、校验、落成合法记录。

但第一版提案在评审这里撞了墙。而且不是撞一次。

三、三轮评审,三次 request-changes

提案经历了三轮独立复核,结论全部是 request-changes。每一轮都在上一轮的基础上挖出更深的坑。

第一轮挖出 8 项发现,最重的两条:提案的 Impact 清单漏列了 5 处 bypass 写点——你以为只堵了一个门,实际上系统里有五条小路;以及 commitHumanEdit 这个”自由通道”压根没纳入设计。

第二轮(10月7日独立复核)确认一轮 8 项中 6 项成立,新发现 3 项 P0、4 项 P1、2 项 P2。这一轮还修正了一轮的关键表述:真正的缺口不是没有 schema 校验,而是没有统一契约——各处校验各管各的,没有中央闸门,绕过一处就全绕过了。

第三轮把前两轮的 P0/P1 全部独立复现,补上一手实证,又新增 2 项 P2、2 项 P3。结论维持 request-changes。

三轮下来,最初的方案被拆解成了一份带 A-E 五项决策的整改计划。拍板结论全部按评审推荐执行:

  • A② 未认领的 vault 首次程序化写入,按调用方 agentId 自动认领;
  • B① hook 作者判定以 .ownership.json 属主为单一事实源,不看邮箱猜身份;
  • C① 全通道统一提交约定:vault(<kind>): <id> + Audit-Category: agent + Actor: <agentId>,让每笔提交都能被机器识别归类;
  • D① T9 模式⑤ 重定向到 human-feedback 通道;
  • E① 非 records 路径的人工提交默认拒绝,白名单放行。

值得强调的是这个过程的性价比:三轮评审不是三轮推翻重来,而是每轮收窄、逐层压实。第一轮找方向性漏洞,第二轮修正一轮误判并独立复现,第三轮补实证收口。最终拍板的五项决策没有一项是评审期间没讨论过的——拍板只是确认,不是裁决。

四、整改计划:三个Phase,从闸门到管道

评审通过后的执行没有拖泥带水。整改计划拆成三个阶段,10月7日一天全部落地。

Phase 1:中央写闸(commit bcc36cd,11:01)

核心是一个新模块 assertCanWriteForRecords——所有写入 records 的路径必须过这道闸,串联 ownership 和 schema 双道检查。设计上有个值得学的细节:结构化返回、never throw。检查失败不抛异常,而是返回带错误码的结构(SCHEMA_INVALID 字段级定位、OWNERSHIP_CONFLICT),调用方决定怎么处理。拒绝会写审计事件,绝不静默。

闸门装上之后做的第一件事不是庆祝,是收口:排查出 5 处 bypass 写点(applyReply、handleRefinement、flipFact、bootstrapSelfPerception、attachLink),全部改走闸门。没有闸门之前,这些内部函数各自写各自的;有了闸门,”写入”这个动作只有一个入口。

存量问题也没放过:迁移脚本对库里已有的非法记录做隔离,Wiki 死链生成清单供手工修复,全程按属主身份落款单笔审计提交。这一步落地后,全量测试 463 用例 0 失败。

Phase 2:hook 兜底与审计扫描(commit c8495ac,11:39)

中央闸门管的是”走正常管道的写入”,那绕过管道直接 commit 的怎么办?答案是 Git pre-commit hook 的 allowlist:graph/records/** 的提交作者必须等于属主邮箱,不匹配直接拒绝提交。豁免清单里保留 human-feedback、quarantine 和元数据路径——意见通道和隔离区本来就该自由写。

配合这套约定,staleness 审计引擎加了两个周期扫描分支:HUMAN_COMMIT_DETECTED(作者分支,抓人直改 records)和 SCHEMA_DRIFT(内容基线分支,抓字段结构漂移)。这一层新增 397 行代码,其中 168 行是 human hook 的专项测试、229 行是扫描器测试——防御机制的每一层都配了自己的回归验证。

Phase 3:human-feedback 通道与收官(commit 7533e47,19:36)

最后一阶段把”人怎么提意见”做完整:human-feedback/ 自由格式暂存 + 第 6 个静默摄取模式。文件 watch 加防重复消费 cursor,笔记经提取管道(写闸内建 schema 校验)程序化落为合法记录。细节上留了活口:不可解析的笔记原地保留,产出 feedback-unprocessable 审计事件,不隔离不丢弃——意见通道本身不能变成黑洞。

spec 同步更新(vault + ingestion 两份),决策记录落档,write-only 改造收官。

五、为什么值得单独写一篇

这场改造有三个值得记录的原因。

第一,它修正了一个”合理”的坏设计。“明文Markdown人人可改”曾是am项目的卖点之一,决策#9 里”人工编辑bypass ownership”是白纸黑字的既有语义。承认既有设计有问题,比修补一个新bug难得多。决策#23 里专门写了一句”对决策#9的部分推翻”——设计文档里明明白白记下自己推翻了自己。

第二,三轮评审证明了”独立实例交叉复核”的价值。三轮全部 request-changes,意味着如果第一轮就放行,5 处 bypass、commitHumanEdit 自由通道、author 判定语义不清这些问题会全部带病上线。而三轮评审的执行成本并不高:每轮都是独立实例读提案+读代码+出报告,红蓝对抗在方案阶段跑,比上线后出事便宜几个数量级。

第三,单日三阶段落地验证了小步快跑的边界。写闸、hook、通道三个阶段各自独立可验证,463 用例全程绿。但注意这个”快”的前提:整改计划是三轮评审磨出来的,五项决策是评审期间讨论透的。执行快是因为方案已经慢过了——评审慢下来换执行提速,这笔账是划算的。

六、现在的写入全景

改造后的写入路径收敛成四条:

  1. agent 程序化写入 → 中央写闸(ownership + schema)→ records;
  2. 人工提意见 → human-feedback/ 暂存 → 提取管道 → 经闸落为合法记录;
  3. 绕道直改 → pre-commit hook allowlist 拒绝,或 staleness 周期扫描捕获;
  4. 存量脏数据 → 迁移脚本隔离 + 死链清单,一次性清过。

事件驱动五道静默摄取模式加 human-feedback 凑成六道,全部经过同一个闸门。

对人的体验变化只有一处:想改记忆,不能再顺手改文件提交了,得往 human-feedback/ 丢一篇自由格式的笔记——剩下的交给管道。看起来麻烦了一点,但这正是这次改造的全部意义:便利让渡给一致性,一致性保住可信度。第二大脑首先是”可信”的,然后才是”好用”的。

下一篇,按系列规划该讲召回优化实战(44%→78%)或者安装指南。至于一直挂着的 wave3 评审接力,那是下一篇里的事了。