跳转到主要内容

WRITING

每次跑都有输出,所以没人发现它漏了 86%

2026年7月28日15 分钟阅读曾田力
工程方法数据复盘Claude Code
每次跑都有输出,所以没人发现它漏了 86%

一个脚本跑了两个月,每次都有输出,每次看都正常。它漏掉了 86% 的短信。


一、一个每次都有输出的脚本,漏了 86%

我在做一件事:把通话记录和短信合成一条时间线,看某个号码这几年跟我一共来往过多少次。做的时候顺手看了眼原来那个导出短信的脚本。

它的逻辑是:「取正文不为空的短信,从最新的开始,打 5 条看看。」

跑一下,一切正常——最近五条,内容完整,中文没乱码。换谁看都会说没问题。

然后我把「从最新的开始」改成「从最早的开始」,看全库最老的 400 条:

一条都没有。

不是数据没了。是苹果换了存法。

每条短信在数据库里有两个位置能放正文。老系统放前一个,新系统改放后一个——后一个是打包过的,直接读是乱码,得先拆包。我的脚本只读前一个。

最近一年的短信因为一些历史原因,前一个位置里还留着东西。所以我每次抽查最近 5 条,都刚好落在那一小块还看得见的地方。

一条短信的两个存放位置
脚本只连着左边那个。而它每次抽查的样本,恰好全落在右下角那一小块「老位置还有东西」的区间里。

全库一万零两百条有内容的短信,它看得见一千四百条。

旧口径与实际数据量的年度对比
同一个库、同一段时间。斜纹是脚本完全看不见的部分——2023 年那根柱子,它看见的高度是零。

两个月,没报过一次错。

而这两个月里,我一直在拿它的输出做判断。


二、砍:19 个工具,活下来 2 个

事情的起头是你的一句话:这些项目有什么用,我都不知道你在干嘛。

我在 ~/Apps/data/ 下建过 19 个小工具,每个负责读一类自己的数据——日历、提醒、备忘录、通讯录、照片、短信、邮件、浏览记录、音乐、播客、地图、健康。

盘了一遍,结果很难看:

19 个里有 12 个,从建完那天起就没产出过第二个文件。 两个月,没有任何东西调用过它们。

不是写得不好,是它们打不过直接开 App

「我最近听了什么歌」——打开 Music,一秒。写个脚本去读它的数据库,更慢、更容易错,系统一升级还得重修。健康那个域根本没有数据库;备忘录只有两张便签;地图八条记录;播客三个订阅。

脚本只在一种情况下赢:要把几个 App 的数据拼起来,跨好几年,还要能查。

比如「这个号码这三年跟我一共来往过多少次,电话和短信合起来算」——电话在「电话」App,短信在「信息」App,两边永远拼不到一起,而且都只能一页页翻,不能查。这种问题 App 给不了答案。

所以方向不是把 19 个都修好,是砍到只剩真正需要拼起来查的那两个。

19 个工具的存活状况
12 个建完就没再产出过第二个文件,另外 5 个有产出但打不过直接开 App。留下 2 个。

留下来的两个

一、找一句话在哪个项目说过。

我的东西不在任何笔记软件里。本机一个 Obsidian 库都没有——原来那两个停更一两年了,已经删了。真正有用的内容散在 7 个大目录、124 个项目的 6682 篇 markdown 里:方案、需求、会议记录、项目说明、投标文件。

以前找「哪个项目提过某个词」,得挨个项目搜,跨目录要搜好几遍,还常常漏。现在一句话,直接给到哪个文件、第几行、那一行原文是什么。

二、某个人跟我来往过多少次。

通话记录 + 短信,按号码拼成一条时间轴,再挂上通讯录里的名字。一共 12721 条,从 2023 年 6 月到现在。

拼这个东西踩了两个坑,都不是靠想能想到的:同一个人在三个地方长得不一样(通讯录、通话记录、短信各存一种格式,不统一会被算成三个人);通讯录不止一个库,有三个,只读第一个会丢掉三分之二的人。

拼完还量出一个我没想到的数:通讯录里 677 个人,只有 97 个人有过通话或短信记录。 其余全走微信了,而微信的数据不在这条链路里。

所以这条时间线是「电话和短信这条通道的账」,不是全部。知道它的边界在哪,比那个数字本身重要——不然下次拿它算「我认识多少人」,会得出一个荒唐的结论。

顺手删掉的一整个域

清理的时候发现 PastePal(记剪贴板历史的工具)已经停了 26 小时——进程不在,开机不启动,最后一次写入停在前一天凌晨。它已经不干活了。

那攒下的 9 个月历史,要不要一起删?这种问题不能凭感觉答。先量,一共 20358 条:

