AI测试 让 AI 改了 4 次代码,现在我有 process() 和 process_fast(),80% 重复

匠测AI说 · 2026年09月15日 · 299 次阅读

这个"病"长什么样

人类程序员把代码堆成屎山,通常要两三年。

AI 把代码堆成屎山,三天。

而且它堆屎山的方式极其单一、极其稳定,你在任何一个让 AI 连续改过五六轮需求的项目里都能看到同一个画面:

  • 你说"加个加急处理",它不改原来的函数,新写一个 process_fast(),跟原来的 process() 长得八成相似
  • 你说"再来个国际订单",它又新写一个 process_international(),三份代码并排,肉眼可见的复制粘贴
  • 你说"测试环境别真发货",它第四个函数 process_dry_run() 出炉,四份拷贝整整齐齐
  • 每个函数都能跑,每个函数单测都过,需求一个没漏——你甚至没法说它做错了

像什么呢?AI 写代码像复印店老板:你要一份文件,它给你一份;你说"帮我改两句话",它整本重新复印一本给你。 你桌上的文件每改一次多一本,最后四本摞在一起,哪本是最新的、它们之间哪里不一样,只有复印机自己"知道"——而它下一轮对话就忘了。

这个病和前面几篇不一样。中度复杂项目崩溃(第 7 篇)是"改一处崩三处",疼在当下;屎山不疼,它是慢性病——代码能跑、测试能过、需求能交付,一切看起来都很好。直到有一天公共规则变了,你要在四个函数里改同一个地方,AI 漏了一处,错误悄无声息地流进生产环境。

人类屎山是没人重构堆出来的;AI 屎山,是它压根没有"重构"这个本能。


我的真实案例

我给一个客户做订单批处理工具,核心就一个函数:读一批订单,校验、算钱、落库。第一轮 AI 交出来的代码相当漂亮,结构清晰,注释齐全,90 分:

def process_orders(orders):
    results = []
    for order in orders:
        # 1. 校验
        if not order.get("id") or not order.get("items"):
            results.append({"id": order.get("id"), "status": "invalid"})
            continue
        # 2. 算钱
        amount = sum(item["price"] * item["qty"] for item in order["items"])
        tax = round(amount * 0.06, 2)          # 税率 6%
        total = round(amount + tax, 2)
        # 3. 落库
        save_order({**order, "amount": amount, "tax": tax, "total": total,
                    "status": "processed"})
        results.append({"id": order["id"], "status": "ok", "total": total})
    return results

四轮需求,四次"完美交付",然后我收获了一座山。

第二轮:加急订单 → process_orders_fast()

客户说:"加急订单逻辑不一样,优先处理,不收税,给合作快递的接口。"

我的指令是:"给订单处理加急通道。" AI 的动作干脆利落——新建 process_orders_fast(),把原函数整段复制过去,改掉中间几行:

def process_orders_fast(orders):
    results = []
    for order in orders:
        if not order.get("id") or not order.get("items"):
            results.append({"id": order.get("id"), "status": "invalid"})
            continue
        amount = sum(item["price"] * item["qty"] for item in order["items"])
        # tax = round(amount * 0.06, 2)        # 加急单免税
        tax = 0
        total = round(amount, 2)
        save_order({**order, "amount": amount, "tax": tax, "total": total,
                    "status": "processed_fast", "priority": True})
        call_express_api(order["id"], total)   # 加急:通知快递
        results.append({"id": order["id"], "status": "ok_fast", "total": total})
    return results

当时我看着没毛病:需求满足、测试通过、加急单跑通了。我没意识到自己刚批准了第一块山寨地皮。

第三轮:国际订单 → process_orders_international()

"国际订单要按币种算汇率,关税另算,走清关接口。"

同样的配方,第三个函数:

def process_orders_international(orders, fx_rate=7.2):
    results = []
    for order in orders:
        if not order.get("id") or not order.get("items"):
            results.append({"id": order.get("id"), "status": "invalid"})
            continue
        amount_usd = sum(item["price"] * item["qty"] for item in order["items"])
        amount = round(amount_usd * fx_rate, 2)
        tax = round(amount * 0.06, 2)
        customs = round(amount * 0.1, 2)       # 关税 10%
        total = round(amount + tax + customs, 2)
        save_order({**order, "amount": amount, "tax": tax, "customs": customs,
                    "total": total, "status": "processed_intl"})
        call_customs_api(order["id"], total)
        results.append({"id": order["id"], "status": "ok_intl", "total": total})
    return results

校验逻辑、遍历骨架、save_order 的字段拼装、results 的结构——和前两个函数一模一样。唯一不同的是"算钱"那两三行。

第四轮:演练模式 → process_orders_dry_run()

"上线前要演练,测试环境跑的时候别真发货、别真落库。"

