Checkout 优惠码:一句话提示词 vs spec,六次真实运行

我们把同一个小型 checkout 仓库交给 Claude Code 六次。三次拿到的是一句话工单,三次拿到的是 spec。结果由一套在任何运行之前就写好的隐藏验收测试评分。模糊提示词大多写出了能用的代码,但没有两次做出同一个产品。

模糊提示词3 次中 2 次通过全部 11 项检查
Spec3 次中 3 次通过全部 11 项检查
真正的差距模糊运行选出了 2 种优惠码限制和 3 种付款行为

实验设置

仓库

一个没有任何依赖的 Node checkout 服务:totals.js 负责金额计算,store.js 存放商品、五个优惠码和一个空的 redemptions 列表,applyCoupon() 是一个直接抛错的桩函数。已有的两个测试能通过。

预置的优惠码覆盖了通常容易出错的情况:减 20%、减 $10、减 $50(比部分购物车金额还大)、一个已过期、一个已停用。

运行方式

Claude Code 2.1.284,claude-opus-5-5,无头模式(claude -p),每次都用一份全新的仓库副本,不加载用户设置、hooks 或 MCP 服务器。每次运行耗时 45 到 66 秒,3 或 4 轮。录制于 2026 年 9 月 28 日。

所有文件都在运行包里:fixture、提示词、隐藏验收测试,以及每次运行的 diff 和最终消息。

两份提示词原文

模糊版(运行 V1 到 V3)

Add coupon codes to checkout. Make sure invalid codes do not break payment.

Spec 版(运行 S1 到 S3),节选

Non-goals
- No admin, no stacking, no tax or rounding changes.
- Do not modify src/totals.js or src/processor.js.

Rules
- A coupon can be redeemed once per user. It counts as
  redeemed only when an order is paid.
- Applying a second coupon replaces the first.

Acceptance criteria
- AC-2 Unknown, disabled or expired code: applyCoupon
  returns { ok: false, reason }. It does not throw.
- AC-4 Redemption is re-checked inside pay.
- AC-5 Amount-off larger than the subtotal floors at 0.
  ... six ACs in total

隐藏验收测试评分表

测试集只调用仓库里原本就有的函数;拒绝无论以抛出错误还是返回错误结果的形式出现都算通过,所以不会因为模糊运行选了不同的 API 而扣分。标为未写明的检查项,测试的是一句话工单从未提到的规则。

检查项类型模糊Spec
H1 百分比优惠码作用于税前小计已写明3/33/3
H2 支付处理方的扣款金额与显示的总价完全一致已写明3/33/3
H3 过期优惠码不改变总价,付款正常已写明3/33/3
H4 已停用的优惠码不会被应用已写明3/33/3
H5 未知优惠码不影响 checkout 付款已写明3/33/3
H6 同一用户不能兑换同一优惠码两次未写明2/33/3
H7 一个用户的兑换不影响另一个用户未写明3/33/3
H8 只应用不付款不会消耗优惠码未写明3/33/3
H9 第二个优惠码替换第一个,不叠加未写明3/33/3
H10 减额大于购物车金额时扣款不会低于 0正确性3/33/3
H11 两个未付款的 checkout 不能同时用掉每人限用一次的优惠码未写明2/33/3

在未改动的仓库上,"拒绝"类检查(H3 到 H6、H10、H11)会直接通过,因为没有任何优惠码会被应用,所以它们只有和 H1、H2 放在一起看才有意义。每次运行都通过了自己写的测试:模糊运行新增 10 到 12 个测试,spec 运行新增 10 或 11 个。

各次运行真正不同的地方:决策

评分表很接近,决策却不是。这些结果是我们直接调用每次运行的代码测出来的,不是读它的总结得出的。

决策V1V2V3S1 到 S3
一个用户能用几次优惠码?一次不限一次一次(spec)
优惠码在应用和付款之间过期按原价扣款,$54.00拒绝第一次 pay()拒绝第一次 pay()按折后价 $43.20 扣款
无效码如何报告抛出 CouponError抛错抛错返回 { ok: false, reason }
优惠码匹配不区分大小写不区分大小写不区分大小写精确匹配
新增公共 APIremoveCouponremoveCouponremoveCoupon,新文件 coupons.js无

同一个提示词,两种优惠码策略

V1 把按用户记录的 redemptions 表当作提示,实现了每人限用一次。V2 看的是同样的数据,得出的结论是:"优惠码数据里没有按用户或一次性的使用限制,所以不做任何限制。"两者都在最终消息里说明了这一点。只有一个符合业务的本意。

