我用AI写完一个项目后才明白:开发效率的瓶颈,根本不是写代码

0 阅读7分钟

以前做一个新功能,最耗时间的是敲代码。

现在有了AI,生成接口、补全类型、编写测试,甚至搭建项目骨架都只需要几分钟。可真正用AI完成一个项目后,我发现了一个很现实的问题:

代码生成得越快,返工不一定越少。

问题往往不是AI不会写,而是需求、边界和数据结构还没确定,代码就已经生成了一大堆。

一、AI能快速写代码,但不知道你真正要什么

假设要开发一个“根据用户ID查询个人信息”的接口,AI很快就能生成:

app.get('/api/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id)

  if (!user) {
    return res.status(404).json({
      message: 'User not found'
    })
  }

  return res.json(user)
})

单独看,这段代码没有明显问题。但放进真实项目后,还要考虑:

  • 用户ID是什么格式;
  • 未登录用户能否调用;
  • 普通用户能否查看他人资料;
  • 手机号、邮箱是否需要脱敏;
  • 注销用户应该返回什么;
  • 数据库查询失败如何处理;
  • 接口是否需要限流。

这些问题没有确定之前,代码写得再快也只是一个“能运行的草稿”。

所以我现在使用AI时,不会一上来就让它生成代码,而是先问:

暂时不要写代码。请先列出这个需求中需要确认的业务规则、异常情况和潜在风险。

这一步看起来慢,实际上能减少大量返工。

二、不要一句话生成整个功能

刚开始使用AI编程时,我经常输入:

使用React、Node.js和MySQL,帮我实现一个完整的用户管理系统。

AI会迅速生成很多文件,看起来效率很高。但真正运行时,往往会发现:

  • 前后端字段名称不一致;
  • 数据库结构与业务逻辑冲突;
  • 不同接口使用了不同的异常格式;
  • 依赖版本与当前项目不兼容;
  • 修改一个字段,需要调整多个文件。

更稳妥的做法,是把一个功能拆成几个可以单独验证的步骤:

  1. 确定数据结构;
  2. 设计接口输入与输出;
  3. 编写数据库访问层;
  4. 实现业务逻辑;
  5. 添加权限与异常处理;
  6. 补充测试;
  7. 最后接入页面。

每完成一步就运行一次,确认没有问题再继续。

AI适合加快每个小步骤,但不适合替你跳过设计过程。

三、先确定接口,再生成前后端代码

多人协作时,最常见的问题并不是代码写错,而是双方理解的数据格式不同。

例如,后端返回:

{
  "token": "xxx",
  "user": {
    "id": 1,
    "name": "Tom"
  }
}

前端却按照下面的结构读取:

{
  "data": {
    "accessToken": "xxx",
    "userInfo": {
      "id": 1,
      "username": "Tom"
    }
  }
}

两边的代码单独看都能运行,连接起来却全是问题。

因此,在生成实现代码前,可以先固定接口类型:

interface LoginRequest {
  email: string
  password: string
}

interface LoginResponse {
  accessToken: string
  expiresIn: number
  user: {
    id: string
    nickname: string
    avatar: string | null
  }
}

然后再告诉AI:

前端、后端和测试代码必须遵守这份类型定义,不要自行修改字段名称和嵌套结构。

当输入和输出明确以后,AI生成的代码会稳定很多,后续联调也更省时间。

四、代码能运行,不代表功能可靠

AI生成的代码经常能够覆盖正常流程,却容易忽略异常情况。

例如:

const response = await fetch('/api/user')
const data = await response.json()
setUser(data)

正常请求时没有问题,但它没有考虑:

  • 网络中断;
  • 请求超时;
  • 登录状态失效;
  • 服务端返回500;
  • 返回内容不是JSON;
  • 组件卸载后请求才结束。

可以改成:

async function loadUser(signal: AbortSignal) {
  const response = await fetch('/api/user', { signal })

  if (response.status === 401) {
    throw new Error('LOGIN_EXPIRED')
  }

  if (!response.ok) {
    throw new Error(`REQUEST_FAILED_${response.status}`)
  }

  return response.json()
}

