# 示例与反例

Source: https://codewiki.com/zh/ai-era/examples-and-counterexamples/

> - **what**: 示例为编码请求提供具体的输入输出案例，反例则指出看似合理的归纳应在哪里停止。
> - **trap**: 一个正常路径可以符合许多错误规则，一长串相似案例也可能没有规定边界、规则交互与失败行为。
> - **fix**: 为每个代表性案例配一个相邻的判别案例，写明字面预期行为；尚未决定的案例要明确标注，不能让模型猜测。

## 是什么，为什么存在

编码请求中的示例，是你希望特定输入与状态产生的一项具体观察。它可以把函数实参映射到返回值，把 HTTP 请求映射到响应，或者把事件映射到状态转换。真正有用的不是示例语法，而是示例明确承诺的行为。

反例用于推翻某条诱人但错误的规则。如果唯一案例是「库存为 4，补货阈值为 8 时执行补货」，实现即使把阈值硬编码为 10，看起来仍然正确。「库存为 8，阈值为 5 时保持不变」可以排除这种归纳。

反例不一定是无效输入。略低于折扣边界的有效订单、查看归档记录的管理员，以及合法的空集合，都可以成为过宽规则的反例。无效输入是一个重要类别，但它回答的是另一个问题：接口如何拒绝定义域以外的值。

调用方依赖这组行为时，示例与反例就属于 API 契约（API contract）的一部分。示例标明必须发生的行为，反例标明相近但不得发生的行为。两者共同缩小了能够合理声称满足请求的实现范围。

这对编码智能体很重要，因为模型必须补全所有空白，才能生成完整代码。它常会选择熟悉的默认规则，例如用真假值做验证、采用排除等号的边界、把拒绝改成规范化，或者按描述中的先后顺序排列分支。流畅的实现会掩盖这些决定，直到相邻的生产案例出现不同结果。

示例仍然只是有限证据，不能证明所有输入都正确。同一个舒适区中的十个案例，可能还不如边界两侧的两个值约束得多。案例的选择比数量更重要。

请求新函数、修改遗留行为、定义解析器、审查生成测试或解释缺陷时，都可以使用这种方法。当「有效」「附近」「活跃」「空」和「超过」等普通用语存在多种合理解释时，它尤其有价值。

目标不是提前规定所有实现细节，而是在稳定的可观察边界上公开产品决定，同时允许内部结构变化。好的案例能够拒绝错误行为，却不会强制采用无关的辅助函数、循环或数据结构。

## 工作原理

先把请求写成一条行为规则，给输入命名，并规定可观察结果。再列出可能改变答案的维度：数值范围、类别、状态、权限、时序、缺失值与失败通道。代表性示例和反例应从这些维度构成的空间中选取。

### 有效案例的组成

每个案例都要有存在理由。否则，后续维护者无法判断某个值是有意选择还是偶然细节，智能体也可能保留噪声，却遗漏真正的规则。

| 部分 | 要记录的内容 | 库存案例 |
| --- | --- | --- |
| 名称 | 该案例确定的决定 | 包含等号的补货阈值 |
| 给定 | 相关输入与先前状态 | `stock: 8`、`reorderAt: 8`、活跃商品 |
| 操作 | 公开操作 | 计算补货决定 |
| 结果 | 字面结果与副作用 | 返回 `"reorder"`，不执行写入 |
| 对照 | 它拒绝的可能规则 | 拒绝 `stock < reorderAt` |

名称应该描述差异，而不是只重复输入。「案例 4」和「库存等于 8」都会迫使读者重新推断理由。「活跃商品恰好位于包含等号的阈值」直接说明了等号出现的原因。

预期输出必须足够独立，才能充当 测试预言机（test oracle）。字面值、稳定错误类型或业务不变量都能提供这种独立性。如果用与生产实现相同的条件计算预期值，只会把同一种误解复制一遍。

### 代表性示例确定中心

代表性示例代表输入空间中的一个有意义区域。对于补货规则，一个低于阈值的普通活跃商品就是有效代表。对于解析器，一个规范的有效字符串可以确定接受形式和结果类型。

代表应来自业务类别，而不是任意分散的数字。如果所有值都是普通有效数量，`10`、`20` 和 `30` 提供的信息很少。活跃商品、停售商品与状态未知的商品有所不同，是因为契约对它们的处理不同。

