无需 Codex 的 Codex Automations:用 Claude Code 和 systemd 打造夜班代理

两周前,OpenAI 推出了 Codex Automations。简单来说:你定义一个触发器(如 cron、代码 push 或新 issue),写下自然语言指令,一个代理在独立的 worktree 中自动执行这一切。整个过程无需人类介入。在你睡觉时,代理自动整理 issues、总结 CI 错误、生成发布文档,甚至优化自身的指令。 听上去像魔法?确实有几分神奇。但他们在 keynote 里没怎么提到的一个细节是:你必须在桌面上运行 Codex App。支持 macOS 和 Windows。没有无头服务器的选项。安装在一个迷你 PC 上并丢一边任其运行?没门。 这时我想:“等等,我已经有这个了。” 你已经拥有的组件 如果你在使用 Claude Code,那么你已经拥有 90% 的基础设施。用命令 claude --print 就能在非交互式会话中执行提示(prompt)。传递指令,获取结果,关闭,不需要图形界面或者打开的终端窗口。这对于一个 cron 工作来说简直完美。 如果你已有一台永远开机的服务器(比如迷你 PC、树莓派,或者每月 5 欧元的 VPS),那么你已经有了调度程序。systemd 或 cron,任选一个,它们都已运行了多年,已经非常稳定。 如果你在用 Gitea、GitHub 或任何支持 API 的代码托管平台,意味着你已经有了存放结果的位置:PR 评论、新建 Issue 或直接提交文件。 用简单点的话说:Codex Automations 是一种范式,而不是一个产品。而且这种范式已经存在很多年了。 ┌─────────────────────────────────────────────┐ │ systemd timer (每隔 N 小时运行) │ │ │ │ │ ▼ │ │ bash/fish 脚本 │ │ │ │ │ ├── git pull --ff-only │ │ ├── claude --print "prompt" │ │ ├── 解析结果 │ │ ├── 通知 (Telegram/email) │ │ └── git push (如果有变更) │ └─────────────────────────────────────────────┘ 自动化的构成 所有的自动化都遵循同样的结构。一个脚本会经过以下步骤: ...

2026年3月11日 · Fernando

一个拥有2500层但实为MD5的神经网络:从中学到的调试经验

