提示词的艺术:通过工程化引导生成式AI产出符合SOLID原则的代码

文章探讨了如何通过精心设计的提示词,引导生成式AI(如ChatGPT)编写出符合SOLID设计原则、易于维护的代码,而非仅仅是能运行的代码。通过一个具体的TypeScript案例,对比了“懒惰提示”与“工程化提示”所产出的截然不同的代码架构,并提供了编写高质量提示词的实用蓝图。

提示词的艺术:通过工程化引导生成式AI产出符合SOLID原则的代码

我们都已经见识过这种魔力。你让 ChatGPT“写一个Python脚本来抓取网站并保存到CSV”,三十秒后,你就得到了可运行的代码。这令人印象深刻,速度飞快,甚至令人陶醉。

但是,当你仔细审视时,问题出现了。

整个逻辑——HTTP请求、HTML解析、数据转换和文件I/O——都被塞进了一个长达200行的函数里。硬编码的依赖随处可见。将输出格式从CSV改为JSON需要重写一半的脚本。

AI给了你能够运行的代码,但它没有给你易于维护的代码。它从第一天起就给了你技术债务。

这就是当前在专业软件工程中使用生成式AI的核心挑战。大型语言模型(LLMs)是在互联网的全部代码上训练的,其中包含大量不良习惯。它们默认选择阻力最小的路径,而这通常意味着紧密耦合的一团糟。

如果我们想使用AI来构建严肃的系统,就必须停止仅仅要求它“写代码”,而是开始要求它工程设计解决方案。我们通过将架构约束——特别是SOLID原则——直接融入到我们的提示词中来实现这一点。

提示词带来的差异:一个案例研究

让我们看一个简单的需求:我们需要一个从外部API获取用户数据并发送欢迎邮件的服务。

懒惰的提示词(及其导致的混乱)

提示词:

“编写一个TypeScript类,从 https://api.example.com/users/{id} 获取用户,然后使用SES向他们发送欢迎邮件。”

AI输出(“初级开发工程师”特供版):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import axios from 'axios';
import * as AWS from 'aws-sdk';

class UserService {
  async registerUser(userId: string) {
    // 1. 获取逻辑与Axios和特定URL紧密耦合
    const response = await axios.get(`https://api.example.com/users/${userId}`);
    const user = response.data;

    // 2. 邮件逻辑与AWS SES紧密耦合
    AWS.config.update({ region: 'us-east-1' });
    const ses = new AWS.SES();
    const params = {
      Destination: { ToAddresses: [user.email] },
      Message: { /* ... 样板代码 ... */ },
      Source: 'noreply@myapp.com',
    };

    await ses.sendEmail(params).promise();
    console.log('User registered and email sent.');
  }
}

为何这违反了SOLID原则:

  • 单一职责原则(SRP):这个类做了两件不同的事情:获取数据和发送邮件。它有两个改变的理由。
  • 开闭原则(OCP):如果我想从AWS SES切换到SendGrid,我必须修改这个类。我无法在不修改其代码的情况下扩展其行为。
  • 依赖倒置原则(DIP):高层策略(registerUser)严重依赖于低层细节(Axios、AWS SDK)。不模拟网络调用就无法对这个类进行单元测试。

工程化的提示词(“高级架构师”方法)

要解决这个问题,我们不需要自己动手写代码。我们只需要明确告诉AI要遵循哪种架构模式。

提示词:

“我需要一个TypeScript类 UserRegistrationService,来协调获取用户和发送欢迎邮件的过程。

关键点在于,这个类必须遵守SOLID原则,特别是依赖倒置原则。

首先,为 UserRepository(用于获取数据)和 EmailSender(用于发送邮件)定义接口。 UserRegistrationService 必须通过其构造函数(构造函数注入)接收这些接口的具体实现。 在服务类本身内部,不要使用任何具体的库(如axios或aws-sdk)。只使用接口。

请展示服务类和接口。”

AI输出(SOLID黄金版):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// --- 接口(契约) ---

// 服务不关心数据从何而来(API、数据库、文件)
interface UserRepository {
  getUserById(id: string): Promise<User>;
}

// 服务不关心邮件如何发送(SES、SendGrid、日志文件)
interface EmailSender {
  sendWelcomeEmail(email: string, name: string): Promise<void>;
}

interface User {
    id: string;
    email: string;
    name: string;
}

// --- 高层策略(应用逻辑) ---

class UserRegistrationService {
  // 依赖项被注入,而非在内部实例化。
  constructor(
    private readonly userRepo: UserRepository,
    private readonly emailSender: EmailSender
  ) {}

  async registerUser(userId: string): Promise<void> {
    // 步骤1:使用抽象的存储库契约获取用户
    const user = await this.userRepo.getUserById(userId);

    if (!user) {
        throw new Error("User not found");
    }

    // 步骤2:使用抽象的邮件发送器契约发送邮件
    await this.emailSender.sendWelcomeEmail(user.email, user.name);

    console.log(`User ${userId} registered successfully.`);
  }
}

看看这其中的区别。AI不仅仅是编写了代码;它设计了一个小型系统。

  • SRP:服务类现在只有一个职责:协调。获取的工作被推给了存储库实现;发送邮件的工作被推给了发送器实现。
  • OCP:我们现在可以创建一个实现 EmailSender 接口的 SendGridEmailSender 类,并将其注入,而无需改动 UserRegistrationService 的任何一行代码。系统对扩展开放,对修改关闭。
  • DIP:高层模块(UserRegistrationService)现在依赖于抽象(接口),而不是具体细节。测试变得轻而易举——我们只需注入满足接口的模拟对象。

设计SOLID提示词的蓝图

你可以将这种方法应用于几乎任何代码生成任务。以下是为获得高质量架构而设计提示词的清单:

  1. 定义角色:首先设定上下文。“扮演一位痴迷于编写干净、可维护代码的高级软件架构师。”
  2. 明确命名原则:不要拐弯抹角。“确保此代码遵守单一职责原则。如有必要,请分解大型函数。”
  3. 要求抽象:如果你的代码涉及外部系统(数据库、API、文件系统),明确要求先定义接口。“在实现业务逻辑之前,先为数据层定义一个接口。”
  4. 强制依赖注入:这是最有效的一个技巧。“主要的业务逻辑类不得自行实例化其依赖项。必须通过构造函数注入来提供。”

结论

生成式AI是一面镜子。如果你给它一个懒惰、模糊的提示词,它会反射回懒惰、模糊的代码。但如果你提供清晰的架构约束,它就能成为一个强大的生产力倍增器,用于产出高质量的专业软件。

不要仅仅要求AI写代码。要求它进行架构设计

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计