代码能跑,构建通过,测试全绿,CI 一片绿。然后人类来读它了。

打开网易新闻 查看精彩图片

问题出在那些编译器管不着的地方:Kafka 消息上用了 IList,序列化没问题,消费端却反序列化不了接口;变量名叫 result,而这个代码库里连循环变量都有正经职位;改了 handler,旁边的 validator 没跟着改,契约测试照样绿,因为它压根没覆盖新字段;旧名字的枚举项还躺在两个目录之外。

这些全都不在 CLAUDE.md 里,但团队里每个人都清楚,包括上周刚入职的那位。这就是缺口:隐性知识。评审者脑子里有,模型没有,除了评审者一次次手动搬运,没有任何东西能把它送过去——而且每次的评论还越来越短。

每条评论都被修了。下一个 PR,同一类问题,换个文件。修评论只消除了今天的症状,只有把"为什么"记下来,才能改变下一个 PR。

先试的是手写清单

做法很朴素:建一个 lessons.md,从 CLAUDE.md 导入,规则是"评审者抓到问题,就加一行"。

三个月后:99 行,记录了 312 次捕获,排第一的那行被命中 19 次。不是 19 个评审者,是同一类问题——"重命名之后,搜整个仓库,别只搜你的 diff"——被反复抓到 19 次,每次带点新花样,评审者的耐心一次比一次少。第二行 10 次,第三行 8 次。这份文件已经变成了排行榜。

每次会话,Claude 的上下文里都有这些行。没用。改到 6 万 token 的时候,那行字早就被三次文件读取和一次测试运行埋到后面去了。记忆不是问题,时机才是。没人会在飞行途中重读安全须知。

然后是四个 bash 钩子加 jq

每个脚本开头都是 command -v jq || exit 0。没装 jq,就静默死掉,没有任何信号。几周后才发现。这套系统实现了完美的沉默,而它存在的唯一目的恰恰是防止沉默。

tacit:记录"纸割伤"

于是有了 tacit,一个 Claude Code 插件,专门存"纸割伤"——小、可重复、会累积的那种伤。它分三部分。

第一,记"为什么",不记"是什么"。钩子在 gh pr view --comments 或 gh api …/comments 之后触发,指向一个技能:逐条分诊评论,修掉有效的,然后每修一条回答两个问题——我当初为什么那么写?这属于哪一类错误?"忘了删枚举"不是根因,"把删除范围限定在我正在编辑的文件里"才是。前者给你一张便利贴,后者给你一条 grep。

第二,两层结构,这个拆分是关键。全局层写"消息类型要跨序列化边界:用具体集合,永远别用接口";本地层写"Kafka 载荷:用 List 不用 IList,grep -rn 'IList' src/Messages/"。一个文件装不下"每行都通用"和"第二次命中就改写成具体命令"这两件事,具体命令是跟技术栈绑死的,所以必须两个文件。作者说自己花了很久才停止跟这个设计较劲。还有作用域变体:.claude/papercuts/sql.md 带 paths 前置元数据,只在写入匹配文件时注入,schema 和原生 .claude/rules/ 一致,升级就是挪个文件。

第三,在当下注入,不是开头注入。每次触发都基于一个事实:命令跑了,或者文件即将被写入。不是每次编辑都注入——同一段文字一次会话重复 200 遍就是墙纸,模型学会跳过它的速度和人类学会跳过 cookie 弹窗一样快。

会被审计的计数器

每条全局行末尾都带 [hits: N, last: YYYY-MM]。同类问题再被抓到就自增、打磨措辞,不许加近似重复项。hits 大于等于 2 时 lint 失败:改写成一条检查,把命令放进本地库,标记为已升级。

但命中数是模型自报的。让模型数自己的错误,等于让猫记录自己打碎花瓶的次数。满足"自增、别重复"最省事的办法就是照样重复,然后报告一颗干净的良心。所以每次捕获都往 papercuts-log.jsonl 追加一行,tacit audit 拿日志和计数器对账:日志多于声称,说明偷偷加了近似重复项;声称多于日志,说明捕获没写日志。要证据,不要直觉。

每层硬上限 30 条。第 31 行就是清单变成装饰品的地方。没人读的清单比没有更糟:它看起来像有覆盖。

两条不变量

  • 绝不输出 permissionDecision: "allow"。它会跳过用户确认,一个只做上下文的钩子如果输出它,就等于静默自动批准 git push。测试里有断言。
  • 绝不抛异常,绝不非零退出。弄坏工具调用的钩子比没有钩子更糟。所有失败都走向沉默。

tacit doctor 之所以存在,是因为一个以"不保持沉默"为职责的系统,静默失败是致命的。

当天写完当天删掉的功能

纠正检测——识别"你做错了,应该这样"这类话。没有有限文法。试过三种:正则,有界、Unicode 安全,但只认英文;通过 API 调 Haiku,关思考、低 effort、4 个输出 token、只回 Y/N,每次约 0.00001 美元,但需要钩子环境里有 ANTHROPIC_API_KEY,订阅会话没有;分离式 claude -p --model haiku,走会话认证,订阅可用,但每次提示要 3.6 秒冷启动——为了说一个 Y 或 N,启动一整个 Claude,等于雇个顾问来点头,而且在桌面应用里也没法认证。

最后上线的是一行提醒:第一次提示时出现,之后每 10 次出现一次,指明哪个技能负责评审、PR 评论和纠正。判断交给主模型——它本来就握着提示,是房间里最聪明的东西,而且免费,任何语言都行,每 10 次提示约 60 token,压缩后重新武装。绕了一整天聪明方案,最后选了最笨的那个。

三个分类器全部从代码树里移除。不是关掉开关,是删除。藏在开关后面的死代码,只是葬礼办得好看一点的死代码。

宿主现在原生做了什么

对照 Claude Code 2.1.278 检查过:自动记忆会记录纠正(type: feedback),按仓库存储,默认开启;项目根目录的 CLAUDE.md 能扛过压缩;路径作用域规则在读写时加载,压缩时被摘要掉。

原生没有的:根因捕获、复发计数器、计数器审计、通用/本地拆分、写入或推送时的清单。这些就是它存在的理由。宿主哪天原生支持了,作者说他会乐意删掉它——他还有别的纸割伤要处理。

安装两条命令:claude plugin marketplace add N1XUS/tacit,claude plugin install tacit@tacit。存储文件是纯 markdown,归你所有,放在插件目录之外。禁用插件,纸割伤还在,跟真的一样。