几周前,世界上最顶尖的量化交易公司之一 Jane Street 发布了一个关于 解释性机制 的谜题。他们手工设计了一个拥有大约2500层线性结构、使用整数权重的神经网络,并以一个简单的问题把它交给了公众:这个网络到底在计算什么功能? 答案是:MD5。一个诞生于1992年的密码哈希算法,完全由矩阵运算和ReLU函数以神经网络的形式实现。 问题的精髓并不在答案,而是在最终赢家破解谜题时的过程。本质上,这个过程是一份调试复杂系统的教科书,它的应用远超机器学习领域。 实验背景 这个谜题并非一个典型的“黑盒”问题。参赛者可以获得模型的完整规范:所有的权重矩阵、偏置以及网络架构都公开了。不需要猜测模型的结构,而是需要理解模型 具体在做什么。 网络接受一个文本字符串作为输入,并返回0或1。官方给的例子是:“vegetable dog” 的输出是0。 拥有大约2500层线性结构、约200万个节点、却没有任何文档,这个问题的核心是:输入和输出之间到底是什么关系? 解题路径:一个调试案例分析 最终赢家 Alex 的解题过程展现了资深工程师常用的调试逻辑。过程中的工具可能因领域不同而有所变化,但思维方式无疑是通用的。 阶段1:观察,而不是立刻动手 Alex 做的第一件事是可视化权重矩阵。他没有直接运行模型,也没有尝试训练它。他只是看了数据。 他的发现是:所有权重都是整数。没有小数,没有浮点数,而是清一色的整数。 这显然是一个重要的信号。使用梯度下降训练的神经网络通常会产生许多小数权重,而整数权重表明这是一个用特定目标手工设计的网络。这不是一个“学习”的模型,而是一个伪装成神经网络的程序。 这告诉我们调试中的第一个关键原则:在解释数据内容之前先观察数据的形状。比如,一个以每100毫秒精确间隔打出的时间戳日志肯定不是来自实际流量;一个JSON对象里的每个字段都精确包含3个元素,也不可能来自生产环境。数据的形状揭示了它的来源。 阶段2:缩小问题规模 Alex 尝试将神经网络转化为一个可满足性(SAT)问题。基本思路是:如果每个神经元都可以看作一个逻辑约束,或许一个SAT求解器能找到让网络输出为1的输入。 他的缩减过程是循序渐进的: 移除了80%的“恒等变换”神经元 合并了只有一个输入且权重为1的节点 压缩了具有相同输入向量的节点 通过这一系列优化,网络从200万个节点缩减到7.5万个节点,最终转化为20万个SAT变量。 但依然没解出来。问题的计算复杂度仍然过高,无法用暴力法解决。 这个阶段带来的重要教训是:正确地简化问题后仍无法解决,这本身也是重要的信息,而不是失败。如果在消除所有“附带复杂度”之后,问题仍然困难,那说明问题的难点是固有的。这一点揭示了它的本质,而在本例中,它的本质是这个函数可能不可逆——我们无法通过输出推断出输入。 阶段3:识别模式 Alex 注意到这个网络中存在一个显著的模式:有32个计算模块周期性地重复出现。32轮完全相同的结构,加上不可逆的性质。 于是他向 ChatGPT 提问:“有哪些密码算法使用32轮计算?” 答案是——MD5。 这是“灵光一闪”的瞬间。但注意,它并不是无来由的突发奇想,而是三个前期资料(整数权重 → 手工设计、不可逆 → 加密算法、32轮 → 特定密码协议)的合并推理,最终形成了一个可验证的假设。 这种模式——通过不断加入约束,直到可能性空间逐渐收敛,与资深工程师在生产环境中诊断问题的方法高度一致。资深工程师并不是提前知道答案,而是通过每一步观察逐步排除整个可能性类别,直到只剩下一种可能性为止。 阶段4:发现问题 接下来的步骤中,Alex 验证了自己的假设:他计算了几组输入的MD5值,并与网络输出进行对比。结果显示,网络输出与MD5哈希值完全一致,当输入长度不超过32字符时。 但是,当输入长度超过32字符时,网络的计算结果就出现了偏差。 这是因为网络中存在一个bug。设计者在处理输入长度编码时出现了错误——在前7层中发生了一个溢出错误,使得网络在处理长输入时与标准MD5的输出结果不同。 Alex 一层一层地追踪错误,直至找到确切的偏差点。 这是一个绝佳的案例:在边界条件下验证假设。如果你的心理模型认为“这个神经网络在计算MD5”,那么仅仅在理想情况下(短输入)验证它是不够的。必须在极端条件下(长输入、空输入、特殊字符)对其进行测试。而当它失败时,这些偏差往往正是定位问题的关键线索。 阶段5:成功破译 最终,确定这是一份具有已知bug的MD5函数后,Alex 提取出了倒数第二层中的目标哈希值偏置项。接着使用词典暴力破解法,找到了谜底——两个用空格分隔的英文单词。 这对调试的启示 Alex 的解题过程与机器学习本身关系不大,它本质上是一场关于调试的教科书式行动: 阶段 在谜题中的体现 在实际调试中的体现 观察形状 整数权重 → 手工设计 日志间隔均匀 → 合成数据 缩小问题规模 200万节点 → 7.5万节点 → SAT问题 完整堆栈轨迹 → 具体组件 累积约束 不可逆 + 32轮 → 加密算法 只在生产中失败 + 只发生在周一 → 定时任务 边界验证 短于32字符可行,长于32字符失败 → 溢出 ASCII正常,UTF-8失败 → 编码问题 利用错误找线索 溢出问题定位到具体层 堆栈追踪定位至具体行 结构完全一致,差异只是领域不同而已。 ...

2026年3月11日 · Fernando

你的LLM缓存让你付出双倍成本省钱(并且有道理)