正向示例还会确定输出结构。`25 -> 25` 可以区分返回数字的解析器与返回原字符串的解析器。HTTP 示例同样应写明状态、稳定字段与相关副作用，不能只说「成功」。

### 反例截断错误规则

反例总是相对于某条候选规则而言。先说明智能体可能推断什么，再选择一个最小案例，使该推断与预期行为产生不同结果。这样，案例就能诊断问题，而不只是显得特殊。

对于 `stock <= reorderAt`，相等案例可以区分包含等号与严格比较。自定义阈值可以区分逐商品阈值与硬编码常量。停售商品可以区分完整策略与只看阈值的规则。

相邻案例很有力，因为它们只改变一个有意义的事实。当阈值为 7 时，`stock: 7` 与 `stock: 8` 的结果不同，而无关字段保持不变，因此结果改变的原因很清楚。如果每个字段都变化，就可能有多条规则解释这组对照。

反例不必都在数值上相邻。对于类别行为，只改变一个类别；对于有状态行为，重复执行操作；对于权限，固定资源，只改变操作者或状态。核心原则是受控对照。

### 边界、分区与交互

按照明确顺序构建案例集：

1. 为每个行为类别选择一个普通有效案例。
2. 在每个包含或排除等号的边界两侧加入精确值。
3. 加入空值、缺失值、格式错误值和越界值，并为它们规定不同的预期失败。
4. 加入一个同时触发两条规则的案例，以确定优先级。
5. 当历史可能改变结果时，加入重复操作或并发操作。
6. 当返回正确值仍然不够时，加入禁止发生的副作用。

第四步通常需要 决策表（decision table）。单条件示例能够证明每条规则存在，却仍会让规则交互保持模糊。一个重叠案例就能揭示「已归档」覆盖「管理员」，还是相反。

禁止行为也是输出的一部分。「返回 `deny`，并且不发出审计成功事件」比单纯的「拒绝」更明确。对于失败案例，要写明状态是否保持不变、重试是否安全，以及调用方从哪个错误通道观察失败。

### 明确保留未知项

遗漏案例不表示实现可以自由选择。如果产品负责人尚未决定是否去除两端空白，就写明「未决定：两端空白」，并请求确认。这个标记可防止生成实现与生成测试默契地接受同一项臆造行为。

在遗留代码中，应区分当前行为与预期行为。特征案例可能记录 `"001"` 当前会解析为 `1`，而新契约可能要求拒绝。明确标注两种状态，避免智能体把旧行为意外保留成兼容性承诺。

当每种重要错误解释都被至少一个案例拒绝，而每项必需行为都有代表性案例时，案例通常已经足够。「足够」取决于风险：授权与金额计算需要的对抗案例比私有显示格式化器更多。只要每个案例各有作用，案例集就可以保持精简。

## 示例

下面的示例使用 Node 严格断言，但重点是案例选择，不是断言库。每个文件都能独立运行，并已用 Node `v24.14.0` 执行；输出展示了每增加一个案例会怎样减少歧义。

### 排除可能的库存规则

第一个低库存案例同时符合四种实现。之后每个反例只改变一项驱动决定的事实，因此错误候选会因明确理由被排除。

<!-- quick -->

```js
// file: reorder_candidates.mjs
import assert from "node:assert/strict";

const implementations = {
  exact: ({ stock, reorderAt, discontinued }) =>
    !discontinued && stock <= reorderAt ? "reorder" : "hold",
  strictBoundary: ({ stock, reorderAt, discontinued }) =>
    !discontinued && stock < reorderAt ? "reorder" : "hold",
  fixedThreshold: ({ stock, discontinued }) =>
    !discontinued && stock <= 10 ? "reorder" : "hold",
  ignoresOverride: ({ stock, reorderAt }) =>
    stock <= reorderAt ? "reorder" : "hold",
};

const cases = [
  {
    name: "representative low stock",
    input: { stock: 4, reorderAt: 8, discontinued: false },
    expected: "reorder",
  },
  {
    name: "counterexample at the inclusive boundary",
    input: { stock: 8, reorderAt: 8, discontinued: false },
    expected: "reorder",
  },
  {
    name: "counterexample with a custom threshold",
    input: { stock: 8, reorderAt: 5, discontinued: false },
    expected: "hold",
  },
  {
    name: "counterexample for the discontinued override",
    input: { stock: 0, reorderAt: 8, discontinued: true },
    expected: "hold",
  },
];

let survivors = Object.entries(implementations);
for (const example of cases) {
  survivors = survivors.filter(([, decide]) =>
    decide(example.input) === example.expected
  );
  assert.ok(survivors.length > 0);
  console.log(`${example.name}: ${survivors.map(([name]) => name).join(", ")}`);
}
```

