大模型技术日新月异的今天,作为一名软件工程师,我们常常惊叹于 AI编码助手 的强大能力,仿佛奇点近在眼前。然而,它们也经常犯错,让我们意识到理想与现实之间仍存在差距。如何充分发挥这些工具的潜力,同时保持对代码质量的控制,是当前软件开发面临的重要课题。本文将深入探讨如何将 AI编码助手 作为战术工具,并坚守我们作为战略架构师的角色,构建更可维护、更具扩展性的软件系统。

战术编程与战略编程:两种不同的开发模式

John K. Ousterhout 在《软件设计哲学》一书中提出了“战术编程”和“战略编程”的概念。战术编程 是一种短视的方法,专注于以最快的速度完成眼前的任务。其主要目标是以最直接的方式实现功能或修复错误。然而,如果团队中的工程师都只采用这种方式工作,技术债务将迅速累积,导致代码难以维护和扩展。

与此相反,战略编程 涉及更高层次的思考。战略程序员不仅仅满足于实现功能,还会退一步审视架构,考虑是否可以进行抽象。他们会思考当前的方法是否过于复杂,是否可以进行重构。战略编程的重点在于设计,工程师的主要目标是控制复杂性,确保代码库的长期健康。战略编程关注的是未来的可维护性、可扩展性和可理解性,而不仅仅是眼前的快速交付。例如,一个战略程序员在实现一个新功能后,会考虑如何将该功能抽象成一个通用的组件,以便在其他地方复用。他们也会考虑如何编写单元测试,以确保该功能在未来不会被意外破坏。

AI编码助手:高效的战术程序员

在我看来,目前最先进的 AI编码助手 (比如 Gemini 2.5 Pro, Claude 3.7) 都是极其出色的 战术程序员。只要你清晰地描述了需要实现的功能,它们就能以很高的成功率完成任务。人类在编写代码时受到诸多限制,例如物理上敲击键盘的速度。然而,对于 AI编码助手 来说,代码行数不再是限制因素:生成 1000 行代码几乎与生成单行代码一样快。

这种能力既是优势也是潜在的风险。我们人类往往倾向于编写尽可能少的代码,以确保 DRY(Don’t Repeat Yourself)和 KISS(Keep It Simple, Stupid)的设计原则。而 AI 的设计目标是预测下一个字符,如果放任自由,很可能会导致代码复杂度飙升、重复行为泛滥,最终变得难以阅读和维护。

例如,假设我们需要在一个电商网站上添加一个新的支付方式。如果我们直接让 AI编码助手 生成代码,它可能会直接在现有的支付处理逻辑中添加一个新的分支,导致代码变得越来越臃肿和难以理解。但是,如果我们能够引导 AI编码助手 使用策略模式,将不同的支付方式封装成独立的类,就可以使代码更加清晰和易于维护。

如何驯服你的AI编码助手

正如 Andrej Karpathy 在他的演讲中建议的那样,我们必须给 AI 系上短缰绳。在任何情况下,都不应该让它自由发挥。人类开发人员必须始终是项目的战略家。你决定目标,你管理复杂性,你驾驭软件项目演进过程中的不确定性。如果缰绳太长,AI 会把你带入一条黑暗的道路,创建一个越来越难以更改且无法理解的代码库。

当然,也有一些时候你可以放松缰绳。可以把它想象成找到一片开阔的场地,让你的 AI 狗狗可以更自由地奔跑。这些是样板任务:从清晰的 API 合同中搭建十几个 REST 端点、编写重复的测试套件或迁移数据格式。即使这样,关键是你不要盲目接受生成的内容。你必须是最终的守门人,在每一行代码进入你的代码库之前都进行审查。

案例分析:报告生成器的战术与战略实现

让我们通过一个具体的例子来说明这一点。假设我们有一个简单的报告生成器,它的作用是接收一个对象列表并生成一个字符串报告。我们的目标是扩展这段代码以支持额外的记录类型。

战术方法:不系缰绳

让我们从一个简单、直接的请求开始,重点关注眼前的需求。例如: “我需要你为 ‘退款’ 添加一个新的记录类型。一个记录看起来像 {‘type’: ‘refund’, ‘originalsaleid’ : ‘S12345’, ‘amount’: 99.99}”。

AI编码助手 可能会生成如下代码:

def generate_report(data):
    # ....
    elif record_type == "refund":
        # Process a refund record
        original_sale_id = record.get("original_sale_id", "N/A")
        amount = record.get("amount", 0)
        report_lines.append(f"[REFUND] Original Sale ID: {original_sale_id}, Amount: ${amount:.2f}")
        total_refunds += amount
    # .....
    # Add a summary footer
    report_lines.append("========================")
    report_lines.append(f"Total Sales: ${total_sales:.2f}")
    report_lines.append(f"Total Expenses: ${total_expenses:.2f}")
    report_lines.append(f"Total Refunds: ${total_refunds:.2f}")
    net_income = total_sales - total_expenses - total_refunds
    report_lines.append(f"Net Income: ${net_income:.2f}")
    return "\n".join(report_lines)

从表面上看,这似乎是一个完全可行的解决方案。但它真的可以维护吗?如果我们还有 10 个这样的请求呢?这是一个典型的战术实现。它快速地解决了问题,但没有考虑到长期的可维护性和可扩展性。这种方式容易导致代码变得越来越复杂,难以理解和修改。

