让AI帮我重构2000行遗留代码:从3小时到15分钟的代价

30秒速览

  • 用Claude 3.5重构2000行电商库存老代码,新代码可维护性暴增,加新功能从3小时缩到15分钟。
  • AI生成代码很快,但会自信地忽略边缘情况,第一个版本直接KeyError崩给我看。
  • 别指望AI自动提炼隐藏业务规则,你得当“人肉编译器”把业务翻译成精准提示词喂给它。
  • 引入Pydantic做数据验证是神来之笔,让防御性编程变得优雅,AI用起来也得心应手。
  • 让AI写单元测试覆盖不全,它只会测试它自己理解的逻辑,你得引导它覆盖各种刁钻角落。
  • 总耗时约两天,省了时间但费了脑子,绝对不能在没测试和人工审核的情况下直接部署AI重构的代码。

我让Claude 3.5 Sonnet去啃那坨2000行的“屎山”

上周,我那个做电商的朋友老张又来找我救火了。他公司有个日活大概20万用户的电商平台,后台有个库存同步模块,代码是五年前一个实习生写的,后来缝缝补补,现在变成了接近2000行的Python脚本。每次业务逻辑要调整,改这玩意儿都得花上至少3小时——不是改代码本身要3小时,是理解这坨“屎山”的逻辑,再小心翼翼地不搞崩其他部分,就得花这么久。老张说:“兄弟,你不是整天捣鼓AI编程吗?能不能让AI给它重写一下?我预算不多,但能接受花点钱省时间。”

我心说这倒是个绝佳的实战测试。我一直好奇,像Claude 3.5 Sonnet、GPT-4o这些顶级大模型,处理这种中等规模、逻辑混乱但业务关键的遗留代码,到底能做到什么程度。是能一键生成优雅解,还是会把坑挖得更深?我接下了这个活,用的主要工具是Cursor(集成了Claude 3.5 Sonnet)和直接调用Claude API做批量处理。

第一步,我得先看看这代码长啥样。老张把文件发过来,我打开一看,好家伙,名不虚传。一个sync_inventory.py,1978行,没有类,全是函数,全局变量到处飞,函数名都是process_a(), handle_b()这种毫无意义的命名。注释倒是有,但一半是“这里很重要”,另一半是“TODO: 这里需要优化”。逻辑是典型的“流水账”式:从数据库读数据,经过七八个处理函数(中间夹杂着各种if-else判断不同渠道),再写入另一个数据库,同时还要发消息到Redis和Kafka。最要命的是,错误处理是“霰弹枪”式的,到处都有try-except,但except里基本都是pass或者打个日志了事。

# 原代码片段示例(已简化脱敏)
def main_process():
    data = get_db_data()  # 某个神秘函数
    # 中间穿插了无数个这样的“处理站”
    tmp_list = []
    for item in data:
        # 超过50行的嵌套if-else,判断sku来源、活动状态、仓库位置...
        if item['src'] == 'A':
            # ... 处理逻辑A
            if item['status'] > 0:
                # ... 子逻辑
                for warehouse in item['wh_list']:
                    # ... 还有循环
        elif item['src'] == 'B':
            # ... 另一个平行宇宙的逻辑B
        # ... 省略更多elif
        tmp_list.append(some_result)
    # 接下来又是一段风格迥异的处理逻辑
    final_output = []
    for tmp in tmp_list:
        # 这里又混入了新的业务判断...
    save_to_db(final_output)
    # 最后,在某些条件下发消息
    if some_global_flag:
        send_to_kafka(final_output)

我的目标很明确:不改变外部输入输出和行为(这是底线),但要让代码变得可读、可维护、可测试。性能最好能有点提升,但这不是首要的。我心里盘算着,如果我自己手动重构,梳理逻辑、设计结构、写测试,没两天搞不定。现在,我把宝押在了AI上。

重构不是请客吃饭:AI生成的第一个版本直接跑崩了

我兴冲冲地把整个文件扔进Cursor,用Chat模式告诉Claude:“请重构这个Python脚本,目标是提高可读性、可维护性和可测试性。保持所有外部接口和行为不变。请使用面向对象的设计,将不同来源的业务逻辑分离。” 然后我泡了杯咖啡,等着奇迹发生。

几分钟后,AI吐出了一个全新版本。看起来确实漂亮多了:有了InventorySyncEngine这个主类,把不同的数据源抽象成了DataSourceA, DataSourceB等子类,用上了dataclass来定义数据结构,错误处理也统一了。代码行数压缩到了大概800行。我心中一喜,这效率,手动重构哪比得上。

