同一个退款功能,Vibe Coding 和 Spec Coding 的结局截然不同
Vibe coding 让人上瘾。用自然语言描述需求,AI 写代码,十分钟就能跑起来一个接口。麻烦在那之后才开始:真实流量会撞上 prompt 里从没提过的情况。这篇文章是一个演示性的对比:同一个退款功能、完全相同的需求,一次从一句话 prompt 出发,一次从一份简短的 spec 出发,让你看清差距在哪里打开。
需求
一个电商平台需要订单退款功能。产品经理的需求简报很直接:
- 支持全额和部分退款
- 调用支付网关(类 Stripe)执行退款
- 跟踪退款状态:待处理、处理中、成功、失败
- 客服通过内部工具触发退款
看起来不复杂,对吧?两条路线都从这里出发。接下来发生的事情,差异巨大。
路线 A:Vibe Coding
Vibe coding 从一条 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:"加锁,防止同一订单并发退款。"
到这里,代码已经被补了四次。每个补丁单独来看都合理,但整体架构已经是一堆反应式修补的拼凑品。没有清晰的状态机,没有记录的不变量,补丁之间的交互也没有测试覆盖。
真实代价
第一个版本只要 10 分钟。时间都花在四个补丁上:每一个都意味着排查线上问题、测试修复、处理客服升级,网关超时那次还得手动把本地记录和网关对账。而且 bug 是随着真实流量一个个冒出来的,清理工作会在首次上线后拖很久。所谓"快"的方式其实并不快。它只是把多巴胺前置了,把痛苦后置了。
路线 B:Spec Coding
同样的需求。同样的 AI。不同的起点:先写 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 里冒出来的每一个边界情况,在这里都有了答案——在写第一行代码之前。
实现
现在给同一个 AI 写 prompt——但把 spec 作为上下文:
"按照这份 spec 实现退款功能。严格遵循状态机。 用 SELECT FOR UPDATE 做并发控制。所有网关调用 携带幂等键。金额以分为单位。" [粘贴 spec]
输出在结构上完全不同。AI 生成了:
- 一个
processRefund函数,包裹在带SELECT FOR UPDATE的数据库事务中 - 累计退款余额检查在事务内部
- 退款创建时生成幂等键(UUID),每次网关调用都携带
- 带指数退避的后台重试任务,最多 3 次
- 与 spec 状态机完全匹配的状态转换
- 拒绝超出可退余额的输入验证
同一个 AI。同样的能力。截然不同的输出——因为输入截然不同。AI 没有变聪明;它得到了更好的约束。
同样的场景,提前覆盖
重复退款?SELECT FOR UPDATE 锁阻止并发处理。幂等键阻止网关重复扣款。两者都在第一版就有,不是后来打补丁加的。
部分退款超额?累计余额检查在事务内执行,和插入操作同一个事务。没有竞态窗口。
网关超时?后台任务用相同的幂等键重试。网关将其视为安全重试,而非新扣款。3 次失败后团队收到告警。
竞态条件?SELECT FOR UPDATE 将同一订单的所有退款操作串行化。第二个请求等第一个完成,然后看到更新后的余额。
正面对比
| 维度 | Vibe Coding | Spec 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 都可下载。
填表单,获取完整的 Markdown 格式功能规格 — 免费,无需注册。