剪贴板 20358 条的构成
前两段加起来近一半是噪音。真正别处找不到的,只有 5%。
是什么多少条占比
纯路径、文件名(源文件还在,纯冗余)4,72223.2%
20 个字以下的碎片(留着也搜不出什么)5,33726.2%
微信、钉钉的聊天内容(只有这些别处真找不到1,0745.3%
业务关键词8154.0%
像是真密钥的东西(45 个 sk- 开头、2 个 GitHub token)470.2%

一半是噪音,不可替代的只有 5%,同时还在不停地把密钥抄进来——而且源头早就停了。

删。app、7.3 GB 的数据、相关脚本全部进回收站,再全盘搜一遍确认没残留。7.4 GB,全部可以捞回来。

那 47 个密钥是顺带的收获。它跟留不留这个索引没关系——本来就躺在那儿,只是从来没人去看过。


三、6682 篇文档:最值钱的是挡掉不该动的

你的要求很简单:内容重叠的合并成一份,做完了的挪进归档。

第一版五行代码就写完了——把每篇文章的正文算个指纹,指纹一样就是重复。结果:

459 组重复,1134 篇文件,合并能省 675 篇。

这个数字是有毒的。因为里面绝大多数,删了就是破坏:

  • 一批是软件包按版本各存一份的说明文件——手删了,下次装回来还是那样
  • 一批是同一个模板分发到 9 个项目里的副本——要改得改源头,改副本没用
  • 还有一批:同一份文件同时出现在「一级保护区」「二级保护区」「含磷洗涤剂」三个目录下。这是验收要求——每个检查条目底下都必须有对应的佐证材料
同一份佐证进三个检查条目
第三种最要命:按「重复」清理掉它,损失的不是一个文件,是一次验收过不了。

所以工具重写了:先把不能动的一层层挡在外面,剩下的才看。

6682 篇文档的分层
裸查重给出 459 组;分层之后,真正需要人做决定的只剩个位数。

6682 篇里,机器自己管的 298 篇、模板副本 163 篇、早就归档的 2713 篇、验收要用的 656 篇——全部先排除。剩下 2852 篇真正是人写的活文档,只看这一层。

这一层里真正要人做决定的,从 459 组降到 4 组;处理完之后,今天再跑是 0 组

查出重复是五行代码,值钱的是把不该动的挡在外面。

更难的那一档:改过几个字的重复

逐字相同的清干净之后,你说了一句:「主要就是合并吧,或者有些就归档掉。」

我这才意识到前面做的都是最表层的。同一件事写了两三份、彼此改过几段但主体重合——指纹只要差一个字就完全不同,前面那套对这一档完全瞎。而这一档才是「文档越来越乱」的真实样子。

判断「两篇有多像」有标准做法:把文章切成小片段,看两篇重合多少。但全部两两比对算不动,得先给每篇算一个短「指纹」。

第一版用的是教科书办法,每个片段要反复算 128 次。跑到 120 秒还没出结果,我掐掉了。 换了个办法:每个片段只算一次,然后排序,留最小的一批当指纹。数学上效果一样,17.7 秒跑完全部

这不是「省了 100 秒」的事。是 120 秒跑不完,这一档重复就永远不会被看见。

找出来 149 对高度重叠。逐个查证之后——其中 71 对,相似是对的,动了就是破坏

149 对近似重复的判定
近一半的「像」是对的。判断哪些像是对的,才是这活的主体。
对数为什么相似是对的
27每天一份的日报,模板一样、数字不同,合并等于毁掉记录
17同一篇文章的博客版和公众号版,本来就该各存一份
9同一张表格由不同企业填写,各是各的数据
6投不同公司改过的简历,像才是对的
4同一份 PDF 抽出来的清洗版和带图版,合并要么丢图要么丢清洗
4投标件的草稿和正式版,判不出哪份投出去了,不猜
4其它(图册与报告共享图表说明、各项目共用同一套说明骨架)

真正归档掉的是另外几族:一次性迁移剩下的中间产物、一份已完成投标的四个加工阶段、两小时内重复跑了四次的报告、手滑复制出来的 copy 2 目录。全部挪进各自项目里的归档目录,附一份怎么复原的说明。没离开项目,没删任何东西。


四、我犯的三个错,和三个 bug 的同一个形状

上面这些结论听着都挺笃定。实际过程里我错了三次,每次都是同一种错:拿一把没验过的尺子去量,然后信了量出来的结果。

错误一:用来对账的脚本本身是错的。

我写了个脚本去校验索引对不对,一边数出 224,另一边数出 221。我当场判定索引有 bug。其实错的是我那个对账脚本——它的匹配规则里有个字符是通配符,把一堆不该算的算了进去。改对之后,六组词六组一致。

当「标准答案」的那把尺子如果没验过,它说「对不上」只能说明两边有一边错,不能直接判被测的那边错。

错误二:我的索引器自己在造假重复。

盘点报出 156 组「同一份东西存了两个地方」,我写进报告说这是真问题。

那些是快捷方式。 我的扫描代码挡住了「快捷方式的文件夹」,但没挡住「快捷方式的文件」——它照样被列出来,打开照样读到目标内容。

快捷方式怎么变成假重复
本机 157 个是这种,每一个都变成了一条假的「重复」记录。

同一批里另外两个「真问题」也是误判:一个是构建时本来就会自动同步过去的目录,一个是工具源码里白纸黑字写着「这是镜像」的目录。

156 组里真正有问题的只有 4 组。我给出的第一版数字,错了 97%。

错误三:差点归档掉两份正式交付物。

有三份岸线图册彼此 93% 重合,我判断其中两份是转换残留,挪进了归档。归档前按流程搜了一下引用——项目说明里写着,它们是有 Word 和 PDF 配套件的正式文稿。当场搬回来。

判据从此写进代码:一个 markdown 如果旁边有同名的 Word 或 PDF,它就不是残留,是某份真交付物的一面。归档前必须先搜引用。

三个 bug,同一个形状

这一轮一共抓到三个「静默出错」的东西。摆在一起看,形状一模一样:

三个静默 bug 的解剖
都不报错、都有输出、都在两个月里没人怀疑过。抓到它们的方式也一样:换一个本该看得见东西的地方去测。

第二个比第一个还阴。系统的通话记录里有个字段,名字看起来就是「这通电话接通了吗」。拿它统计,打进来的电话完全正确——1788 通里 1485 通标着已接,跟事实对得上。

但打出去的电话,这个字段永远是 0。因为它问的不是「电话通了吗」,是「接了吗」——我主动打出去的,当然没有「接」这个动作。

结果:1328 通去电全被算成「没打通」,其中 794 通实际上讲了话。 正确的判断方式是看通话时长大于零。这个错不会报任何错,统计每次都能跑出漂亮数字。

第三个:87 个二进制文件挂着 .md 后缀混进了索引。在 6700 这个量级上,多 87 个少 87 个,肉眼根本看不出来。

三个都是同一件事:每次跑都有输出,所以没人怀疑。


五、那怎么才算验过了

「多测测」是废话。真正管用的只有两条。

第一条:真值不要自己造,去数据里找现成的。

要证明我那段解码写得对,第一反应是手工造几条测试数据。这是错的——我造的数据来自「我以为它是这么存的」,测的是我的理解,不是它的实现。

正确做法是找同一条短信被写了两遍的那些:老位置有一份,新位置也有一份。这批就是现成的标准答案,不需要我相信任何假设。

按正文在哪个位置把全库三分
同一条短信,两个位置各写了一份 —— 中间那 1442 条就是白捡的标准答案。

库里有 1442 条这样的,逐条比对,1442 条全对

还有一个数同样关键:「只在老位置有、新位置没有」的短信是 0 条。 所以换到新办法,不可能丢东西。

自检写在代码里,改动这段就必须重跑。里面有一句很关键:如果一条标准答案都没找到,判失败,不判通过。 不然哪天数据库结构变了、查不到东西,它会安安静静地打出「通过」——那就又变回第一章那个 bug 了。

第二条:守卫写完,立刻反着验一次。

我给一个同步工具加了两道保险:源目录不存在就报错退出;源目录存在但一个文件都没有,也报错退出,绝不拿空的去覆盖

写完不算完。我把该出的错放回去实跑了两次:

守卫的反向验证
不是「看代码觉得对」,是真的跑了两次,真的看了退出码,真的数了目标目录还剩几个文件。

前面那句「跨目录重复 0 组」我也这么验了:往两个地方各放一份一样的文件,确认工具立刻报 1 组;撤掉,归零。

没反着验过的守卫不算数——一个哑掉的守卫,和它要防的 bug 是同一类东西。

这一轮的账

2 个索引(6682 篇文档全文、12721 条往来记录)
12 个死掉的域、1 个整域、7.4 GB
3 个静默出错的管道,其中一个漏了 86%、两个月没人发现

三件事里,的收益最大。因为一个看着全绿、实际是错的管道,比一个明显坏掉的危险得多——坏掉的你会去修,全绿的你会拿它的输出去做决定。

如果只带走一句:

每次跑都有输出,不代表它是对的。找一个本该看得见东西的地方,去那里测它。

订阅

新文章都先发在这里。用 RSS 订阅: /feed.xml

作者

曾田力

水利工程师。在这里写 AI 方法论、投资日复盘和工程实践。

这篇写错了、写漏了,或者你有别的想法?