逐日AI
第 1 周 · D4约 4 小时

迭代与评估:小样本测试集、A/B、版本管理、常见反模式

把「感觉这版更好」变成「这版在十条测试上多对了三条」:建小样本测试集、跑 A/B 对比、给提示词上版本号,并认出最常见的几种反模式。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能为一个提示词建一份十条左右的测试集,并说出每条测什么
  2. 能写一个 A/B 评估脚本,用同一批输入比较两版提示词的通过率
  3. 能给提示词做版本管理,并说出五种常见反模式各怎么识别

前三天我们一直在改提示词,但一直回避了一个问题:改完之后,怎么知道它变好了?在聊天窗口里试一两次「感觉不错」,放进生产里跑一千次可能有两百次翻车。今天把提示词当成代码来对待——有测试、有对比、有版本号。读完正文、做完实验之后,回到页面顶部把三条目标勾掉。

小白版讲解

凭感觉改提示词为什么会越改越差

新同事按你的便签办了一件事,结果不太对。你顺手在便签上加一句,他再办一次,这次对了。你很满意,直到一周后发现他把另外三件原本办得好的事也办错了——因为你那句新加的话,恰好跟另外三件事的要求冲突。你没发现,是因为你每次只看了「这一件事」。

提示词的迭代如果凭感觉,就是这个过程的加速版。你看到一条输入出错,改一句话,再跑那一条,对了,收工。但模型对提示词的响应是全局的:你为了修一个边界情况加的规则,会改变它对所有输入的行为。没有一份固定的输入集合让你每次都全跑一遍,你就永远只能看到「刚修的那一条」,看不到「被顺手弄坏的那几条」。于是提示词越改越长、行为越来越不可预测,最后没人敢动它。

这个问题在写代码时早就解决了:改动之前有测试,改动之后全跑,红了就知道哪里坏了。提示词缺的就是这个——一条基线。没有基线,「变好」和「变差」都只是感觉;有了基线,「这版在十条测试上从六条对变成九条对,但第十条原来对现在错了」才是可以讨论、可以决策的事实。今天要建的就是这条基线,它不需要很大,十条就够起步。

小样本测试集:十条够不够,每条该长什么样

先回答「十条够不够」。对正式产品当然不够,但对「把提示词从凭感觉改变成有依据改」这一步,十条已经能过滤掉绝大多数的盲改。原因是提示词的错误高度集中:模型在正常输入上几乎不会错,错的全在边界和刁难上。所以十条的价值不在数量,在分布

一份好的小测试集大致三类各占三四条。正常输入,规范写法,两版都该对,它们的作用是「守底线」——新版如果把这些弄坏了就是严重回退。边界输入,需求里没写默认值、字段可选、路径带参数、写法不规范(「todos 的创建接口」而不是 POST /todos),它们测的是提示词有没有把默认规则说清楚。刁难输入,带干扰信息(先讲了一段线上事故再说需求)、中途改口(「是 A 接口——哦不对,是 B」)、夹带无关要求(「顺便把响应改成分页的」),它们测的是提示词能不能让模型抓住重点。

每条用例有三部分:输入、人工标注的标准答案、类别标签。标准答案是人工的,这一步没有捷径——你标错一条,整份评估就偏了,而且你会以为是提示词的问题去反复改提示词。每条写完自己再读一遍输入,确认答案唯一。以贯穿全课的任务为例,一条边界用例长这样(沿用 D3 的抽取任务,标准答案只标四个关键字段):

JSONJSON
{
  "id": "c04",
  "kind": "edge",
  "input": "POST /todos 补校验:title 必填,不超过 200 字符。(需求里没写失败返回什么)",
  "expected": { "endpoint": "/todos", "method": "POST", "failureStatus": 400, "fields": ["title"] }
}

它测的是一件很具体的事:需求没写状态码时,模型会不会按团队默认用 400,还是自己猜一个 422。D2 讲过的「模型不可能知道的样本」也可以放一两条进来,看它是老实说不知道还是编一个。

