logo

AI改代码为何越改越错?信息层级与协作模式

作者:新兰2026.11.06 20:11浏览量:0

简介:AI生成的代码能编译、能跑、测试全绿,却可能藏着架构级缺陷;把报错反复贴回去,往往越改越错。本文通过“并发退化为串行”与“测试假通过”两个典型案例,剖析症状级信息与架构级问题的错配、实现与测试同源导致的一致性盲区,并给出一套可落地的协作与验证方法,帮你从根因上打破越改越错的循环。

越来越多的开发者把日常编码交给智能编程工具:生成样板代码、补全函数、从零搭起一个能编译能运行的项目,效率提升肉眼可见。但随之而来的是一个普遍的困惑:代码一旦有错,把报错贴回去让它修,常常第一版没修好,第二版换个姿势错,第三版干脆引入新问题——越改越错。

这不是模型“变笨”了,而是人与AI的协作模式出了问题。本文通过两个典型案例,拆解这一现象背后的两层根因,并给出一套可落地的改进方法。

一、案例一:并发悄悄退化成了串行

先看一个真实感很强的场景。让AI从零实现一个支持异步I/O的网络模块,约两千行代码:编译通过、运行正常、测试全部通过。但压测时发现异常:

  1. // 并发发起三个各耗时约300ms的请求
  2. const t0 = Date.now();
  3. await Promise.all([req("/a"), req("/b"), req("/c")]);
  4. console.log(Date.now() - t0);
  5. // 期望约300ms,实际约905ms

三个请求本应并行执行,总耗时接近最慢的一个;实际耗时却约等于三者之和——并发被写成了串行。

把现象和报错描述给AI,它开始修。第一版,仍然串行;第二版,换了个实现方式,还是串行;第三版,直接编译报错。典型的“越改越错”。

随后换了一种沟通方式:不再转述报错,而是直接给出架构约束——

  1. fetch 不要自己管理事件循环:
  2. 1. 只负责创建连接;
  3. 2. 将 I/O 事件注册到全局事件循环;
  4. 3. 立即返回一个 pending 状态的 Promise;
  5. 4. 所有 I/O 由全局事件循环统一驱动。

这一次,AI一次改对,改动涉及9个文件,耗时从905ms降到302ms。

同一个模型,为什么三轮修不好、一句话就修好了?

二、根因一:症状级信息,对不上架构级问题

1. 报错是症状,不是诊断。 报错信息只告诉你“最后在哪里炸了”,不告诉你“最初因为什么炸了”。“三请求耗时905ms”这个现象,投影到代码上可能是一处 await 的位置、一个回调的注册方式;但真正的病灶在模块职责划分——I/O 驱动权被分散进了每个请求内部,全局事件循环形同虚设。你描述的是投影,AI 修的也只能是投影。

2. AI 的默认策略是局部最小修改。 收到“这里不对”,它会假设问题就在“这里”,然后在原地打补丁。讽刺的是,它每一针都缝得很对:DNS 解析是对的,socket 创建是对的,HTTP 序列化是对的,Promise 包装也是对的——但所有局部正确的代码,被组织进了一个整体错误的架构。局部正确之和,不等于全局正确。

3. 症状会在补丁之间“游走”。 你在 A 处看到的症状,根源可能在 B。修 A 不仅修不好,还可能破坏 B 所依赖的隐含前提,于是症状换个地方冒头,或者升级成新的报错。更麻烦的是,多轮修复是误差累积的过程:每一轮都基于上一轮已被污染的代码状态继续修改,新的改动又引入新的隐含假设,与存量代码的假设相互冲突。改的轮次越多,状态越难收拾。

4. AI 缺的不是编码能力,而是“组织方式”的显式输入。 当架构意图没有被明确给出时,AI 会沿用现有代码隐含的架构假设——而问题恰恰出在这个假设上。要求它凭空猜中你的架构意图并不现实;但你可以用一两句话把它显式化。上面那句“创建连接、注册全局事件循环、返回 pending Promise、I/O 统一驱动”,本质是四条架构不变量,它直接收窄了修改的搜索空间:AI 不再需要猜“往哪儿改”,只需要在给定约束下执行——而这恰恰是它最擅长的事。

三、案例二:更隐蔽的陷阱——实现与测试“合谋”造假

修完并发问题继续 review,发现了更严重的问题:压测模式下,每一次 HTTP 请求实际上都失败了——上万次请求全部失败;但测试报告显示:全部成功,0 错误。