```text
representative low stock: exact, strictBoundary, fixedThreshold, ignoresOverride
counterexample at the inclusive boundary: exact, fixedThreshold, ignoresOverride
counterexample with a custom threshold: exact, ignoresOverride
counterexample for the discontinued override: exact
```


<!-- /quick -->

仅靠代表性案例，几乎无法确认等号、配置或生命周期状态。它为智能体提供了有用的中心，但四条规则在这里完全一致。在一个点上结果相同，不代表它们在其他位置也相同。

相等案例只排除 `strictBoundary`，自定义阈值只排除 `fixedThreshold`，停售案例则排除最后一条只看阈值的规则。这是一组判别集，因为每行都能把预期行为与至少一种可信错误分开。

该文件比较候选实现，是为了让逐步收窄的过程可见。真实提示词不必列出候选代码，但应该在文字中点明可能的误解：包含等号的边界、逐商品阈值，以及停售商品覆盖规则。

### 规定接受形式与失败类别

「解析一个批量大小，例如 `25`」没有确定类型转换、空白、前导零、后缀与范围错误。下面的案例规定一个规范输入，以及四个具有调用方可见错误类型的反例。

```js
// file: batch_size_examples.mjs
import assert from "node:assert/strict";

function parseBatchSize(raw) {
  if (typeof raw !== "string") {
    throw new TypeError("batch size must be a string");
  }
  if (!/^[1-9]\d{0,2}$/.test(raw)) {
    throw new TypeError("canonical decimal string required");
  }
  const value = Number(raw);
  if (value > 100) {
    throw new RangeError("batch size must be at most 100");
  }
  return value;
}

const cases = [
  { input: "25", expected: 25 },
  { input: "001", error: TypeError },
  { input: "25 items", error: TypeError },
  { input: 25, error: TypeError },
  { input: "101", error: RangeError },
];

for (const example of cases) {
  try {
    const actual = parseBatchSize(example.input);
    assert.equal(actual, example.expected);
    console.log(`${JSON.stringify(example.input)} -> ${actual}`);
  } catch (error) {
    assert.ok(error instanceof example.error);
    console.log(`${JSON.stringify(example.input)} -> ${error.name}`);
  }
}
```

```text
"25" -> 25
"001" -> TypeError
"25 items" -> TypeError
25 -> TypeError
"101" -> RangeError
```

虽然 `"001"` 只含数字，前导零案例仍会排除宽松的数字转换。带后缀的字符串排除局部解析，数字输入则防止智能体只因 JavaScript 处理方便就接受多种类型。

两种错误类别保留了一项有用差异。`TypeError` 表示输入表示形式不被接受，`RangeError` 表示形式有效，但值超出支持范围。如果调用方不使用这种差异，统一采用一种稳定验证错误可能形成更简单的契约。

案例集仍然没有明示 `"1"` 与 `"100"` 两个边界。生产解析器还应加入它们，以及低于范围的 `"0"`。反例会让剩余缺口显现出来，却不会自动填补缺口。

### 用决策表覆盖交互

角色、所有权与归档状态单独看来都很简单。下面的反例会同时激活它们，让分支优先级变得可观察。

```js
// file: permission_matrix.mjs
import assert from "node:assert/strict";

function documentPermission({ role, isOwner, archived }) {
  if (!new Set(["admin", "member", "viewer"]).has(role)) {
    throw new TypeError("unknown role");
  }
  if (archived) return role === "admin" ? "view" : "deny";
  if (role === "admin") return "edit";
  if (role === "member" && isOwner) return "edit";
  return "view";
}

const cases = [
  {
    name: "admin edits an active document",
    input: { role: "admin", isOwner: false, archived: false },
    expected: "edit",
  },
  {
    name: "member edits an owned active document",
    input: { role: "member", isOwner: true, archived: false },
    expected: "edit",
  },
  {
    name: "member only views another active document",
    input: { role: "member", isOwner: false, archived: false },
    expected: "view",
  },
  {
    name: "viewer ownership does not grant editing",
    input: { role: "viewer", isOwner: true, archived: false },
    expected: "view",
  },
  {
    name: "archive removes admin editing",
    input: { role: "admin", isOwner: true, archived: true },
    expected: "view",
  },
  {
    name: "archive blocks an owning member",
    input: { role: "member", isOwner: true, archived: true },
    expected: "deny",
  },
];

for (const example of cases) {
  const actual = documentPermission(example.input);
  assert.equal(actual, example.expected, example.name);
  console.log(`${example.name}: ${actual}`);
}
```

