如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?
我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算
我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。
但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。
虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。
人工智能让这个问题变得更加复杂了。
它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。
但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果:还是那个系统,只不过被更快地重新编写了一遍而已。
这并不一定算是真正的现代化改造。
在本次教程中,我想向你们展示一种在旧系统迁移过程中使用人工智能的不同方法。
不要把人工智能仅仅看作是一种自动化的代码翻译工具,而是应该利用它来帮助你完成以下任务:
理解那些不熟悉的代码库;
识别业务规则和隐藏的依赖关系;
构建一个行为保障机制;
确定逐步迁移的具体边界;
在替换原有代码之前先进行重构;
自动化那些重复性的转换工作;
比较旧系统和新系统的行为差异;
在问题影响到生产环境之前及时发现回归现象。
这些示例中使用了TypeScript,但整个流程并不一定依赖于TypeScript或Node.js。
最重要的是工作流程。人工智能确实可以让迁移工作变得更加高效,但最终还是需要工程师来决定哪些内容值得进行迁移。
先决条件
你应当具备以下基础知识:
TypeScript的基础知识;
单元测试和集成测试的相关技能;
依赖注入机制的理解;
软件架构设计的基本概念;
具备处理现有代码库的经验。
这些示例中使用了Vitest作为测试框架,但如果你使用Jest或其他测试工具,这些方法同样适用。
目录
如何避免进行一对一的旧代码迁移
想象一下,在一个旧的订单处理系统中,你发现了如下这个函数:
async function processOrder(order: Order) {
if (!order.customer.active) {
throw new Error("客户处于非活跃状态");
}
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,
"订单已处理完毕",
);
return finalAmount;
}
这个函数确实能够正常工作,但它同时承担了太多功能:它需要验证客户信息、应用定价规则、保存数据、通过支付方式完成收费操作,还要发送通知。
如果进行一对一的迁移,虽然可以使用新的TypeScript语法和库来重构这个函数,但所有这些功能仍然会保留在同一个代码中。
你可能会用新的控制器替换旧的控制器,用新的服务替换旧的服务,用新的ORM框架替换旧的ORM框架……但这样一来,同样的架构问题依然会存在。
这就是人工智能有时会带来麻烦的地方之一。
如果你的要求仅仅是“将这个旧代码转换为TypeScript”,那么人工智能通常会按照你的要求去保留代码的结构,因为这正是你交给它的任务。
但在让人工智能对代码进行改造之前,先要思考两个问题:
哪些功能是必须保留下来的?
哪些设计理念也是需要保留的?
这两个问题是不同的。
有时候,虽然实现方式已经过时,但某些功能仍然非常重要;有时候,功能本身很重要,但实现方式应该被替换掉;而有时候,你会发现两者都不必保留。
这种区分应该在大规模迁移开始之前就做好。
在修改旧代码库之前,如何对其进行分析
在进行旧代码迁移时,第一个困难通常在于弄清楚自己实际上拥有什么代码。
如果存在文档的话,这些文档会提供很大的帮助。但在许多系统中,真正的文档分散在以下各种地方:
条件语句中,
数据库约束规则中,
定时执行的脚本中,
注释中,
日志记录中,
集成代码中,
测试用例中,
配置文件中,
以及人们头脑中的知识中。
在这个方面,人工智能可以帮助节省时间,因为它不需要去做出架构上的决策。
以之前提到的processOrder函数为例。与其让人工智能重新编写这个函数,不如先提出这样的问题:
请分析这个函数中包含的业务规则;列出它所产生的一切副作用;确定它依赖于哪些外部系统;找出哪些部分可以被表述为纯函数;思考在修改这个函数之前,哪些可观察的行为应该通过测试来加以保护。切勿直接重写这个函数。最后这条指令的重要性可能超乎你的想象。
当分析和转换在同一条请求中同时进行时,人工智能工具就能轻松解决那些你尚未完全理解的设计问题。
我更倾向于先明确地进行分析。
对于规模较大的代码库,需要在多个层面上重复这一过程。
在仓库层面,需要关注以下内容:
入口点,
数据库访问逻辑,
外部API接口,
消息队列,
后台作业,
定时任务,
配置信息,
共享状态数据,
认证机制,
授权流程。
在模块层面,需要关注以下内容:
业务规则,
依赖关系,
副作用,
重复出现的逻辑代码,
耦合度过高的类,
隐含的契约关系。
在函数层面,需要关注以下内容:
输入参数,
输出结果,
异常处理机制,
状态变化逻辑,
外部调用接口,
边界情况处理。
人工智能可以大大加快这种探索过程。但其分析结果仍需与实际的代码库、测试数据、数据库结构、日志记录以及生产环境中的表现进行对照验证。
无论解释得多么有说服力,对代码的解读终究只是一种解释而已。代码库才是真实的依据。
在重构之前如何编写特征测试用例
老旧软件的一个棘手之处在于,那些异常行为并不一定是偶然出现的。
你可能会发现一些明显有问题的代码,但后来才会发现系统的其他部分实际上依赖于这些代码。
这时,特征测试用例就派上了用场。
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("为高级客户提供10%的折扣", () => {
expect(
calculateDiscount("PREMIUM", 100),
).toBe(10);
});
it("普通客户不会享受折扣", () => {
expect(
calculateDiscount("REGULAR", 100),
).toBe(0);
});
it("当订单总金额为零时,函数应返回0", () => {
expect(
calculateDiscount("PREMIUM", 0),
).toBe(0);
});
});
人工智能有助于扩展这一安全保障机制。
例如:
为这个函数生成相应的测试用例。
要确保原有的功能表现不受影响。
需要涵盖以下情况:
- 正常输入数据,
- 边界值输入,
- 非有效输入数据,
- 异常情况,
- 可观察到的副作用。
切勿重新设计该函数本身。
之后再审查这些测试用例所检测的内容。
你的目的不是证明原有的功能是正确的,而是要记录在对其进行重构后哪些部分会发生变化。
这种变化确实很重要。
如果某个测试用例检测到了后来被认定为错误的行为,那么就应该有意识地修改相关代码。你需要避免的是在无意中改变函数的功能,却在部署之后才发现这些差异。
如何寻找安全的迁移切入点
传统应用程序很少需要一次性全部被替换掉。
通常情况下,我们需要为旧系统和新系统创造一个可以暂时共存的环境。
在《高效处理遗留代码》一书中,Feathers也提到了“迁移切入点”这一概念:即可以在不修改周围其他代码的情况下调整某部分功能的地方。
订单处理的例子就提供了一个这样的切入点。
原始函数包含以下功能:
客户信息验证,
折扣计算,
数据存储与持久化操作,
支付处理,
以及电子邮件通知功能。
前两项属于业务逻辑范畴,而后几项则与基础设施相关。这一点暗示了可能存在一个合适的划分界限。
业务/应用层面的职责:
客户规则制定,
定价策略管理,
订单处理流程设计。
基础设施层面的职责:
数据库管理,
支付服务提供商相关功能,
电子邮件发送服务相关功能。
人工智能可以帮助我们识别这些职责划分的潜在切入点。
例如:
分析这些文件,找出以下内容:
- 业务规则,
- 基础设施相关问题,
- 可能产生的副作用,
- 被共享的可变状态,
> 重复出现的代码逻辑,
> 会导致独立测试变得困难的依赖关系。
然后提出可能的划分界限建议。
目前还不要直接修改代码。
需要再次强调的是,人工智能提供的结果只是辅助工程决策的工具,而不是决策的依据。
当你找到一个合适的迁移切入点后,你就获得了这样一个机会:可以在不重新编写整个应用程序的情况下推进现代化改造工作。
如何朝着明确划分职责的方向进行代码重构
一旦你理解了某段代码的功能,并为它的当前行为编写好了测试用例,那么对其进行重构就会变得相对安全多了。
例如,定价规则就可以被改写成一个纯粹的函数:
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("客户处于非活跃状态");
}
}
基础设施相关功能可以基于契约来进行设计:
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 thisnotifications.sendOrderConfirmation(
order.customer.email,
);
return finalAmount;
}
}
这种重构并没有什么特别革命性的地方,这恰恰正是它的目的所在。
现代化改造并不一定需要采用复杂的架构。
通常,重要的改进就在于让各种职责变得足够明确,这样在后续进行修改时,就无需再了解整个应用程序的运作机制了。
如何利用人工智能来实现机械性的系统改造
一旦各项边界被清晰地界定出来,人工智能在实施这些改造过程中就会变得非常有用。
很多时候,需要进行大量的迁移工作,而这些工作往往具有重复性:
转换API接口;
修改框架的使用规范;
生成适配器代码;
调整配置文件的内容;
更新类型定义;
更换数据访问相关的库;
修改那些重复出现的集成代码。
这些正是适合运用人工智能来处理的环节。
假设旧系统是直接执行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;
}
然后,再对相关的转换过程进行约束和限制。
使用新的数据库客户端来实现CustomerRepository。
约束条件:
- 保持CustomerRepository接口不变。
- 使用参数化查询。
- 不要将业务规则放入仓库中。
- 保留现有的空值处理机制。
- 保留现有的错误处理方式。
- 只返回实现代码本身。
这与以下要求有很大的不同:
对这段数据库代码进行现代化改造。
在第一种情况下,你是首先做出了架构上的决策,然后让AI在这个框架内完成具体的实现工作。
我认为,在迁移工作中,AI的作用最为显著。在那些重要的决策已经确定之后,AI能够帮助我们免除繁琐的机械性工作。
如何分阶段进行迁移
当有成千上万的文件同时发生变化时,大规模的迁移工作会变得难以理解和管理。
因此,一个更合适的迁移单位通常是某个具体的业务功能。
不要一次性迁移所有的控制器、服务或仓库,而应该先迁移一个完整的业务功能。
举个例子:
创建订单功能
API接口
应用程序逻辑
业务规则
数据持久化机制
测试代码
完成一个功能的迁移后,再继续进行下一个功能的迁移。
这样做有几个优点:首先,迁移工作更接近于可部署的软件状态;其次,为AI工具提供的输入信息量也会更少。
这样,测试也会更加有针对性。而且,如果出现任何问题,也更容易定位故障原因。
对于分阶段迁移来说,一个有用的初始提示可以是:“目前我们只需要进行分析工作”:
我们正在迁移“创建订单”功能。
旧的实现代码位于/legacy/orders目录下。
目标架构将系统分为三个部分:
- 业务逻辑层,
- 应用程序层,
- 基础设施层。
位于/tests/legacy目录下的测试用例用于验证那些必须保持兼容性的行为。
请分析当前的实现代码,并列出以下内容:
1. 业务规则,
2>外部依赖关系,
3>可能产生的副作用,
4>迁移过程中可能遇到的风险,
5>需要修改的文件列表。
目前还不要开始生成新的代码。
仔细阅读这些分析结果,然后再制定具体的迁移计划。
这种迁移方式也适用于渐进式替换策略,比如Martin Fowler提出的“绞杀者图”方法——新功能会逐步取代旧系统,而不需要一次性完成全部替换工作。
这里的关键词就是“渐进式”。
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`请求,然后对比它们的响应结果:
状态码
响应数据内容
数据库中的变化
触发的事件
外部调用的情况
出现的错误
需要强调的一点是:出现差异并不一定意味着存在缺陷。有时候,系统的行为发生变化也是正常的。关键在于要让这些差异变得显而易见,这样人们才能有意识地对其进行分析。
人工智能在这方面也能提供帮助。
分析这些行为上的差异,
按照可能的原因将它们分组。
特别需要注意以下方面:
- 四舍五入操作;
- 对空值的处理;
- 时区转换;
- 数据验证;
- 数据序列化;
- 数据映射。
除非有足够的证据支持这一结论,否则不要将某处不一致视为缺陷。
如何利用影子流量来检测回归问题
最终,测试用例会变得不再具有足够的代表性。
旧版本系统:
200
{ "total": 90 }
新版本系统:
200
{ "total": 90 }
结果一致
或者:
旧版本系统:
200
{ "total": 90 }
新版本系统:
200
{ "total": 100 }
结果不一致
收集这些差异数据,就能了解新系统在真实使用环境中的表现,而不会立即让用户受到实际影响。
不过,采用这种技术也会带来一些运营上的考虑因素。你需要仔细思考以下问题:
是否会产生不可逆的副作用
支付相关操作的处理方式
电子邮件的发送情况
数据写入过程
用户隐私问题
生产环境的负载情况
外部API的使用情况
通常来说,影子版本的系统应该避免执行那些不可逆的操作。
例如,可以用一个记录功能来替代真实的支付处理模块:
export class RecordingPaymentGateway
implements PaymentGateway {
public readonly calls: Array<{
card: string;
amount: number;
}> = [];
async charge(
card: string,
amount: number,
): Promise<void>> {
this.calls.push({
card,
amount,
});
}
}
现在你可以确保在不会向客户收取两次费用的情况下实现收费功能了。
如何测试你真正想要的架构
如果迁移的目标之一是优化架构,那么仅仅保证行为上的兼容性是不够的。
假设你已经确定了以下约束条件:
业务代码不得依赖于基础设施代码。
如果这条规则只存在于架构图中,那么在迁移过程中最终还是会有人违反它。
因此,必须对其进行测试。
对于简单的项目,你可以检查代码中的导入依赖关系;而对于复杂的项目,则应该使用能够强制执行这些架构规则的工具来进行测试。
具体的工具并不重要,关键是要遵循这个原则:
如果某个架构约束非常重要,那么就必须确保违反它会导致明显的后果。
你可能会需要制定如下规则:
业务代码不得依赖于基础设施代码
业务代码不得依赖于HTTP框架
应用程序代码不得直接依赖数据库驱动程序
各个模块不得导入其他模块的内部实现细节
为什么在人工智能辅助的迁移过程中这一点如此重要呢?因为人工智能非常擅长找到让代码能够编译通过的方法。
如果直接调用另一个模块就能解决当前的问题,那么生成的代码很可能会这样做——除非这一行为本身属于必须遵守的约束条件。
通过进行架构测试,无论是人类还是人工智能工具,都更难以意外地违反这些规则。
如何决定哪些任务应该由人工智能来处理
我并不会把所有的迁移任务都视为同等重要。有些任务确实非常适合自动化处理。
人工智能通常有用的任务
解释那些难以理解的代码
识别各种依赖关系
提取潜在的业务规则
生成用于测试的用例
编写重复性代码模板
更新框架的API接口
翻译那些机械性、重复性的代码
制作迁移检查清单
比较不同的实现方案
对回归测试的结果进行分类整理
起草技术文档
需要经过深入工程评审的任务
确定模块之间的边界
提取业务领域的核心概念
重构那些耦合度过高的类
选择合适的迁移顺序
修改数据模型
设计系统各模块之间的集成接口
我会由人类来负责决策的任务
最终目标架构的设计
哪些行为差异是可以接受的
安全方面的限制条件
数据迁移的具体策略
部署方案的选择
回滚机制的规划
如何消除旧有的系统行为
是否接受生产环境中的风险
这并不是因为人工智能无法提出架构设计方案,它完全有能力做到。
问题在于责任归属以及具体的应用环境。
架构选择的背后,其实蕴含着各种限制因素、历史背景、组织能力、业务优先级以及运营风险,而这些信息可能根本不存在于代码库中。
模型可以帮助我们探索这些选择,但最终还是需要有人来承担这些决策带来的后果。
如何判断迁移是否真正改善了系统性能
“迁移进度”是一个很容易被量化的指标。
例如:
代码库中有37%的部分已经完成了迁移。
但这个数据并不能说明系统的性能是否真的得到了提升。
任何现代化改造项目都应该关注多种类型的评估结果。运营方面的指标可能包括:
部署频率
变更失败率
平均恢复时间
生产环境中的问题数量
代码构建耗时
工程方面的指标可能包括:
测试覆盖率
高复杂度代码的数量
重复存在的业务规则
模块间的依赖关系
违反架构规范的情况
修改某项功能所需的时间
与迁移相关的特定指标可能还包括:
代码退化率
新路径处理的请求占比
仍未解决的行为异常问题
回滚操作的频率
仍在使用的旧版本组件
具体应该选择哪些指标,取决于系统的实际情况。但最重要的是要避免陷入这种对“成功”的错误定义:
旧代码库的规模变小了 = 现代化改造成功了。
人工智能使得在更短的时间内处理更多代码成为可能,因此衡量这些代码转换的质量反而变得更加重要,而不是相反。
我最担心的人工智能辅助迁移带来的风险
生成了一些“看似正常”的代码,这确实是一个明显的风险。但我更担心的是那些表面上看起来合理、但实际上可能隐藏问题的代码。
这些生成的代码能够编译通过,甚至可能比原始实现看起来更加整洁,也能通过一些简单的测试。但它们仍然有可能悄悄地改变某些业务规则,而人们却根本意识不到这一点。
举个很小的例子:
if (customer.balance > 0) {
charge(customer);
}
当你不明白某个条件存在的理由时,往往会想要优化代码。
但也许“零”在业务中具有特殊的含义,负余额也可能是被允许的。
又或许这个条件是六年前在一次生产事故之后才被添加进去的,而且从未被记录下来。
如果人工智能无法获取这些信息,它就无法理解代码背后的真实含义。因此,我非常重视特性测试和行为对比分析。
转型速度越快,相应的验证流程就必须越严格。
否则,你只会加快引入未知变化的速度而已。
实用的迁移工作流程
如果必须将整个流程简化为一种可重复执行的步骤序列,我会选择这种方式。
1. 明确现状
需要分析的内容包括:
系统行为
依赖关系
业务规则
可能产生的副作用
数据结构
系统集成方式
可以利用人工智能来加速这一分析过程,但切不可在开始阶段就直接构建新系统。
2. 采取保护措施
需要完成的工作包括:
特性测试
集成测试
API接口测试
系统行为记录
必须确保当前系统的运行状态可以被清晰地观察到。
3. 进行设计规划
需要确定的内容包括:
系统边界
接口规范
- 各模块的职责划分
迁移过程中的关键节点
这些工作必须在大规模转型之前完成。
4. 代码重构
需要确保系统的某部分可以进行独立修改,而不会影响到其他部分的正常运行。
5. 实施转型
在重复性的实现工作中,应充分利用人工智能来提高效率。
同时,必须为这些自动化流程设定明确的架构约束。
6>对比测试结果
需要使用相同的输入数据,分别运行旧系统和新系统的代码,然后分析两者之间的差异。
7>逐步发布新系统
根据你所处的环境,可以选择以下方法来逐步推出新系统:
功能开关机制
小规模测试部署
模拟生产环境的测试环境
- 持续监控系统运行状态
具备回滚能力
8>移除旧系统路径
切勿让两个系统长期同时运行。如果不对旧系统进行彻底移除,最终会形成另一种陈旧的架构。
结论
人工智能改变了传统系统现代化改造的运作模式。
许多过去需要耗费大量工程师时间的工作,现在都可以快速完成:阅读不熟悉的代码、生成测试用例、更新API接口、优化重复性的实现逻辑,以及分析不同系统之间的差异等等。
这些确实非常有用。但人工智能并不能替代人类在现代化改造过程中所需要做出的判断和决策。
真正困难的问题仍然存在:
哪些系统行为仍然是至关重要的?
哪些部分应该被彻底淘汰?
哪些依赖关系应该被保留下来?
系统的边界应该如何划定?
系统行为发生多大的变化才是可以接受的?
新系统何时才能达到可以承载生产环境负载的水平?
如果仅仅利用人工智能来辅助代码翻译工作,那么技术债务的清理速度确实会大大加快。
<如果你将这种方法与特性测试、逐步重构、明确的架构边界、差异性测试以及受控的部署流程结合起来,那么在推进系统升级的过程中,你就更有机会改善系统的性能。
<我们的目标并不是将同一个系统迁移到更新的技术框架中,而是要深入理解这个系统,保护其关键功能,对其进行重构,并逐步实现迁移;同时要验证改造后的结果,最终得到一个比最初版本更简洁、更高效的系统。
<人工智能可以帮助缩短这一过程,但它仍然无法决定最终应该达到什么样的目标。 相关文章
在生产环境中进行部署时会发生什么?一场幕后揭秘之旅
你提交代码后,几分钟后,这些代码就会真正开始为用户提供服务。 在这两个时刻之间,有一系列复杂的流程在运转:代码构建、生成相应的成果文件、数据库迁移、健康检查以及流量分配调整。每一位生产工程师都依赖这一整套流程,而许多团队至今仍然亲自负责这些工作的执行。 部署基础设施如今已经悄然成为一种运营负担。它最初只是出于技术上的必要性而存在的——因为当时没有其他可行的解决方案,所以每个团队都必须自行搭建这套系统。 如今,这已经成为另一个需要工程师维护的系统,它会消耗大量的值班时间、开发资源,甚至还会在凌晨两点占用本该用于更重要工作的精力。 在这篇文章中,我们将详细探讨一次实际的生产环境部署过程:从代码构建
阅读全文
即时响应机制与循环处理机制:开发者指南
对许多开发者来说,人工智能的工作流程大致是这样的:编写一个提示语,获取模型给出的回复,提取其中有用的信息,然后继续下一步工作。这种工作流程涵盖了范围相当广泛的任务,从总结文档内容到起草电子邮件,再到解释代码逻辑。然而,当某项任务需要多个步骤、外部数据的支持,或者决策结果取决于模型刚刚返回的信息时,这种工作流程就会遇到问题。这时,开发者不得不手动重新编写提示语,手工调整输出结果,去做那些本应由系统来完成的工作。在这种情况下,单一的提示语就不再是一种有效的工具了,而设计一个能够多次向模型发送请求的系统,才成为了真正需要完成的任务。有两种术语可以用来描述这两种不同的工作模式: ** “提示语工程”是
阅读全文
如何使用Pydantic AI构建具备生产级功能的智能代理
使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题
阅读全文
如何在没有服务器的情况下为静态网站添加动态功能
静态网站目前正受到人们的青睐,这是有原因的。一个包含HTML、CSS和JavaScript文件的文件夹,加载速度很快,托管成本也很低,而且几乎不可能出现故障。 像 Astro 、 Eleventy 和 Hugo 这样的工具,能够利用Markdown文件和模板帮您生成这样的网站结构。而Netlify、Vercel以及Cloudflare Pages等托管服务,则可以通过内容分发网络来提供这些生成的网站内容,而且通常还是免费的。 不过,您的网站还需要具备实际的功能。读者可能想要留下评论,或者有人想通过电子邮件与您联系。也许您还想实时显示价格信息,需要用户登录后才能查看某些页面,或者在网站发布之前收
阅读全文