第四个函数。这次连复制都懒得装了,AI 在回复里写:"基于 process_orders 增加 dry-run 版本,复用相同的校验和计价逻辑。" 我当时还觉得它挺有复用意识——打开文件一看,复用的方式是第四份拷贝,把 save_order 换成日志打印。

四个函数,580 行代码。如果用抽象后的写法,整个模块 120 行。

第五轮:山塌了

税率调整,6% → 6.5%。我让 AI 改税率。

它改得又快又准——改了 process_orders 一处。 三天后客户对账发现国际订单的税差了一截:process_orders_international 里还是老税率,process_orders_fast 倒是不用改(免税),process_orders_dry_run 里也藏了一份税率,演练环境没人发现,等上线切真实环境时差点炸。

一个规则,四个副本,改一漏二(其中一个恰好不用改)。这时候我才回头看清这座山的全貌:

  • 校验规则后来加过一条"金额上限",四个函数只加了两个
  • 空订单的状态字符串 "invalid" 在四份代码里有两份被拼成了 "invlid"(AI 复制时手滑),报表按状态分组,丢单丢了一个月
  • 我让 AI 给这四个函数补异常捕获,它补了四种风格,日志格式全不一样

每一次单看都是正确的需求交付,连起来看是一场持续四次的复制事故。

重构:120 行的样子

后来我花了一晚上带着 AI 把它重写成一条管道 + 差异点参数化:

from dataclasses import dataclass

@dataclass
class OrderPolicy:
    name: str
    tax_rate: float = 0.065
    customs_rate: float = 0.0
    fx_rate: float = 1.0
    persist: bool = True          # dry-run 时 False
    extra_hook: callable = None   # 加急→快递,国际→清关

POLICIES = {
    "normal":     OrderPolicy("processed"),
    "fast":       OrderPolicy("processed_fast", tax_rate=0,
                              extra_hook=lambda o, t: call_express_api(o["id"], t)),
    "intl":       OrderPolicy("processed_intl", customs_rate=0.1, fx_rate=7.2,
                              extra_hook=lambda o, t: call_customs_api(o["id"], t)),
    "dry_run":    OrderPolicy("dry_run", persist=False),
}

def process_orders(orders, mode="normal"):
    policy = POLICIES[mode]
    results = []
    for order in orders:
        invalid = validate(order)              # 校验只有一份
        if invalid:
            results.append({"id": order.get("id"), "status": "invalid"})
            continue
        pricing = price(order, policy)          # 计价只有一份
        if policy.persist:
            save_order(build_record(order, pricing, policy))
        else:
            log_dry_run(order, pricing)
        if policy.extra_hook:
            policy.extra_hook(order, pricing["total"])
        results.append({"id": order["id"], "status": "ok", **pricing})
    return results

公共步骤(校验、计价、落库、组结果)各只有一份;四种业务的差异全部收进一张策略表。以后税率再变,改一个默认值;再来第五种订单,加一段配置——连新函数都不用写

580 行 → 120 行,四个函数 → 一个函数 + 一张表。这才是这个模块第一天就该有的样子。


为什么 AI 会这样?

1. 复制是"最安全"的动作,改原代码有风险

你要站在模型的角度想:面前有一个跑得好好的函数,你让它加新逻辑。它有两个选择——

  • A:改原函数,把新逻辑织进去。风险:万一改坏了现有行为,正常订单受影响
  • B:复制一份,在副本上改。风险:零,原函数纹丝不动

模型天然选 B,因为"不破坏既有行为"是它最强的约束之一。 你每次说"加功能",它的最优解都是"追加",永远不是"重构后融入"。它不是不会抽象,是抽象要动旧代码,而动旧代码在它的风险排序里是下策。

2. 抽象需要看见"第二次重复",但每轮对话只有一个新需求

人什么时候会提取公共函数?写第二遍、第三遍相似代码的时候——"等等,这段我上周写过",这个念头触发抽象。

AI 没有"上周"。每一轮对话里,它只看到当前这一个需求,和上下文里的代码。即便代码文件里三个相似函数都躺着,"识别重复→归纳共性→设计抽象"也需要你明确下令,它不会自发做。重复是跨轮次长出来的,而它的视野是按轮次分配的。

3. 重构没有可见产出

你让它加急件,它交付 process_fast(),你能立刻验证:加急单走通了 ✅
你让它重构,它交付……还是那些功能,跑出来一模一样。"什么都没变"在验收时最没有存在感。

模型倾向于产出可见增量,人也倾向于催可见增量。于是双方默契地不断往房子加盖,没人拆承重墙里的重复。

4. 它不知道"这是第几版",也不为三个月后负责

人类工程师写复制代码时心里至少会"咯噔"一下:以后这地方要改两处。AI 没有这个心理负担——它不为下一个会话里的自己负责,更不为三个月后接手的你负责。

