许多工程师在拿到遗留代码库时,首先想做的就是改动它。我完全理解这种冲动。
你打开一个 1500 行的类,里面数据库调用与业务规则交织,配置值散落在仓库各处,没人愿意触碰的方法,还有指向多年前已消失系统的注释。
随后,AI 编程助手主动提出要解释整个类。
于是你提出:
重构这个类。
但这通常为时过早。
我在与遗留系统打交道中得到的一个教训是,代码可能很丑陋,却仍然蕴含重要知识。
一个奇怪的条件可能编码了业务异常;一个重复的计算可能存在,因为两个看似相同的流程实际上并不相同;命名糟糕的数据库列可能仍然是外部契约的一部分。
而且,一个无人理解的方法可能是防止八年前发生的生产事故再次发生的唯一保障。
AI 大大降低了阅读陌生软件的难度,这很有价值。但它也让人在尚未理解之前就更容易去修改代码。
在本教程中,我将展示如何在重构或迁移之前使用 AI 进行我认为应该先进行的工作:代码库考古。
你将学习如何使用 AI 来帮助你:
绘制仓库结构,
识别入口点,
追踪依赖关系,
将业务规则与基础设施分离,
发现隐藏的副作用,
检查数据流,
发现隐式契约,
检测重复行为,
构建依赖图,
识别不确定区域,
并将这些发现转化为现代化计划。
示例使用 TypeScript,但该流程适用于大多数语言和技术栈。
目标不是让 AI 告诉你代码的含义然后盲目相信。目标是利用 AI 减少你寻找正确问题所花费的时间。
你应该熟悉:
阅读现有代码库
TypeScript 或类似的面向对象语言
基本的软件架构
依赖注入
单元和集成测试
使用能够检查仓库文件的 AI 编程助手
你不需要特定的 AI 提供商,因为工作流比模型更重要。
遗留代码常常制造出虚假的紧迫感。
你看到明显耦合或重复的代码,立刻想要清理它。
考虑这个函数:
async function approveOrder(order: Order) {
if (order.total > 10000 && !order.customer.verified) {
throw new Error("Manual verification required");
}
if (
order.customer.country === "AR" &&
order.paymentMethod === "TRANSFER"
) {
order.status = "PENDING";
} else {
order.status = "APPROVED";
}
await orders.save(order);
if (order.status === "APPROVED") {
await billing.createInvoice(order);
}
await audit.log({
action: "ORDER_APPROVAL",
orderId: order.id,
status: order.status,
});
return order;
}
乍一看,这里有几个明显的重构机会:
您可以提取验证逻辑。
您可以将状态计算隔离出来。
您可以将计费放在接口之后。
您可以创建审批策略。
这些想法可能都合理,但首先您需要回答以下问题:
为什么 10000 很重要?
为什么阿根廷银行转账会一直处于待处理状态?
发票的创建必须在持久化之后发生吗?
ORDER_APPROVAL 是否被其他系统消费?
订单是否可以在其他地方从 PENDING 转为 APPROVED?
是否有任何东西依赖于确切的异常消息?
仅凭语法无法回答这些问题。
理解从此开始。
与其向您的 AI 工具提问:
Refactor this function using clean architecture.
从以下内容开始:
Analyze this function without changing it.
Identify:
1. explicit business rules,
2. likely business rules that need confirmation,
3. side effects,
4. external dependencies,
5. state transitions,
6. magic values,
7. assumptions that cannot be proven from this file alone.
Do not propose a refactor yet.
那最后一句很重要:暂时不要提出重构方案。
你希望模型保持在调查状态,而不是直接跳到解决方案。
面对陌生的遗留系统,我不会先逐文件阅读,而是先试图了解应用的整体形态。
仓库中已经藏有架构线索。
可以查看诸如下面这些目录:
src/
controllers/
services/
repositories/
models/
jobs/
workers/
scripts/
migrations/
config/
integrations/
tests/
但不要假设目录名称就是真实架构。
services 目录可包含业务逻辑、基础设施、编排以及随机工具函数。
models 目录可能包含数据库实体,而非领域模型。
utils 文件夹可能隐藏应用一半业务逻辑。
将结构作为证据,而非真理。
有用的首个 AI 请求:
Inspect the repository structure.
Do not analyze individual implementation details yet.
Identify:
- application entry points,
- major modules,
- database technologies,
- external integrations,
- background processing,
- scheduled tasks,
- authentication mechanisms,
- configuration sources,
- tests,
- likely architectural boundaries.
For each conclusion, reference the files or directories
that support it.
Mark anything uncertain explicitly.
引用文件的需求很重要。没有它,AI 可能会给出一个看似合理但实际上并不存在的架构。
你希望得到更接近的:
HTTP API
Evidence:
- src/server.ts
- src/routes/orders.ts
- src/routes/customers.ts
Background processing
Evidence:
- src/workers/paymentWorker.ts
- src/queues/index.ts
Scheduled jobs
Evidence:
- src/jobs/reconcileInvoices.ts
- src/cron.ts
现在您拥有可以验证的地图。
Web 应用程序通常有一个明显的 HTTP 入口点。但遗留系统往往还有更多。
业务操作可能从以下几个方面开始:
一个 API 请求,
一个定时任务,
一个队列消费者,
一个数据库触发器,
一个 CLI 脚本,
一个文件导入,
一个邮件处理器,
一个 webhook,
或者另一个应用直接调用数据库。
如果您仅分析控制器,可能会遗漏系统的一半。
假设您搜索订单创建,会发现:
POST /orders
我们很容易假设所有订单都通过该端点进入。
然后你会发现:
jobs/importMarketplaceOrders.ts
workers/retryFailedOrders.ts
scripts/migratePendingOrders.ts
integrations/shopify/webhook.ts
现在,同一个业务对象有四条额外的入口路径。
这会改变你对重构的思考方式。
向 AI 提问:
Find every location that can create, modify,
approve, cancel, or persist an Order.
Include:
- HTTP endpoints,
- background workers,
- scheduled jobs,
- scripts,
- imports,
- webhooks,
- direct repository calls.
Group the results by operation.
For every result, include the file path and
the relevant function or class.
然后通过仓库搜索验证这些结果。
例如:
rg "orders\.save|orders\.insert|createOrder|approveOrder" src
AI 应该提升搜索速度,而不是取代它。
仅仅了解单个文件是不够的。
通常重要的是理解 业务能力。
例如:
创建一个订单。
该能力可能会经过多个层级:
HTTP Request
↓
Controller
↓
Application Service
↓
Pricing
↓
Inventory
↓
Persistence
↓
Payment
↓
Notification
代码可能组织得不够整洁,因而追踪该能力就显得尤为有用。
选择一个实际工作流程,然后问:
Trace the "Create Order" capability from its entry point
until all observable side effects are complete.
For each step, show:
- file,
- function or class,
- input,
- output,
- state change,
- external call,
- error behavior.
Do not summarize multiple steps into one.
你想要的是一串可以检查的序列。
例如:
1. POST /orders
src/routes/orders.ts
2. OrdersController.create()
src/controllers/OrdersController.ts
3. OrderService.create()
src/services/OrderService.ts
4. calculatePrice()
src/services/pricing.ts
5. inventory.reserve()
src/integrations/inventory.ts
6. ordersRepository.save()
src/repositories/orders.ts
7. paymentQueue.publish()
src/queues/payment.ts
这比对架构的泛泛解释更有用。
现在你可以提出诸如以下问题:
事务实际上从哪里开始?
如果支付发布失败会发生什么?
库存预订是否可逆?
订单能否被保存两次?
哪些步骤是同步的?
哪些故障会被重试?
这些是现代化的问题。
在代码库考古过程中,你能做的最有用的事情之一是确定业务行为所在的位置。
遗留应用程序经常将其与基础设施混合在一起。
考虑:
async function saveCustomer(customer: Customer) {
if (
customer.type === "ENTERPRISE" &&
customer.creditLimit < 50000
) {
throw new Error("Invalid enterprise credit limit");
}
const connection = await mysql.getConnection();
await connection.query(
"INSERT INTO customers (...) VALUES (...)",
[...]
);
await redis.del(`customer:${customer.id}`);
await eventBus.publish(
"customer.updated",
customer
);
}
至少有一条业务规则:
Enterprise customers must have a credit limit >= 50000.
另外还有几个基础设施方面的问题:
MySQL
Redis
Event bus
请AI对代码进行分类:
Classify each responsibility in this function as one of:
- business rule,
- application orchestration,
- persistence,
- caching,
- messaging,
- logging,
- validation,
- unknown.
Explain why.
Do not move or rewrite any code.
unknown 类别很有用。你不希望模型把每一行都强行塞进一个整洁的架构理论里。
有些代码在你看到更多上下文之前确实是模糊的。
副作用是迁移风险的主要来源之一。
一个函数被称为:
updateCustomer()
它可能远不止于更新客户。
它可能会:
写入数据库
使缓存失效
发出事件
发送电子邮件
更新分析数据
写入审计记录
调度另一个作业
如果你重构函数且只保留其返回值,就可能在没有任何编译错误的情况下破坏生产行为。
一个有用的调查提示如下:
List every observable side effect produced directly
or indirectly by this function.
For each one, identify:
- the side effect,
- where it happens,
- whether it is synchronous or asynchronous,
- whether failure propagates,
- whether it appears retryable,
- whether it is idempotent,
- whether it can be safely repeated.
Mark uncertain answers as unknown.
最后那个属性,幂等性,非常重要。
假设一个工作进程执行以下操作:
await chargeCard(order);
await markOrderAsPaid(order);
如果工作进程在这两行之间崩溃并重试,会发生什么?可能会对客户收取两次费用。而这一点从函数名中看不出来。
理解重试语义是理解代码库的一部分。
并非所有契约都通过接口声明。遗留应用中包含许多隐式契约。
例如:
return {
status: "ok",
value: customer.balance.toFixed(2),
};
某些外部调用方可能依赖于:
{
"status": "ok",
"value": "100.00"
}
将 value 从字符串改为数字,看起来可能像是一种改进:
{
"status": "ok",
"value": 100
}
它也可能导致客户端崩溃。
在以下位置查找契约:
API 响应,
事件,
数据库结构,
CSV 导出,
文件名,
环境变量,
错误消息,
队列有效负载,
以及 webhook 正文。
询问:
Identify outputs from this module that could be consumed
outside the module.
Include:
- HTTP responses,
- emitted events,
- queue messages,
- files,
- database records,
- exceptions,
- logs used for automated processing.
For each output, explain what evidence suggests that it
may be an external or implicit contract.
措辞很重要:
什么证据表明
不是:
告诉我存在哪些合同
因为你可能无法从当前存储库证明消费者的存在。
重复的代码易于检测。重复的 业务含义 更难。
您可能会发现:
if (customer.type === "PREMIUM") {
discount = total * 0.1;
}
在一个模块中。
而在其他地方:
if (account.plan === "GOLD") {
price = price * 0.9;
}
它们可能代表相同的业务规则,也可能不是。
AI 对识别候选者很有用。
问:
Search the repository for business rules related to
customer discounts.
Group implementations that appear semantically related,
even if variable names differ.
For each group:
- list file locations,
- describe the apparent rule,
- highlight differences,
- do not assume the rules should be unified.
这条最后的指令很重要。
重复有时是偶然的。
有时它代表两个独立演化的领域。
不要让 AI 助手把它变成:
similar
进入:
must be merged
没有证据。
在某个时刻,你需要了解系统中哪些部分依赖于哪些其他部分。
你不需要一个完美的企业架构图。一个轻量级依赖图就足以开始。
例如:
Orders
├── Customers
├── Inventory
├── Payments
├── Notifications
└── Database
Payments
├── Payment Provider
├── Audit
└── Database
让AI提取模块级依赖关系:
Build a module dependency map from the repository.
Only include dependencies supported by imports,
constructor dependencies, explicit calls, or configuration.
Output:
Module A -> Module B
For each dependency, provide at least one source file
that demonstrates it.
Do not infer dependencies from names alone.
您可以随后将结果与自动化工具进行比较。
对于 JavaScript 或 TypeScript 项目,依赖分析工具可以帮助您发现:
循环依赖
跨模块导入
高入度
高出度
AI 有助于解释这些依赖为何重要;静态分析则更擅长证明它们的存在。
两者结合使用。
这是整个过程中的重要环节之一。
有用的系统图不仅包含答案,还包含不确定性。
我喜欢保持一个类似以下的明确列表:
## Open Questions
- Why is the enterprise credit threshold 50,000?
- Is `ORDER_APPROVAL` consumed outside this repository?
- Can marketplace orders bypass inventory validation?
- Is `customer.balance` allowed to be negative?
- What process transitions PENDING orders to APPROVED?
- Is `legacy_customer_id` still used by another system?
你可以让AI生成这个列表:
Based on everything analyzed so far, list the questions
that can't be answered safely from the repository.
Focus on questions that would matter during:
- refactoring,
- migration,
- schema changes,
- interface changes,
- removal of code.
Do not answer the questions.
我喜欢这个提示,因为它与我们通常要求 AI 做的事情相反。它要求模型找出它不应该假装知道的地方。
现代化计划应包含这些未知因素。
即使 AI 生成的解释不完整,也可能听起来很有说服力。因此,每个重要的发现都应有另一来源的证据作支撑。
我使用一个简单的层级结构。
如果 AI 说某个函数只被调用一次,则去搜索它。
rg "approveOrder" .
测试常能暴露实现代码未说明的假设。
请查找:
expected errors
special values
boundary cases
fixture data
historical behavior
模式可能会揭示诸如以下关键信息:
可为空字段
外键
默认值
遗留列
约束
状态值
生产遥测可以告诉您所谓的未使用路径是否仍然活跃。
Git 历史有时可以回答源代码无法回答的问题。
例如:
git log -S "Manual verification required" --all
或者:
git blame src/orders/approveOrder.ts
引入奇怪条件的提交中可能包含解释。
这是AI可以帮助总结历史的领域。
Review the commits that changed this function.
Build a timeline of behavior changes.
For each change, include:
- commit,
- date,
- behavior changed,
- stated reason if available.
Do not infer a reason if the commit history does not provide one.
这可以节省惊人的时间。
一旦你理解了一项功能,就可以开始做出决策。但在此之前则不行。
假设你的调查得出了以下结果:
Create Order
Business rules:
- active customer required
- premium customers receive 10% discount
- inventory must be available
Side effects:
- order persisted
- inventory reserved
- payment queued
- confirmation email sent
External contracts:
- POST /orders response
- payment queue payload
- order.created event
Unknowns:
- retry semantics for inventory reservation
- whether event consumers require exact field names
现在,您可以决定要保护什么了。
例如:
Protect first:
- pricing behavior
- API response
- payment payload
- event schema
然后决定哪些部分可以重构。
Candidate boundaries:
- pricing policy
- inventory gateway
- payment publisher
- notification service
然后确定需要调查的内容。
Block migration until understood:
- inventory retry behavior
- event consumers
这已经是一个迁移计划。
注意 AI 没有做的事情:它没有决定目标架构。
它帮助使当前架构变得足够可观察,以便你能够做出那个决定。
如果我必须把这个过程简化为可重复的步骤,我会使用以下步骤。
识别:
入口点
模块
持久化
集成
工作进程
作业
测试
配置
不要重构任何内容。
选择一些具体的东西:
Create Order
Approve Loan
Generate Invoice
Register Customer
Cancel Subscription
不要试图一次性理解整个产品。
跟随:
input
↓
business logic
↓
state changes
↓
external calls
↓
output
记录所有涉及的文件。
区分:
显式规则
可能的规则
基础设施行为
未知项
查找:
写入
消息
电子邮件
作业
缓存更改
外部调用
寻找:
API
事件模式
数据库假设
导出文件
错误行为
记录:
module -> module
并识别耦合。
不要掩盖不确定性。创建一个明确的列表。
使用:
代码库搜索
测试
模式
日志
Git 历史
生产遥测
决定:
哪些行为必须保留,
哪些代码可以删除,
应引入哪些边界,
哪些需要测试,
以及哪些可以先迁移。
在遗留系统现代化项目的开始阶段,我会避免使用几类提示。
例如:
Rewrite this application using Clean Architecture.
或者:
Convert this monolith into microservices.
或者:
Modernize this entire repository.
甚至:
Find all the bad code.
问题不在于 AI 无法从这些提示中得到有用的输出——它完全可以。
问题在于这些问题本身已经包含了解决方案。
你在寻求:
Clean Architecture
Microservices
Rewrite
Bad code
在尚未明确系统的实际需求之前。
更好的顺序是:
What exists?
↓
Why does it exist?
↓
What behavior matters?
↓
What is uncertain?
↓
What should change?
该序列在第一小时会较慢,但通常在项目剩余阶段会快很多。
人们往往根据AI编码工具生成的代码量来评估它们。
对于遗留系统,我认为这忽略了它们价值的一部分。
最有用的输出之一可以是:
我无法根据现有代码确定此条件存在的原因。
或者:
此事件在当前仓库中似乎没有消费者,但不能排除外部消费者的存在。
或者:
这两个折扣计算看起来相似,但在零价值订单上的行为有所不同。
这些是有用的发现,能告诉工程师应该在哪里进行调查。
自信但错误的答案要危险得多。
在处理遗留系统时,不确定性就是信息。应当如此对待。
AI 使得探索不熟悉的代码库变得容易得多。
你可以用它来总结模块、追踪执行路径、提取候选业务规则、查找副作用、比较实现、分析 Git 历史以及构建依赖图。
这可以省去大量机械性的调查工作。
但理解一个系统并不等同于为其生成解释。遗留应用程序可能包含源代码之外的上下文:
生产行为,
过去的事件,
外部消费者,
业务例外,
未文档化的集成,
以及组织历史。
AI 可以帮助你找到证据。它无法制造缺失的历史。
这就是我更倾向于先把它用作调查者,再用作转换者的原因。
可以从以下开始:
What does this system actually do?
然后问:
What do I still not understand?
只有在那之后,你才应该问:
What should I change?
AI让你修改软件越快,这个顺序就越重要。
因为修改你理解的代码是工程;而修改你不理解的代码则是实验。
而生产环境通常是进行这种实验最昂贵的地方。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。