AI 代码需求实战:从“一句话需求“到“字段级 Spec“

0 阅读9分钟

摘要: 写 AI 代码需求时,最容易忽略的是"数据长什么样":字段名、类型、单位、归属、边界。我把需求从"一句话"拆到"字段级 Spec"三层,把 AI 最容易猜错的 4 类信息写清楚,翻车概率明显下降。附 4 类信息清单 + TS interface 和 zod 落地示例。

前阵子接了个导出需求

前阵子接了个需求,订单列表要加导出。这事我懒得自己写,直接在对话里让 AI 干,说了一句"写个订单导出功能"。

结果同一句话,我在三个不同的对话里让 AI 写,写出来的东西互相都对不上。第一个版本字段叫 order_amount,第二个叫 totalAmount,第三个更离谱,叫 orderMoney。金额单位一个按分存(12990),一个按元算(129.90)。空值处理,一个直接抛异常,一个默默返回 0。单个看,每个版本都能跑,放到一起,就是三份互相看不懂的代码。

我第一次看到三版代码的时候,第一反应是"AI 这届不行"。后来琢磨了半天,发现问题不在 AI,在我那句需求太省了。省到 AI 只能靠猜。

AI 不是在随机乱猜,是在按自己的习惯猜

这里有个容易误会的点。同一段需求,同一个上下文,AI 的输出其实挺稳定,不是什么"随机乱写"。真正的问题在别处:

AI 生成代码时,输入里没写的信息,它只能按训练数据里的普遍习惯来补。普遍习惯是什么——字段名用 camelCase、金额按"元"、空值返回 null、分页参数叫 page 和 size。

可真实项目往往完全是另一套约定。订单表按 snake_case 建,金额按分存避免浮点误差,空值要抛错不能静默。这些约定散落在你的代码库、数据库、老接口里,你嘴上知道,但没写进给 AI 的输入里。

所以 AI 不是"故意写错",是它看不到你的约定,只能拿自己那套普遍习惯去覆盖你的约定。上下文里没有的东西,它就用自己的默认值。

这跟我前面写的那几篇 AI 代码系列文章是一条线。信任分级那篇讲"AI 不知道你没告诉它的信息",越权那篇讲"AI 不知道订单归谁",测试那篇讲"AI 不知道调用方的数据长什么样"。绕来绕去,根子都是同一件事:信息缺口集中在数据层——字段名、类型、单位、归属、边界。

一句话需求,只给了 AI"要做什么功能"的意图,没给"数据长什么样"的契约。

第一版改进:功能清单,还是不够

我想着那就写详细点。把"导出订单"扩成功能点列表:

  • 支持按时间范围筛选订单
  • 支持勾选导出哪些列
  • 导出的 Excel 要包含订单号、金额、下单时间、收货人
  • 文件超过 5 万行要分文件

写完自己看了一遍,发现问题还在。这份清单定义的是"做什么",但金额按分还是按元存,还是没说。AI 读到"导出金额",照样按它的习惯当成"元"直接导出去。

功能清单把"功能意图"说清了,但"数据契约"依然是空的。缺的那部分,恰恰是翻车高发区。

第二版改进:字段级 Spec

后来我把需求改成了"字段级"的写法。核心就一件事:把 AI 最容易猜错的 4 类信息,一条条写出来。

维度没写的后果(AI 会怎么猜)对应前面哪篇的坑
字段定义(名字/类型/单位/默认值)单位猜成元、类型猜错、可空性乱来本弹陪跑:金额 ×100
数据来源与归属不知道订单归谁、谁能看越权翻车那篇的 IDOR
边界规则(空值/异常/极值)null 直接崩、异常不处理测试翻车那篇的 format 函数
实现约束(库/版本/风格)随手 import 不存在的库质量管控那篇的防线

这个表不是我自己发明的,是把前面几篇翻车文的根因收拢出来,发现全落在数据层的四个位置。

具体到订单导出,我的字段级需求长这样:

字段契约(导出订单):
- order_id: string,订单号,主键
- total_amount: number,单位分,金额 = 该值 / 100
- status: enum('pending','paid','shipped','cancelled')
- buyer_name: string,可空,空值导出为空字符串
- created_at: ISO8601 字符串,按下单时间倒序
数据归属:只能导出当前登录用户自己的订单
边界:total_amount 为空视为 0,不允许抛异常中断导出
实现:TypeScript + Node,不引入新依赖

同样是让 AI 写,这次它没再自由发挥。字段名、单位、可空性、归属、边界,全部有据可依。

三版需求对比

维度一句话需求功能清单字段级 Spec
字段名未定义,AI 猜未定义,AI 猜显式定义
单位未定义,默认"元"未定义,默认"元"显式定义(分)
归属未定义未定义显式定义
边界/异常未定义未定义显式定义
AI 输出一致性每次都不一样大体一致稳定
翻车概率高中低

一句话需求适合"无所谓细节"的探索性任务;功能清单适合"拼装已有能力"的常规任务;只要涉及金额、日期、状态、权限这些有约定数据,直接上字段级 Spec。

两种写法在生成端走的是完全不同的路,画出来是这样:

无契约时:
一句话需求 ──> AI 猜字段名/单位/边界 ──> 三种代码互相看不懂 ──> 上线才暴露

