首页 / 文章 / 如何用AI现代化遗留应用而避免变成重写
← 返回
IT技术

如何用AI现代化遗留应用而避免变成重写

✍️ zhirenhun 📅 2026/8/18 👁 126 阅读 ⏱ 39 分钟
如何用AI现代化遗留应用而避免变成重写

我曾见过一些遗留系统迁移被认为很成功,仅仅因为旧框架从代码仓库中消失了。

六个月后,团队仍在处理同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署问题。

技术虽然变了,但整个系统并没有发生太大变化。

AI 让这个问题变得更加有趣。

它可以比人工团队更快地完成代码翻译。它可以解释陌生的类、生成测试、创建适配器、更新 API,并消除大量重复性工作。

但如果你将一个 AI 编码工具指向一个旧应用程序,只是简单地要求它把所有内容迁移到现代技术栈,那么你很有可能得到的正是你所要求的:同一个系统,只是重写得更快了。

这并不一定是现代化。

在本教程中,我想向你展示在遗留系统迁移中使用 AI 的另一种方式。

不要把 AI 当作自动化的代码翻译器,而是要用它来帮助你:

示例使用 TypeScript,但这个过程本身并不依赖于 TypeScript 或 Node.js。

重要的部分是工作流程。AI 可以让迁移工作更快,但工程团队仍然需要决定什么值得迁移。

先决条件

你应该熟悉以下内容:

示例使用 Vitest,但如果你使用 Jest 或其他测试框架,这些思路也同样适用。

目录

如何避免一对一的遗留系统迁移

想象一下,你在一个旧订单处理系统中发现了这个函数:

async function processOrder(order: Order) {
  if (!order.customer.active) {
    throw new Error("Inactive customer");
  }

  const discount =
    order.customer.type === "PREMIUM"
      ? order.total * 0.1
      : 0;

  const finalAmount = order.total - discount;

  await db.orders.insert({
    customerId: order.customer.id,
    amount: finalAmount,
  });

  await paymentGateway.charge(
    order.customer.card,
    finalAmount,
  );

  await mailer.send(
    order.customer.email,
    "Order processed",
  );

  return finalAmount;
}

这个函数可以工作,但它也做了很多事情。

它验证客户、应用定价规则、持久化数据、扣款支付方式并发送通知。

一对一的迁移可能会将其变成一个更漂亮的TypeScript服务,使用更新的库,但同时保留所有这些职责。

你可能会用新的控制器替换旧的控制器,用新的服务替换旧的服务,用新的ORM替换旧的ORM……但仍然保留相同的架构问题。

这是AI可能对你不利的最早环节之一。

如果你的提示词是:

Convert this legacy class to TypeScript.

模型通常会保留结构,因为保留结构正是你交给它的任务。

在要求AI转换代码之前,先区分两个问题:

  1. 哪些行为必须保留?

  2. 哪些设计应该保留?

这两个问题并不相同。

有时实现是旧的,但其行为仍然至关重要。有时行为很重要,但实现应该消失。有时你会发现两者都不需要保留。

这种区分应该在批量迁移开始之前完成。

如何在修改旧代码库之前对其进行梳理

遗留系统迁移的第一个难点通常是弄清楚你实际拥有什么。

文档如果存在的话会有所帮助。但在许多系统中,真正的文档分布在以下各处:

在这个领域,AI可以节省时间,而无需被要求做出架构决策。

以之前的processOrder函数为例。与其要求AI重写它,不如从这样的问题开始:

Identify the business rules in this function.

List every side effect.

Which external systems does it depend on?

Which parts could be expressed as pure functions?

Which observable behaviors should probably be protected
with tests before this function is changed?

Do not rewrite the function.

最后一条指令的重要性可能超乎想象。

当分析和转换发生在同一个请求中时,AI工具很容易解决你尚未完全理解的设计问题。

我倾向于先进行明确的分析。

对于较大的代码库,应在多个层面上重复这一过程。

在仓库层面,查找:

在模块级别,查找:

在函数级别,查找:

AI可以大大加快这种探索。但其发现应与实际仓库、测试、模式、日志和生产行为进行核对。

对代码的自信解释仍然只是解释。仓库仍然是事实的最终来源。

如何在重构前构建特征测试

遗留软件令人不安的一点是,奇怪的行为不一定是偶然的。

你可能会发现一段看似明显错误的代码,后来又发现业务的另一部分依赖于它。

这就是特征测试的用武之地。

Michael Feathers在《修改代码的艺术》中讨论了这种方法:不是从描述系统应有的行为开始,而是先捕获它当前的行为。