但喜悦只持续了不到五分钟。当我尝试用一份真实的测试数据运行新代码时,它直接抛出了一个KeyError。我仔细一看,问题出在AI对原始数据结构的理解上。原代码里,数据是一个深度嵌套的字典,有些字段在某些条件下不存在,原代码用了大量的.get('field', default)来规避。AI在重构时,虽然定义了漂亮的dataclass,但在从原始字典向dataclass转换的步骤中,它假设某些字段总是存在的,直接用了字典键访问,导致崩溃。

# AI生成的有问题的转换代码
@dataclass
class InventoryItem:
    sku_id: str
    quantity: int
    warehouse_code: str
    # ... 其他字段

# 在某个处理函数中
def _convert_raw_item(raw: dict) -> InventoryItem:
    # 问题在这里!raw['warehouse_code'] 在某些旧数据中可能不存在
    return InventoryItem(
        sku_id=raw['sku_id'],
        quantity=raw['quantity'],
        warehouse_code=raw['warehouse_code'],  # KeyError 风险点!
        # ...
    )

这是我踩的第一个大坑:AI对代码的“理解”是统计意义上的,它看到了大多数地方有warehouse_code这个键,就假设它总是存在,但它无法洞察那些隐藏在深层条件分支或历史数据中的边缘情况。 原代码的防御性写法(虽然丑陋)恰恰是应对这些“坑”的经验体现。

我不得不停下来,手动补充了数据验证和默认值逻辑。这也让我意识到,不能指望AI一次性完美重构。我需要更精细的引导。于是,我调整了策略:分模块,带上下文,给示例。 我不再一次性处理整个文件,而是把脚本按功能块(比如数据获取、A渠道处理、B渠道处理、持久化)拆开,分别让AI重构每个块,并且我会提供更详细的指令,包括指出原代码中那些隐晦的默认值逻辑。

“人肉编译器”的诞生:我如何给AI喂指令才能少返工

分块重构的策略对了,但怎么“喂”指令又成了学问。光说“重构这个函数”不够。我总结出一套比较高效的“提示词配方”,核心是:提供背景、指定风格、约束边界、给出正反例。

首先,我会为每个代码块写一段“背景说明”,放在提示词最前面:

"""
以下是电商库存同步模块中,处理‘渠道A’订单数据的函数。
业务背景:渠道A的API返回的数据中,`stock_info`字段可能缺失,若缺失则表示库存为0。
`tags`字段是一个用逗号分隔的字符串,需要转换为列表。
原函数中的`_merge_special_offer`是一个全局函数,需要保留其调用。
目标:将此函数重写为一个类的方法,提高可读性,并添加适当的类型提示和日志。
"""

其次,我会明确代码风格和工具要求:

  • 使用Python 3.10+语法。
  • 使用Pydantic进行数据验证和序列化(我决定引入这个库来更好地处理数据缺省值)。
  • 使用`logging`模块记录`INFO`和`ERROR`级别日志,错误必须被捕获并记录,不能简单`pass`。
  • 使用`typing`模块提供完整的类型提示。

然后,给出一个“好代码”和“坏代码”的简单示例对比,让AI明白我的具体期望:

# 我希望避免的(原风格):
try:
    do_something()
except:
    pass

# 我期望的:
try:
    result = do_something()
except ValueError as e:
    logger.error(f"处理数据时发生值错误: {e}, 数据: {some_data}")
    return None
except Exception as e:
    logger.exception(f"处理数据时发生未知错误")
    raise

最后,我会把需要重构的原代码贴上去。这样一套组合拳下来,AI生成的代码质量明显高了很多,第一次可用的概率从不到30%提升到了70%左右。剩下的30%,主要是一些非常具体的业务规则,AI还是需要我人工校正。

比如,有一个规则是“如果商品同时参与闪购和普通促销,则优先扣减闪购库存,但剩余库存要标记为预占”。这个规则在原代码里被埋藏在一个三层嵌套的if语句里。AI重构时,虽然拆解了嵌套,但生成的逻辑顺序错了。我必须明确指出:“注意,规则A必须在规则B之前判断,因为规则A具有更高优先级。” AI才能修正。

这个阶段,我感觉自己像个“人肉编译器”,把高级的、模糊的业务需求,“编译”成AI能精确理解的提示词。虽然累,但比直接手写所有代码还是快多了。

类型提示与Pydantic:AI帮我把防御性编程提升了一个维度

在重构过程中,我决定做一件原代码完全没有的事:引入完整的类型提示和Pydantic模型。这不仅是为了代码美观,更是为了让AI和我自己都能更好地理解数据的流动。我让AI为所有主要的数据结构创建Pydantic的`BaseModel`。

