摘要: 写 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 不存在的库 | 质量管控那篇的防线 |
这个表不是我自己发明的,是把前面几篇翻车文的根因收拢出来,发现全落在数据层的四个位置。
具体到订单导出,我的字段级需求长这样:
text
字段契约(导出订单):
- 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。
两种写法在生成端走的是完全不同的路,画出来是这样:
text
无契约时:
一句话需求 ──> 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 的推导规则):
ts
// 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 怎么拦):
text
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 校验,再按契约字段做换算:
ts
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 那一步。这也解释了为什么契约比"心里记住"可靠------它把"单位是分"这个约定从人脑搬进了代码执行路径。
运行结果(合法行 + 非法行各跑一次):
text
合法行: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 摆到它面前。