考虑这个函数:

export function calculateDiscount(
  customerType: string,
  total: number,
): number {
  if (customerType === "PREMIUM") {
    return total * 0.1;
  }

  return 0;
}

你可以通过测试来保护其当前行为:

import { describe, expect, it } from "vitest";
import { calculateDiscount } from "./calculateDiscount";

describe("calculateDiscount", () => {
  it("applies a 10 percent discount to premium customers", () => {
    expect(
      calculateDiscount("PREMIUM", 100),
    ).toBe(10);
  });

  it("does not discount regular customers", () => {
    expect(
      calculateDiscount("REGULAR", 100),
    ).toBe(0);
  });

  it("returns zero when the order total is zero", () => {
    expect(
      calculateDiscount("PREMIUM", 0),
    ).toBe(0);
  });
});

AI有助于扩展这一安全网。

例如:

Generate characterization tests for this function.

Preserve the existing behavior.

Include:
- normal inputs,
- boundary values,
- invalid inputs,
- exceptions,
- observable side effects.

Do not redesign the function.

然后检查它生成的内容。

你不是在证明旧行为是正确的。你是在记录如果重构它会改变什么。

这个区别很重要。

如果一个测试捕获了你后来认为是 bug 的行为,那就故意去改变它。你要避免的是在部署之后意外地改变行为并发现这种差异。

如何找到安全的迁移接缝

遗留应用程序很少需要一次性全部替换。

它们通常需要新旧系统可以临时共存的地方。

Feathers 在《有效处理遗留代码》中也描述了接缝的概念:一个你可以改变行为而无须修改其周围所有东西的地方。

订单处理示例提供了一个可能的接缝。

原始函数包含:

前两个自然属于业务行为,其他涉及基础设施。这暗示了一个可能的边界。

领域/应用职责:

基础设施职责:

AI 可以帮助识别这些边界的候选者。

例如:

Analyze these files and identify:

- business rules,
- infrastructure concerns,
- side effects,
- shared mutable state,
- duplicated logic,
- dependencies that make isolated testing difficult.

Suggest possible boundaries.

Do not rewrite the code yet.

同样,AI 输出是工程决策的输入。它不应该自动成为决策。

当你找到一个好的接缝处,你就获得了一个无需重写整个应用程序即可推进现代化的位置。

如何重构以明确职责

一旦你理解了代码的某个部分,并围绕其当前行为编写了测试,重构就会变得不那么危险。

定价规则可以变成一个纯函数:

export function calculateDiscount(
  customerType: string,
  total: number,
): number {
  if (customerType === "PREMIUM") {
    return total * 0.1;
  }

  return 0;
}

客户验证可以分离:

export function validateCustomer(
  customer: Customer,
): void {
  if (!customer.active) {
    throw new Error("Inactive customer");
  }
}

基础设施可以移到合约之后:

export interface OrderRepository {
  save(order: PersistedOrder): Promise;
}

export interface PaymentGateway {
  charge(
    card: string,
    amount: number,
  ): Promise;
}

export interface NotificationService {
  sendOrderConfirmation(
    email: string,
  ): Promise;
}

应用程序的工作流程变得更易于阅读:

export class ProcessOrder {
  constructor(
    private readonly orders: OrderRepository,
    private readonly payments: PaymentGateway,
    private readonly notifications: NotificationService,
  ) {}

  async execute(order: Order): Promise {
    validateCustomer(order.customer);

    const discount = calculateDiscount(
      order.customer.type,
      order.total,
    );

    const finalAmount =
      order.total - discount;

    await this.orders.save({
      customerId: order.customer.id,
      amount: finalAmount,
    });

    await this.payments.charge(
      order.customer.card,
      finalAmount,
    );

    await this.notifications.sendOrderConfirmation(
      order.customer.email,
    );

    return finalAmount;
  }
}

这次重构中没有什么特别革命性的东西。这正是重点的一部分。

现代化并不需要奇特的架构。

通常,重要的改进仅仅是让职责足够明确,使得下一次改动不需要理解整个应用程序。

如何将AI用于机械性转换

一旦边界清晰,AI在实现方面就会变得更加有用。

大量的迁移工作是必要的但重复的,数量之多令人惊讶:

这些都是使用AI的好地方。

假设旧系统直接执行SQL:

async function getCustomer(id: number) {
  const result = await db.query(
    `SELECT * FROM customer WHERE id = ${id}`,
  );

  return result[0];
}

在生成新的实现之前,先定义你想要的契约:

