同一个退款功能,Vibe Coding 和 Spec Coding 的结局截然不同

Vibe coding 让人上瘾。用自然语言描述需求,AI 写代码,十分钟就能跑起来一个接口。麻烦在那之后才开始:真实流量会撞上 prompt 里从没提过的情况。这篇文章是一个演示性的对比:同一个退款功能、完全相同的需求,一次从一句话 prompt 出发,一次从一份简短的 spec 出发,让你看清差距在哪里打开。

statusreviewedpublished2026-04-11last-review2026-11-06read7 分钟author编辑部policy编辑政策

需求

一个电商平台需要订单退款功能。产品经理的需求简报很直接:

看起来不复杂,对吧?两条路线都从这里出发。接下来发生的事情,差异巨大。

两条分岔路:Vibe Coding 直接开始给 AI 写 prompt,Spec Coding 先写技术规格文档
同一个起点,两条截然不同的路

路线 A:Vibe Coding

Vibe coding 从一条 prompt 开始:

Prompt
"帮我用 Node.js 写一个订单退款 API。支持全额和部分退款。
调用支付网关执行退款。跟踪退款状态。用 Express 和 Postgres。"

六十秒后,AI 生成了一个干净的 RefundController,包含 createRefund 和 getRefundStatus 两个接口。它验证订单是否存在,检查退款金额是否超过订单总额,调用 paymentGateway.refund(),保存结果。代码看起来很专业,happy path 跑得通。

上线。

Bug #1:重复退款

客服点了退款按钮,页面卡了一秒,又点了一次。同一笔订单被退了两次。代码里没有幂等性检查。

修复 prompt:"加一个检查,防止同一订单重复退款。"

AI 加了一个数据库查询:如果该订单已有退款记录,就拒绝请求。能用——直到下一个 bug 出现。

Bug #2:部分退款超额

客户买了 200 美元的商品。客服先退了 50 美元,再退 80 美元,再退 100 美元。累计退款 230 美元——超过了订单金额。之前的重复检查只看是否有完全重复的记录,不看累计金额。

修复 prompt:"跟踪累计退款金额,拒绝超额退款。"

AI 加了一个 SUM(amount) 查询。但这个查询和插入操作没有在同一个事务里,所以两个并发的部分退款请求可以同时通过检查。

Bug #3:网关超时

支付网关超时了。退款记录已经在数据库里创建,状态是"处理中",但网关的响应永远没来。退款卡住了。客服看到状态是"处理中",但无法重试——重复检查阻止了他们。钱到底退了没有?没人知道。

修复 prompt:"给网关超时加重试逻辑,以及查询网关退款状态的能力。"

AI 加了一个重试循环——没有指数退避,没有给网关调用加幂等键,重试本身也没有超时。重试反而可能在网关那边创建另一笔重复扣款。

Bug #4:竞态条件

两个客服同时处理同一笔订单的退款。两个请求都通过了累计金额检查(第一笔退款还没提交),都调用了网关,都成功了。客户被退了两次款。

修复 prompt:"加锁,防止同一订单并发退款。"

到这里,代码已经被补了四次。每个补丁单独来看都合理,但整体架构已经是一堆反应式修补的拼凑品。没有清晰的状态机,没有记录的不变量,补丁之间的交互也没有测试覆盖。

Vibe Coding 的补丁螺旋:每次修 bug 都引入新的复杂性和新的边界情况,形成越来越混乱的补丁网
补丁螺旋:每一个修复都打开新的漏洞

真实代价

第一个版本只要 10 分钟。时间都花在四个补丁上:每一个都意味着排查线上问题、测试修复、处理客服升级,网关超时那次还得手动把本地记录和网关对账。而且 bug 是随着真实流量一个个冒出来的,清理工作会在首次上线后拖很久。所谓"快"的方式其实并不快。它只是把多巴胺前置了,把痛苦后置了。

路线 B:Spec Coding

同样的需求。同样的 AI。不同的起点:先写 spec。

Spec

退款功能 — 最小可行 Spec
# 功能:订单退款处理

## 目标
安全处理电商订单退款,确保不超退、不重复处理、
网关对账正确。

## 非目标
- 客户自助退款门户(后续阶段)
- 退款原因分析和报表
- 自动退款审批规则

## 状态机
  pending → processing → succeeded
  pending → processing → failed → pending(可重试)

同一订单同一时间只允许一笔退款处于 "processing" 状态。

## 验收标准

Given 订单金额 $200,已退款 $0
When 客服发起 $50 退款
Then 创建退款记录,状态为 "pending"
  And 使用幂等键调用网关
  And 网关成功后,状态变为 "succeeded"
  And 可退余额变为 $150。

Given 订单金额 $200,已退款 $150
When 客服发起 $75 退款
Then 请求被拒绝,提示"超出可退余额"
  And 不调用网关。

Given 一笔退款处于 "processing" 状态
When 同一订单收到新的退款请求
Then 请求被拒绝,提示"退款正在处理中"
  And 不调用网关。

Given 一笔退款处于 "processing" 状态
When 网关超时
Then 退款状态保持 "processing"
  And 后台任务以指数退避策略重试
  And 重试使用相同的幂等键
  And 3 次失败后,状态变为 "failed"
  And 通知支付团队。