```text
admin edits an active document: edit
member edits an owned active document: edit
member only views another active document: view
viewer ownership does not grant editing: view
archive removes admin editing: view
archive blocks an owning member: deny
```

前两行确定普通编辑路径。查看者兼所有者一行是「所有者都能编辑」的反例，因为所有权只为成员授予编辑权。归档案例确定归档状态会覆盖编辑权限，但管理员仍然保留读取权限。

这不是三种角色、两种所有权状态与两种归档状态的完整笛卡尔积，而是一组精选矩阵，其中每个遗漏行都应能从经过审查的规则推导出来。当风险高，或者规则实现难以检查时，应生成完整组合。

授权必须在用户界面之下实施，并在真实策略边界上测试。这些案例规定了决策函数，却不能证明每条路由都会调用它，也不能证明数据读取实施了相同范围。

## 陷阱

### 只展示一个正常路径

> **陷阱:** 单个成功输入会诱导最短且最熟悉的归纳。生成函数可能硬编码示例常量，使用真假值，或者让所有类别都采用已展示类别的处理方式，但仍能精确复现这个示例。

**修复方法：** 增加案例前，先写出两条可能的错误规则。保留代表性示例，再为每条错误规则增加一个最小反例，使它产生不同的可观察结果。

### 增加许多相关案例

> **陷阱:** 五个免运费案例如果全是大额法国标准订单，看起来覆盖很多，实际却在重复测试一个区域。它们没有确定精确阈值、加急配送优先级、国外目的地或无效金额。

**修复方法：** 建立维度表，并标明每个案例覆盖哪些值。用边界对、类别变化，以及至少一个规则重叠案例替换重复行。

### 把所有无效输入都叫反例

> **陷阱:** 把有效对照与格式错误输入混在一起，会掩盖两个不同契约。智能体可能正确拒绝字符串，却仍然对有效边界值应用错误规则。

**修复方法：** 把案例标为代表性有效行为、有效反例或无效输入拒绝。对于拒绝的输入，应另行规定错误通道与禁止的状态变化。

### 让实现生成预言机

> **陷阱:** 要求智能体在同一次工作中实现函数并编造所有预期输出，可能会让同一个错误条件同时出现在比较两侧。生成测试通过时，只能证明内部一致，不能证明符合产品意图。

**修复方法：** 在生成前批准字面结果与错误类别，或者从独立策略来源推导它们。改变一条边界或优先级规则，并确认至少有一个案例失败。

### 过度规定偶然细节

> **陷阱:** 如果案例断言辅助函数名称、分支顺序、日志文字与对象标识，它可能拒绝正确的重构。智能体会模仿当前实现，而不是保留调用方可见行为。

**修复方法：** 在公开边界上断言结果、稳定失败、状态转换与必需副作用。只有内部交互本身属于契约时才提及它，例如「最多扣款一次」。

### 把遗漏案例当成默认行为

> **陷阱:** 「这里有一些示例」可能被理解为其他行为均可自行决定。生成代码于是会去除空白、接受未知枚举值，或自行排列优先级，却没有让新策略显现出来。

**修复方法：** 在提示词中加入未决案例列表，并要求实现前先提问。作出决定后，把每个有后果的答案变成命名案例或显式规则。

<!-- deep -->

## 构建最小判别集

从实践角度说，如果每个案例都能排除一种可信缺陷或确定一项必需行为，而且删除它会留下有意义的歧义，那么案例集就是最小的。通常无需追求数学上的最小性。真正有用的约束是让每行都能证明自身维护成本合理。

### 建模竞争行为

选择更多数据前，先写出简短的候选规则。在库存示例中，候选包括严格比较、固定阈值和忽略停售状态。这个步骤把「提示词似乎很模糊」转化成案例可以暴露的具体差异。

候选不必写成代码。一张小表通常更清楚：