几周前我写了一篇文章,解释了为什么你发给Claude的99%数据已经在缓存里。KV张量、VRAM、本地SSD——所有的内部机制。但我遗漏了最痛的部分:账单。 因为prompt缓存是那种看起来像超值优惠的东西,但一旦仔细看看数字,你就会发现为了节省成本,你竟然被要求付更多的钱。 成本悖论 让我们用一些数字来说明。在Claude Sonnet的定价中: 项目 每百万tokens价格 常规输入 $3.00 缓存写入 $3.75 (1.25倍) 缓存读取 $0.30 (0.10倍) 注意一件事:写入缓存的成本比直接处理输入高25%。你为了让下一次使用更便宜而需要额外付费。 这就像加入Costco那样。年费有点心疼。但如果买得足够多,就会值回票价。 问题是,“足够多”取决于你能在缓存过期前读取它的次数。 什么时候你会亏钱? 假设你用cache_control发了一个100K tokens的prompt。第一次请求: 100,000 tokens × $3.75/M = $0.375 (缓存写入) 如果你没有用缓存发送它: 100,000 tokens × $3.00/M = $0.300 (常规输入) 你额外多付了$0.075,比常规价格高出25%。你亏钱了。 现在第二次请求,同样有100K tokens的前缀: 100,000 tokens × $0.30/M = $0.030 (缓存读取) 对比来看: 100,000 tokens × $3.00/M = $0.300 (常规未缓存输入) 你节省了$0.27。两次请求的总成本不仅回本了之前多支付的$0.075,现在还实现了盈亏平衡。 盈亏平衡点是1.4次读取。 用更直白的话说:如果你计划在接下来的5分钟内至少重复使用该前缀两次,缓存就是值得的。 为什么对Claude Code来说使用缓存是明智选择 在Claude Code的一次会话中,每条消息都会包含system prompt、工具定义以及整个对话历史。每条消息都在重复发送相同的上下文。如果没有缓存,每次发送“把这个按钮换个颜色”,你都需要为相同的150K tokens上下文支付$3.00/M。 没人能负担得起这个。 而有了prompt缓存,你只需为写入缓存支付一次费用,之后每次读取只需要$0.30/M。在包含150K上下文的50条消息会话中,差距很明显: 无缓存: 50 × 150,000 × $3.00/M = $22.50 有缓存: 1 × 150,000 × $3.75/M + 49 × 150,000 × $0.30/M = $2.77 从$22.50降到$2.77。节省了88%。这就是为什么Anthropic默认在Claude Code中启用缓存。如果不这样做,从经济角度看是不可行的。 ...

2026年3月10日 · Fernando

从 /simplify 到绝地委员会:如何与 Kent Beck、Martin Fowler 和 Mike Acton 一起进行代码审查

Claude Code 提供了一个名为 /simplify 的 slash command,可以自动审查你的代码。我用它检查了一个大幅变更——大约 8 个文件、500 行代码——结果让人有些意外。它确实发现了些我可能漏掉的点,但也带来了不少无用信息,让我浪费了不少时间。 所以我把它拆解了,然后又重新像拼图一样组装回来。 /simplify 是如何工作的 这是 Claude Code 内置的一项功能(无需额外安装)。它会并行运行三个代理,分别从三个不同的角度来审查代码变更: 代码复用(Code Reuse) —— 是否有可以替换新代码的现有工具? 代码质量(Code Quality) —— 冗余状态、复制粘贴、不良抽象、stringly-typed code 等。 效率(Efficiency) —— 不必要的 I/O、未充分利用的并发、内存泄漏等。 这三个代理会各自给出发现的问题,之后系统尝试直接修复它们。 找到的亮点 代码复用代理发现我在测试代码的两处重复了一个完全相同的辅助函数:相同的名字、相同的代码内容,但分布在两个不同的文件中。我把它提取到一个共享模块里,干净利落。 效率代理指出了一个处理循环中不必要的磁盘操作:每次迭代都加载状态、修改后存储、读取数据、重新加载、再存储。写操作重复了两次,而实际上只需要一次。我没注意到这些隐患,但工具发现了。 还发现了一个内存缓冲区在错误路径中没有清除。如果在分配和释放之间发生错误,会产生内存泄漏。这种问题在主路径上已经被处理到了,但显然 copy-paste 草率遗漏了某些细节。 到这里为止,还算满意。三个发现,全都合法且可操作。但 /simplify 的问题并不在于它发现了什么,而是在于它发现了太多不重要的东西。 存在的缺陷 低级别的问题噪音过多。 它建议我删除一个 struct 的字段,因为 “它与一个计算属性是重复的”。这个字段占用 8 个字节,但被代码和测试中的十多处引用使用。做这个改动带来的代码修改工作量,远远超过了省下这几个字节的好处。 缺乏对项目上下文的理解。 它标记了一个并发模式为 HIGH 严重性,并且的确指出这是一个潜在风险。这一点没错,书面上看是合理的。但实际上,这个问题已经在项目的 CLAUDE.md 文档中记录了,工具链中专门配置了 lint 来应对,并且项目内还有一个相关的 issue 在处理。而 /simplify 并不知道这些,因为它只能基于代码变更的 diff 操作,缺乏项目整体的视角。 无法分辨“错误”和“可优化”。 上文提到的双磁盘操作的确效率低下,但并不是错误。而并发模式的问题就是真正的潜在炸弹。这两者的严重性却都被标记为 MEDIUM,优先级看上去一样,这种扁平的优先级划分很难起到实际帮助。 对外部数据强行推荐 enums。 它建议把某些 DTO 中的字段从字符串转换为枚举,但这些字段只是从外部 API 拉取后用于显示。把它们改成枚举需要自定义解码逻辑,却提供不了任何实际好处——如果对方 API 增加了新值,这样的枚举反倒会导致解析出错。枚举在这种情况下,不如保持字符串更稳妥。 ...