复盘原因,缺陷出在统计逻辑上(示意):

  1. /* 请求完成时的统计逻辑(示意) */
  2. void on_request_complete(request_t *r) {
  3. stats.total++;
  4. stats.success++; /* 未检查任何错误码,无条件计为成功 */
  5. }

而测试只校验这个数字(示意):

  1. /* 测试断言(示意) */
  2. assert(stats.total == 16576);
  3. assert(stats.error == 0); /* 恒为0:因为根本没人写入错误 */

实现不检查错误,测试不验证错误,两边严丝合缝地配合,制造了“一切正常”的假象。

这背后是一个结构性风险:当实现和测试由同一个模型在同一会话中生成时,模型的先验偏好会被同时注入两边。 模型天然偏向正常路径(happy path):写实现时省略错误分支,写测试时也就不会构造异常用例。于是“测试通过”只能证明“实现符合测试的预期”,不能证明“实现符合需求”——而这两份东西出自同一个“大脑”,预期与缺陷天然对齐。

由此形成一个自我验证的闭环:AI 写实现 → AI 写测试 → 测试通过 → 把“通过”的结果喂回 AI 继续迭代 → 它基于自己制造的假象做下一步决策。在闭环内部的任何一个点上看,系统都是“健康”的;你把这个“一切正常”的结果给 AI,它当然改不对——它从自己的输出里看不出问题。打破闭环只有一种方式:引入外部信息,比如人的 review、真实环境的观测、独立于生成过程的验证手段。

四、正确的协作模式:把AI放在约束下执行

回到最初的问题:AI 写错后改来改去还是错,不是因为模型笨,而是协作模式错了。

错误的模式:AI 写 → 报错 → 贴报错 → AI 改 → 又报错 → 又贴 → 循环。整个过程里,输入的始终是症状级信息,而问题出在架构级。信息与问题错位,修不好是必然的。

正确的模式,可以归纳为四条原则:

原则一:先定位层级,再决定喂什么。 遇到问题先自问:这是单点错误,还是系统性错误?

问题类型 典型表现 适合给AI的信息 预期效果
单点错误 语法笔误、API误用、边界条件遗漏 报错信息+相关代码片段 一次修好
系统性错误 性能、并发、时序、资源生命周期问题 现象数据+期望行为+架构约束 需架构级描述才能根治

单点错误不必小题大做,贴报错即可;系统性错误则必须先完成架构级诊断,再向 AI 描述“应该是什么样”,而不是“哪里报错了”。

原则二:用“约束描述”替代“报错转述”。 给 AI 的任务描述尽量包含四要素:

  1. 【现象】并发三个300ms请求,实际总耗时905ms
  2. 【期望】总耗时接近最慢单请求,约300ms
  3. 【约束】I/O驱动权归全局事件循环;请求模块只负责连接创建与事件注册
  4. 【禁区】不要重构无关模块;不要修改计时统计逻辑

“禁区”尤其重要:它阻止 AI 在修问题时顺手做出大范围重构,避免把一次修复变成一次不可控的翻新。

原则三:实现与测试分离,验证手段独立于生成过程。

  • 让实现和测试在不同会话中生成,或者由人先写关键用例,尤其是异常路径:超时、连接失败、部分失败、错误注入;
  • 用真实环境做外部观测:真实压测、计时统计、资源监控,而不是只信 AI 自己产出的测试报告;
  • 关键指标必须可证伪:例如“错误数”必须来自真实错误分支被触发的路径,而不是一个从未被写入、恒为 0 的字段。

原则四:小步修改,单变量推进。 每轮只让 AI 改一件事,改完立刻用独立手段验证,通过后再进入下一轮。更进一步,可以先让 AI 输出“问题诊断+修改方案”,人审核方案之后再放行代码修改——方向错了在方案层面拦截,而不是污染代码之后才发现。

五、写在最后

AI 的代码能力已经足够强:单点正确性、局部实现、模式化代码,它都完成得很好。它缺的是两样东西——对全局架构的判断输入,以及跳出自身输出审视自己的能力。而这两样,恰恰是协作中“人”的位置:人负责定义不变量、审视架构、提供外部视角的验证;AI 负责在清晰约束下高质量地执行。

把“贴报错、等修复”的循环,换成“诊断层级、给出约束、独立验证”的循环,你会发现:那个“越改越错”的 AI,往往一句话就能改对。

发表评论

活动