export interface CustomerRepository {
  findById(id: number): Promise;
}

然后约束该变换:

Implement CustomerRepository using the new database client.

Constraints:

- Keep the CustomerRepository interface unchanged.
- Use parameterized queries.
- Do not move business rules into the repository.
- Preserve the existing null behavior.
- Preserve the existing error semantics.
- Return only the implementation.

这是一个非常不同的请求:

Modernize this database code.

在第一种情况下,你做出了架构决策,并让AI在该边界内实施。

这正是我认为AI在迁移工作中最有用的地方。在重要决策已经做出之后,它消除了机械性的工作。

如何以小垂直切片进行迁移

当数千个文件同时变化时,大型迁移变得难以推理。

更安全的变更单元通常是业务能力。

不要迁移所有控制器,然后所有服务,最后所有存储库,而是迁移一个完整的能力。

例如:

创建订单

然后移动到下一个能力。

这有几个优点。首先,迁移更接近可部署的软件。其次,你提供给AI工具的上下文保持更小。

测试也变得更加聚焦。如果出现问题,更容易隔离故障。

对于垂直切片,一个有用的第一个提示是仅分析:

We are migrating the Create Order capability.

The legacy implementation is under /legacy/orders.

The target architecture separates:
- domain,
- application,
- infrastructure.

The characterization tests under /tests/legacy
describe behavior that must remain compatible.

Analyze the current implementation.

List:
1. business rules,
2. external dependencies,
3. side effects,
4. likely migration risks,
5. files that need to change.

Do not generate code yet.

审查该输出,然后规划实际的转换。

这也与马丁·福勒的扼杀者无花果方法等增量替换策略兼容,即新功能逐步取代旧系统,而不是要求一次性大规模切换。

关键词语是逐步

AI可以加快转换速度。但这并不能降低“大爆炸”式迁移的风险。

如何比较遗留行为与现代行为

单元测试提供了一种安全保障。

对于迁移,我还喜欢直接比较新旧实现。

假设两个系统都能处理相同的订单,你可以通过每个系统运行相同的测试夹具:

const inputs = [
  premiumCustomerOrder,
  standardCustomerOrder,
  inactiveCustomerOrder,
];

for (const input of inputs) {
  const legacyResult =
    await legacyProcessor(input);

  const modernResult =
    await modernProcessor(input);

  expect(modernResult).toEqual(legacyResult);
}

这是一种简单的差分测试形式。你可以在HTTP边界上做同样的事情。

向两个版本发送相同的POST /orders请求并比较:

重要的一点是:差异并不自动意味着缺陷。有时行为本就应该改变。有价值的是让差异变得可见,以便有人必须刻意对其进行分类。

AI在这里也能提供帮助。

如果你有数百个不匹配项,你可以请它进行分组:

Analyze these behavioral mismatches.

Group them by likely cause.

Pay particular attention to:
- rounding,
- null handling,
- timezone conversion,
- validation,
- serialization,
- data mapping.

Do not label a mismatch as a defect unless the
available evidence supports that conclusion.

这是对人工智能的一种良好应用,因为该模型正在减少调查工作。它并不是在判断生产行为是否可接受。

如何使用影子流量发现回归问题

最终,测试夹具将不再具有足够的代表性。

生产系统接收到的输入组合,是没有人想到要放入测试套件的。

观察这些差异的一种方法是影子流量。旧应用继续处理用户的请求,同时该请求的副本也会发送到新实现。新结果仅用于比较,不会返回给用户。

例如:

Legacy:
200
{ "total": 90 }

Modern:
200
{ "total": 90 }

MATCH

或:

Legacy:
200
{ "total": 90 }

Modern:
200
{ "total": 100 }

MISMATCH

收集这些不匹配项可以为你提供证据,证明新系统在真实流量下的行为表现,而无需立即将其暴露给用户。

这项技术伴随着运维方面的考量。你需要仔细思考:

影子实例通常应避免执行不可逆的副作用操作。

例如,用记录型适配器替换真实的支付适配器:

export class RecordingPaymentGateway
  implements PaymentGateway {

  public readonly calls: Array<{
    card: string;
    amount: number;
  }> = [];

  async charge(
    card: string,
    amount: number,
  ): Promise {
    this.calls.push({
      card,
      amount,
    });
  }
}

现在,您可以在不向客户重复收费的情况下,比较计费意图与收费行为。

如何测试你真正想要的架构

如果迁移的目标之一是改进架构,那么仅具备行为兼容性是不够的。

假设你已经决定了这样一个约束:

领域代码不得依赖基础设施代码。