## 边界情况
- 并发请求:在检查可退余额前对订单行加 SELECT FOR UPDATE
- 幂等性:每次退款尝试生成 UUID,网关调用携带此幂等键
- 部分退款精度:所有金额以分为单位(整数),不用浮点数
- 网关对账:每晚定时任务对比本地退款记录与网关结算报告

## 回滚方案
- 功能开关:refund_processing_v2
- 回滚后禁止新建退款;进行中的退款由后台任务继续处理
- 无需回滚数据库迁移(仅新增字段)

写到这个程度的 spec 是几个小时的事,不是几天;团队评审也拖不长,因为决策已经写在纸面上。路线 A 里冒出来的每一个边界情况,在这里都有了答案——在写第一行代码之前。

清晰的退款状态机图:显示各状态(pending、processing、succeeded、failed)及明确的转换规则和约束
状态机让合法的转换显式化,非法的转换不可能发生

实现

现在给同一个 AI 写 prompt——但把 spec 作为上下文:

Prompt(附 spec)
"按照这份 spec 实现退款功能。严格遵循状态机。
用 SELECT FOR UPDATE 做并发控制。所有网关调用
携带幂等键。金额以分为单位。"

[粘贴 spec]

输出在结构上完全不同。AI 生成了:

同一个 AI。同样的能力。截然不同的输出——因为输入截然不同。AI 没有变聪明;它得到了更好的约束。

同样的场景,提前覆盖

重复退款?SELECT FOR UPDATE 锁阻止并发处理。幂等键阻止网关重复扣款。两者都在第一版就有,不是后来打补丁加的。

部分退款超额?累计余额检查在事务内执行,和插入操作同一个事务。没有竞态窗口。

网关超时?后台任务用相同的幂等键重试。网关将其视为安全重试,而非新扣款。3 次失败后团队收到告警。

竞态条件?SELECT FOR UPDATE 将同一订单的所有退款操作串行化。第二个请求等第一个完成,然后看到更新后的余额。

Spec Coding 工作流:Spec → 评审 → 编码 → 测试,每一步都建立在前一步经过验证的决策之上
Spec coding 前置的是决策,不是代码

正面对比

维度Vibe CodingSpec Coding
首个可运行接口耗时几分钟几个小时(含写 spec)
达到生产就绪耗时没有上限:每冒出一个 bug 就多一轮补丁首版之后很快,因为边界情况已提前决定
本文这些边界情况流入生产全部四个没有:每一个都对应一条验收标准或边界规则
面向客户的故障模式重复退款、超额退款被行锁和事务内余额检查挡住
代码架构反应式补丁拼凑连贯的,与 spec 一致
AI 输出质量仅覆盖 happy path覆盖所有指定的边界情况
新人上手读代码 + Slack 记录 + 事故报告读 spec
上线信心低——"还有什么会炸?"高——验收标准已验证

在这个对比里,vibe coding 路线只在第一版上线之前"更快"。之后,在每一个重要维度上都更慢。

教训不是"别用 AI"

两条路线用的是同一个 AI。差别在于输入,不在于工具。Vibe coding 给 AI 自由去做你还没做的决策。Spec coding 先把这些决策显式地做完,然后交给 AI 一个有清晰正确性定义的约束问题。

AI 编程工具是力量倍增器。但把力量倍增器用在不明确的方向上,放大的是混乱。用在清晰的 spec 上,放大的是精确度。

Vibe coding 有它的用武之地——原型、探索、一次性脚本、黑客马拉松。这些场景里边界情况不重要,因为代码不会面对生产流量。但当你在构建涉及真实资金、真实用户或真实数据的东西时,你需要 spec。

花在退款 spec 上的几个小时,换掉的是路线 A 里四轮线上排查、打补丁和对账。这不是什么效率技巧。这是一种根本不同的工作方式。

如果你准备好切换了,30 天落地计划是一个好的起点。如果你想深入了解 spec 如何改善 AI 生成的代码,AI Prompt 指南有更详细的 prompt 结构说明。

先上线哪一小块

如果是这个退款功能,不要一开始就把所有退款分支都做完。先上线能证明契约安全的最小切片:全额退款、幂等性、支付渠道待确认状态,以及客服能看到的处理结果。

第一版可上线切片:
- 只支持全额退款
- 必须传 idempotency key
- 支付渠道待确认状态对客服可见
- 重复点击返回原始 refund_id
- 回滚时关闭退款创建,但保留状态查询

推迟到后续:
- 部分退款
- 多币种调整
- 批量退款
- 自动退款原因分类

这个切片比产品想象的小,但比 UI 演示大。它先验证最危险的决策,再扩展功能宽度。

想看真实运行记录?我们用真实 agent 做过类似的 vibe 与 spec 对比:Claude Code(Opus 5.5)上的 24 次运行,vibe prompt 对比 spec,由隐藏验收测试打分,所有 diff 都可下载。

交互式生成 Spec
填表单,获取完整的 Markdown 格式功能规格 — 免费,无需注册。
试试 Spec 生成器