让 AI 写代码最爽的一刻是 prompt 下去、代码出来的那 30 秒;最崩的一刻是打开页面发现它「做完了」但根本不是你要的。

我上周把 Vibe Coding 第一课(莫欣)的讲义啃完,加了 5 道代码题实操,把 SpecDD + TDD 这套流程整理成 5 步。核心就一句话:AI 只能「应试」,不能「自证」——它写没写完、写对没写对,必须有一个先于代码存在的客观检查说了算。

先把两个词掰开

SpecDD(Spec-Driven Development,定义驱动开发):动手之前,先和 AI 把「什么算做对了」写成一份规格文件——场景、输入输出、关键行为、异常处理、验收方式,再加一条「本次不做什么」。检验标准很朴素:照着这份 Spec 干活,还有没有要猜的地方。 有,就还没到开工的时候。

TDD(Test-Driven Development,测试驱动开发):代码还没写,先写一个小测试表达 Spec 里的预期行为。这时功能未实现,测试必然失败(红)→ 写最小代码让它通过(绿)→ 整理代码不改行为(重构)。AI 场景里,这个测试就是防止 Agent「自称做完」的验收单。

关系一句话说清:SpecDD 定义验收标准,TDD 把标准变成先于代码存在的检查。AI 只能「应试」不能「自证」。

第 1 步:先画图纸再开工

让 AI 写码前,先写个文件讲清五件事:做什么给谁用|输入什么输出什么|正常该怎么表现|出了问题怎么办|最后怎么验收。再补一句「本次不做什么」:不自动回复、不管理后台。

边界清单比愿望清单好控得多。「做到什么程度算过」没有定义时,AI 给你做词频图也算「实现了需求」——但词频图、自动回复、问答页面解决的是三个不同的问题。

我做题时最深的一个体会是「口径必须写进 Spec」。题目要求按时间范围过滤消息,sent_at(发送时间)和 collected_at(采集时间)两种口径结果不同,Spec 确认后必须显式传入,不许默认;没有发送时间的消息「保留未知」,不许偷换成采集时间:

def filter_by_range(messages: list[dict], start, end, field: str) -> list[dict]:
    if field not in ("sent_at", "collected_at"):
        raise ValueError("时间口径必须由 Spec 明确指定")
    out = []
    for m in messages:
        t = m.get(field)
        if t is None:
            continue                 # 未知就是未知,不偷换口径
        if start <= t <= end:
            out.append(m)
    return out

第 2 步:AI 给你规划后,只盯一样东西:它自己脑补的

规划输出里那些「写得很肯定、但你从没提过」的规则——比如我遇到的「只分析带问号的评论」——是 AI 替你做的决定,不是需求。第一动作不是采纳也不是删掉,是追问:这条规则来自用户需求,还是你自己的建议?

审查规划可以按四类拆:已确认需求 / Agent 假设 / 实现方案 / 开发任务。重点盯第二类,跑得越顺的脑补规则越危险。

第 3 步:验收清单写在开工前

TDD 循环:红→绿→重构。两个坑:

  1. 红灯要坏得对。缺依赖、模块导不进来这类报错是「考场坏了」,不算验收——先修运行条件,这时候还不能说明业务规则得到了检查。
  2. 测试第一次就绿,别造假红灯。要么目标行为早已实现(保留即可),要么断言漏了关键结果——是断言的问题,不是改坏代码表演一次的事。

第 4 步:AI 应用最核心——验证分两种,别混

查逻辑(去重、筛选、引用不越界)→ 固定假数据 + 模型替身,逐字段断言,像数学题有标准答案。数数这种活是程序干的,不归模型:

class FakeModel:
    def __init__(self, answer): self.answer = answer; self.call_count = 0
    def complete(self, msgs, q):
        self.call_count += 1
        return self.answer

def start_analysis(snapshot: list[dict], question: str, model) -> dict:
    msgs = tuple(snapshot)           # 快照语义:本批输入就此冻结
    if len(msgs) == 0:
        return {"status": "empty_range"}
    ans = model.complete(msgs, question)
    return {"status": "ok", "answer": ans,
            "count": len(msgs),      # 程序统计,不是模型统计
            "input_ids": [m["msg_id"] for m in msgs]}

快照为空时断言 call_count == 0——模型一次都不能被调用;引用了范围外的 msg_id 断言被拒。这类用替身测,稳定、免费、可重复。

看效果(摘要准不准、有没有编)→ 真实调用、多样本反复跑。程序只能校验「引用没越界」,校验不了「引用真能支撑回答」——后者是语义判断,标准是「措辞可以变,意思必须留住」。一次通过≠稳定。

第 5 步:别信「测试全部通过」这句话

AI 报告全部通过时,看四项汇报里有没有:真实模型调用过吗?页面实际操作过吗? 程序逻辑测试 100% 通过,只说明确定性逻辑没问题;模型效果和用户流程仍然是待验证状态。这是两个轨道,不能互相顶替。

速查表

模块核心
Spec场景+输入输出+关键行为+异常+验收;「哪些还要靠猜」
审规划已确认需求/Agent假设/实现方案/开发任务;盯「很肯定但没讨论过」
用例前置+输入操作+预期+不应发生+检查方法
TDD红灯(因目标行为)→最小绿灯→重构;不为通过删断言
双轨验证程序逻辑用替身精确断言;模型效果真实调用+语义标准

一句话总结:一句话生成是表演,说清什么叫「做对了」才是工程。

本篇最早于 2026 年 10 月 16 日发布于小红书,此为博客完整版。