同一个退款功能,Vibe Coding 和 Spec Coding 的结局截然不同
Vibe coding 让人上瘾。用自然语言描述需求,AI 写代码,十分钟就能跑起来一个接口。我曾经深信不疑——直到用这种方式上线了一个退款功能,然后花了接下来两周时间修复那些一份 90 分钟的 spec 就能完全避免的 bug。这篇文章用完全相同的需求,走两条路线,让你亲眼看到差距在哪里打开。
需求
一个电商平台需要订单退款功能。产品经理的需求简报很直接:
- 支持全额和部分退款
- 调用支付网关(类 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 分钟。四个补丁用了两周——包括排查时间、测试、客服升级工单,以及一次手动对账网关记录。所谓"快"的方式其实并不快。它只是把多巴胺前置了,把痛苦后置了。
路线 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 花了 90 分钟写,30 分钟和团队评审。路线 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 |
|---|---|---|
| 首个可运行接口耗时 | 10 分钟 | 3 小时(含写 spec) |
| 达到生产就绪耗时 | 2 周以上 | 4 小时 |
| 线上发现的 bug | 4 个严重 | 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 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 AI 编码治理 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
填表单,获取完整的 Markdown 格式功能规格 — 免费,无需注册。
编辑说明
本文介绍了 vibe coding 与 spec coding 的对比,面向软件交付团队。退款场景为工程说明示例,非财务或法律建议。
- 作者详情:Daniel Marsh
- 编辑政策:文章审核与更新机制
- 勘误联系:联系编辑