怎么判对错:精确匹配、字段比对与用模型当裁判

有了输入和标准答案,下一个问题是「怎么算对」。这一步的松紧直接决定评估有没有意义,太死会把写法差异算成错误,太松会放过真的错误。三种做法各有适用范围。

精确匹配:输出整个序列化后和标准答案完全相等。只适合输出是单个值的任务,比如分类结果就是一个标签。对结构化输出它太死了——模型把 POST 写成 post、把字段数组的顺序换一下,意思一样却判错,你会去修一个根本不存在的问题。

字段比对:逐字段比较,每个字段用自己的规则。方法忽略大小写;路径忽略末尾斜杠、缺前导斜杠视为可容忍;字段列表去重后忽略顺序和大小写;状态码精确相等。原则是比你在意的字段,容忍你不在意的差异。这是结构化输出的标配,也是 D3 把输出变成固定字段的最大回报——如果输出是自由文本,这一步根本做不了。判分函数返回的应该是「不匹配的字段名列表」而不是布尔值,因为汇总时你要知道错在哪:十条里有四条都是 failureStatus 错,问题就很明确是默认值没说清。

score.ts
const norm = (s: string): string => s.trim().toLowerCase()
const normPath = (s: string): string => {
  const p = norm(s).replace(/\/+$/, '')
  return p.startsWith('/') ? p : `/${p}` // 缺前导斜杠是写法差异,不是理解错误
}
 
// 返回不匹配的字段名列表;空数组即通过。比布尔值多带了「错在哪」这层信息
function score(actual: Record<string, unknown>, expected: Expected): string[] {
  const diffs: string[] = []
  if (typeof actual.endpoint !== 'string' || normPath(actual.endpoint) !== normPath(expected.endpoint)) diffs.push('endpoint')
  if (typeof actual.method !== 'string' || norm(actual.method) !== norm(expected.method)) diffs.push('method')
  if (actual.failureStatus !== expected.failureStatus) diffs.push('failureStatus') // 状态码不容忍
  const got = Array.isArray(actual.fields) ? [...new Set(actual.fields.map((f) => norm(String(f))))].sort() : null
  const want = [...new Set(expected.fields.map(norm))].sort()
  if (!got || got.length !== want.length || got.some((f, i) => f !== want[i])) diffs.push('fields')
  return diffs
}

用模型当裁判:输出是自由文本(一段摘要、一封邮件)时,没有字段可比,就让另一个模型按你写的评分标准打分。它能覆盖前两种做不到的任务,但代价不小:裁判本身会错、会偏向长的和格式漂亮的回答、评分标准写得含糊它就打得随意。用它的前提是评分标准要像便签一样具体(「摘要是否包含状态码、字段名、失败行为三个信息点,各一分」),并且要定期抽样人工核对裁判的判断。规律是:能用字段比对就不要用裁判;必须用裁判的地方,先用十条人工打过分的样本校准它。

A/B 对比:同一批输入、两版提示词、一张表看结果

判分有了,A/B 就只是一个循环:对测试集里每一条输入,分别用 A 版和 B 版的提示词跑一次抽取,各自判分,填进同一张表。表的每一行是一条用例,两列是两版的结果,通过的打勾,不通过的写出错在哪个字段。

TextText
case  kind         v1                        v2
c01   normal       ✅                         ✅
c04   edge         ❌ failureStatus           ✅
c09   adversarial  ❌ endpoint                ✅
c10   adversarial  ✅                         ❌ failureStatus
 
通过率:v1 6/10(60%)   v2 9/10(90%)
v2 修好的:c04, c05, c07, c09
v2 弄坏的:c10   ← 这些要写进 CHANGELOG 的「已知回退」

表下面的汇总有三行,重要程度和多数人的直觉相反。通过率最不重要——它只告诉你总分。修好的告诉你这版改动确实解决了目标问题。弄坏的最重要:它告诉你新版引入了什么回退。上面这个例子里 v2 从六条对变成九条对,看起来大胜,但 c10 是原来对现在错的——v2 加了一条「没写状态码就用 400」,模型学得太死,需求里明明写了 422 也改成了 400。没有这张表,这个回退会静静地进入生产。

