在修改现有的代码库之前,如何利用人工智能来理解这些代码的含义
许多工程师在继承现有的代码库时,首先想要做的就是修改这些代码。我完全理解这种冲动。 当你打开一个长达1500行的类文件时,你会看到其中混杂着数据库调用、业务规则、分散在各处的配置值、根本没人愿意去修改的方法,以及那些提到早已被淘汰的系统的注释。 这时,一款人工智能编码助手主动提出可以帮助你理解整个代码库的结构。 于是你就会问: 请重构这个类。 但通常来说,现在还不是进行重构的时候。 从处理遗留系统的经验中,我认识到:代码虽然可能写得很糟糕,但却可能蕴含着重要的信息。 某个奇怪的条件实际上可能代表着某种业务规则;重复出现的计算逻辑可能是由于两个看似相同的流程其实并不完全相同所致;一个命名很糟糕的
许多工程师在继承现有的代码库时,首先想要做的就是修改这些代码。我完全理解这种冲动。
当你打开一个长达1500行的类文件时,你会看到其中混杂着数据库调用、业务规则、分散在各处的配置值、根本没人愿意去修改的方法,以及那些提到早已被淘汰的系统的注释。
这时,一款人工智能编码助手主动提出可以帮助你理解整个代码库的结构。
于是你就会问:
请重构这个类。
但通常来说,现在还不是进行重构的时候。
从处理遗留系统的经验中,我认识到:代码虽然可能写得很糟糕,但却可能蕴含着重要的信息。
某个奇怪的条件实际上可能代表着某种业务规则;重复出现的计算逻辑可能是由于两个看似相同的流程其实并不完全相同所致;一个命名很糟糕的数据库字段也可能与某些外部合同相关联。
而那些没人能看懂的方法,说不定正是防止八年前发生过的生产故障再次发生的关键因素。
人工智能确实让阅读陌生的软件变得容易多了,这一点非常有价值。但同时,它也使得在尚未完全理解代码之前就对其进行修改变得更加容易。
在这篇教程中,我将向你们展示如何利用人工智能来完成一项我认为应该在重构或迁移之前先做的事情:对代码库进行“考古分析”。
你们将学会如何使用人工智能来帮助自己:
梳理整个代码库的结构;
确定代码的入口点;
追踪各种依赖关系;
区分业务规则与基础设施部分;
发现那些隐藏的副作用;
检查数据流的处理流程;
揭示那些隐含的契约关系;
识别重复出现的代码逻辑;
构建依赖关系图;
找出存在不确定性的地方;
并将这些分析结果转化为现代化改造计划。
这些示例中使用的是TypeScript,但这个方法其实适用于大多数编程语言和开发框架。
我们的目标并不是让人工智能来解释代码的含义,然后直接信任它的答案。而是利用人工智能来帮助我们节省寻找正确问题的时间。
先决条件
你应当具备以下能力:
阅读现有的代码库;
熟悉TypeScript或类似的面向对象编程语言;
了解基本的软件架构设计;
掌握依赖注入技术;
会进行单元测试和集成测试;
能够使用能够查看代码库文件的人工智能编码助手。
你并不需要特定的人工智能工具,因为工作流程才是最重要的因素,而非模型本身。
目录
为什么在重构之前必须先了解代码的运作原理
遗留代码往往会给人一种迫切需要对其进行改造的错觉。
当你看到某些代码存在明显的耦合或重复现象时,就会立刻想要将其优化。
来看这个函数:
async function approveOrder(order: Order) {
if (order.total > 10000 && !order.customer.verified) {
throw new Error("需要人工验证");
}
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这个操作?订单的状态能否在其他地方从“待处理”变为“已批准”?
是否有某些功能依赖于具体的异常信息?
仅凭代码语法,你是无法回答这些问题的。
这时,理解代码的运作原理就显得十分重要了。
不要直接让你的AI工具来帮忙重构:
使用清晰的架构来重构这个函数。
而应该先从以下步骤开始:
如何从代码仓库开始分析,而非从类结构入手
当我面对一个不熟悉的遗留系统时,我不会先去阅读每一个文件。我会先尝试了解整个应用程序的整体架构。
代码仓库本身就包含了许多关于系统结构的线索。
你可以寻找以下这样的目录:
src/
controllers/
services/
repositories/
models/
jobs/
workers/
scripts/
migrations/
config/
integrations/
tests/
但不要以为目录名称就能真实反映系统的架构结构。
一个名为services的目录可能包含了业务逻辑、基础设施代码、调度任务以及一些辅助功能。
而名为models的目录里,实际上可能是数据库实体而非领域模型。
一个叫utils的文件夹,很可能隐藏了应用程序中一半的业务逻辑代码。
应将这种结构视为参考依据,而不是绝对事实。
一个有用的初始请求可以是:
检查代码库的结构。
暂时不要分析具体的实现细节。
需要确定以下内容:
- 应用程序的入口点,
- 主要模块,
- 使用的数据库技术,
- 外部集成方案,
- 后台处理流程,
- 定时任务,
- 认证机制,
- 配置文件来源,
- 测试用例,
- 可能存在的架构边界。
对于每一个结论,都要引用支持它的文件或目录。
对任何不确定的部分,必须明确标注出来。
引用相关文件这一点非常重要。如果没有这个步骤,AI可能会给出一个看似合理但实际上并不存在的架构方案。
你真正需要的是类似这样的信息:
HTTP接口
参考文件:
- src/server.ts
- src/routes/orders.ts
- src/routes/customers.ts
后台处理任务
参考文件:
- src/workers/paymentWorker.ts
- src/queues/index.ts
定时作业
参考文件:
- src/jobs/reconcileInvoices.ts
- src/cron.ts
现在你已经有了一份可以用来验证信息的清单。
如何找到真正的入口点
Web应用程序通常有一个显而易见的HTTP入口点,但旧系统往往会有多个入口点。
某个业务操作可能会通过以下方式启动:
一个API请求,
一个定时作业,
一个队列处理程序,
一个数据库触发器,
一个命令行脚本,
文件的导入操作,
一个邮件处理程序,
一个Webhook,
或者另一个直接调用数据库的应用程序。
如果你只分析控制器代码,那么你很可能会忽略系统的一半功能。
假设你搜索与订单创建相关的操作,发现了以下内容:
POST /orders
你很容易就会认为所有订单都是通过这个接口进入系统的。
但后来你又发现:
jobs/importMarketplaceOrders.ts
workers/retryFailedOrders.ts
scripts/migratePendingOrders.ts
integrations/shopify/webhook.ts
现在,同一个业务操作实际上有四种不同的入口路径。
这种情况会改变你对于代码重构的思考方式。
你可以这样请求AI:
rg "orders.save|orders.insert|createOrder|approveOrder" src
人工智能应该用来加速搜索过程,而不是取代它。
如何通过代码库追踪业务功能
仅仅了解单个文件是远远不够的。
真正重要的是要理解业务功能本身。
例如:
创建一个订单。
这一功能可能会涉及多个层次:
HTTP请求
↓
控制器
↓
应用服务
↓
定价模块
↓
库存管理系统
↓
持久化存储层
↓
支付处理流程
↓
通知系统
代码的实际组织结构可能并不如此清晰,而这正是追踪业务功能的重要原因。
选择某个具体的工作流程,然后进行如下分析:
从“创建订单”这一功能的起点开始追踪,
直到所有相关的操作都完成为止。
对于每一步,需要展示以下内容:
- 所涉及的文件;
- 相关的函数或类;
- 输入数据;
- 输出结果;
- 状态变化情况;
> 是否有外部调用;
> 发生错误时的行为表现。
不要将多个步骤合并成一步来描述。
你需要得到一个可以仔细检查的流程序列。
例如:
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("企业客户的信用额度无效");
}
const connection = await mysql.getConnection();
await connection.query(
"INSERT INTO customers (...) VALUES (...)",
[...]
);
await redis.del(`customer:${customer.id}`);
await eventBus.publish(
"customer.updated",
customer
);
}
至少存在一条业务规则:
企业客户必须拥有大于或等于50,000的信用额度。
此外还涉及一些基础设施方面的问题:
MySQL、Redis、事件总线
让AI来对这段代码进行分类:
如何发现隐藏的副作用
副作用是导致系统迁移风险的最大因素之一。
一个名为“updateCustomer”的函数
updateCustomer()
可能不仅仅用于更新客户信息,它还可能执行其他操作:
将数据写入数据库
使缓存失效
触发某个事件
发送电子邮件
更新分析数据
生成审计记录
安排其他任务执行
如果你对这个函数进行重构,但只保留它的返回值,那么就可能会破坏系统的正常运行状态,而编译器却不会报错。
一个有用的调查提示是:
await chargeCard(order);
await markOrderAsPaid(order);
如果这个工作进程在执行这两条指令的过程中崩溃,然后重新尝试执行,会发生什么?结果可能是向客户收取两次费用。而这种行为从函数名称中是无法看出来的。
理解代码中的重试机制是理解整个代码库的重要部分。
如何发现隐含的契约关系
并不是所有的契约关系都是通过接口明确声明的。许多旧版应用程序中都存在隐含的契约关系。
例如:
return {
status: "ok",
value: customer.balance.toFixed(2),
};
某些外部调用者可能会依赖这样的返回结果:
{
"status": "ok",
"value": "100.00"
}
将返回值从字符串类型改为数字类型,表面上看似乎是一种改进:
{
"status": "ok",
"value": 100
}
它也可能会破坏客户端的正常运行。
需要在以下地方查找相关契约信息:
API响应结果,
发生的事件,
数据库结构,
CSV导出文件,
文件名,
环境变量,
错误信息,
队列中的数据内容,
以及Webhook请求体。
需要询问的问题是:
找出这个模块中那些可以在模块外部被使用的输出结果。
包括以下内容:
- HTTP响应结果,
- 发生的事件,
- 队列中的消息,
- 文件内容,
- 数据库记录,
> 异常信息,
> 用于自动处理的日志记录。
对于每一项输出结果,都需要说明有哪些证据表明它可能构成一种外部契约或隐含的规则。
措辞非常重要:
需要说明的是“哪些证据表明……”
而不是:“请告诉我存在哪些契约……”
因为从当前代码库中可能无法证明这些契约的存在。
如何利用AI查找重复的业务规则
重复的代码很容易被发现,但具有相同业务含义的代码却较难被发现。
你可能会在某个模块中看到这样的代码:
if (customer.type === "PREMIUM") {
discount = total * 0.1;
}
而在另一个地方,你又会看到类似的代码:
if (account.plan === "GOLD") {
price = price * 0.9;
}
这些代码可能代表相同的业务规则,也可能并不相同。
AI在帮助识别这类重复内容方面非常有用。
需要询问的问题是:
在代码库中搜索与客户折扣相关的业务规则。
将那些在语义上相关的实现方式归为一组,即使变量名称不同也是如此。
对于每一组规则:
- 列出相关文件的位置,
- 描述这些规则的含义,
- 指出它们之间的差异,
> 不要假设这些规则必须被统一起来。
最后这条指令非常重要。
有时候代码重复是偶然发生的,
有时则是因为两个系统独立发展而导致了这种重复。
不要让AI助手在没有足够证据的情况下,就把“相似的内容”认定为“必须合并的部分”。
如何构建轻量级的依赖关系图
在某些时候,你需要了解系统中的各个部分之间存在着哪些依赖关系。
你并不需要一份完美的企业级架构图,一个轻量级的依赖关系图就足以满足你的需求了。
例如:
Orders
├── Customers
├── Inventory
├── Payments
├── Notifications
└── Database
Payments
├── Payment Provider
├── Audit
└── Database
可以让AI帮助提取模块级别的依赖关系:
如何标记自己尚未理解的内容
这是整个流程中最重要的环节之一。
一个有用的系统架构图不仅会列出已知答案,也会反映其中存在的不确定性。
我喜欢制作这样一份明确的清单:
## 待解决的问题
- 为什么企业信用门槛定为50,000?
- `ORDER_APPROVAL`这个函数是否在代码库之外也被使用?
> 市场订单能否绕过库存验证流程?
> `customer.balance`字段的值是否可以为负数?
> 是什么流程将待处理的订单状态变为“已批准”?
> 是否还有其他系统仍在使用`legacy_customer_id`这个字段?
你也可以让人工智能帮你生成这样的清单:
根据目前分析的结果,列出那些无法从代码库中得到明确答案的问题。
重点关注在以下情况下可能会遇到问题的问题:
- 代码重构时
- 迁移系统时
- 数据表结构发生变化时
- 接口设计发生变更时
- 删除某些代码后
我喜欢这种提问方式,因为它与我们通常要求人工智能完成的任务正好相反——它让模型明确指出哪些地方是不应该假装自己了解的。
任何现代化改造计划都应当将这些未知因素纳入考虑范围。
如何验证人工智能得出的结论与系统实际情况是否一致
即使人工智能生成的解释并不完整,它们也可能听起来很有说服力。因此,每一个重要的发现都应该有其他的证据来源来进行验证。
我采用了一种简单的验证方法。
代码库搜索
如果人工智能提示某个函数只被调用过一次,那就去实际查找一下这个函数是否存在。
rg "approveOrder" .
测试用例
测试用例往往能揭示实现代码中未明确说明的假设。
需要重点关注以下内容:
预期会出现的错误
特殊值
边界情况
测试数据
系统的历史运行行为
数据库架构
数据库架构可以帮助我们了解一些关键信息,例如:
可为空字段
外键关系
默认值设置
遗留的列
约束条件
状态码的含义
日志与可观测性数据
生产环境中的监控数据可以帮助我们判断那些看似已被弃用的代码路径是否仍在被使用。
版本历史记录
Git版本历史记录有时也能帮助我们解答源代码中无法解决的问题。
例如:
git log -S "需要人工验证" --all
或者:
git blame src/orders/approveOrder.ts
引入了那个奇怪条件的提交中,可能会包含相关的解释。
在这个方面,人工智能可以帮助我们总结代码的历史变化:
查看那些修改了该功能的提交记录。
整理出这些功能变化的时间线。
对于每一处变更,需要记录以下信息:
- 提交信息;
- 提交日期;
- 变更的内容;
- 如果有说明原因的话,也请一并记录下来。
如果提交历史中没有提供原因,就不要自行推测。
这样做能够节省大量的时间。
如何将代码库的理解转化为迁移计划
只有当你真正理解了某个功能或模块的工作原理后,才能开始做出决策。在此之前,不要急于行动。
假设你的调查得出了以下结果:
创建订单
业务规则:
- 需要现有客户;
- 高级客户可享受10%的折扣;
- 必须有足够的库存。
副作用:
- 订单会被保存下来;
- 库存会被预留;
- 支付流程会被启动;
>确认邮件会发送给客户。
外部接口:
- /orders接口的响应数据;
>支付队列中的数据;
>“订单创建”事件。
未知因素:
- 库存预留的重试机制;
>事件处理方是否要求使用特定的字段名称。
现在你可以决定哪些部分需要重点保护了。
优先保护的内容包括:
- 价格计算逻辑;
>API响应数据;
>支付相关的数据结构;
>事件通知机制。
接下来,再确定哪些部分可以进行重构。
适合进行重构的模块包括:
- 价格策略相关模块;
>库存管理模块;
>支付处理模块;
>通知服务模块。
最后,还需要找出哪些部分还需要进一步研究。
实用的代码库分析工作流程
如果我要将这个过程简化成可以重复执行的步骤,我会采用以下方法:
1. 分析代码库的结构
需要明确的内容包括:
系统的入口点;
各个模块的功能;
数据持久化机制;
系统之间的集成方式;
执行相关任务的进程或组件;
各种自动化测试流程;
系统的配置信息。
在这个阶段,不要急于对任何代码进行重构。
2. 选择其中一个功能进行深入分析
先挑选一个具体的功能来进行研究:
创建订单
审批贷款
生成发票
注册客户
取消订阅
避免试图一次性理解整个产品的所有功能。3. 从头到尾追踪整个流程
请按照以下步骤操作:
输入数据
↓
业务逻辑处理
↓
状态变化
↓>外部调用
↓>输出结果
记录所有涉及的文件。
4. 提取业务规则
需要将这些规则区分开来:
明确的规则
可能存在的规则
基础设施相关的行为规范
尚不清楚的部分
5. 识别副作用
需要查找以下内容:
写入操作
生成的消息
发送的电子邮件
后台作业
缓存变化
外部调用
6. 发现系统契约
需要寻找以下内容:
API接口
事件处理规则
数据库相关假设
被导出的文件
错误处理机制
7. 映射依赖关系
需要记录以下内容:
模块A → 模块B
同时识别出各组件之间的耦合关系。
8. 记录未知因素
不要隐瞒那些不确定的因素,应该列出一份明确的清单。
9. 验证结果
可以使用以下方法进行验证:
代码库搜索
测试用例
数据结构设计图
日志记录
Git版本历史记录
生产环境监控数据
10. 只有在确认无误后才能制定变更计划
首先需要确定:
哪些功能必须保留下来
哪些代码可以被删除
需要设置哪些新的边界条件
哪些部分需要进行测试
哪些内容可以先进行迁移
我不会首先让AI去完成这些任务
在开始任何传统的系统现代化改造项目时,我会避免使用某些特定的指令。
例如:
使用清晰架构重新设计这个应用程序。
或者:
对整个代码库进行现代化改造。
甚至还有:
当前系统现状是什么?
↓
为什么会存在这样的现状?
↓>哪些功能是真正重要的?
↓>哪些部分还存在不确定性?
↓>哪些地方需要做出改变?
虽然这种步骤在项目刚开始时会花费更多时间,但通常在整个项目中会节省很多精力。
最有用的人工智能输出有时其实是一个问题
人们往往通过人工智能工具生成了多少代码来评估它们的价值。
但对于那些老旧的系统来说,这种评估方式忽略了它们真正的价值所在。
事实上,人工智能最有用的输出结果往往就是这样的:
根据现有的代码,我无法弄清楚为什么会出现这种情况。
或者:
在当前的代码库中,这个功能似乎没有对应的用户,但也不能排除存在外部使用者的可能性。
又或者:
这两种折扣计算方法看起来很相似,但在处理零值订单时,它们的表现却有所不同。
这些信息对于工程师来说非常有用,它们能指引工程师去哪些地方进行进一步的调查。
一个虽然看似合理但实际上错误的答案,反而会更加危险。
在处理老旧系统时,不确定性本身也是一种有价值的信息——应该把这种不确定性当作有用的信息来对待。
结论
人工智能让那些不熟悉的代码库变得更加容易被理解和分析。
你可以利用它来总结各个模块的功能、追踪程序的执行路径、提取可能的业务规则、发现潜在的副作用、比较不同的实现方式、分析Git代码的历史记录,以及构建依赖关系图。
这些功能能够大大减少繁琐的手动调查工作。
然而,理解一个系统与为它生成一份详细的解释是两回事。老旧的应用程序中包含许多那些在源代码之外才存在的信息:
实际运行中的表现情况,
过去曾经发生过的问题,
外部使用者的情况,
可能出现的业务异常情况,
那些没有被记录在文档中的集成方案,
以及该系统的历史背景等。
人工智能可以帮助你找到这些信息,但它无法创造那些缺失的历史记录。
正因如此,我更倾向于将人工智能作为辅助工具来帮助我们进行调查和分析,而不是直接用它来生成最终的结果。
正确的使用步骤应该是:
这个系统到底是用来做什么的?
然后问自己:
我应该对哪些部分进行修改?
人工智能能让你更快地修改软件代码,因此这个顺序就显得尤为重要。
因为修改那些你自己能够理解的代码属于工程实践的范围;而修改那些你不了解的代码,则更像是一种实验行为。
而在生产环境中进行这种实验,通常会带来巨大的风险和成本。
相关文章
如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?
我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算
阅读全文
如何管理代码库中的上下文文件,从而让人工智能编码助手产生更优质的成果
你向编码助手请求创建一个新的端点,90秒后,这个新的端点就已经可以正常使用了。 然后你查看代码变更内容,发现它引入了一个并不存在于你的`package.json`文件中的验证库;尽管你们的团队在去年春天就已经改用了Node.js的测试框架,但它仍然使用Jest来编写测试用例;此外,由于它不知道代码库中其他处理函数都是通过服务来调用的,所以它直接从路由处理函数内部访问了数据库。 代码可以运行,它编写的测试用例也能通过,但你们还是不得不重写大部分代码。 这些情况都不是模型本身的问题——它确实给出了一个看似合理的解决方案,但它之所以会犯这些错误,是因为没有人告诉它这个特定的代码库是如何运作的。 你们
阅读全文
SpaceXAI推出了Grok机器人,用于辅助人工智能代理实现自主运行。
SpaceXAI推出了Grok Bot这一系统——这套由持久性人工智能代理组成的系统运行在专用的云计算机上,能够与网站、应用程序、收件箱以及其他工具进行交互。 作者:丹尼尔·多明格斯
阅读全文
如何使用Pydantic AI构建具备生产级功能的智能代理
使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题
阅读全文