2026年3月9日 · Fernando

/loop 在 Claude Code 中:一个与终端共存的 cron

几个月来,我一直用自制的 cron 来执行 Claude Code 的任务。一个 Bash 脚本启动一个 headless 会话,给它一个 prompt,等待任务完成后关闭。它能运作,虽然运作得勉强,但能用。如果有需要,我把代码放在 GitHub 供大家参考。 而上周五,Anthropic 发布了 2.1.71 版本,引入了 /loop。一个原生的调度器,就直接内置在 Claude Code 会话中。 我的第一反应是:“我的项目凉了。” 试用之后的第二反应是:“嗯…还没死,但离死不远了。” /loop 的功能 语法相当简洁: /loop 5m check the deploy status 这个命令告诉 Claude Code:“每 5 分钟,执行这个 prompt 一次。” 不需要退出会话,也不需要 cron,也不用脚本。Claude 来解析时间间隔,安排任务,并在会话仍然开启且处于 *空闲* 状态时执行它。 你可以串联多个斜杠命令: /loop 20m /review-pr 1234 /loop 1h make test 2>&1 | tail -5 每个 loop 都有一个“安全网”:三天后它会自动到期。如果你曾经留下一个忘记关闭的 cron,你大概能理解这种设计有多贴心。 ## 我会如何使用它 试用一天后,我已经发现了三个明确的用途: **1. 在开发过程中监控测试。** 现在我正在 Tokamak 上实现一个重要功能(比如冷启动的五个优化阶段)。与其每次修改一点代码就运行 `make test`,不如直接这样做: /loop 10m make test 2>&1 | grep -E “passed|failed” ...

2026年3月9日 · Fernando

33,000 行 XML 告诉你 heavyWork() 函数耗时过长:如何驯服 xctrace 应对大语言模型 (LLM)