两个工程细节。一是同一批输入,两版必须跑完全相同的用例,换了用例就没法比。二是随机性:真模型对同一输入两次可能给不同结果,所以正式评估时每条跑三次取多数,或者把温度设为 0;离线练习时用固定的模拟输出即可。今天的实验在离线模式下用规则模拟了「v1 在哪些边界上错、v2 修了什么又弄坏了什么」,让你没有 API key 也能看到完整的现象。

版本管理:提示词是代码,要进仓库、要有变更记录

到这里,提示词已经具备了代码的全部特征:它是一个有参数的函数(D3),有一份测试集,有一个能跑出通过率的评估脚本。那就应该像代码一样管:进版本库、有版本号、有变更记录

具体做法不复杂。提示词正文放在一个独立文件里(而不是散在业务代码的字符串拼接中),每一版有一个 id。旁边放一份变更记录,每一条写四件事:改了什么、为什么改(对应哪几条测试失败)、跑出来的通过率、已知回退。

TextText
## v2(当前)
- 改动:补了五条规则——默认状态码 400、方法大写与路径规范化、字段名原样保留、改口以最后为准、忽略顺带要求
- 原因:v1 在 c04(没写状态码猜成 422)、c05(口语路径还原错)、c07(漏了 pageSize)、c09(改口只认第一次)上失败
- 结果:v1 6/10 → v2 9/10
- 已知回退:c10——「没写就 400」学得太死,需求写了 422 也被改成 400。下一版把这条规则改成「写了就用需求里的」并放到规则列表最前面

它和代码版本管理有一处不同值得说:代码的改动通常是「局部的」,改一个函数不影响另一个;提示词的改动是「全局的」,加一句话可能改变所有输入的行为。所以提示词的变更记录里**「已知回退」这一栏是必填项**,而代码的提交信息里通常没有这一栏。另一处不同是回滚的代价:提示词回滚只是换一个字符串,成本几乎为零,所以线上出问题时「先回滚到上一版、再慢慢查」是比修代码更理所当然的选择——前提是你有版本号可以回。

五种常见反模式

最后把前三天里陆续提到的坑收成一张清单。每条都给一个识别方法,因为反模式的难点不在改,在发现。

越写越长。每次出错就加一句,半年后提示词两千字,没人知道哪句还在起作用。识别:删掉一段跑测试集,通过率不变,那段就是死的。有了测试集,「删减」第一次变得安全。

万能角色。「你是一位精通前后端、安全、测试与文档的全栈专家」。角色越全信息量越低,模型不知道你想让它关注什么。识别:角色描述里能不能删掉一半而不影响任何一条测试。

否定式指令。「不要客套」「不要解释」「不要用 Markdown」堆一排。模型对光秃秃的否定执行得不稳定,而且否定句会把那个词带进上下文(写「不要提到竞品」有时反而让它提到)。识别:每条否定能不能改写成带理由的肯定(「直接从第一段开始,因为输出会被程序按段解析」)。

示例污染。D2 讲过的:示例的格式跟说明冲突、示例的类别分布有偏、示例里有「等等」。识别:把示例的输出部分单独拿出来跟格式栏逐字比。

只测一次。改完在聊天窗口试一条就上线。识别不需要方法——今天整篇讲的就是它。

源码导读

动手实验

🧪 D4 实验:十条测试集加 A/B 评估脚本

代码位置:labs/prompt-engineering-5days/day-04-prompt-ab-eval

