AI 的打字猴子——以及我为什么写了一个禁止 AI 写代码的工具
那个死锁凌晨一点半。产线停了四十分钟。不是设备故障——是产线追踪系统卡死了。PLC 信号正常OPC UA 订阅正常传感器数据一直在采集但数据到不了 SQL Server。操作工手动抄了四十分钟的生产批号。代码是我写的——不是 AI 写的我按的 Tab。这是一个典型的工业数据管道C# 后台服务System.Threading.Channels 做生产者-消费者OPC UA SDK 的回调线程里 Write 到 Channel一个后台 Task 从 Channel Read 出来批量 SqlBulkCopy。架构看起来合理。代码看起来体面。Channel.CreateBounded(10000)有背压。await 都用对了。CancellationToken 都传了。测试环境跑了两周没问题。用 BenchmarkDotNet 压了 50 万条也没有丢数据。然后投产第三周夜班操作工按了急停按钮。急停触发了几百个传感器在五秒内同时上报状态变化。OPC SDK 的回调线程瞬间灌入 Channel。Channel 满了。回调线程——它是 OPC SDK 内部的单线程事件循环——在 WaitToWriteAsync 上阻塞了。但 AI 在错误处理里加了一个 try-catchcatch 分支里写了一句 _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#。ChannelIAsyncEnumerableawait foreach——语法糖拉满。我扫一眼。它继续生成。CancellationToken 到处都传了IDisposable 用 await using 包了。像是在 InfoQ 上读过文章的人写的。我按 Tab。C 底层通信模块也一样。智能指针RAIIstd::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 管道但它不知道 OPC SDK 的回调在哪个线程上执行。它不知道 WaitToWriteAsync 在那个线程上阻塞意味着什么。它不知道产线急停按钮按下去之后几百个传感器会在五秒内同时上报——这不是高并发这是物理世界的级联事件。C 也是。AI 能写出 std::lock_guardstd::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 的回调线程是什么——你确定要在这上面阻塞吗。我需要的是更好的设计。不是更多的代码。

相关新闻