让AI审查代码时,也不要只问“有没有问题”,这种提问通常只能得到一些宽泛建议。

更有效的方式是:

请从权限控制、异常处理、并发请求、数据安全、资源释放和可测试性六个方面检查这段代码,并说明每个问题会在什么情况下发生。

检查范围越明确,AI越容易发现真正有价值的问题。

五、AI生成的代码,至少要经过三层验证

1. 静态检查

先处理最基础的问题:

  • 类型是否正确;
  • 变量是否可能为空;
  • 依赖是否真实存在;
  • API用法是否适用于当前版本;
  • 是否存在未处理的Promise;
  • 是否符合项目规范。

TypeScript、ESLint和格式化工具可以发现不少低级错误。能交给工具检查的内容,没有必要完全依靠人工阅读。

2. 运行验证

不要因为代码“看起来合理”就直接接受。

至少应该测试:

  • 正常输入;
  • 空值;
  • 错误格式;
  • 重复提交;
  • 超长内容;
  • 请求失败;
  • 并发操作;
  • 权限不足。

AI更容易生成理想条件下的实现,而真实用户不会永远按照理想流程操作。

3. 业务验证

代码通过测试,也不代表业务一定正确。

例如:

  • 优惠券接口可以正常调用,但允许同一用户重复领取;
  • 订单取消成功了,却没有恢复库存;
  • 邮箱修改成功了,却没有进行身份验证;
  • 删除用户成功了,却破坏了历史订单关联。

技术测试确认程序是否按照代码运行,业务验证确认代码做的是不是正确的事情。两者缺一不可。

六、让AI参与审查,不要替你做决定

AI很适合充当第二名代码审查者。

完成一个功能后,可以把需求、接口定义和核心代码一起提供给它,让它检查:

  • 是否遗漏异常分支;
  • 是否存在重复逻辑;
  • 函数职责是否过多;
  • 是否存在安全风险;
  • 测试是否覆盖关键路径;
  • 实现是否偏离原始需求。

但AI给出的修改建议不能全部照搬。

有些建议理论上更加优雅,却会让简单项目变得复杂;有些抽象能够减少几行重复代码,却提高了理解成本;还有些方案适合大型系统,并不适合当前项目。

面对一条修改建议,可以先判断:

  1. 它解决的是实际问题,还是假设中的问题?
  2. 修改后是否更容易理解和维护?
  3. 增加的复杂度是否符合项目规模?

AI可以帮你发现问题,但最终取舍仍然需要开发者完成。

七、比提示词更重要的,是项目上下文

很多人会收藏各种“万能提示词”,但真正影响生成质量的,往往是AI是否了解项目背景。

例如:

  • 为什么用户数据采用软删除;
  • 为什么订单接口必须保证幂等;
  • 为什么缓存时间设置为五分钟;
  • 为什么这个组件没有继续拆分;
  • 为什么订单编号不使用数据库自增ID。

这些决策如果没有记录,不仅AI不知道,开发者过一段时间也可能忘记。

可以在项目中保留一份简单的决策记录:

## 用户删除策略

采用软删除,不直接移除数据库记录。

原因:
1. 订单与操作日志需要保留用户关联;
2. 误删除后需要支持恢复;
3. 管理后台需要查询历史用户。

注意:
所有普通用户查询默认排除 deleted_at 不为空的数据。

后续让AI修改功能时,把这些记录一并提供,比反复调整提示词更有效。

结语

AI确实让写代码变快了,但开发效率的瓶颈也随之发生了变化。

过去,我们花大量时间查询语法、查找API和编写重复代码;现在,更重要的是明确需求、设计接口、识别边界,并判断生成结果是否可靠。

真正高效的AI编程,不是一次生成几千行代码,而是让每一段生成结果都能被理解、验证和维护。

AI可以快速给出答案,但决定应该问什么、哪些答案可以使用,依然是开发者最重要的能力。