Dayward AI
Week 1 · D4About 4 hours

Iteration and Evaluation: Small Test Sets, A/B Testing, Version Control, Common Anti-Patterns

Turn "this version feels better" into "this version got three more of ten test cases right": build a small test set, run an A/B comparison, version your prompts, and recognize the most common anti-patterns.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Build a test set of about ten cases for a prompt, and state what each case is testing
  2. Write an A/B evaluation script that compares the pass rate of two prompt versions on the same inputs
  3. Version-control a prompt, and explain how to recognize five common anti-patterns

For three days we have been editing prompts while dodging one question: after you edit, how do you know it got better? Two tries in a chat window that feel fine can turn into two hundred failures out of a thousand production runs. Today we treat the prompt as code — with tests, with comparisons, with a version number. When you have read the walkthrough and finished the lab, come back to the top of the page and check off the three goals.

Plain-Language Walkthrough

Why editing prompts by feel makes them worse over time

The colleague follows the ticket, you tack on one more line, and a week later you discover three other tasks that used to go fine are now breaking — because that one new line conflicts with them, and you only ever checked "this one task." The first round went wrong, you added a sentence, they redid it, and this time it was right. You were pleased. You did not notice the collateral damage, because each time you looked at one task only.

Iterating on a prompt by feel is that same process, sped up. You see one input come out wrong, change a sentence, rerun that one input, see it pass, and call it done. But a model's response to a prompt is global: the rule you added to fix one edge case changes its behavior on every input. Without a fixed set of inputs you rerun in full every time, you will forever see only the case you just fixed and never the cases you just broke. So the prompt gets longer, its behavior gets less predictable, and eventually nobody dares touch it.

Software solved this problem long ago: have tests before the change, run them all after the change, and a red one tells you what broke. What prompts lack is exactly that — a baseline. Without a baseline, "better" and "worse" are both feelings. With one, "this version went from six of ten to nine of ten, but the tenth used to pass and now fails" is a fact you can discuss and decide on. Building that baseline is today's job, and it does not have to be large. Ten cases are enough to start.

A small test set: are ten cases enough, and what should each look like

Start with whether ten is enough. For a mature product, obviously not. But for the step from editing by feel to editing on evidence, ten already filters out the overwhelming majority of blind changes. The reason is that prompt failures are highly concentrated: models almost never get normal inputs wrong, and all the failures live at the edges and in the adversarial cases. So the value of ten cases is not in the count, it is in the distribution.

A good small test set is roughly three or four cases in each of three categories. Normal inputs, written conventionally, which both versions should get right. Their job is to hold the floor — if a new version breaks these, that is a serious regression. Edge inputs, where the requirement omits a default, a field is optional, the path carries a parameter, or the phrasing is loose ("the create endpoint for todos" instead of POST /todos). These test whether the prompt spells out its default rules. Adversarial inputs, carrying distractors (a paragraph about a production incident before the actual requirement), a change of mind mid-sentence ("endpoint A — no wait, B"), or a smuggled extra demand ("and make the response paginated while you are at it"). These test whether the prompt keeps the model on the point.

Each case has three parts: the input, a human-labeled expected answer, and a category tag. The expected answer is human work, and there is no shortcut — label one case wrong and the whole evaluation is skewed, and you will think the prompt is at fault and keep editing the prompt. After writing each case, reread the input and confirm the answer is unique. Using the task that runs through the course, an edge case looks like this. It reuses D3's extraction task and labels only the four key fields:

JSONJSON
{
  "id": "c04",
  "kind": "edge",
  "input": "POST /todos, add validation: title is required, at most 200 characters. (The requirement says nothing about what a failure returns.)",
  "expected": { "endpoint": "/todos", "method": "POST", "failureStatus": 400, "fields": ["title"] }
}

It tests one very specific thing: when the requirement omits a status code, does the model apply the team default of 400, or invent a 422? The "samples the model could not possibly know" from D2 belong here too, one or two of them, to see whether it honestly says so or manufactures an answer.

How to decide what counts as right: exact match, field comparison, and LLM-as-judge

With inputs and expected answers in hand, the next question is what counts as right. How tight you make this decides whether the evaluation means anything: too tight and you count phrasing differences as errors, too loose and you let real errors through. Three approaches, each with its own range.

Exact match: the serialized output equals the expected answer exactly. Suitable only when the output is a single value, such as a classification label. For structured output it is far too tight — the model writes post instead of POST, or reorders the field array, and you score it wrong even though the meaning is identical, sending you off to fix a problem that does not exist.