有契约时:
字段级 Spec ──> AI 引用契约字段 ──> 一致代码 ──> 开发期 zod 校验兜底

区别在第一步就定了。无契约时 AI 拿自己的习惯补信息,有契约时 AI 拿你的约定补信息,后面全顺着走。

为了验证不是运气,我做了个简单对照测试:同一份需求,无契约和有契约各让 AI 生成几次,把结果摊开对比:

验证项无契约(几次结果)有契约(几次结果)
字段名order_amount / totalAmount / orderMoney 轮着来统一 total_amount
单位有按分有按元全部按分
空值处理有抛异常有返回 0全部按契约约定

结果很直观:差距不在 AI 的水平,在输入里有没有那份契约。

落地:把 Spec 写进代码仓库,而不是 Word

字段级 Spec 我见过两种落法。一种是写成文档,放到 wiki 里,结果 AI 根本看不到,等于白写。

我的做法是把 Spec 变成代码仓库里可被引用的文件。一份 TS 类型定义,加上一份 zod 运行时校验(我用的是 zod 4.5.4 + TypeScript 5.9,zod 官方文档 里有完整的 schema API,TypeScript 类型推断 讲清楚了 z.infer 的推导规则):

// spec/export-order.ts
import { z } from "zod";

export const ExportOrder = z.object({
  order_id: z.string(),
  total_amount: z.number().int(),   // 单位:分,必须是整数
  status: z.enum(["pending", "paid", "shipped", "cancelled"]),
  buyer_name: z.string().nullable(),
  created_at: z.string(),          // ISO8601
});
export type ExportOrder = z.infer<typeof ExportOrder>;

为啥看这段: 一份可执行的数据契约。AI 编码工具基于代码库索引,大概率会把这份文件带进上下文,模型看到字段定义就会照着用,而不是自己发明。同时它本身也是运行时校验器,数据对不上会在开发期就报错,不会拖到上线。

运行结果(故意传错单位,看 zod 怎么拦):

input: { order_id: "O001", total_amount: 129.9, status: "paid", buyer_name: null, created_at: "2026-08-31T10:00:00Z" }
zod parse: ❌ total_amount: Expected integer, received float
说明:金额按分存,129.9 分不是合法整数分,zod 在开发期就拦下来了,根本走不到导出。

真正写导出的时候,流程是这样的——先 parse 校验,再按契约字段做换算:

import { ExportOrder } from "./spec/export-order";

const rows = [
  { order_id: "O001", total_amount: 12990, status: "paid", buyer_name: null, created_at: "2026-08-31T10:00:00Z" },
];

rows.forEach((row) => {
  const parsed = ExportOrder.parse(row);   // 进导出前先过契约
  writeExcel({
    order: parsed.order_id,
    amount: (parsed.total_amount / 100).toFixed(2),  // 契约里写了单位是分,这里就忘不了除以 100
  });
});

为啥看这段: 校验发生在数据进导出流程之前。parse 一旦不过,这一行直接抛错,代码根本走不到写 Excel 那一步。这也解释了为什么契约比"心里记住"可靠——它把"单位是分"这个约定从人脑搬进了代码执行路径。

运行结果(合法行 + 非法行各跑一次):

合法行:ExportOrder.parse({...total_amount: 12990...}) → 通过,amount 输出 "129.90"
非法行:ExportOrder.parse({...total_amount: 129.9...}) → 抛错,导出流程在此中断
对比:同样的字段名从 `order_amount`/`totalAmount`/`orderMoney` 三种乱象收敛为契约里唯一的 `total_amount`

再配一份 markdown,把"为什么"写清楚(单位为什么是分、归属规则为什么这么定),给人和 AI 一起看。这样契约就有了三重载体:类型约束(写代码时)、运行时校验(跑起来时)、文档说明(沟通时)。

要说明白一点,这套做法能起作用的前提,是 AI 工具能读到你代码库里的文件。我在 Cursor 这类基于项目索引的工具里用,契约文件经常被自动带进上下文。如果是纯网页对话框,没有代码库可读,那就得手动把字段表贴进去——这也是为什么我主张把 Spec 放在仓库里,而不是躺在 wiki。

这套做法不是万能的

把丑话说在前面。字段级 Spec 解决的是"数据契约"这一类问题,它有明确的边界:

复杂业务逻辑,比如多步骤审批的状态机流转,光靠字段定义约束不住,还是得人工 review(我之前那篇代码 Review 讲的就是这个)。Spec 写太厚也有反效果,一屏塞满字段,AI 反而抓不住重点,缩手缩脚。我的经验是一份 Spec 控制在一个文件、能一屏看完,超过这个量就该拆。

另外,如果你的 AI 工具根本不读代码库文件,那这份 Spec 就得自己贴。最后,它防的是"AI 不知道约定",防不了"约定本身设计错了"——字段契约只能忠实还原你的业务规则,不能替你拍板规则对不对。

回头看

写 AI 代码需求这件事,我最大的变化是心态:以前觉得"需求写得越详细越好",现在觉得关键是把 AI 要猜的信息写出来。功能描述可以简,数据契约不能省。

一句话需求 + 一张字段表,比写三页功能描述管用。这句话我用几次翻车换来的,现在每次让 AI 碰钱、碰状态、碰权限,都会先把字段级 Spec 摆到它面前。