战略方法:系上缰绳

让我们再试一次。除了列出我们的初始需求外,我们现在还对 AI 如何实现该功能施加了一些约束。我们提出了一种抽象,着眼于未来的可维护性。例如: “分析 @tacticalexample.py 我想使用策略模式或命令模式来重构它,使其具有可扩展性。定义一个 Record 接口,其中包含一个 process(reportcontext) 方法。reportcontext 将是一个简单的类,用于保存运行总计(销售额、支出、退款等)。实现 SaleRecord、ExpenseRecord 和一个新的 RefundRecord,它们都符合这个接口。然后,主 generatereport 函数应该简单地迭代记录并调用每个记录的 process 方法,而不需要知道具体的类型。”

AI编码助手 可能会生成如下代码:

from abc import ABC, abstractmethod

class ReportContext:
    def __init__(self):
        self.total_sales = 0
        self.total_expenses = 0
        self.total_refunds = 0
        self.report_lines = []

class Record(ABC):
    @abstractmethod
    def process(self, context: ReportContext):
        """Process the record and update the report context."""
        pass

....

class RefundRecord(Record):
    """Represents a refund."""
    def __init__(self, reason: str, amount: float):
        self.reason = reason
        self.amount = amount

    def process(self, context: ReportContext):
        context.total_refunds += self.amount
        # For now, we just record the refund. Financial impact could be handled here.
        context.report_lines.append(f"[REFUND] Reason: {self.reason}, Amount: ${self.amount:.2f}")

def generate_report(records: list[Record]):
    report_title = "Sales and Expense Report"
    context = ReportContext()
    for record in records:
        record.process(context)

    # Generate the final report string
    header = [report_title, "=" * len(report_title)]
    footer = [
        "=" * len(report_title),
        f"Total Sales: ${context.total_sales:.2f}",
        f"Total Expenses: ${context.total_expenses:.2f}",
        f"Total Refunds: ${context.total_refunds:.2f}",
        f"Net Income: ${context.total_sales - context.total_expenses - context.total_refunds:.2f}"
    ]
    return "\n".join(header + context.report_lines + footer)

# Example Usage:
if __name__ == "__main__":
    data = [
        {"type": "sale", "product": "Laptop", "amount": 1200.00},
        {"type": "expense", "category": "Office Supplies", "amount": 75.50},
        {"type": "sale", "product": "Mouse", "amount": 25.00},
        {"type": "refund", "reason": "Defective Item", "amount": 1200.00},
        {"type": "expense", "category": "Software", "amount": 250.00},
    ]

    # Mapping from type string to Record class
    record_mapping = {
        "sale": SaleRecord,
        "expense": ExpenseRecord,
        "refund": RefundRecord
    }

    # Create a list of Record objects from the raw data
    financial_records = []
    for item in data:
        record_type = item.pop("type")
        if record_class := record_mapping.get(record_type):
            financial_records.append(record_class(**item))

    # Generate and print the report
    report = generate_report(financial_records)
    print(report)

现在,无论你是否同意这是一个更好的实现,这都不是重点。我想说明的是,AI编码助手 如果没有缰绳,就会简单地继续添加到现有的结构中,而不会退一步思考复杂性。这条阻力最小的路径并不总是正确的路径,这就是为什么我们应该谨慎,并给出严格约束的指令。通过策略模式,我们可以更容易地添加新的记录类型,而不需要修改核心的报告生成逻辑。这种方式提高了代码的可扩展性和可维护性。

如何更好地与AI编码助手协作

或许通过一些巧妙的提示工程,我们可以得到一个更清晰的解决方案:但在我的经验中,没有一个理想的提示能够产生一致的、战略性的结果。

与其使用模糊的、高级的命令(例如 “构建用户个人资料页面”),不如使用精确的、战术性的提示来缩短缰绳:

  • “为遵循 api-types.ts 中定义的合同的新端点搭建组件结构,但将核心业务逻辑的实现留空。”
  • “重构此函数使其成为纯函数。它应该将其依赖项作为参数,而不是从外部范围访问它们。”
  • “基于这些业务规则,为 calculateDiscount 函数生成三个边缘案例单元测试。”

通过将 AI 用作一个专注的、战术性的工具,我们可以充分利用它的力量,而不会放弃我们最重要的角色:战略程序员,确保代码库长期保持整洁、连贯和可维护。 AI编码助手 可以铺设砖块,但我们必须始终是建筑师。

总结:人机协作,共筑软件未来

大模型 驱动的软件开发时代,AI编码助手 正在改变我们的工作方式。它们可以极大地提高我们的效率,但同时也带来了一些挑战。我们需要认识到 AI编码助手 的局限性,并发挥我们的战略思考能力,才能构建出高质量的软件系统。通过将 AI编码助手 作为战术工具,并坚守我们作为战略架构师的角色,我们可以充分利用 大模型 的力量,共筑软件的未来。关键在于,我们要始终保持对代码库的控制权,并确保代码库的长期健康。未来的软件开发将是人与 AI 协作的时代,只有我们能够充分理解并利用 AI 的优势,才能在这个时代取得成功。