这步操作带来了意想不到的好处。当AI在生成处理函数时,由于输入输出都有了明确的类型定义,它生成的代码逻辑更连贯,减少了那些无厘头的类型转换错误。更重要的是,Pydantic的字段验证和默认值设置,天然地解决了我们之前遇到的`KeyError`问题。

# AI在良好提示下生成的Pydantic模型和用法
from pydantic import BaseModel, Field, validator
from typing import Optional, List

class ChannelAInventoryData(BaseModel):
    """渠道A的库存数据模型"""
    sku_id: str
    quantity: int = Field(ge=0, description="库存数量,必须非负") # 利用Field添加约束
    warehouse_code: Optional[str] = None  # 明确标注可能为None
    tags: Optional[str] = None
    stock_info: Optional[dict] = None

    # 使用validator处理复杂默认逻辑
    @validator('quantity', pre=True, always=True)
    def set_quantity_if_missing(cls, v, values):
        """如果stock_info缺失,则quantity应为0"""
        if v is not None:
            return v
        # 如果quantity未提供,但stock_info提供了,从其中解析(这里省略)
        # 如果stock_info也未提供,默认为0
        if values.get('stock_info') is None:
            return 0
        # 否则,尝试从stock_info解析...
        # return parsed_quantity
        return 0 # 简化示例

    @property
    def tags_list(self) -> List[str]:
        """将tags字符串转换为列表的业务逻辑"""
        if self.tags:
            return [tag.strip() for tag in self.tags.split(',') if tag.strip()]
        return []

class InventorySyncProcessor:
    def process_channel_a(self, raw_data: dict) -> Optional[ChannelAInventoryData]:
        try:
            # Pydantic会自动进行类型转换和验证,缺失字段会用默认值
            inventory_item = ChannelAInventoryData(**raw_data)
            # 后续处理逻辑可以直接使用 inventory_item.sku_id, inventory_item.tags_list 等
            # 完全不用担心KeyError
            return self._apply_business_rules(inventory_item)
        except ValidationError as e:
            logger.error(f"渠道A数据验证失败: {e}, 原始数据: {raw_data}")
            return None

这个改变是革命性的。原来散落在各处、隐晦的默认值逻辑,现在被集中、显式地定义在了数据模型里。任何开发者(包括未来的AI)一看这个模型,就知道处理渠道A的数据时,`warehouse_code`可能没有,`quantity`怎么确定。代码的可维护性指数级上升。

当然,引入Pydantic也有代价。首先是依赖增加,需要安装`pydantic`。其次,对于极度复杂的嵌套数据验证,配置起来可能有点繁琐。但在我们这个场景下,利远大于弊。AI在生成这些模型和验证器时表现非常出色,我只需要描述清楚业务规则。

单元测试:AI写的测试比我预想的更“狡猾”

代码重构了,行为对不对,心里没底。老张那边有现成的集成测试环境,但跑一次全套要半小时。我不能每改一次就丢上去跑。所以,编写单元测试是必须的。我又把这事交给了AI:“请为上面这个`ChannelAInventoryData`模型和`process_channel_a`方法编写单元测试,使用pytest。覆盖主要成功路径和关键错误路径(如数据缺失、格式错误、业务规则触发等)。”

AI刷刷地生成了一堆测试用例。我看了一下,覆盖得还挺全:正常数据、缺失`stock_info`、`tags`字符串格式、负数`quantity`等等。但很快,我发现了一个有趣的现象:AI生成的测试,严重依赖于它刚刚生成的代码逻辑。 换句话说,它是在测试它自己“认为”的逻辑。

比如,在测试`tags_list`属性时,它写的测试用例是:

def test_tags_list_conversion():
    data = ChannelAInventoryData(sku_id="test", tags="a,b,c")
    assert data.tags_list == ["a", "b", "c"]

这没问题。但它没有去测试原代码中可能存在的边缘情况,比如`tags`字符串开头结尾有空格、连续多个逗号、空字符串等。因为这些边缘情况,在我给AI的“背景说明”和原代码片段中,可能没有被强调。AI生成的测试,更像是“确认性测试”,而不是“破坏性测试”。

为了得到更健壮的测试,我不得不再给AI补充指令:“请考虑以下边缘情况,并补充测试用例:tags为None,tags为空字符串,tags为‘a,,b’,tags字符串前后带空格。” AI这才补上。这让我明白,让AI写测试,你依然需要具备良好的测试思维,去引导它覆盖那些容易出错的角落。 它是个高效的执行者,但不是测试策略的设计师。

另一个小坑是关于测试数据准备的。AI喜欢在每一个测试函数里都从头构造测试数据,导致大量重复。我不得不额外提示它:“使用pytest的fixture来共享公共的测试数据。” 它才生成出更地道的测试代码。