如果这条规则只存在于架构图中,迁移压力最终会破坏它。

所以要测试它。

对于简单的项目,你可以检查导入关系。对于较大的项目,使用能够强制实施架构规则的依赖分析工具。

具体的工具并不重要,重要的是原则:

如果一项架构约束很重要,就让违反它变得可见。

你可能需要这样的规则:

为什么这在AI辅助迁移中很重要?因为AI非常擅长找到让代码编译通过的方法。

如果直接深入另一个模块能解决眼前的问题,生成的代码可能就会这么做,除非边界是约束的一部分。

架构测试为人类和AI工具提供了一个更难被意外违反的边界。

如何决定哪些任务应由AI处理

我不会同等对待所有迁移任务。有些任务适合自动化。

AI通常有用的任务

我希望进行重大工程审查的任务

我倾向于保留在人类所有权下的决策

这并不是因为AI无法生成架构提案。它可以。

问题在于责任和上下文。

架构选择是约束、历史、组织能力、业务优先级和运营风险的后果,而这些可能不存在于代码库中的任何地方。

模型可以帮助你探索这些选择,但仍然需要有人来为其负责。

如何衡量迁移是否真正改进了系统

迁移速度是一个吸引人的指标,因为它易于展示。

例如:

37%的代码库已完成迁移。

这并不能告诉你系统是否变得更好。

现代化工作应该关注多种结果。运营指标可能包括:

工程指标可能包括:

迁移特定指标可能包括:

具体指标取决于系统。重要的是避免这种对成功的定义:

旧代码库更小 = 现代化成功。

AI使得在更短的时间内转换更多代码成为可能。这使得衡量转换质量变得更加重要,而不是更不重要。

在AI辅助迁移中我最担心的风险

幻觉代码是一个明显的风险。但我更担心的是貌似合理的代码

生成的代码可以编译通过。它看起来可能比原始实现更整洁。它甚至可以通过一个浅层的测试套件。但它仍然可能微妙地改变一个没人意识到存在的业务规则。

考虑一个像这样的小例子:

if (customer.balance > 0) {
  charge(customer);
}

当你不理解某个条件存在的原因时,很容易想要清理代码。

但也许零具有特殊的业务含义。

也许负余额是合法的。

也许这个条件是在六年前的一次生产事故后引入的,而且从未留下文档。

AI无法从可获取的信息中恢复不存在的上下文。这就是为什么我非常强调特征化测试和行为对比。

转换速度越快,验证过程就需要越强大。

否则,你只是在提高引入未知变更的速度。

实用迁移工作流程

如果必须将整个过程简化为一个可重复的序列,我会使用这个。

1. 理解

映射:

使用AI加速调查。不要一开始就生成新系统。

2. 保护

构建:

让当前行为可观察。

3. 设计

选择:

在大规模转换之前完成这些。

4. 重构

创建足够的分离,使系统的一部分可以移动,而不会拖着其他所有东西一起动。

5. 转换

大量使用AI处理重复性的实现工作。

给它明确的架构约束。

6. 比较

对相同的输入运行旧行为和新行为,并调查差异。

7. 逐步发布

使用适合你的环境的机制:

8. 移除旧路径

不要让两个系统无限期地同时运行。一项从未移除遗留路径的迁移最终会创造另一个遗留架构。

结论

AI改变了遗留系统现代化的经济性。

许多曾经耗费工程时长的现在可以更快地完成:阅读不熟悉的代码、生成测试、更新API、翻译重复的实现,以及调查系统之间的差异。

这很有用。但这并不是现代化中最需要判断力的部分。

困难的问题依然存在:

如果你只使用AI来翻译代码,你可以更快地迁移技术债务。

如果你将其与特征化测试、增量重构、明确的架构边界、差分测试和控制式发布结合起来,你就有更大的机会在迁移系统的同时改进它。

目标不是将同一个系统迁移到更新的技术栈上。而是理解它、保护其重要行为、重构它、增量迁移、验证结果,并最终得到一个比你开始时更简单的系统。

AI可以缩短这条路径。但它仍然无法决定目的地应该是什么。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

← 上一篇
连续批处理可改善P50,但可能毁掉P99:实测权衡
下一篇 →
如何利用智能体上下文提高Playwright测试覆盖率

📌 相关推荐

GraphRAG 是推理问题,而非数据库问题
2026/8/30
构建市场时光机:使用 Python 和 WebSocket 重放交易会话
2026/8/30
如何自行基准测试LLM推理:值得信赖的数字设计标准
2026/8/30
← 返回文章列表