最后一个知道完整答案的人

文件保存成功。

日志里清清楚楚地写着:

saved 14,276 total

只是昨天,这个数字还是 28,060。

笔者最近在整理一个公开网页资料库。前面几年已经陆续收好,每一条都有正文。机器为了这些数据断断续续跑了很多天。

接下来只是想再补一年。

2016 年预计有 1,454 条,所以事情本来应该是一道很简单的加法:

28,060 + 1,454 = 29,514

最后变成了 14,276。

更奇怪的是,文件并没有坏。

它可以正常打开,格式正确,每一条记录都能读。没有乱码,没有半截文件,也没有报错。

如果不是还记得昨天的数据量,甚至很难意识到出了事。

事情从一次很普通的卡顿开始。

Codex启动的采集任务跑到一半不动了。Codex 关掉后台窗口,准备重新启动。

窗口确实关了,然而Codex没有意识到里面那个 Python 程序却没有真正退出。

于是新的任务重新启动以后,后台其实有两个程序,都在处理硬盘里同一个数据文件。

真正麻烦的地方,是那个文件原来的保存方式。

程序每采集完一批,就把内存里的全部数据重新写回硬盘里的文件。两万八千多条正文加起来大约 1.4 GB,完整写一遍需要一点时间。

这是个很简单、也很常见的写法。

只是它有一个问题。

写文件的时候,旧版本已经开始被覆盖,新版本却还没有写完。

有点像维护一本很厚的账。

每次更新,不是在简单最后补几页,而是把桌上的旧账先记一份在脑子里然后擦掉,再从第一页开始重新抄一本并加入新的内容。

而抄到一半的时候,新账本看起来依然很正常。字迹完整,页码连续,随便翻一页都没有问题。

它唯一的缺点,是后半本还没抄回来。

根据后来留下的数字,最可能发生的事情就是这样。

旧程序正在重新写那 28,060 条数据,刚写到 13,826 条时,新程序打开了同一个文件。

对新程序来说,它看到的没有任何异常。

13,826 条记录,格式正确,全部能读。

它不知道几个小时前这里还有 28,060 条,于是把眼前这些当成了完整的历史数据,又加入自己刚刚新采集到的 450 条:

13,826 + 450 = 14,276

然后认真保存。

于是最后留下来的,不是一个损坏的文件。

而是一个非常完整、非常健康、只是少了一半内容的文件。

后来回头看,这件事其实本来很好避免。

文件更新时,可以先在旁边写好一份新的,确认完整以后,再把旧版本换掉。至少不应该让一个只写了一半的文件,有机会被另一个程序当成正式版本读进去。

数据库领域早就有很成熟的办法处理这种事情。

只是当时没人特别去想。

Codex 要做的是继续采集数据。原来的代码能跑,保存也一直成功。任务很多,上下文也很多,一个不起眼的文件写入方式,没有理由突然成为注意力中心。

直到有一天,这个原本不起眼的选择突然变得很贵。

笔者发现数字不对以后,要Codex 检查后台,很快Codex发现可能同时跑着两个 Python 程序。

接下来Codex的动作也很自然:

先把俩进程都关掉。

别再让它们继续互相干扰。

两个进程被结束以后,机器终于安静了。

正心疼好多天的活被意外删除了,笔者突然意识到一个问题。

等一下。

那个老程序启动的时候,不是已经把完整的 28,060 条数据读进内存了吗?

很可能是。

磁盘上的文件后来虽然已经少了一半,老程序自己的内存里,却未必跟着变少。

换成刚才的账本故事,就是桌上的正式账已经抄坏了,但旁边那个老会计手里,很可能还拿着抄写前的完整底稿。

大家发现账乱了,于是赶快清场。

然后把老会计也解雇了。

等办公室终于安静下来以后,才突然有人想起来:

刚才被赶走的那个人,好像是最后一个还拿着完整账本的人。

可这时已经晚了。

程序一关,对应的内存空间也就被回收。

第一次Codex犯错误,让磁盘上的数据少了一半。

第二次Codex犯错误,则是把最后一次恢复的机会也一起清掉了。

这件事倒没有让笔者觉得 Codex 不好用。

恰恰相反。

现在的 Coding Agent 已经足够好用,好到很多过去需要自己一行一行写、一遍一遍检查的东西,真的可以放手让它往前做。

它会写程序,会启动任务,会看日志,会发现异常,会重跑,也会帮你清理后台留下来的进程。

很多时候,它比人快得多。

但也正因为事情变得这么顺,有些原本会在亲手实现过程中顺便想一遍的问题,反而很容易一起滑过去。

比如这一次,真正重要的问题其实只有一句:

这份文件如果只写到一半,会发生什么?

当时没人问。

不是因为这个问题很难。

而是因为还有太多别的事情要做,而且程序一直工作得很好。

Codex 也不会天然知道,眼前这份文件和一个随时可以重新生成的临时文件有什么不同。除非上下文里已经把这件事变成一个需要特别保护的边界,否则一个简单、快速、平时也完全够用的实现,很自然就会被保留下来。

人自己写一次性脚本时,其实也常常如此。

只是 Coding Agent 让实现变得更快了。很多以前会因为麻烦而停下来想一想的地方,现在几分钟就过去了。

有时候这是巨大的好处。

有时候,也会把一个原本不起眼的小决定,一路带到很远的地方。哎,或许如果人来做,想到这1.4GB数据前前后后采集了好多天,会心疼三秒从而可能会采取防御性保存的方法。

好在数据最终还能重新采集。

只是又要花不少时间,而且重新拿到的网页,也未必和那一天完全一样。

现在回想起来,最让人记住的倒不是 28,060 为什么变成了 14,276。

而是机器终于安静下来以后,才突然意识到:

最后一个知道完整答案的人,刚刚被我们解雇了。