一个金额问题,三种答案

当优惠码在付款前失效时,V1 会悄悄按比顾客看到的更高的金额扣款;V2 和 V3 会拒绝一次付款,前端现在得处理这种情况。每一种都说得通,但没有一种是团队里有人做出的选择。

前端契约也跟着漂移

所有模糊运行遇到无效码都会抛错;spec 运行返回 { ok, reason }。按其中一种写的前端,换到另一种上要么遇到未捕获的异常,要么悄悄忽略错误。工单从没说过前端团队是按哪种契约开发的。

spec 没有解决的问题

三次 spec 运行都接受了应用之后才过期的优惠码,并按折后总价扣款。spec 要求 pay 重新检查兑换状态,但对过期只字未提,所以 agent 就照做了,三次都是如此。S1 在最终消息里指出了这一点:

Re-checks at pay: pay only re-checks redemption, as AC-4 asks. A coupon that
expires or is disabled after being applied is still honoured at payment. If
you'd rather reject it there, pay can reuse the same checks applyCoupon runs.

这才是结果的真实样子。spec 不会让 agent 更聪明,它让 agent 的行为可预测,包括它的缺口。这里的缺口在 spec 里一行、在运行总结里一行就能看到,而不是分散在三种不同的实现里。

决定 H11 的那一行

两个未付款的 checkout,一个用户,一个限用一次的优惠码。两边是否都能拿到折扣,取决于付款时的一次重新检查。以下来自运行 S1 的 src/checkout.js:

export function pay(checkoutId) {
  const co = mustGet(checkoutId);
  if (co.status !== 'open') throw new Error(`checkout ${checkoutId} is ${co.status}`);
  // The coupon may have been redeemed on another order since it was applied here.
  if (co.couponCode && isRedeemed(co.userId, co.couponCode)) co.couponCode = null;
  const summary = getSummary(checkoutId);
  const ch = charge(co.userId, summary.total);
  ...
  if (co.couponCode) db.redemptions.push({ code: co.couponCode, userId: co.userId, orderId: order.id });

V1 和 V3 在没有被要求的情况下写了等价的检查。V2 根本没有按用户的规则,所以也就没什么需要重新检查的。

从六次运行中能得到什么

评审时别只看"能不能用"

在一个小而结构清晰的仓库上,当前的模型通常凭一句话就能交付能用的代码。评审要看的是它选了哪些规则:使用限制、异常路径上钱怎么处理,以及错误契约。

把涉及钱的规则写下来

消除了大部分差异的是两句话:"每个用户限用一次,以付款为准"和"无效码返回结果,绝不抛错"。即使其他一切都交给 agent,这两条也应该写进 spec。

把最终消息当 diff 来读

每次运行都列出了自己的假设。模糊运行的这些假设,正是工单本该包含的评审清单;合并前把它们转成验收标准。

这次测试的局限

  • 每种变体跑三次,足以展示差异,但不足以估计比率。"3 次中 2 次"的意思是"发生过一次",而不是"三分之一的概率会失败"。
  • 这个仓库小而干净。更大的代码库给 agent 更多误读的机会,也有更多地方可以放优惠码规则。
  • spec 和隐藏验收测试都是我们写的,所以 spec 自然覆盖了我们测试的内容。测试集在第一次运行前就已冻结,也从未给 agent 看过;spec 在过期问题上的缺口说明它并不是照着测试写的。
  • 只测了一个模型和一个工具。其他 agent 的结果可能不同;运行包就是为了让你自己验证。

同一个案例在 Sonnet 5.5、Haiku 4.5 和 Fable 5.1 上

没有 spec 时,Haiku 4.5 在 3 次运行中 3 次都没通过两项优惠码重复使用检查;有 spec 时,它每次都通过了全部 11 项检查。

查看跨模型结果

相关内容

完整的优惠码规格包

同一个功能做成可直接评审的规格包:任务、验收标准、QA fixture 和回滚方案。

打开优惠码案例

API 错误信封运行实录

九次运行,缺一份文档就决定了移动端 App 会不会崩。

打开 API 运行实录

拆分姓名迁移运行实录

九次针对表结构变更的运行,其中一次删掉了其他服务还在读的列。

打开迁移运行实录

在 agent 替你选规则之前,先把规则写下来

生成器会把一句话工单变成非目标、规则和验收标准,你可以直接贴在提示词前面。

编辑说明

本页的每个数字都来自运行包中录制的运行。评分前我们没有修改 agent 的任何输出。