| |
一个Dictionary<String, UInt64>包含了900条记录,约55KB,但并不算大。
但问题在于,更让人抓狂的是:这个文件是应用自己生成的。它不是来自于外部的API,也不是Claude Code交付的文件。这是Tokamak自己生成和读取的内部状态文件,用于记录每次扫描的会话进度。
“那你为什么不用Core Data或者SQLite呢?你应用里不是已经有这些东西了吗?”这是个好问题。原因是这个文件是一个一次性缓存文件。就算它被损坏了,只需要删除文件,下次扫描时重新读取整个文件即可,数据根本不会有任何丢失。而且我可以简单地用cat session-offsets.json | jq .来进行调试(用Core Data的话就需要用sqlite3并进入沙盒路径),这也是一个Sendable数据结构,不需要来回折腾的后台事务。并且,如果Core Data的SQLite文件损坏了,也不会影响这个偏移量的存储(反过来也是如此)。对于一个只有55KB大小的扁平字典,去创建一个支持schema迁移的实体未免有些大材小用。
所以问题并不是格式,而是访问方式。
以下是AI生成的扫描循环代码:
| |
每次迭代读两次磁盘,共900次迭代。这意味着总共有1,800次I/O操作,原本应该只需要2次:一次读取和一次写入。
数据(xctrace不会说谎)
我用Instruments(性能监控工具)定位了这个问题。以下是数据:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 总样本数 | 7,260 | 489 |
OffsetStore.load() 样本数 | 1,704(88%) | 10(2%) |
| 检查时间 | >20秒 | <0.5秒 |
| CPU占用率 | 81% | ~1.5% |
扫描时间的88%都浪费在反复读取和解析这900行JSON上了。就像西西弗斯推石头,只不过这次他用的是JSONDecoder。
修复方法(令人羞愧)
| |
请注意:数据结构本身并没有变化。它仍然是一个Dictionary<String, UInt64>。哈希表的性能已经非常高效。本质问题是在每次迭代中重新从磁盘构建它。
无效的解决方案:在CLAUDE.md中添加“不要这样做”
修复之后,我在项目的CLAUDE.md中特别加了一句说明:
“绝对不要在循环内进行I/O操作(磁盘、网络、JSON解码、Core Data查询等)——如果你可以在循环前进行数据加载,再单独保存,记住要这么做。”
但这完全没用。
几周之后,当我向项目添加第二个服务(Codex)时,AI又生成了完全相同的模式。即使有这一段说明。这就像竖了个“请勿践踏草地”的标志,却指望别人都遵守一样。
为什么会这样?因为LLM并不真正理解这条规则。它只是见过类似的代码。从统计学角度来看,在训练数据中,它见过的大部分代码都是进行单次I/O,而不是在900次迭代中不断重复I/O。模式load → use → save是它认为最可能的方法。而这个函数是否会出现在一个包含900次迭代的for循环中,对模型来说是无关的上下文。
无效的解决方案:静态代码检查工具
不存在可以检测出这个问题的静态代码检查工具。SwiftLint、ESLint、Ruff或者Clippy,全都不可能。想一想:这段代码在语法上是完全正确的,也在语义上是有效的。对offsetStore.offset(for:)的每次调用都合理合法,问题本身不在任何一行代码,而在组合的方式上。
如果用我的课程《对抗性开发》中的一个概念来说,这个问题出现在代码的不同意义层级:
| 层级 | 问题 | 出现错误? |
|---|---|---|
| 1. 信号 | 这是代码吗? | 否 |
| 2. 语言 | 是有效的Swift代码吗? | 否 |
| 3. 语法 | 是否可以编译? | 否 |
| 4. 局部语义 | 函数是否实现了它的功能? | 否 |
| 5. 系统语义 | 是否遵循约定和性能需求? | 是 |
| 6. 架构 | 是否可以扩展且不降级? | 是 |
错误出在了第5到第6层。恰恰是当前2026年LLM仍无法做到的层次。它在语法和局部逻辑上可能无懈可击,但问题是涌现的:当一个无需问题的逻辑单元,出现在性能敏感场景中时,才会显现问题。
静态代码检查工具的工作范围是第2到第4层。它们对组合和性能毫无感知。这就像要求Word中的拼写检查器帮你找到逻辑谬误一样,不可能实现。
唯一有效的方法:事后性能测试
初次修复这个问题后,我写了如下测试:
| |
这是一个简单粗暴的回归测试。1000个文件,限制在3秒完成,否则测试失败。如果有人(无论是人类还是AI)再次在循环内加入I/O,这个测试的时间将从0.2秒增加到30秒,并立刻报错。
结果证明它是有效的。
当AI为第二个服务生成相同问题的代码时,第一个服务的性能测试仍然通过(因为是不同的服务)。但为新服务写的性能测试马上失败了。所以,这个测试成功检测到了回归问题并防止了更坏的结果。而CLAUDE.md的警告,或任何静态代码检查工具,都无法做到这一点。
最终确认
这个问题完美地印证了我所说的《对抗性开发》的核心观点:绝不相信,只做验证。
- 你不能相信AI不会犯新手级的错误。它会,而且会反复犯错。哪怕你已经明确告诉它不要这么做。
- 你不能相信静态代码检查工具会帮你发现这些问题。它们做不到,因为问题高于抽象的层级。
你可以做的是:
- 性能测试,作为事后发现问题的安全网
- 实际性能分析工具(xctrace, Instruments)测量真实开销,避免主观猜测
- 深度防御策略:多层验证,因为单一层验证不足以覆盖所有缺陷
简单来说:验证的系统就像洋葱,它有分层的结构。当一层失效时,下一层会捕捉问题。
给怀疑论者的附言
“但是,费尔南多,一个人类程序员就不会犯这个错误了吗?”
初学者会,哪怕是高级程序员也未必能在第一次完美进行优化——但他们有经验,所以会在代码审查中察觉到问题。AI生成代码的问题是它的巨大产量:10分钟能生成50个文件,而没有人会一行一行地检查这些文件。不用说,光是看它们就能让人感到疲惫。
这也是为什么验证必须是自动化的,而不是依赖人工。性能测试不会累,它不会分心,也不会倦怠。每次运行make test都会自动检测是否有异常。
和我在防止幻觉的五种策略中提到的一样:验证系统一定要与生成系统分离。如果AI负责写代码,验证环节一定得由其他安全系统负责。在这里,验证系统就是用时间维度来静静观察的时钟。