验收标准:

  1. MOCK=1 pnpm start 跑 starter,对比表有 10 行,三类输入每类至少 3 条。
  2. 判分后 v1 不再因为方法小写或字段顺序被判错:MOCK 下 v1 通过 6/10,失败的四条各报出具体字段名。
  3. 汇总里能看到两版通过率、「v2 修好的」四条、「v2 弄坏的」一条,以及 v2 按类别的通过数。
  4. CHANGELOG.md 的 v2 条目填完,通过率是脚本跑出来的数字,已知回退写了 c10 与下一版打算怎么改。
  5. 改一版 v3 修 c10 的回退再跑一遍,把结果追加进变更记录,包括它有没有弄坏别的。

这个实验没有 API key 也能做完全部五条。打开 labs/prompt-engineering-5days/day-04-prompt-ab-eval,先跑 solution/ 看完整的对比表,再回到 starter/

  1. solution/MOCK=1 pnpm start,看清十条用例的三类分布、v1 与 v2 各自在哪几条上失败、以及「修好的」与「弄坏的」两行。
  2. 切到 starter/ 跑一次,注意现在只有 4 条用例、v1 四条全挂(因为判分过死)、汇总是占位文字——这三处就是三个代码练习。
  3. 补全 src/cases.ts 到十条:3 条边界(口语化写法、PUT 全量更新、GET 查询参数)与 3 条刁难(带干扰信息、中途改口、夹带无关要求),每条人工标注 expected 并再读一遍确认答案唯一。
  4. 补全 score 为逐字段比对(容忍大小写、末尾斜杠、字段顺序,状态码精确),再跑一次,v1 应通过 6/10 且失败的四条各报出字段名。
  5. 补全汇总(通过率、修好的、弄坏的、分类通过数),把数字填进 CHANGELOG.md 的 v2 条目;然后改一版 v3 修 c10 的回退,再跑再记。

面试题

今天 3 道题在下方题库区,分别对应测试集的设计、提示词版本化与代码版本管理的差别、用模型当裁判的边界。第二题是海外面试里出现频率最高的提示词工程题,先自己推一遍再看分析。

检查清单与明日预告

  • 能为一个提示词建一份十条左右的测试集,并说出每条测什么
  • 能写一个 A/B 评估脚本,用同一批输入比较两版提示词的通过率
  • 能给提示词做版本管理,并说出五种常见反模式各怎么识别
  • A/B 评估实验的 5 条验收标准全部通过
  • 3 道面试题不看要点也能答出至少 2 道

明天(D5)是本课最后一天,回答一个你迟早会遇到的问题:同一份提示词,从一家模型搬到另一家,为什么会坏、坏在哪几处、怎么把系统提示组织成搬起来不痛的结构。你今天建的测试集会直接派上用场——迁移之后跑一遍,通过率就是「有没有退步」的答案。D5 结尾还会告诉你接下来该学 Claude 课还是 Codex 课,以及怎么选。

