示例与反例

用代表性输入、预期输出和反例消除编码请求中的歧义。

难度 进阶 时长 标准深度约 14分钟
版本 Node 24
what

示例为编码请求提供具体的输入输出案例,反例则指出看似合理的归纳应在哪里停止。

trap

一个正常路径可以符合许多错误规则,一长串相似案例也可能没有规定边界、规则交互与失败行为。

fix

为每个代表性案例配一个相邻的判别案例,写明字面预期行为;尚未决定的案例要明确标注,不能让模型猜测。

是什么,为什么存在

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

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

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

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

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

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

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

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

工作原理

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

有效案例的组成

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

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

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

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

代表性示例确定中心

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

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

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

反例截断错误规则

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

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

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

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

边界、分区与交互

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

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

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

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

明确保留未知项

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

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

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

示例

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

排除可能的库存规则

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

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(", ")}`);
}
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

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

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

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

规定接受形式与失败类别

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

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}`);
  }
}
"25" -> 25
"001" -> TypeError
"25 items" -> TypeError
25 -> TypeError
"101" -> RangeError

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

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

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

用决策表覆盖交互

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

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}`);
}
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

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

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

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

陷阱

只展示一个正常路径

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

增加许多相关案例

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

把所有无效输入都叫反例

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

让实现生成预言机

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

过度规定偶然细节

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

把遗漏案例当成默认行为

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

深入 构建最小判别集

构建最小判别集

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

建模竞争行为

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

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

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

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

逐步选择对照

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

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

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

保持预言机独立

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

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

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

测试这些测试

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

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

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

保持可追溯性

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

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

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

了解示例不能证明什么

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

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

延伸阅读

检查点

5个问题 · 1 道输出预测题 · 1 道找错题

复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?