上周让 Claude Code 动一个两个月没碰的仓库,改支付回调。它改完,测试全过。我多留了个心眼,回头看它这一路翻过哪些文件——定义回调接口的 base 文件,不在它的清单里。
先停一下,问个问题:它没读,凭什么改对?
不是运气。它改出来的调用签名严丝合缝,运气没这种准头。剩下只有一种解释:它记得。模型见过这套代码,参数里存了一段模糊但好使的“这仓库长什么样”的记忆。有篇刚出的论文把这事在七个模型、五个工具框架上做了实验,一个结论把我那天的心虚做实了:因为模型记得相关仓库,读取行为不再能预测成功。
先别急着说“不读也能对,不是挺好”。不读也能对,意味着“读没读”这个我一直拿来判断它上没上道的信号,废了。
我写到这里才承认,自己原来多依赖这个信号。它看没看关键文件、有没有在 base 定义前停过,我默认这套动作和最终质量之间有某种关系。现在这条关系断了——它跟“懂不懂”没关系,只跟“记没记住”有关系。这个区分我以前没细想过。
再往下呢?论文里有个更刁钻的实验:把库里的标识符重命名,让模型对真实库的记忆失效。结果七个模型在同一个位置翻车,过了同一批测试,漏了同一批该测的——不是各自翻各自的车。这说明参数记忆不是随机撒的坏运气,是结构性的:所有模型都记住了差不多的东西,也在同一个地方缺着同一块。换模型绕不开那个洞。
反过来,它既不记得、也没读到,会怎么样?会停下来问吗?
不会停。事实缺失的时候代理照样行动。缺失的事实不导致缺失的工作,导致错误的工作——它自己编一个文件、猜一个数值,把洞填上。这跟“幻觉”不完全是一回事。幻觉是你碰巧看见它胡说;这里是它有产出地、有模有样地把空白处填满。用基于读取的检测工具看,你看到的是完整的工作链:看起来读了上下文,看起来写了代码,测试也过了。洞上面已经盖好土,你看不出下面缺一块。你甚至不能让它自己报告。有模型永远说自己卡住了,有模型从来没说过一次——连“我受阻了”这么基本的一句话,跨模型都不可信。
还有个我第一反应“不对吧”的点。我们总觉得离得近的上下文才管用。论文说,不是。提供的事实,不管离编辑点多远,都一样有效。事实不是概率,是二进制的——在就是在,不在就是不在。距离只影响 token 账单,不影响它会不会用。
我一开始也以为,那好,我主动把事实喂给它,它就不瞎猜了。方向没错。但实验里有一条对称的坑:当标准文件和真实代码不一致时,代理会遵循标准——哪怕标准给的是更差的代码。
想想那种活了好几个迭代期的约定文档。“错误统一用 HTTP 200 加 code 字段返回”。实际代码早改成正常 4xx 了。你没喂,它去看代码;你喂了,它反而照文件把好实现改回去。
这个点我停得最久。比“它会编”更磨人。编造好歹你还有一层戒心;过期的事实是你主动放进上下文的,它执行得乖乖的。出了事你没法怪它,只能怪自己又维护漏了一条。这么说吧,上下文的代价不只是 token。你引用的每一条约定、文档、schema,都变成一条必须维护“它现在还是这样”的边。一旦脱节,不会在日志里冒红,只会安静地变成一条会复发的坑。
所以收回到最前面。那论文的建议读着不像新方法,倒像是所有被自己监控系统骗过的人凑一块总结的经验:别再在“写之前读没读”上较劲了,去比较产物。这里“比较实际产物”不是让 CI 跑测试——它自己编的事实能给自己打掩护,测试过得了,需求对不上。是把产物拉回最初那条需求层面看:这个 diff 动的到底是不是它该动的地方,还是动了一堆它假装需要动的地方。
上周那次它没读却改对,不代表我看走眼。意味着我撞上了它恰好记得这件事。而“它恰好记得”和“它真的知道”,从外头看,一模一样。
这才是我睡前会想一会儿的那个点。再往下,是怎么提前知道它哪里记得、哪里记错——这个我还没想通。但“它读没读文件”,肯定不是答案。