分层测试优先提示:首次运行即获得正确代码

发布: (2026年3月20日 GMT+8 06:19)
11 分钟阅读
原文: Dev.to

Source: Dev.to

如果你使用 AI 来帮助编码,最常见的失败模式并不是模型懒惰——而是目标模糊。

你请求一个修复,助手猜测 “正确” 是什么意思,结果得到看似合理但稍有偏差的代码:错误的边缘情况、错误的文件、错误的抽象、错误的依赖,或者正确的思路实现得过于宽泛。

降低这种失败率的一个简单方法是不要先请求修复。
先请求 测试

为什么这样有效

大多数代码提示问题实际上是规格问题。

当你说:

Fix the currency formatter for German locales.

时,会有十几个隐藏的问题:

  • 哪个文件拥有该行为?
  • 具体期待什么格式?
  • 使用的是哪个运行时或测试框架?
  • 是否允许使用外部库?
  • 解决方案应该是最小化的还是架构性的?
  • 什么算作“完成”?

测试以一种通常的文字描述不会提供的方式回答这些问题。它为助手提供了一个可观察的目标,而不是模糊的意图。

这带来了三个直接好处:

  1. 正确性变得可执行。 你可以运行结果,而不是争论它。
  2. 范围保持更小。 当一个断言就能解决时,助手不太可能重构整个仓库的一半。
  3. 审查更容易。 变更是由失败的案例所证明,而不是手写的解释。

三步工作流

1. 提供最小可运行的上下文

只给出足够的信息,让助手能够定位工作内容。

包括:

  • 所涉及的文件或模块
  • 使用的语言/运行时
  • 测试框架
  • 一个具体的输入/输出示例
  • 任何相关约束,例如 “不添加新依赖”

示例

I have formatCurrency(amount, locale) in src/format.js.
Runtime: Node 22.
Tests use Jest.
Expected behavior: formatCurrency(12.5, 'de-DE') should return '12,50 €'.
No new dependencies.

这就足够了。无需提供三段式的背景说明。

2. 仅请求一个失败的测试

这是关键一步。不要立即请求实现代码。 要求生成一个聚焦的测试文件,使用现有项目约定,只检查你关心的行为。

示例提示

Create a Jest test for src/format.js that asserts
formatCurrency(12.5, 'de-DE') returns '12,50 €'.
Only return the test file contents and any minimal config change required.
Do not implement the function yet.

为什么要分步?
因为这会迫使助手在动手编写代码前先思考预期行为。这本身就能大幅减少偏差。它还能让你立刻得到一个可运行的产物。如果测试有错误、含糊或不符合你的约定,你可以在任何实现之前就发现并纠正。

3. 请求最小的修复让测试通过

运行测试。如果失败,粘贴错误输出并请求最小的代码改动来满足该案例。

示例提示

This test fails with the following output:
[paste stack trace]

Implement the smallest change in src/format.js that makes this test pass.
Avoid unrelated refactors.
Return a unified diff.

这种表述很重要。 “最小改动” 与 “避免无关的重构” 并非装饰语——它们引导助手远离大幅重写,专注于可审查的补丁。

一个具体的例子

问题: CSV 导出错误——包含换行符的字段在电子表格应用中打开时会被拆分。

一个模糊的提示会是:

修复 CSV 导出转义。

这几乎必然会得到一个模糊的答案。

相反,下面是一个有结构的版本。

第 1 步 – 上下文

File: src/export/csv.ts
Runtime: Node 22
Tests: Vitest
Bug: values containing newlines are split into multiple rows in spreadsheet apps
Constraint: preserve the newline; do not replace it with spaces

第 2 步 – 先写测试

import { describe, it, expect } from 'vitest';
import { toCsvRow } from '../../src/export/csv';

describe('toCsvRow', () => {
  it('quotes fields containing newlines', () => {
    const row = toCsvRow(['hello\nworld', 'ok']);
    expect(row).toBe('"hello\nworld",ok');
  });
});

第 3 步 – 最小实现请求

现在助手有了一个明确的目标:

  • 保留换行符
  • 为该字段加引号
  • 其他字段保持不变

这比“修复 CSV 导出”要紧凑得多。你更有可能得到一个正确的、本地的补丁,而不是一次投机性的重写。

为什么这通常比 “写代码” 提示更有效

直接实现的提示会让模型直接跳到解决方案的形态。有时这样可行,但往往助理会过早提交。

先写测试的提示会延迟这种提交。它让助理在选择结构之前,先通过可见行为来定义成功。这通常会在以下几个方面提升结果:

  • 更少的虚构抽象
  • 更少不必要的依赖
  • 更好地保留已有约定
  • 更容易调试,因为失败的断言会缩小搜索空间

它还能帮助 思考得更清晰。编写或审查测试会迫使你明确需求。你可能会发现预期输出有误、边界情况描述不完整,或者实际问题与最初想象的稍有不同。

该模式的优秀提示

以下是您可以改编的几个模板提示:

目标提示
提供上下文“我在 src/date.js 中有一个函数 parseDate(str)。运行时环境:Node 20。测试使用 Mocha。期望:parseDate('2023‑01‑01') 返回一个表示 UTC 午夜的 Date。不添加新依赖。”
请求一个失败的测试“编写一个 Mocha 测试,断言 parseDate('2023‑01‑01') 返回的 Date 满足 getTime() === 1672531200000。仅返回测试文件内容。不要修改实现。”
请求最小修复“上述测试失败,错误信息为:expected 1672531200000 but got 0。提供对 src/date.js 的最小改动,使测试通过。返回统一差异(unified diff),并避免无关的更改。”

您可以自由组合这些部分,以适应您的语言、运行时和测试框架。关键始终是:上下文 → 失败的测试 → 最小修复

一致的指导

  • “首先编写一个聚焦的失败测试。”
  • “使用现有的项目约定。”
  • “暂时不要实现。”
  • “仅返回测试文件内容。”
  • “实现最小的改动以使该测试通过。”
  • “避免不相关的重构。”
  • “返回统一的 diff。”

这些小约束综合起来,使工作流更加可靠。

常见错误

一次请求太多

One bug, one test, one patch.
如果在单个提示中请求三个边缘情况、一次重构以及更新文档,你会失去主要优势:紧密的反馈。

给出无环境的提示

仅说“写一个测试”,而不指明 Jest、Vitest、pytest 或文件结构,会导致得到的想法在错误的格式中。

接受未运行的测试

生成的测试仍然是代码——请运行它。一个损坏的测试文件不是合同;它只是一段格式更好但仍是幻觉的代码。

让实现无限增长

测试一旦存在,就要坚持原则。请求最小的通过更改,而不是“最干净的长期重构”。在确保正确性后,你随时可以进行重构。

何时不使用它

当行为已知且正确性至关重要时,这种模式表现出色。它在以下情况下用处较小:

  • 探索性原型开发
  • 开放式设计工作
  • 行为仍在变化的大规模重构

在这些情况下,先写规格或先制定计划的提示可能更合适。

结束语

Scaffolded Test‑First Prompting 有效,因为它用可运行的目标取代了模糊的意图。

与其让 AI 猜测 “fixed” 的含义,你可以:

  1. 定义一个失败的示例。
  2. 使该示例可执行。
  3. 请求最小的代码改动以满足它。

这种习惯通常能让你更快收敛,得到更简洁的补丁,并减少奇怪的绕路。

如果你每天都在使用 AI 编码,这是最容易采用的工作流升级之一:先测试,后补丁,全部运行

0 浏览
Back to Blog

相关文章

阅读更多 »