import pytest
from .inventory_models import ChannelAInventoryData

@pytest.fixture
def valid_channel_a_raw_data():
    """有效的渠道A原始数据fixture"""
    return {
        "sku_id": "SKU12345",
        "quantity": 100,
        "warehouse_code": "WH_EAST",
        "tags": "hot,new",
        "stock_info": {"available": True}
    }

def test_process_channel_a_success(sync_processor, valid_channel_a_raw_data):
    """测试成功处理有效数据"""
    result = sync_processor.process_channel_a(valid_channel_a_raw_data)
    assert result is not None
    assert result.sku_id == "SKU12345"
    assert result.quantity == 100

def test_process_channel_a_missing_stock_info(sync_processor):
    """测试缺失stock_info时,quantity默认为0"""
    data = {"sku_id": "SKU999", "tags": "clearance"}
    # 这里依赖Pydantic模型的默认逻辑
    result = sync_processor.process_channel_a(data)
    # 我们需要断言业务逻辑,比如quantity是否为0
    # 这要求我们对AI的提示足够细致,说明业务规则
    assert result.quantity == 0

最终,AI帮我生成了大约30个单元测试,覆盖了核心模块。我自己补充了大约10个针对特别隐晦业务的测试。运行一遍,不到2秒。这为我后续的集成验证提供了巨大的信心。

从3小时到15分钟:效率提升的背后,我付出了什么代价?

经过大概两天的断断续续的工作(其中包含我自己的本职工作),重构完成了。我把新代码部署到测试环境,用过去一个月的真实数据跑了一遍,结果和老代码完全一致。性能呢?由于引入了Pydantic的验证开销和更清晰的结构,单次执行时间略有增加,从大约120ms变成了150ms。但这完全在可接受范围内,因为整个同步任务是定时任务,不追求毫秒级延迟。

最关键的可维护性指标对比如下:

指标 重构前 重构后 变化
代码行数 (逻辑行) ~1978 ~820 -58%
文件数 1个巨型文件 1个主入口 + 4个模块 + 2个数据模型文件 结构清晰
环形复杂度 (平均函数) 15+ (难以准确测量) 3-8 大幅下降
添加一个新渠道的预估时间 3小时+ (理解+修改) 15-30分钟 (依葫芦画瓢) -85% ~ -90%
单元测试覆盖率 0% ~85% (核心逻辑) 从无到有

老张很满意,特别是听到“以后加新渠道,熟练的话可能就15分钟”时。那么,我付出了什么代价呢?

1. 时间代价: 总耗时约16人时(两天,每天穿插着干)。如果是我自己纯手动重构,估计需要24-32人时。AI确实帮我节省了时间,但绝不是“一键生成”。大量的时间花在了:拆解任务、编写精准提示词、反复验证AI输出、补充业务知识、编写和补充测试用例上。我扮演的是“架构师”和“质检员”的角色。

2. 认知代价: 我必须比AI更懂业务。AI无法从混乱的代码中自动提炼出那些未写在注释里的业务规则(比如那个“闪购优先”规则)。我必须先自己理解,或者通过运行旧代码观察,然后再教给AI。这个过程要求我对代码有深入的理解,并不能当甩手掌柜。

3. 金钱代价: 频繁使用Cursor和Claude API,花费了大约15美元。对于企业场景,可以忽略不计。

4. 风险代价: 这是最关键的。AI可能会“自信地”犯错误,生成逻辑正确但业务错误的代码,或者引入一些看似合理实则脆弱的假设(如最初的`KeyError`)。如果没有我这个人肉“业务逻辑校验器”和后续的全面测试,直接上线就是灾难。**AI重构,绝对不能在无监督、无测试的情况下直接部署到生产环境。**

所以,结论很清晰:AI是一个强大的“加速器”和“初级工程师”,它能快速完成模式化、结构化的代码转换,并能极大提升代码的“颜值”和基础质量。但它无法替代你对业务的理解和作为最终责任人的判断力。用它来重构遗留代码,就像用一台高性能的挖掘机去清理废墟——你仍然需要驾驶员精确地操控方向,并且清楚底下哪里埋着管线,哪里是承重墙。弄好了,效率倍增;弄不好,塌得更快。

这次实战之后,我对AI编程工具的态度更务实了。它不是什么银弹,但它确实是我工具箱里一把变得异常锋利的瑞士军刀。下次再遇到这种“屎山”,我知道该怎么和这位“AI同事”搭档了:我负责定方向、讲业务、做验收;它负责干脏活、出苦力、搞格式化。这么配合,挺好。

发表评论