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

同一个退款功能,Vibe Coding 和 Spec Coding 的结局截然不同
Daniel Marsh · Spec-First 工程笔记

Vibe coding 让人上瘾。用自然语言描述需求,AI 写代码,十分钟就能跑起来一个接口。我曾经深信不疑——直到用这种方式上线了一个退款功能,然后花了接下来两周时间修复那些一份 90 分钟的 spec 就能完全避免的 bug。这篇文章用完全相同的需求,走两条路线,让你亲眼看到差距在哪里打开。

发布于 2026-04-11 · ✓ 已更新 2026-05-06 · 阅读约 7 分钟 · 作者:Daniel Marsh · 审校:编辑政策

需求

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

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

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

路线 A:Vibe Coding

Vibe coding 从一条 prompt 开始:

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

六十秒后,AI 生成了一个干净的 RefundController,包含 createRefundgetRefundStatus 两个接口。它验证订单是否存在,检查退款金额是否超过订单总额,调用 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 分钟。四个补丁用了两周——包括排查时间、测试、客服升级工单,以及一次手动对账网关记录。所谓"快"的方式其实并不快。它只是把多巴胺前置了,把痛苦后置了。

路线 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 花了 90 分钟写,30 分钟和团队评审。路线 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
首个可运行接口耗时10 分钟3 小时(含写 spec)
达到生产就绪耗时2 周以上4 小时
线上发现的 bug4 个严重0
客户影响事故2 次(重复退款、超额退款)0
代码架构反应式补丁拼凑连贯的,与 spec 一致
AI 输出质量仅覆盖 happy path覆盖所有指定的边界情况
新人上手读代码 + Slack 记录 + 事故报告读 spec
上线信心低——"还有什么会炸?"高——验收标准已验证

Vibe coding 路线在最初 10 分钟里确实"更快"。之后,在每一个重要维度上都更慢。

教训不是"别用 AI"

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

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

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

我花在写退款 spec 上的 90 分钟,省下了两周的事故响应。这不是什么效率技巧。这是一种根本不同的工作方式

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

我会先上线哪一小块

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

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

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

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

可复制产物:Spec-First 启动块

团队第一次在真实变更里试 Spec-First 时,可以先用这段。它故意保持短,方便放进工单或 PR。

Spec-First 启动块:同一个退款功能,Vibe Coding 和 Spec Coding 的结局截然不同

本次要做的决策:
- 用一个真实变更验证 Spec-First 是否能减少理解偏差和返工。

责任人检查:
- 产品责任人:
- 工程责任人:
- QA 或运维评审:

范围边界:
- 本次包含:
- 本次不包含:
- 仍需确认的假设:

验收证据:
- 测试或 fixture:
- 日志、指标或截图:
- 人工复核步骤:

范围边界:评审者必须能拒绝不清楚的目标、缺失的非目标,以及没有证据的验收标准。

评审追问:
- 没参加需求会的人还会误解哪里?
- 哪个证据能证明这次改动足够安全,可以发布?

编辑复核记录

复核日期:2026-04-28。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。

关键词:vibe coding · spec coding · spec-first 开发 · AI 编程 · 订单退款 · 幂等性 · 状态机 · 边界情况

专题阅读路径

这篇文章归入 AI 编码治理 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。

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

编辑说明

本文介绍了 vibe coding 与 spec coding 的对比,面向软件交付团队。退款场景为工程说明示例,非财务或法律建议。