人类程序员把代码堆成屎山,通常要两三年。
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
四轮需求,四次"完美交付",然后我收获了一座山。
客户说:"加急订单逻辑不一样,优先处理,不收税,给合作快递的接口。"
我的指令是:"给订单处理加急通道。" 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
当时我看着没毛病:需求满足、测试通过、加急单跑通了。我没意识到自己刚批准了第一块山寨地皮。
"国际订单要按币种算汇率,关税另算,走清关接口。"
同样的配方,第三个函数:
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 的结构——和前两个函数一模一样。唯一不同的是"算钱"那两三行。
"上线前要演练,测试环境跑的时候别真发货、别真落库。"
第四个函数。这次连复制都懒得装了,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 把它重写成一条管道 + 差异点参数化:
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 行,四个函数 → 一个函数 + 一张表。这才是这个模块第一天就该有的样子。
1. 复制是"最安全"的动作,改原代码有风险
你要站在模型的角度想:面前有一个跑得好好的函数,你让它加新逻辑。它有两个选择——
模型天然选 B,因为"不破坏既有行为"是它最强的约束之一。 你每次说"加功能",它的最优解都是"追加",永远不是"重构后融入"。它不是不会抽象,是抽象要动旧代码,而动旧代码在它的风险排序里是下策。
2. 抽象需要看见"第二次重复",但每轮对话只有一个新需求
人什么时候会提取公共函数?写第二遍、第三遍相似代码的时候——"等等,这段我上周写过",这个念头触发抽象。
AI 没有"上周"。每一轮对话里,它只看到当前这一个需求,和上下文里的代码。即便代码文件里三个相似函数都躺着,"识别重复→归纳共性→设计抽象"也需要你明确下令,它不会自发做。重复是跨轮次长出来的,而它的视野是按轮次分配的。
3. 重构没有可见产出
你让它加急件,它交付 process_fast(),你能立刻验证:加急单走通了 ✅
你让它重构,它交付……还是那些功能,跑出来一模一样。"什么都没变"在验收时最没有存在感。
模型倾向于产出可见增量,人也倾向于催可见增量。于是双方默契地不断往房子加盖,没人拆承重墙里的重复。
4. 它不知道"这是第几版",也不为三个月后负责
人类工程师写复制代码时心里至少会"咯噔"一下:以后这地方要改两处。AI 没有这个心理负担——它不为下一个会话里的自己负责,更不为三个月后接手的你负责。
代码的默认走向是混乱(熵增),重构是逆熵,必须外部持续输入能量。 你不下令、不定规矩,AI 协作的代码会以人类团队 5-10 倍的速度滑向屎山——因为它产出代码太快了,混乱累积的速度也同比放大。
人类团队的老规矩:第一次写,第二次抄,第三次还出现——必须提取抽象。对 AI 要更严,我现在的项目规范直接写在给它的约束里:
## 代码规范(每次生成/修改必须遵守)
1. 单个函数不超过 30 行,嵌套不超过 3 层
2. 相似逻辑出现第 2 次,必须提取公共函数,禁止复制整段后局部修改
3. 加需求优先"扩展/修改现有结构"(加参数、加策略、加配置),
不允许新建 xxx_fast / xxx_v2 / xxx_new 式的平行函数
4. 如果确实需要新函数,先说明:为什么现有函数不能通过参数/策略满足?
把规矩前置,比它写完你再让返工省 10 倍口舌。AI 对"明确写出的规范"服从度相当高,它缺的从来不是能力,是指令。
复盘我的四轮事故,我的指令全是"给订单处理加急通道"这种只说业务、不说代码动作的话——它默认选了风险最小的复制。
后来我把指令改成显式的:
"加急订单不要新写函数。在现有 process_orders 上扩展:用一个 mode 参数区分,公共的校验和遍历保持一份,计价差异用策略表/参数表达。先给我看结构设计,确认后再写。"
"禁止复制,先设计再写"这句话,能挡掉八成的平行函数。
功能迭代期允许 AI 快,但每隔几轮(我一般 3-4 个新需求后)专门开一轮对话,任务只有一个:
"这轮不加任何功能。审查当前模块:列出所有重复/超长函数/平行函数,给出重构方案,重构后保证行为不变,把现有测试跑一遍证明。"
重构必须有测试兜底——让 AI 先把现有四个函数的输入输出快照成测试用例(characterization tests,特征测试/表征测试),再动刀。有测试锁行为,重构才不是赌博;这也是我做测试的老本行给 AI 协作上的保险。
固定加一句验收台词:
"交付前自查:列出本次新增/修改的函数中,与既有代码重复率超过 50% 的段落;凡是重复的,先提取再交付。"
也可以上工具:Python 用 pylint --disable=all --enable=duplicates 或 jscpd(多语言通用)扫重复块,把结果贴给 AI 让它自己消重。机器扫重复比人眼可靠,让工具当那个不留情面的审查员。
不是所有重复都同等危险。校验、计价、状态流转、权限判断这类业务规则的重复是致命的(规则一变雷就炸);工具函数里的偶发重复可以容忍。
给 AI 立一条最高优先级:
"计税、校验、状态变更等核心业务规则,整个代码库只允许存在一份实现。场景差异通过参数/策略/配置表达,不许在不同函数里各写一遍。"
我重构后的 OrderPolicy 策略表就是这个原则的落地——规则单点,差异数据化。公共逻辑放一处,是屎山和架构的分界线。
AI 写代码的默认动作是"追加"不是"重构":复制一份改两行,风险最小、交付最快、也最像干活——于是四个需求交付完,你得到四个 80% 重复的函数和一座三天封顶的屎山。治病的关键不是等它良心发现,而是你亲自当逆熵的外力:规范前置(重复 2 次必提取)、指令显式(说"改"不说"加")、定期还债(重构轮 + 特征测试锁行为)、核心规则单点存在。记住,代码不是写出来的,是改出来的——不让 AI 改旧代码,你得到的永远是复印件。