上周,我在用 Instruments 工具对一个 Swift 应用进行性能分析。没什么稀奇的:运行 xctrace record,再运行 xctrace export,然后把导出的 XML 拷贝到 Claude Code 的上下文,让它帮忙分析热点。 结果 Claude 跟我说:"XML 文件太大,无法可靠地处理。" 33,553 行 XML,只为分析一个只有两个函数的程序。 真正的问题 xctrace export 是一个很棒的工具。它什么都给你:每一个采样点、每一个调用栈、每一帧的二进制信息、内存地址、UUID,简直面面俱到,精准无比,完美无缺。 但问题,也正是源于它的完美无缺。 对一个应用程序进行性能分析时,我并不需要所有的 3,044 个细节采样点。我不需要知道第 1,847 个采样点在 00:02.847.882 捕捉到了 libswiftCore.dylib 内存地址为 0x1027ec9a8 的内容。我只需要知道 heavyWork() 花掉了 70% 的时间,而 lightWork() 只用了 30%。 用大白话来说:我需要的是 10 行总结,而不是 33,000 行繁文缛节。 为什么 XML 格式是正确的选择(但噪音不可取) 在有人提出“2026 年了还用 XML 才是问题”之前——并不是这样。 XML 对于 xctrace 的功能来说,是非常理想的格式。试想一下: 层次结构:一个调用栈是一个框架的树状结构。一个采样点包含了一个调用栈,一个线程,一个进程。XML 自然而然地可以建模这些内容。 自描述性:每个元素都有名字、带类型的属性,并且结构可以被验证。你不用去猜 CSV 第七列的内容代表什么。 优雅的去重:xctrace 使用了 id 和 ref 系统,首次定义一个框架时是这样的:id="59" name="heavyWork()",后续只需引用 ref="59"。可以看作是一种序列化的 flyweight pattern。 可以用标准工具解析:XPath、xmllint、xml.etree.ElementTree…… 不需要专属解析器。 xctrace 的 XML 格式并不是冗余。它是 Instruments 所需的结构化信息,用来重建交互式调用树、对比运行情况,以及按线程和进程进行筛选。它是专门为一个可以展开和折叠节点的 GUI 工具设计的。 ...

2026年3月8日 · Fernando

错误路径应当不可能,而非仅仅禁用

“我有一个 shell,我很有创造力。” —— Claude 解释为什么他用一个 47 行的脚本,并作为一个字符串传递给 python -c 来执行 这句话是真的。我开发的 AI 代理说过——好吧,不是用确切的这些话,但他的行为证实了。他需要启动一个 ETL pipeline 的某个进程。虽然正确的启动命令写在了 Makefile 里,但出了点问题。而他呢?他并没有询问,而是做了任何拥有 root 权限且无人监管的程序员都会做的事:即兴发挥。 太离谱了(manda huevos)。 无人察觉的捏造操作 我之前写过一篇博客讨论代码生成过程中的幻觉问题:LLM(大语言模型)会凭空捏造一个 JSON 字段,围绕它创建 DTO,生成测试代码,最后你手上有 90 个“绿灯”的测试,验证的却是虚构出来的内容。这是个严重的问题,但至少它是静态的。被捏造的代码不会到处乱跑,等着有人来审查。 不过,还有另一种更危险的捏造:操作性捏造。这一问题发生在代理不是编写代码,而是凭空创造出“执行路径”时。 问题的模式永远是一样的: 正确路径失败 → 代理寻找捷径 → 捷径“成功” → 潜在损害 举两个真实案例,都是关于一个 ETL pipeline 聚合多个 web 数据源的。 案例 1:字符串里的脚本。 这个 pipeline 有一个命令 make scrape-fuente,会启动一个守护进程,从而启动多个worker。守护进程负责监控、重启崩溃的 worker,并关闭空闲连接。有一天,代理需要启动一个抓取(scrape)任务。但由于依赖问题,make 运行失败了。他怎么办?他创建了一个内联的 47 行 Python 脚本,把它作为字符串传递给 python -c "..." 运行。没有错误处理,没有 watchdog,也没有清理操作。的确运行了……直到一个 worker 卡住,没人来重启它。这样的情况导致数据不完整、连接未关闭,而我直到三天后才发现。 案例 2:孤独的 worker。 另一个会话,同一个 pipeline。这个代理直接运行了 voyeur worker,跳过了守护进程。worker 开始抓取数据,遇到了一个网络超时,然后陷入了重试的死循环,不断消耗资源。而没有守护进程,也没有集中日志记录,没有人知道发生了什么。几小时过去,服务器一直在不停尝试访问一个返回 503 状态码的页面。 ...

2026年2月27日 · Fernando

Claude Code Remote Control 的结构解析:尚未开放的隐藏 API

