我变成了 AI 的打字猴子——以及我为什么写了一个禁止 AI 写代码的工具
我变成了 AI 的打字猴子——以及我为什么写了一个禁止 AI 写代码的工具
当 AI 替我写代码时,我以为自己变成了 10x 工程师。实际上,我变成了一只会按 Tab 的猴子,面对崩溃堆栈时连自己的代码都读不懂。
那个死锁
凌晨一点半。产线停了四十分钟。
不是设备故障——是产线追踪系统卡死了。PLC 信号正常,OPC UA 订阅正常,传感器数据一直在采集,但数据到不了 SQL Server。操作工手动抄了四十分钟的生产批号。
代码是我写的——不,是 AI 写的,我按的 Tab。
这是一个典型的工业数据管道:C# 后台服务,System.Threading.Channels 做生产者-消费者,OPC UA SDK 的回调线程里 Write 到 Channel,一个后台 Task 从 Channel Read 出来批量 SqlBulkCopy。架构看起来合理。代码看起来体面。Channel.CreateBounded<SensorRecord>(10000),有背压。await 都用对了。CancellationToken 都传了。
测试环境跑了两周没问题。用 BenchmarkDotNet 压了 50 万条也没有丢数据。
然后投产第三周,夜班操作工按了急停按钮。
急停触发了几百个传感器在五秒内同时上报状态变化。OPC SDK 的回调线程瞬间灌入 Channel。Channel 满了。回调线程——它是 OPC SDK 内部的单线程事件循环——在 WaitToWriteAsync 上阻塞了。但 AI 在错误处理里加了一个 try-catch,catch 分支里写了一句 _logger.LogWarning("Channel full, retrying..."),然后同步重试。同步重试。在 OPC SDK 的回调线程上。
回调线程堵住了。OPC SDK 的 KeepAlive 超时。OPC UA 会话断开。Channel 的 Consumer 端还在等数据——但 Producer 端已经跟着 OPC 会话一起死了。Consumer 端没有超时机制,永远在 WaitToReadAsync。
死锁。
我盯着 dump 看了两个小时才理清这条因果链。AI 不知道 OPC SDK 的回调线程是什么。它不知道 WaitToWriteAsync 在那个线程上意味着什么。它只是看到一个 Channel、一个 await、一个它认为"完善"的错误处理——然后拼出了一段所有工具都检查通过的代码。
AI 不理解系统中的每一个组件有它自己的执行模型。 它理解 C# 的语法。它不理解 OPC UA 会话的生命周期、急停事件的物理含义、凌晨三点一个人值班的车间里"系统卡死"意味着什么。
它只是把 token 组合成了看起来像异步管道的形状。
我不是在写代码,我是在按 Tab
这就是过去一年我和 AI 的真实关系。
它生成一段 C#。Channel<SensorRecord>,IAsyncEnumerable<T>,await foreach——语法糖拉满。我扫一眼。它继续生成。CancellationToken 到处都传了,IDisposable 用 await using 包了。像是在 InfoQ 上读过文章的人写的。我按 Tab。
C++ 底层通信模块也一样。智能指针,RAII,std::lock_guard。Clang-Tidy 绿了。我按 Tab。
我变成了打字猴子。AI 喂我 token,我吞下去,产出一个又一个提交。代码仓库在膨胀。同事扫一眼说"Looks fine."
但我不知道那些代码到底做了什么。
不是"不完全理解"——是完全不理解。我没有在写代码。我在对着一份我既没有设计也没有推敲的文本按确认键。
代码不再从我手里流过
比 Bug 更让我不安的,是某种更根本的东西消失了。
以前写代码,一个函数从空白的编辑器里长出来:先写签名,再搭骨架,然后填充逻辑,最后处理错误路径。手指在键盘上,脑子在数据流上。写完一段,退后一步看整体比例——这里太长了,拆出去。那里太紧了,松一松。代码在我手里被反复拿捏、折叠、抛光。它不是一次性写对的,但它是我的。每一行我都知道为什么在那里,每一个判断我都亲自做过。
这种感觉很难描述。有点像木工刨木头——刨花卷起来,你能感觉到刀锋吃进木纹的深浅。写代码也有"手感"。你知道一个 if 放这里是对的,因为你对这个函数的气息有感觉。那个感觉来自于你亲手把每一个变量、每一个分支、每一个异常路径都想过一遍。
Tab 把这个感觉杀死了。
AI 吐出来的代码读起来没问题。但它没有重量。你不知道为什么那个 try-catch 放在那里——是你刻意设计的防御层,还是 AI 的模板惯性?你不知道那个 Channel.CreateBounded(10000) 里的 10000 是怎么来的——是你算过内存预算的结果,还是一个随机采样?每一行都可能是深思熟虑的,也可能是随机生成的——而你分不清。
这种感觉很微妙,但很重要。就像你开了十年的手动挡突然换成了自动驾驶——车还在走,但你不知道轮子现在在干嘛。
代码不再从我手里流过。它从 AI 的模型参数里流出来,经过我的 Tab 键,直接落进了仓库。
我失去了对代码的掌控感。而掌控感是工程师对自己的代码最基本的心理所有权。没有它,我只是一个按 Tab 的操作员。
AI 代码的三个致命问题
一、语法正确,行为错误
C# 不是你语法写对了就能跑的。AI 能写出漂亮的 Channel<SensorRecord> 管道,但它不知道 OPC SDK 的回调在哪个线程上执行。它不知道 WaitToWriteAsync 在那个线程上阻塞意味着什么。它不知道产线急停按钮按下去之后,几百个传感器会在五秒内同时上报——这不是"高并发",这是物理世界的级联事件。
C++ 也是。AI 能写出 std::lock_guard<std::mutex>。但它不知道你的图像采集线程和 PLC 状态轮询线程之间有一个隐式的顺序依赖——采集必须先初始化,轮询必须在采集之后开始。AI 给每个资源加了锁,代码不会 data race。但当初始化顺序被现场工程师改了配置之后,系统静默地处理了空帧——一个月后质检发现漏检了三千个零件。
语法正确。行为正确?AI 不关心行为。它只关心 token。
二、出了事你只能自己扛
AI 写的代码不出问题时——谢天谢地,99% 的时间不出问题。
但工业环境和互联网不一样。工业环境里你面对的不是"用户看到 500 错误"——你面对的是产线停了,夜班操作工在等,车间主任在打电话,你的手机在响。
而那个 Bug 在三周前就埋下了。在一个 PR 里。AI 写的。你按的 Tab。
你翻出 dump 文件。你二分 git blame。你找到那个 commit。你点进去看——你不认识这些代码。你没有写过它们。你没有设计过它们。你甚至不记得这个 PR 的 Context。
现在你要在一段不是由你设计的代码里、在一个不是你设计的架构里、定位一个跨线程的时序 Bug。OT 环境中没有热更新,没有 feature flag,没有灰度——你要么在停机窗口里修好它,要么让产线停到天亮。
排查时间变成了——理解一个陌生人的设计意图,加上定位 Bug 本身。前者比后者长五倍。
三、它没让我更快
有句话我憋了很久:AI 在工业软件开发中没有让我更快。
写一个 CRUD 的 MES 工单页面?快。生成一个 Modbus TCP 协议解析?还行——反正有现成的库,AI 帮你拼一下参数。
但设计一个产线数据采集管道——要考虑 OPC UA 会话管理、PLC 通信超时重试、Channel 背压时的降级策略、断网缓存、与 MES 数据库的事务边界——在这些场景里,AI 的效率是负的。
它花 15 秒生成 200 行 C#。你花三个小时理解那 200 行:哪些 Task 在哪个线程上跑?Channel 的 Bounded 容量在生产峰值下够不够?背压策略是丢数据还是阻塞——如果阻塞,阻塞在哪个线程上?OPC 回调线程堵住了会话会不会断开?
然后你删掉 120 行重写。
如果你自己先想清楚:画一张数据流图,定义清楚线程模型和背压策略,设计好断网恢复的状态机,然后再动手——你会写 60 行。第一次就对。
AI 省下来的打字时间,被理解它的代码和修复它埋的雷的时间,连本带利地吃了回去。 在 C++ 里,利息高利贷级别——因为你不仅要理解逻辑,还要理解内存。
问题不是 AI,是我们用错了
我说这些不是要骂 AI。Claude 在不写代码的时候是个非常好的思考伙伴。它能和你在同一层抽象上讨论问题,能列出 trade-off,能指出你遗漏的边界条件。
问题是我们跳过了思考,直接让它写代码。
因为我们想快点看到东西在跑。因为"思考"没有可见的产出,而"代码行数"有。因为按 Tab 比想清楚容易得多。
而 AI 工具的设计者强化了这个错误。每一个 AI 编程工具都在告诉你:说你要什么,我来写。设计?不需要。接口?不需要。ownership 语义?不需要。你要的只是更多的代码。
不。我要的不是更多的代码。我要的是更好的代码。更好的代码来自更好的决策。更好的决策来自更深入的思考。
软件工程是设计活动
这是我从多年开发生涯里学到的最重要的一件事——
写代码不是软件工程。设计才是。
工业环境是最诚实的裁判。你面对的不是"用户投诉多不多"——你面对的是产线停不停。物理设备不会原谅你的并发 Bug。PLC 不会因为你用的是最新 C# 版本就对你网开一面。OPC UA 会话断了就是断了。
所以在工业软件里,想清楚再写不是一种方法论偏好。是生存本能。
先画数据流。先定义线程模型。先理清组件的生命周期和依赖顺序。先在纸上把急停按钮按下之后的级联状态变迁走一遍。然后才打开 IDE。
这也是我所有可靠代码的来源——不是更快的打字,不是更聪明的 AI prompt。是更好的设计。
AI 应该服务这个过程。不是一个替你写代码的打字机。是一个帮你思考的搭档。
转机来自一个叫 Superpowers 的开源项目。
当我在设计一个设备通信中间件——管理 PLC、扫码枪和视觉相机的连接。它问了我 9 轮。有一个瞬间——
"如果网络闪断导致 OPC UA 会话断开,你的重连逻辑会重建 Subscription。但重建 Subscription 期间新产生的 PLC 事件——你是丢掉了,还是 OPC 服务器会帮你缓存?"
"OPC UA 服务器有重传队列,应该不会丢。"
"队列多大?如果断线持续了 30 秒,队列溢出之后的行为是什么——丢最老的还是拒绝新的?你的系统能接受哪种行为?"
它是一套给 Claude Code 用的 Skills——不是让 AI 替你写代码,而是给 AI 装上结构化的思考协议。比如"写之前先 brainstorm"、"实现之前先写计划"、"提交之前先验证"——每一个 Skill 不是一个功能,是一个强制性的工作流。AI 不能跳过步骤,不能偷懒,不能在你没确认的情况下擅自写代码。
这个逻辑击中了我。
我想要的不是一个更聪明的代码生成器。我想要的是一个设计搭档——在我动手之前,系统地和我不厌其烦地过每一个边界维度。线程模型。资源生命周期。异常路径。背压策略。断线恢复。急停行为。问那些我自己容易漏掉的问题。挑战那些我自己懒得深想的假设。
所以我照着 Superpowers 的 Skill 结构,写了一个专注于工程设计的版本。它不写代码——写代码是我的事。它只做一件事:帮我在写之前把问题想透。
如果你也有类似的感受——SKILL在 GitHub 上。
结语
我用 C++ 写设备驱动和视觉算法。用 C# 写产线调度和数据管道。我享受把一个物理系统正确地建模成软件的过程——信号进来,数据流动,设备响应,产线运转。
我不需要一个 AI 替我做这件事。
我需要一个搭档——在我上线之前问我"OPC UA 的 KeepAlive 超时设了多少",在遗漏急停路径时提醒我,在我选错了线程模型时说"OPC SDK 的回调线程是什么——你确定要在这上面阻塞吗"。
我需要的是更好的设计。不是更多的代码。
因为代码可以把产线跑起来。设计可以保证它在凌晨一点半急停按钮按下去之后,还能正确地停,正确地恢复,正确地活过来。
好的代码不是写出来的。是想出来的。
产线不停,设计先行。

浙公网安备 33010602011771号