Field comparison: compare field by field, each field with its own rule. Method ignores case; path ignores a trailing slash and tolerates a missing leading slash; the field list is deduplicated and compared ignoring order and case; the status code must match exactly. The principle is compare the fields you care about and tolerate the differences you do not. This is the standard for structured output, and it is the biggest payoff of D3 turning the output into fixed fields — with free text, this step is simply impossible. The scoring function should return a list of mismatched field names, not a boolean, because when you aggregate you need to know what went wrong: if four of ten failures are all failureStatus, the problem is unmistakably that the default was never stated.

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}` // a missing leading slash is phrasing, not misunderstanding
}
 
// Returns the list of mismatched field names; an empty array means pass.
// One layer more information than a boolean: it tells you what went wrong.
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') // status codes get no tolerance
  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
}

LLM-as-judge: when the output is free text — a summary, an email — there are no fields to compare, so you have another model score it against rubric you write. It covers tasks the first two approaches cannot touch, and the cost is real: the judge itself makes mistakes, it leans toward longer and prettier answers, and a vague rubric produces arbitrary scores. Using it presupposes that the rubric is as concrete as a work ticket ("does the summary include the status code, the field names, and the failure behavior — one point each"), and that you periodically sample the judge's verdicts by hand. The rule of thumb: if field comparison can do the job, do not reach for a judge; where a judge is unavoidable, calibrate it first against ten samples a human has already scored.

A/B comparison: one set of inputs, two prompt versions, one table

With scoring in place, A/B is just a loop: for each input in the test set, run the extraction once with version A's prompt and once with version B's, score each, and write both into the same table. Each row is one case, the two columns are the two versions, passes get a check, and failures name the field that broke.

TextText
case  kind         v1                        v2
c01   normal       PASS                      PASS
c04   edge         FAIL failureStatus        PASS
c09   adversarial  FAIL endpoint             PASS
c10   adversarial  PASS                      FAIL failureStatus
 
Pass rate: v1 6/10 (60%)   v2 9/10 (90%)
Fixed by v2:  c04, c05, c07, c09
Broken by v2: c10   <- these go in the CHANGELOG under known regressions

The three summary lines below the table matter in the opposite order from most people's intuition. Pass rate matters least — it only gives you a total. Fixed tells you the change actually solved the problem you were aiming at. Broken matters most: it tells you what regression the new version introduced. In the example above, v2 goes from six to nine and looks like a clear win, but c10 used to pass and now fails — v2 added a rule saying "use 400 when no status code is given," the model took it too literally, and it now rewrites an explicitly requested 422 into 400. Without this table, that regression walks quietly into production.

Two engineering details. First, the same set of inputs: both versions must run identical cases, and changing the cases makes the comparison meaningless. Second, randomness: a real model can give two different answers to the same input, so for a serious evaluation run each case three times and take the majority, or set the temperature to 0. For offline practice, fixed mock outputs are fine. Today's lab uses rules in offline mode to simulate which edges v1 gets wrong and what v2 fixes and breaks, so you can see the whole picture without an API key.

Version control: a prompt is code, so it belongs in the repo with a changelog

By now the prompt has every characteristic of code: it is a function with parameters (D3), it has a test set, and it has an evaluation script that produces a pass rate. So manage it like code: in the repository, with a version number and a changelog.

The mechanics are simple. The prompt body lives in its own file, rather than scattered across string concatenation in business code, and each version has an id. Next to it sits a changelog, and each entry records four things: what changed, why (which failing tests it targets), the pass rate that came out, and the known regressions.

TextText
## v2 (current)
- Change: added five rules - default status 400, uppercase method and normalized path,
  keep field names verbatim, a change of mind means the last one wins, ignore smuggled extras
- Reason: v1 failed c04 (guessed 422 with no status given), c05 (mangled a colloquial path),
  c07 (missed pageSize), c09 (honored only the first of two conflicting endpoints)
- Result: v1 6/10 -> v2 9/10
- Known regression: c10 - "no status means 400" was learned too literally, so an explicitly
  requested 422 gets rewritten to 400. Next version restates it as "use the requirement's own
  status when it gives one" and moves it to the top of the rule list

One difference from code version control is worth calling out. Code changes are usually local: editing one function does not affect another. Prompt changes are global: adding one sentence can change behavior on every input. So a prompt changelog treats known regressions as a mandatory field, whereas a commit message usually has no such field. The other difference is the cost of rollback: rolling a prompt back is swapping one string, essentially free, which makes "roll back to the previous version first, investigate afterward" an even more obvious response to a production problem than it is with code — provided you have a version number to roll back to.

Five common anti-patterns

Finally, collect the traps mentioned across the past three days into one list. Each comes with a way to recognize it, because the hard part of an anti-pattern is not fixing it but spotting it.

Growth by accretion. Every failure earns another sentence, and six months later the prompt is two thousand words and nobody knows which lines still do anything. Recognize it: delete a paragraph and run the test set. If the pass rate is unchanged, that paragraph is dead. With a test set, deletion becomes safe for the first time.

The universal role. "You are a full-stack expert in front end, back end, security, testing, and documentation." The more complete the role, the less information it carries, and the model has no idea what you want it to focus on. Recognize it: can you delete half the role description without affecting a single test case?

Negative instruction pileup. "No pleasantries," "no explanations," "no Markdown," stacked in a row. Models follow bare negatives unreliably, and a negative also drags the word into context (writing "do not mention competitors" sometimes makes it mention them). Recognize it: can each negative be rewritten as a positive with a reason ("start directly with the first section, because the output is parsed section by section")?

Contaminated examples. From D2: the example's format conflicts with the description, the example labels are skewed, or an example contains "and so on." Recognize it: pull the output halves of the examples out on their own and compare them word for word against the format box.

Testing once. Try one case in a chat window after the edit and ship it. This one needs no recognition method — the entire day was about it.

Source Reading

Hands-On Lab

🧪 D4 lab: a ten-case test set plus an A/B evaluation script

Code location: labs/prompt-engineering-5days/day-04-prompt-ab-eval

Acceptance criteria:

  1. MOCK=1 pnpm start runs the starter and the comparison table has 10 rows, with at least 3 cases in each of the three categories.
  2. After scoring, v1 is no longer marked wrong for a lowercase method or a reordered field list: under MOCK, v1 passes 6/10 and each of the four failures names the specific field.
  3. The summary shows both pass rates, the four cases fixed by v2, the one broken by v2, and v2's pass counts per category.
  4. The v2 entry in CHANGELOG.md is complete, its pass rate is the number the script produced, and the known regression records c10 plus how the next version will address it.
  5. Write a v3 that fixes c10's regression, rerun, and append the result to the changelog, including whether it broke anything else.

All five criteria of this lab are doable without an API key. Open labs/prompt-engineering-5days/day-04-prompt-ab-eval, run solution/ first to see the full comparison table, then go back to starter/:

  1. In solution/, run MOCK=1 pnpm start. Study the three-category distribution across the ten cases, which cases each of v1 and v2 fails, and the fixed and broken lines.
  2. Switch to starter/ and run once. Notice that there are only 4 cases, that v1 fails all four (because scoring is too tight), and that the summary is placeholder text — those three spots are the three code exercises.
  3. Extend src/cases.ts to ten: 3 edge cases (colloquial phrasing, a PUT full update, GET query parameters) and 3 adversarial ones (with distractors, a change of mind, a smuggled extra demand), labeling each expected answer by hand and rereading the input to confirm the answer is unique.
  4. Complete score as a field-by-field comparison (tolerating case, trailing slashes, and field order, with the status code exact), then rerun: v1 should pass 6/10 with each of the four failures naming its field.
  5. Complete the summary (pass rates, fixed, broken, per-category counts) and put the numbers into the v2 entry of CHANGELOG.md. Then write a v3 that fixes c10's regression, rerun, and record it.

Interview Questions

Today's three questions are in the question bank below, covering test set design, how prompt versioning differs from code version control, and the boundaries of LLM-as-judge. The second is the most frequently asked prompt engineering question in interviews outside China; derive it yourself before reading the analysis.

Checklist and Tomorrow

  • Build a test set of about ten cases for a prompt, and state what each case is testing
  • Write an A/B evaluation script that compares the pass rate of two prompt versions on the same inputs
  • Version-control a prompt, and explain how to recognize five common anti-patterns
  • All 5 acceptance criteria of the A/B evaluation lab pass
  • You can answer at least 2 of the 3 interview questions without looking at the points

Tomorrow (D5) is the last day of the course, and it answers a question you will hit sooner or later: when the same prompt moves from one model to another, why does it break, exactly where does it break, and how do you organize a system prompt so that moving it does not hurt. The test set you built today goes straight to work — run it after the migration and the pass rate is your answer on whether anything regressed. D5 also closes with what to study next, the Claude course or the Codex course, and how to choose.

Interview questions

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

    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.

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

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

    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?提示词版本化怎么做?它和代码版本管理有什么不同?线上出问题你先做什么?
    Common in ChinaCommon overseasIntermediate#prompt-versioning#evaluation

    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.

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

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

    答题要点

    • 提示词正文独立成文件、每版有 id,变更记录写改动、原因、脚本跑出的通过率、已知回退
    • 与代码的不同:改动是全局的,所以「已知回退」必填;回滚成本几乎为零
    • 线上出问题先回滚上一版,再把触发输入加进测试集查
    • 版本要记录在哪个模型上测的,换模型版本通过率会变
  • Is using a model to grade another model's output reliable? When is it acceptable, and when must a human look?用模型给模型打分靠谱吗?什么时候可以用,什么时候必须人工看?
    Common in ChinaCommon overseasIntermediate#evaluation#llm-as-judge

    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.

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

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

    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

    答题要点

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

Comments

Sign in to join the discussion

No comments yet — be the first.