我变成了 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 的回调线程是什么------你确定要在这上面阻塞吗"。
我需要的是更好的设计。不是更多的代码。
因为代码可以把产线跑起来。设计可以保证它在凌晨一点半急停按钮按下去之后,还能正确地停,正确地恢复,正确地活过来。
好的代码不是写出来的。是想出来的。
产线不停,设计先行。