我的AI在for循环中从磁盘读取JSON 900次(为什么没有静态代码检查工具能拯救你)

上周,我的AI写了一段代码,每次都从磁盘读取一个JSON文件,对其解析后再进行一次查找,然后总共重复了900次。这意味着每次迭代里都会经历:打开文件,解码JSON,找到某个值,再全部丢弃从头开始。 这个错误是我在教学生编程的第一个月就会告诉他们千万不要犯的。 发生了什么事(长话短说) 我在开发Tokamak,一个用于macOS的菜单栏应用,用来监控Claude Max的配额使用情况。它的一部分功能需要扫描约900个Claude Code会话数据生成的JSONL文件(JSON Lines)。对于每个文件,它需要知道在上一次的扫描中读取的字节偏移量(增量读取——只读取新数据)。 偏移量都保存在一个JSON文件中: { "version": 1, "offsets": { "proyecto-a/sesion-1.jsonl": 48231, "proyecto-b/sesion-2.jsonl": 12044 } } --- 一个`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生成的扫描循环代码: ```swift for file in files { // 900个文件 let storedOffset = offsetStore.offset(for: file.relativePath) // ↑ 每次读取和解析磁盘上的JSON文件!!! if file.fileSize == storedOffset { continue } // ... 读取文件,更新偏移量 ... offsetStore.setOffset(newOffset, for: file.relativePath) // ↑ 再读一次,同一个JSON文件,然后修改并保存。 } 每次迭代读两次磁盘,共900次迭代。这意味着总共有1,800次I/O操作,原本应该只需要2次:一次读取和一次写入。 ...

2026年2月24日 · Fernando

为什么你发送给Claude的99%内容都已经被缓存了

我正在构建一个应用来监控我在Claude Code中的token消费。几天前,查看原始数据时,我遇到了这样的情况: cacheReadInputTokens: 4.241.579.174 inputTokens: 1.293.019 从缓存中读取的四十二亿个tokens。一百三十万个"新鲜"tokens。这是**99.97%**的缓存命中率。 我的第一反应是认为出了什么问题。没人能达到99%的缓存率。Redis不行。Cloudflare不行。你妈妈说她已经知道你要吃什么的时候也不行。 但事实证明它没有坏。就是这样工作的。而原因既优雅又反直觉。 缓存的不是文本 这里是大多数解释都不够深入的地方。当你看到"提示词缓存"时,你会想到类似Redis的东西:保存问题,保存答案,如果有人问同样的问题就返回同样的答案。 完全不是这样。 缓存的是KV张量——transformer在预填充阶段计算的Key和Value矩阵。用通俗的话说:当LLM收到你的提示词时,它首先要做的是将所有这些文本转换为内部数字表示(embeddings),然后与权重矩阵相乘,得到注意力机制生成响应所需的"键"(K)和"值"(V)。 这种计算是极其昂贵的。在一个200,000个tokens的提示词中(在Claude Code中很常见,对话历史会累积),我们谈论的是数十亿次矩阵乘法运算。这是最消耗GPU的部分,最耗时的部分,成本最高的部分。 这就是巧妙之处:在你的一条消息和下一条消息之间,99%的提示词不会改变。系统提示词是相同的。之前的对话历史是相同的。它读取的文件是相同的。唯一新的是你的最后一条消息。 为什么要重新计算你30秒前已经计算过的东西呢? 匹配机制的工作原理 仅仅缓存是不够的。你必须知道缓存什么时候有用。这里Anthropic使用了一个优雅的技巧:按前缀的累积哈希。 提示词的每个块(system、tools、消息)生成一个哈希。但不是单独的哈希:是累积哈希。第3块的哈希包括第1、2、3块的内容。如果前面任何块中的任何东西发生变化,后面所有块的哈希也会改变。 当新请求到达时,系统从标记有cache_control的点开始向后搜索,逐块比较哈希,直到找到匹配的最长前缀。所有匹配的→从缓存读取。只有新的→重新计算。 这就像一部你已经看了40遍的电影。你不需要看完整部电影就知道会发生什么。你只需要从与你记忆中不同的点开始看。 注意这个数据:系统只向后检查最多20个块。超过这个范围,它就停止搜索。这是一个实用的决定,避免花在搜索缓存上的时间超过直接计算张量的时间。 为什么Claude Code有99%的缓存命中率 现在你知道匹配是如何工作的了,99%就不再神秘了。看看Claude Code中典型会话发生的情况: 消息1(会话中的第一条): 系统提示词 (8K tokens) + 工具 (2K tokens) + 你的消息 (500 tokens) = 10,500 tokens → 全部计算,全部写入缓存 消息2: 系统提示词 (8K) + 工具 (2K) + 消息1 (500) + 响应1 (3K) + 你的消息2 (500) = 14,000 tokens → 前面的10,500个 → 缓存命中(我们之前已经计算过) → 新的3,500个 → 计算并添加到缓存 缓存命中率:75% 消息10: 系统提示词 + 工具 + 9条消息 + 9个响应 + 你的消息10 = ~150,000 tokens → 前面的~149,500个 → 缓存命中 → 新的~500个 → 计算 缓存命中率:99.7% 看到了吗?对话历史只是增长。每条新消息都是累积总数的微小部分。缓存比率以自然对数的确定性收敛到99%。 ...

2026年2月19日 · Fernando

5种对抗代码幻觉的防御措施(但只有3种真正有效)

上周我讲述了我的AI如何凭空编造完整JSON结构并将其封装进DTO、测试夹具和验证测试中的故事。90个测试全部通过,却是一场骗局。 那篇文章是诊断报告。而本文是治疗方案。 发现问题后,我的反应和任何自尊心受损的工程师一样:连续数天疯狂研究防止重蹈覆辙的方法。我阅读论文、测试工具、分析API真实数据,最终为应用构建了一套防御系统。 调查结果让我惊讶:在五类应对措施中,只有三种真正奏效。剩下两种充其量只是"善意表演"。 思维模型:你与AI的对决 在介绍具体措施前,需要先建立认知框架。最好的类比来自深度学习领域: 在**生成对抗网络(GAN)**中存在两个互相竞争的神经网络: 生成器负责产出内容(图像/文本等) 判别器负责鉴别真伪 系统通过两者的对抗持续进化。生成器变得更擅长欺骗,判别器变得更擅长识别。 当使用LLM编程时,你正身处一个非自愿的GAN环境: LLM是生成器。它产出代码、DTO、测试和夹具。 你是判别器。必须辨别真实与虚构的内容。 但存在一个残酷的不对称性:生成器永不疲倦,而你会。LLM可以不费吹灰之力生成50个文件。你检查到第10个就开始疲劳,第11个文件就可能被漏检。 这就是我在1Password每天47次TouchID验证中提到的"授权疲劳"。依赖人类时刻保持警觉的安全措施不过是纸糊的墙。 重点监控边界 你不需要(也不应该)逐行检查。需要重点盯防的是边界——代码与外部世界交互的环节: 边界 核心问题 外部API DTO字段是否真实存在于API? 依赖包 该依赖是否存在且名称正确? 数据库表 表结构是否包含这些字段? URL/端点 端点是否存在且响应合规? 铁律:LLM对外部世界的任何声明都需验证后再采信。 它说话时的信心程度不能作为判断依据。Anthropic在官方文档中也承认: “Claude有时会生成包含虚构信息的回答…这些回答往往以自信、权威的语气呈现。” LLM说"我确定"和说"我觉得"时,错误的概率其实完全相同。 自动化判别流程 终极目标是摆脱对人为主观能动性的依赖,将验证自动化: 改进前: LLM生成 → 人工抽查 → 合并 改进后: LLM生成 → CI用真实数据验证 → 人工检查差异 → 合并 下文将介绍的五种措施,都是实现这种自动化判别角色的方法。其中有些有效,有些则不尽如人意。 数据实证(致怀疑者) 如果你觉得"这事不会发生在我身上",请看实际研究数据: LLM推荐的21.7%开源包是虚构的。商业模型降至5.2%(仍意味着每20个就有1个虚构包) GPT-4o针对低频API的有效调用率仅38.58%。比抛硬币的胜率还低 当前最佳代码幻觉检测方法的准确率仅22-33%。换句话说:每四个虚构字段只能抓到一个 研究人员上传了LLM常虚构的空包名。3个月收获3万次下载。业界称其为"垃圾注册"(slopsquatting) AAAI 2025发表的研究CodeHalu将代码幻觉分为四类: 类型 特征 实例 字段映射 字段映射关系错误 混淆user_id和account_id 命名虚构 字段名不存在 使用response.quota.percentage而非真实字段response.utilization 资源虚构 虚构API资源 虚构的active_flags字段 逻辑虚构 看似合理实则错误的业务逻辑 基于虚构字段的判断逻辑isPaid = !activeFlags.isEmpty 我的遭遇正是资源虚构导致逻辑虚构。虚构字段不存在,而依赖它的业务逻辑却天衣无缝。堪称教科书级的"自洽虚构"。 ...

2026年2月16日 · Fernando