代码的默认走向是混乱(熵增),重构是逆熵,必须外部持续输入能量。 你不下令、不定规矩,AI 协作的代码会以人类团队 5-10 倍的速度滑向屎山——因为它产出代码太快了,混乱累积的速度也同比放大。


这种病的代价

  • 🔴 一个规则,N 处副本:税率、校验、状态流转改一次要同步改 N 个函数,漏一个就是静默错误——不报错、不崩溃、数据悄悄错
  • 🔴 修 Bug 按倍数收费:公共逻辑的一个 bug,在四份代码里就是四个 bug;让 AI 修,它还可能只修三处,第四处留雷
  • 🔴 上下文窗口被垃圾占满:580 行重复代码喂进对话,有效信息可能只有 150 行。后续每一轮协作都更慢、更贵、更易出错——屎山会直接吃掉你的 token 预算
  • 🔴 重构窗口指数关闭:四个函数时重构要一晚上,八个函数时没人敢动。AI 自己也会拒绝:"该模块耦合较复杂,建议分阶段评估"——翻译:我也怕了
  • 🔴 假进度:每个函数都有单测、每个需求都验收通过,团队以为在高速前进,实际是在高速复印。技术债的利息在每次改动里复利

怎么治

方法 1:把"三次法则"变成给 AI 的硬性规范

人类团队的老规矩:第一次写,第二次抄,第三次还出现——必须提取抽象。对 AI 要更严,我现在的项目规范直接写在给它的约束里:

## 代码规范(每次生成/修改必须遵守)
1. 单个函数不超过 30 行,嵌套不超过 3 层
2. 相似逻辑出现第 2 次,必须提取公共函数,禁止复制整段后局部修改
3. 加需求优先"扩展/修改现有结构"(加参数、加策略、加配置),
   不允许新建 xxx_fast / xxx_v2 / xxx_new 式的平行函数
4. 如果确实需要新函数,先说明:为什么现有函数不能通过参数/策略满足?

把规矩前置,比它写完你再让返工省 10 倍口舌。AI 对"明确写出的规范"服从度相当高,它缺的从来不是能力,是指令。

方法 2:下需求时明确说"改"还是"加"

复盘我的四轮事故,我的指令全是"给订单处理加急通道"这种只说业务、不说代码动作的话——它默认选了风险最小的复制。

后来我把指令改成显式的:

"加急订单不要新写函数。在现有 process_orders 上扩展:用一个 mode 参数区分,公共的校验和遍历保持一份,计价差异用策略表/参数表达。先给我看结构设计,确认后再写。"

"禁止复制,先设计再写"这句话,能挡掉八成的平行函数。

方法 3:定期开"只还债"的重构轮

功能迭代期允许 AI 快,但每隔几轮(我一般 3-4 个新需求后)专门开一轮对话,任务只有一个:

"这轮不加任何功能。审查当前模块:列出所有重复/超长函数/平行函数,给出重构方案,重构后保证行为不变,把现有测试跑一遍证明。"

重构必须有测试兜底——让 AI 先把现有四个函数的输入输出快照成测试用例(characterization tests,特征测试/表征测试),再动刀。有测试锁行为,重构才不是赌博;这也是我做测试的老本行给 AI 协作上的保险。

方法 4:让 AI 每次交付前自审重复度

固定加一句验收台词:

"交付前自查:列出本次新增/修改的函数中,与既有代码重复率超过 50% 的段落;凡是重复的,先提取再交付。"

也可以上工具:Python 用 pylint --disable=all --enable=duplicatesjscpd(多语言通用)扫重复块,把结果贴给 AI 让它自己消重。机器扫重复比人眼可靠,让工具当那个不留情面的审查员。

方法 5:守住"核心规则单点存在"这条红线

不是所有重复都同等危险。校验、计价、状态流转、权限判断这类业务规则的重复是致命的(规则一变雷就炸);工具函数里的偶发重复可以容忍。

给 AI 立一条最高优先级:

"计税、校验、状态变更等核心业务规则,整个代码库只允许存在一份实现。场景差异通过参数/策略/配置表达,不许在不同函数里各写一遍。"

我重构后的 OrderPolicy 策略表就是这个原则的落地——规则单点,差异数据化。公共逻辑放一处,是屎山和架构的分界线。


一句话总结

AI 写代码的默认动作是"追加"不是"重构":复制一份改两行,风险最小、交付最快、也最像干活——于是四个需求交付完,你得到四个 80% 重复的函数和一座三天封顶的屎山。治病的关键不是等它良心发现,而是你亲自当逆熵的外力:规范前置(重复 2 次必提取)、指令显式(说"改"不说"加")、定期还债(重构轮 + 特征测试锁行为)、核心规则单点存在。记住,代码不是写出来的,是改出来的——不让 AI 改旧代码,你得到的永远是复印件。

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册