面试题库

  • 怎么给一个提示词建测试集?十条样本该怎么挑,标准答案从哪来?How do you build a test set for a prompt? How would you choose ten samples, and where do the expected answers come from?
    国内高频海外高频基础#evaluation#test-set

    分析过程 · 先想清楚再作答

    1. 这题在筛「有没有真的建过测试集」。答「多找一些输入跑一跑」的人没建过;建过的人第一句会说分布——因为提示词的错误全集中在边界上。
    2. 拆法:三类各占三四条。正常输入守底线,新版弄坏它们就是严重回退;边界输入(没写默认值、可选字段、不规范写法)测默认规则说清没说清;刁难输入(干扰信息、中途改口、夹带无关要求)测能不能抓住重点。可以再放一两条模型不可能知道的样本,看它是否老实说不知道。
    3. 标准答案只能人工标,这一步没有捷径;标错一条整份评估就偏,而且你会误以为是提示词的问题去反复改。每条写完再读一遍输入确认答案唯一。
    4. 结论:十条够起步,价值在分布不在数量;最好的来源是过去每一次「它又错了」的真实输入,一周就能攒出比想象出来的更真实的测试集。
    5. 可预期的追问:测试集怎么增长?每次想改提示词先把触发的那条输入加进去再改;以及「测试集会不会泄漏进提示词」——用例不能直接当 few-shot 示例,否则是在测记忆而不是泛化。

    How to reason about it · think before answering

    1. This screens for whether the candidate has actually built one. 'Collect some inputs and run them' means no; people who have start with distribution, because prompt errors cluster at the edges.
    2. Three classes with three or four each: normal inputs guard the baseline; edge inputs (missing defaults, optional fields, informal phrasing) test whether default rules are explicit; adversarial inputs (distractors, mid-sentence corrections, unrelated asks) test focus. Add one or two unknowable items to check honesty.
    3. Expected answers are labeled by hand, no shortcut; one mislabeled case skews the whole evaluation and sends you chasing a phantom prompt bug. Re-read each input after labeling to confirm the answer is unique.
    4. Conclusion: ten is enough to start, value lies in distribution not count, and the best source is every real 'it failed again' input from the past week.
    5. Follow-ups: how does the set grow? Add the triggering input before every prompt change. And leakage — test cases must not double as few-shot examples, or you are measuring memorization rather than generalization.

    答题要点

    • 价值在分布不在数量:正常、边界、刁难三类各三四条,再放一两条模型不可能知道的
    • 正常输入守底线,边界测默认规则,刁难测抓重点
    • 标准答案人工标注、逐条复核,标错一条整份评估就偏
    • 最好的来源是真实出错的输入;每次想改提示词先把那条加进测试集

    Key points

    • Distribution over count: three or four each of normal, edge and adversarial, plus a couple of unknowable items
    • Normal cases guard the baseline, edge cases test defaults, adversarial cases test focus
    • Expected answers are hand-labeled and re-checked; one wrong label skews everything
    • Best source is real failures; add the triggering input before each prompt change
  • 提示词版本化怎么做?它和代码版本管理有什么不同?线上出问题你先做什么?How do you version prompts? How does it differ from versioning code, and what is your first move when production misbehaves?
    国内高频海外高频进阶#prompt-versioning#evaluation

    分析过程 · 先想清楚再作答

    1. 这题是海外面试里提示词工程方向出现频率最高的一道,考的是「有没有把提示词当成生产资产管理过」。答「放进 git」是最低分,面试官要听的是变更记录里写什么、以及为什么和代码不一样。
    2. 拆法:先说形态——提示词正文独立成文件、每版有 id、与业务代码解耦;再说变更记录四件事——改了什么、为什么改(对应哪几条测试失败)、跑出来的通过率、已知回退。通过率必须是脚本跑出来的数字。
    3. 不同点是区分度所在:代码改动通常是局部的,提示词改动是全局的——加一句话可能改变所有输入的行为,所以「已知回退」是必填项而代码提交信息里没有这一栏;另一点是回滚成本几乎为零,只是换一个字符串。
    4. 结论:线上出问题第一步是回滚到上一版,再拿触发问题的输入补进测试集慢慢查——前提是你有版本号可回、有测试集可跑。
    5. 追问:提示词版本要不要和模型版本绑定?要——同一份提示词在不同模型版本上通过率会变,记录里要写清是在哪个模型上测的;另一个追问是多环境怎么灰度,答案是按版本 id 分流并对比两版的线上指标,跟代码灰度一样。

    How to reason about it · think before answering

    1. One of the most frequent prompt-engineering interview questions abroad; it tests whether you have managed prompts as production assets. 'Put it in git' is the floor; the interviewer wants the changelog contents and why prompts differ from code.
    2. Shape first: prompt text in its own file, an id per version, decoupled from business code. Then the changelog's four items: what changed, why (which test cases failed), the measured pass rate, known regressions. The pass rate must come from the script.
    3. The difference is the differentiator: code changes are usually local, prompt changes are global — one added sentence can shift behavior on every input, so 'known regressions' is mandatory where commit messages have no such field. Also rollback is nearly free, just swap a string.
    4. Conclusion: when production misbehaves, roll back to the previous version first, then add the triggering input to the test set and investigate — which only works if you have version ids and a test set.
    5. Follow-ups: bind prompt versions to model versions? Yes — pass rates shift across model versions, so record which model was used. And canarying: route by version id and compare live metrics, same as code.

    答题要点

    • 提示词正文独立成文件、每版有 id,变更记录写改动、原因、脚本跑出的通过率、已知回退
    • 与代码的不同:改动是全局的,所以「已知回退」必填;回滚成本几乎为零
    • 线上出问题先回滚上一版,再把触发输入加进测试集查
    • 版本要记录在哪个模型上测的,换模型版本通过率会变

    Key points

    • Prompt text lives in its own file with a version id; the changelog records change, reason, measured pass rate, known regressions
    • Unlike code, prompt changes are global, so known regressions are mandatory; rollback is nearly free
    • On a production issue, roll back first, then add the triggering input to the test set
    • Record which model version was tested, since pass rates shift across models
  • 用模型给模型打分靠谱吗?什么时候可以用,什么时候必须人工看?Is using a model to grade another model's output reliable? When is it acceptable, and when must a human look?
    国内高频海外高频进阶#evaluation#llm-as-judge

    分析过程 · 先想清楚再作答

    1. 这题在考「知道裁判也会错」。答「用更强的模型当裁判就行」的人没校准过裁判;答出「能用字段比对就不用裁判」才说明有判断力。
    2. 拆法:先分任务。输出是固定字段就用代码逐字段比对,不需要裁判;输出是自由文本(摘要、邮件、解释)才没有字段可比,这时裁判是唯一能规模化的办法。
    3. 再说裁判的偏差:偏向长的、格式漂亮的、和自己风格接近的回答;评分标准含糊时打分随意;对事实性错误不敏感。所以评分标准要像便签一样具体——列出信息点、每点一分——而不是「给这段摘要打 1 到 10 分」。
    4. 结论:裁判可以用,前提是先拿十条人工打过分的样本校准它,看它和人的一致率;上线后定期抽样复核;对涉及事实、安全、金额的输出必须人工看。
    5. 追问方向:裁判和被评的模型是同一家会怎样?会有自我偏好,尽量换一家或至少换一个版本;另一个追问是「裁判的成本」,每条评估都是一次完整调用,测试集大了要算钱,所以能用代码判的部分先用代码判掉。

    How to reason about it · think before answering

    1. This tests whether you know the judge is fallible too. 'Use a stronger model as the judge' means you never calibrated one; 'prefer field comparison whenever possible' shows judgment.
    2. Split by task: structured output gets field-by-field code comparison, no judge needed; free text (summaries, emails, explanations) has no fields, and a judge is the only scalable option.
    3. Judge biases: longer and prettier answers score higher, stylistic similarity gets rewarded, vague rubrics produce noisy scores, factual errors are under-penalized. So the rubric must be concrete — list the information points, one point each — not 'rate this summary 1 to 10'.
    4. Conclusion: usable once calibrated against ten human-scored samples with an agreement rate you accept; spot-check regularly; anything involving facts, safety or money still gets human review.
    5. Follow-ups: same vendor for judge and judged? Expect self-preference, so switch vendor or at least version. And cost — every judgment is a full call, so let code handle whatever it can first.

    答题要点

    • 能用字段比对就不用裁判;裁判只用于没有字段可比的自由文本
    • 裁判偏向长的、格式漂亮的回答,评分标准含糊就打得随意,所以标准要具体到信息点
    • 先用十条人工打分样本校准裁判,上线后定期抽样复核
    • 涉及事实、安全、金额的输出必须人工看;裁判尽量换一家或换版本以避免自我偏好

    Key points

    • Prefer field comparison; reserve the judge for free text with nothing to compare
    • Judges favor long, well-formatted answers and score noisily on vague rubrics, so rubrics must list concrete points
    • Calibrate against ten human-scored samples first, then spot-check regularly
    • Facts, safety and money always get human review; use a different vendor or version to avoid self-preference

评论

登录后即可参与讨论

还没有评论,来说第一句。