2026 年 2 月 25 日,Anthropic 宣布了针对 Claude Code 的 Remote Control 功能。构思是这样的:你在终端启动 Claude Code,然后拿着手机躺在沙发上,通过 claude.ai 继续你的会话。无需 SSH。无需 tmux。也不需要远程终端窗口。 听起来很酷,对吧? 但实际上,当我在终端输入 /rc 激活后,它给了我一个 URL。我打开手机,登录进去… 然后 claude.ai 提示我 “this feature is not active in your organization”(此功能尚未在您的账户中启用)。2026 年 2 月,我订阅的是 Max 5x 账户,每月按时付款。但什么都没用。 于是,我决定做任何理性人都会做的事:反编译 API,弄清楚后台到底发生了什么。 什么是 Remote Control(官方说法) 本质上:它是一个连接本地 CLI 和 claude.ai 网页的桥梁。Claude Code 的 CLI 仍在你的机器上运行(可以访问你的代码、终端和文件),但你可以通过任何浏览器查看它、批准 tool calls,并发送消息。 Anthropic 将它作为一项 “research preview"(研究预览)推出,仅面向 Max 用户。官方的期望是,你可以启动一个长任务(/rc + 提示词),关上笔记本电脑,去健身房,然后通过手机查看任务进度。 然而,正如我们接下来要讨论的,现实要微妙得多。 我们发现的底层真相 当你运行 /rc 时,Claude Code 会在后台做很多事情。而且,这些行动会留下大量的痕迹——在 JSONL 文件中、在 debug logs 里、在 API 响应里。让我们逐一拆解。 ...

2026年2月27日 · Fernando

RustyClaw:我要用 Rust 重写一个AI代理(因为梗在召唤我)

“你知道 Rust 最棒的一点是什么吗?它不会允许你编译粗制滥造的代码。你知道最糟糕的一点是什么吗?起初你写的所有代码都是粗制滥造的。” —— 蟹老板,大概是这样说的 比一个 AI 代理更好的是什么?是一个用 Rust 重写 的 AI 代理。 如果你上网超过五分钟,就会知道这个梗。不管是什么项目:文本编辑器、DNS 服务器、BMI 计算器,总会有人跳出来评论“你应该用 Rust 重写它”。这就是 Rewrite It In Rust —— 简称 RIIR,和地心引力一样不可避免的存在。 好吧,那我就来做一次真的。我将把一个有 8,300 行代码的 Python AI 代理移植到 Rust。但不是因为这个梗在召唤我(嗯…有一点是因为它)。我这么做,是因为我需要一个实验对象。 论点 最近几周,我一直在写关于静默失败、五种防止幻觉的方法、以及"一个 LLM 如何生成看似正确但实际上错误的代码"的文章。我甚至还给它起了个名字:对抗性开发。永远不要相信,总要验证。 很多理论,是时候实践了。 于是,我需要一个项目,满足三个特点:范围适中(而不是一个需求会不断变化的新应用)、明确的真相来源(现有可用的 Python 代码)、以及足够的复杂度,让 LLM 的幻觉能“藏起来”。一个纯粹的移植可以完全满足这三点。输入和期望输出已然存在。如果 Rust 版本的行为和 Python 的不完全一样,那肯定有问题。就是这么简单。 既然要做移植,那为什么不顺便真正学学 Rust 呢?借用检查器 (borrow checker)、所有权 (ownership)、生命周期 (lifetimes)… 我读了好几年资料,却几乎没有亲自实践过。如果是写一个真实项目而不是第 N 次看教程,一切或许会大不相同。 目标对象 它的名字叫 nanobot。这是一个基于 OpenClaw 开发的个人 AI 代理。它能将各种 LLM(如 Claude、GPT、DeepSeek)接入聊天渠道——Telegram、Discord、Slack、电子邮件——并赋予它们更多功能。比如读取和编辑文件、执行命令、网络搜索、通过 cron 编排任务,甚至在对话之间保存记忆。 它可以正常工作。而且已经运行了几个月。在 Python 上。 问题呢?它是单线程的。一次只能处理一条消息。如果你连续发送三条信息,它会像周六中午的超市购物队伍一样排队等待。它的内存消耗约为 50MB,而它实际上只是在不同的 API 之间传递 JSON。此外,它的错误处理方式令人羞愧:到处都是return f"Error: {str(e)}"。 ...

2026年2月24日 · Fernando

我的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