| 候选解释 | 是否符合低库存示例 | 判别案例 |
| --- | --- | --- |
| 只有低于阈值才补货 | 是 | 库存恰好等于阈值 |
| 所有商品都使用阈值 10 | 是 | 库存为 8，阈值为 5 |
| 停售状态无关紧要 | 是 | 低于阈值的停售商品 |
| 预期完整规则 | 是 | 通过所有已批准案例 |

这张表也会暴露重复案例。两个案例如果都只排除严格比较，可能因可读性而都值得保留，但不应在无意间重复提供同一种判别能力。

### 逐步选择对照

先为每项必需行为选择一个代表。然后选择一个案例，尽量排除剩余错误解释中范围最大或风险最高的一组。每次选择后重新计算剩余范围，不要机械生成固定数量的「边缘案例」。

风险会改变顺序。在授权逻辑中，应先选择一个即使多项授权条件同时成立也必须拒绝的案例。在计费逻辑中，应从精确金额边界与局部失败状态开始。对于格式化器，容易困惑却没有危害的空白案例可以稍后处理。

优先选择只改变一项事实的对照，因为失败更容易诊断。有时必须改变两个条件才能进入重叠状态，此时要同时命名两项条件，并明确写出优先级问题。受控复杂度比假装交互不存在更可靠。

### 保持预言机独立

案例输入可以生成，但预期结果仍然需要可信来源。产品规则、协议文档、已审查夹具和简单不变量都可以充当这种来源。生成实现不能担任自己的裁判。

对于小型决策表，字面预期值很容易审计。面对大型定义域，可以把少量命名字面案例与性质结合，且性质逻辑应不同于生产算法。排序检查可以断言顺序与元素保留，无需再实现一遍同样的排序。

模糊测试或基于性质的测试发现反例后，缩减后的结果很有价值。应保留最小可复现输入、适用时的种子、被违反的性质与预期行为。没有这些事实的原始随机失败很难转化成提示词或回归案例。

### 测试这些测试

判别案例应该能因其声称捕获的缺陷而失败。临时把 `<` 换成 `<=`，颠倒两条策略分支，或者针对修复前实现运行检查。如果案例仍然通过，它的预期值、设置或观察边界就有问题。

当智能体同时编写代码与测试时，这种先红后绿的证据尤其有用。它证明测试能够看见预期差异，而不只是执行新路径。记录最终运行前，必须恢复正确实现。

不要在共享工作树中随意修改生产代码。应使用可丢弃补丁、本地候选函数，或者能干净恢复的变异测试工具。只有最终仓库状态明确时，证据才有意义。

### 保持可追溯性

把每个案例关联到一条规则、缺陷或风险。`inclusive-reorder-threshold` 这类稳定名称可以让失败易于搜索，也能让审查者理解为何必须保留某个特殊值。不要只使用离开问题跟踪器就失去意义的工单名称。

策略变化时，应同时更新规则与案例。不能只因生成代码产生了新值，就翻转预期结果。要记录旧反例现在变成了代表性示例、被另一项对照替换，还是已不再属于定义域。

案例应该经得起内部重构。如果新实现改变私有辅助函数调用，却保留所有已批准观察，案例集就应该保持通过。某案例若只因内部结构变化而失败，它测试的就是设计选择，而该选择需要另行说明理由。

### 了解示例不能证明什么

有限案例无法证明无限输入空间上的正确性。它们也不能证明生产接线关系、每个入口的授权实施、不存在数据竞争，或者兼容从未运行过的环境。这些主张需要性质、静态分析、集成检查、负载测试或运行证据。

因此，示例与反例既是提示词设计工具，也是验证的种子，但不构成完整验证策略。把批准的案例转成可执行检查，在适用位置用性质扩展它们，并审查测试套件仍未覆盖的维度。

<!-- /deep -->

[检查点: ai-era/examples-and-counterexamples](https://codewiki.com/zh/ai-era/examples-and-counterexamples/#checkpoint)

## 延伸阅读

- [GitHub Copilot 文档：提示词工程](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering)
- [Cucumber 文档：Gherkin 参考](https://cucumber.io/docs/gherkin/reference/)
- [Node.js 24 文档：严格断言模式](https://nodejs.org/docs/latest-v24.x/api/assert.html#strict-assertion-mode)
- [Testing Library：指导原则](https://testing-library.com/docs/guiding-principles/)
- [Google Testing Blog：变更检测式测试的危害](https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html)
