跳到主要内容

12205 行新增,1500 行删除,我盯的是后面那个

阿舟
阿舟

· 阅读约 6 分钟

八月底的一个晚上,我在 dev.to 上刷到一篇 Dev log #19。作者同时在推 py-libp2p、一个 agent 编排器,还有几个前端工具,开头直接甩数字:一周 53 次提交、13 个 PR、连续 7 天没断,新增 12205 行,删掉大概 1500 行。他自己给这周的定性是"偏增长"。

"偏增长"这三个字,看得我有点出神。

写 dev log 这事我干过一阵,后来自己停了。不是没活干,是写着写着它变成流水账——而流水账里信息量最低的那个数字,恰好就是行数。

接着往下读。真正让我停下来的是几条特别不起眼的记录,大概是这样:

  • WebRTC 传输那块,把生产路径切到 close_peer_connection,免得连接挂住不释放;
  • CLI 的错误处理清了一遍,避免异步命令失败时甩出没人接管的 Promise 拒绝;
  • 有个 tailnet 代理非正常退出时会把子进程留在系统里;
  • Vitest 在 Node 26 上跑不起来,根因落在 jsdom 的 localStorage 实现。

四条,没有一条是"我做了个新功能"。

共同点一眼就看得出来——都不是不会写,是没收尾。连接没关,进程没杀,Promise 甩出去没人接,运行时的行为变了没人跟着改。这类东西有个特别讨厌的性质:它不报错。happy path 一路绿灯,测试全过,demo 录得漂漂亮亮,账记在下个月。

画外音:每次我以为自己"写完"了,其实只是"跑通"了。

关闭这件事,AI 兜不住

我自己的体感是三条。

第一,关闭路径几乎全在异常分支上。语料里 happy path 占绝对多数,模型补出来的代码天然朝"顺利走完"那个方向长。finally 它写得出来,但它不会主动想起来要写。你给它一个"把这条消息发出去"的任务,它大概率交这个:

const conn = await dial(peerId)
await conn.send(frame)

你要的其实是这个:

const conn = await dial(peerId)
try {
  await conn.send(frame)
} finally {
  await conn.close()
}

差的那一行,在本地跑一百遍都不会现形。

第二,泄漏没有即时反馈。功能写错,一跑就炸,反馈循环是秒级的;句柄忘了关,跑一万次都正常,等你看见的时候通常已经在线上,而且是在负载压上来之后。没有反馈信号的错误,模型学不会,人也很容易放过——因为它从来没咬过你。

第三,边界处"谁负责关"没有单一答案。跨进程、跨运行时、跨版本的地方尤其明显。Node 26 换了 jsdom 的实现,localStorage 的行为跟着变,这个锅该谁背?一个代理非正常退出,子进程归不归父进程收尸,取决于谁装了 signal handler。这类问题的性质不是找一个 bug,是定一个归属。而"定归属"这件事 AI 给不了你,它不知道你们上次是怎么约定的,因为你们从来没写下来过。

那个 WebRTC 的改动,我猜(这个词这里只能用,因为我确实没看到代码)原本是测试路径和生产路径走两套关闭逻辑。这是很经典的形态:测试里连接生命周期短,进程随手就退了,句柄关没关没人发现;生产里长连接常驻,挂一个是一个,等你发现的时候已经攒了几个月。所以"把生产路径切过去"这一行 diff 的实际价值,可能比那 12205 行里的任何一百行都高。

扯远了。我本来还想顺着说测试用例有同样的毛病——断言逻辑不能交给 AI 自己当裁判——这个下次单开一篇,先收回来。

加限制比加功能难

那篇里还有一条,乍看是加法,细想还是减法:在 DHT 里做了可配置的子网多样性限制,给整张路由表加了单个 IP 组的上限。表面是个新参数,实际它删掉的是"路由表被同一个 /24 塞满"这个可能性。

加限制比加功能难得多。功能的收益当场看得见,限制的收益是"没发生的事"——防 Sybil 防住了没人给你发锦旗,防漏了全是你的锅。而且限制类代码还得顶住工单:为什么我的节点连不上?所以每次看到有人主动去写这种代码,我都会多看一眼。

评论区有个叫 Alex Shev 的留了一条,说编排类项目成败的关键在同步边界,建议把每次更新当作事件,消费者写成幂等的。两个赞。

我截图存了。幂等消费者这词听着很分布式,翻译到今天的主题其实就一句话:重复一遍也不会更糟。也就是收尾这件事要可重入——关一次和关两次结果得一样,这次没关成,重试还能关。

这里我把刚写好的一段删了。我本想论证"所有收不了尾的东西都能靠幂等解决",写到一半发现不对。前面那个 tailnet 遗留进程,父进程已经被 kill 了,没有任何重试机会,幂等一点用没有,只能靠孤儿回收兜住。设计能覆盖的边界,也有它自己的边界,别拿它当护身符。

一个指向自己的默认值

还有一条我特别有共鸣:他把 obsidian-vault 当生产仓库维护,那周 35 次提交,主要是同步 UI 状态和修模板,然后发现 Concept 模板的父指针递归指向了模板自己。

自指。这类脏数据几乎可以预判——模板里某个字段的默认值指向它自己,生成阶段不报错,因为结构合法;渲染的时候才炸,因为你点开任何一个条目,都站在无限套娃的门口。最恶心的是它没法写单测覆盖,你只能某天碰巧点到。

那篇底下还附了技术栈统计:Python 大概 84MB 代码,主要是 libp2p 内部;TypeScript 58MB 左右,覆盖前端工具和 CLI;剩一点 Lua 是 nvim 配置,Shell 和 Makefile 喂给 CI。

我看完第一反应是:84MB 的 Python 里,有多少行是负责"关"的?这个数字统计不出来,所以没人统计。

至于 53 次提交、13 个 PR——小 commit 我自己是真喜欢,revert 的时候你会感谢当初的自己。但为了一个好看的周更数字去拆 commit,那是另一种表演,跟这周加了多少行一样不值钱。

说回那 12205 行

在一个 AI 已经能写代码的年份,+12205 真不算本事的证明。实习生一小时吐出来的东西,够你审一下午。

这个阶段稀缺的从来不是产出,是收尾。连接关没关、子进程有没有人领回去、那个指向自己的默认值改没改、Node 26 上的 jsdom 是不是还是原来那个 jsdom——这些都不出现在 diff 统计里。

Dev log 如果只记加了多少行,记的恰好是这里面最不值钱的那个数。我现在看别人的周报,第一眼先找那一周他删掉了什么、关掉了什么、有没有一条是"发现某个东西从三周前就没干净过"。

给自己定条规矩:任何一次让 agent 干完的活,收尾清单里必须有一行是"这次新增的东西里,有哪些将来是要被关掉的"。写不出这一行,说明我根本不知道它动了什么。

12205 行我不眼馋。我眼馋的是他记得把那 1500 行的删除,单独拎出来记了一笔。

阿舟
阿舟

写代码写到一半开始怀疑人生,靠 AI 工具续命,顺手把踩过的坑都记下来。

查看主页 →