前言:代码不是写出来的,是"规"出来的
前三篇文章,我写了项目背景、技术选型、架构设计。文章发出去后,不少朋友问我:"你这些文档写得这么细,那代码到底怎么写?是不是还是要自己手写?"
我的答案是:代码不是手写的,也不是 AI 瞎写的,是"规"出来的。
在把任务清单交给 AI 之前,我花了整整一周,做了一件看起来"最枯燥"的事:把每一个任务,升级成"接口规格文档"。
这篇文章,我就讲这个过程------怎么把"打开 B 站"这个需求,变成 AI 能直接生成代码的"函数签名 + 参数说明 + 验收标准"。
一、从"任务清单"到"接口规格":为什么不能跳步
在第三篇文章结束时,我的项目已经有了三份任务清单(边缘端、云端、安卓端),每份清单都拆到了"原子任务"粒度。
| 任务清单长这样 | 问题 |
|---|---|
E7.2: 实现 open_app 命令 |
AI 不知道函数名、参数类型、返回值、错误处理 |
C6.1: 定义 open_app 工具 |
AI 不知道工具描述、参数 schema、返回格式 |
A4.1: 实现 AudioRecorder 类 |
AI 不知道有哪些方法、参数、回调 |
如果我直接把这份清单丢给 AI,它会怎么做?
-
第一次生成:函数名叫
OpenApp -
第二次生成:函数名叫
LaunchApp -
第三次生成:参数用
pkg,第四次用packageName
每次都不一样。不是 AI 不行,是我没给它"标准答案"。
所以,在让 AI 写代码之前,我必须先把"标准答案"写好------这就是接口规格文档。
二、接口规格文档长什么样
我把每个任务,都升级成包含以下内容的规格:
| 要素 | 说明 |
|---|---|
| 文件路径 | 代码放哪 |
| 函数/类/接口签名 | 完整的代码定义 |
| 参数说明 | 类型、含义、示例 |
| 返回值说明 | 成功/失败的结构 |
| 实现逻辑 | 关键步骤(不是代码,是步骤) |
| 验收标准 | 可执行的命令或断言 |
边缘端 E7.2 的接口规格(节选)
markdown
### E7.2: 实现 open_app 命令
**文件**: `internal/adb/adb.go`
**函数签名**:
```go
func (c *ADBController) OpenApp(ctx context.Context, packageName string) error
参数:
-
ctx: 超时控制,默认 5 秒 -
packageName: 应用包名,如tv.danmaku.bili/.ui.splash.SplashActivity
返回值:
-
nil: 成功 -
error: 失败时返回,包含错误码E1002
实现逻辑:
-
先调用
ensureConnected(ctx)确保 ADB 已连接 -
执行
adb shell am start -n {packageName} -
执行成功返回 nil,失败返回错误
验收标准:
-
单元测试:
TestOpenApp验证命令拼接正确 -
手动测试:
OpenApp(ctx, "tv.danmaku.bili/...")后,电视打开 B 站
text
### 云端 C6.1 的接口规格
```markdown
### C6.1: 定义 open_app 工具
**文件**: `src/tools/openApp.ts`
**工具定义**:
```typescript
export const openAppTool = tool(async ({ packageName }) => {
return JSON.stringify({ action: "open_app", params: { package: packageName } });
}, {
name: "open_app",
description: "打开电视上的应用",
schema: z.object({
packageName: z.string().describe("应用包名,如 tv.danmaku.bili/.ui.splash.SplashActivity")
})
});
验收标准:
- 调用
openAppTool.invoke({ packageName: "tv.danmaku.bili/..." })返回{"action":"open_app","params":{"package":"..."}}
text
### 安卓端 A4.1 的接口规格
```markdown
### A4.1: 实现 AudioRecorder 类
**文件**: `app/src/main/java/com/homesense/AudioRecorder.kt`
**接口定义**:
```kotlin
class AudioRecorder(private val onResult: (File) -> Unit) {
fun start(durationMs: Long = 3000)
fun stop()
fun release()
}
实现细节:
-
使用
MediaRecorder -
输出格式:
OutputFormat.MPEG_4 -
音频编码:
AudioEncoder.AAC -
采样率:16000,声道:单声道
-
文件路径:
context.cacheDir/audio_${timestamp}.m4a
验收标准:
- 调用
start(3000),3 秒后onResult收到文件,文件大小 < 20KB
text
## 三、我花了多久,值得吗
三份任务清单,Phase 1 一共 30 多个核心任务。我花了 **3 天**,把每个任务升级成接口规格。
这 3 天值不值?
| 对比 | 没接口规格 | 有接口规格 |
|------|-----------|-----------|
| AI 生成代码 | 每次猜,可能 3 轮才能对 | 1 轮通过 |
| 代码一致性 | 同一功能不同命名 | 全项目统一 |
| 验收 | 手动看代码对不对 | 跑验收命令,过就是过 |
| 返工率 | 约 40% | < 5% |
**3 天的投入,换来后面 2 周零返工。这笔账,太值了。**
## 四、接口规格文档的“铁律”
在写接口规格时,我给自己定了三条铁律:
### 铁律 1:签名必须完整
```go
// 不完整
func OpenApp(pkg string) error
// 完整
func (c *ADBController) OpenApp(ctx context.Context, packageName string) error
铁律 2:参数必须有示例
typescript
// 不完整
packageName: z.string()
// 完整
packageName: z.string().describe("应用包名,如 tv.danmaku.bili/.ui.splash.SplashActivity")
铁律 3:验收必须可执行
markdown
// 不可执行
验收标准:功能正常
// 可执行
验收标准:
- 单元测试:`go test -run TestOpenApp` 通过
- 手动测试:`OpenApp(ctx, "tv.danmaku.bili/...")` 后,电视打开 B 站
五、拿到接口规格后,AI 的反应
我把接口规格文档发给 AI 后,它的表现让我吃惊:
以前:
我:实现 open_app 命令
AI:好的,我创建了一个
LaunchApp函数...我:不对,函数名应该是
OpenAppAI:好的,我改成
OpenApp...我:参数名应该是
packageNameAI:...
现在:
我:请按 E7.2 接口规格实现 open_app 命令
AI:已实现
internal/adb/adb.go中的OpenApp方法,参数为packageName,包含超时控制和错误码 E1002。单元测试已通过。
一次过。
六、这个过程教会我什么
-
AI 不是读心术,你需要给它"标准答案"
接口规格就是标准答案。写得越细,AI 猜得越少。
-
3 天的投入,换 2 周的零返工
前期懒,后期苦。接口规格是最值得花时间的地方。
-
接口规格不是"写文档",是"定契约"
你写的不是给 AI 看的说明书,是你和 AI 之间的合同。合同明确,执行就稳。
-
验收标准必须是"机器能跑的"
"功能正常"是废话。"
go test通过"才是验收。
七、下一篇预告
接口规格写完了,任务清单变成了"可执行合同"。下一篇文章,我会写:
《第五篇:逐行验收------我是怎么让 AI 按规格生成代码,并一行行验证的》
内容包括:
-
怎么把接口规格发给 AI,一次只发一个任务
-
怎么验收 AI 生成的代码(不是看,是跑)
-
怎么用 Makefile 把验收自动化
-
遇到 AI 输出不对时,怎么让它在原文件上修正
如果你也在用 AI 写代码,欢迎关注。这条路,我们一起趟。
附:接口规格文档的完整示例(边缘端)
我把边缘端 E7 的接口规格完整版贴在下面,供参考:
markdown
### E7: ADBController 实现
**文件**: `internal/adb/adb.go`
**结构体定义**:
```go
type ADBController struct {
mu sync.Mutex
device gadb.Device
tvIP string
tvPort int
connected bool
}
E7.1: 连接管理
函数签名:
go
func (c *ADBController) ensureConnected(ctx context.Context) error
-
指数退避重试(1s,2s,4s),最多 3 次
-
使用
gadb.Connect建立连接 -
连接失败返回
E1001错误
E7.2: open_app 命令
函数签名:
go
func (c *ADBController) OpenApp(ctx context.Context, packageName string) error
-
先调用
ensureConnected -
执行
adb shell am start -n {packageName} -
成功返回 nil,失败返回
E1002
E7.3: tap 命令
函数签名:
go
func (c *ADBController) Tap(ctx context.Context, x, y int) error
- 执行
adb shell input tap x y
...(其余命令类似)
text
**有了这份规格,AI 写的代码和我想的完全一样。**