Debugging Prompts

Fix a misbehaving prompt the way you fix code: collect failing cases, guess the cause, change one thing, and rerun every case to catch what you broke.

15 minLesson 7 of 8
On this page
  1. The loop
  2. A debugging session
  3. Finding the cause
  4. A tiny test runner
  5. Try it yourself
  6. Common mistakes
  7. Check yourself
  8. What to practise next

A prompt has worked for weeks. Then someone forwards a bad reply. You add a line, "Be careful with this kind of request", try the bad case again, and it looks fine. A week later, three different cases that used to work are broken, and nobody knows which edit did it.

That is debugging by feel. Prompts deserve the same habits as code: reproduce the problem, test against a fixed set of cases, change one thing at a time, and check you did not break anything else.

The loop

  1. Collect cases. Gather 5 to 20 real inputs: the ones that failed, plus a spread of ones that already work. Write down the right answer for each.
  2. Run them all and record which pass.
  3. Read the failures next to the prompt, and guess why. Name one cause.
  4. Change one thing that targets that cause.
  5. Rerun every case, not just the one you were fixing. Keep the change only if the total got better and no important case broke.

Step 5 is the one people skip. A prompt is a single piece of text that affects every input, so a fix for one case can quietly change many others.

A debugging session

A support team uses a prompt to label incoming tickets as urgent, normal or low. They collected six tickets and the label each should get. The tracer follows an illustrative session through three versions of the prompt.

Three versions, six test ticketsUse the arrows or ← → keys
  1. test set
    expectedv1v2v3
    Site down for allurgent
    Calm: no one can payurgent
    Feature idealow
    FURIOUS about typolow
    Reports page slownormal
    One user locked outurgent
    version
    none yet

    1Six real tickets and the label a support lead says each should get. The locked-out user cannot run payroll, due today. This is the test set. It includes tickets that already work, not only the ones that failed.

  2. test set
    expectedv1v2v3
    Site down for allurgenturgent
    Calm: no one can payurgentnormal
    Feature idealowlow
    FURIOUS about typolowurgent
    Reports page slownormalnormal
    One user locked outurgenturgent
    version
    v1
    passed
    4 / 6

    2v1: "Label this ticket's priority: urgent, normal or low." Four pass. The calm ticket about payments failing got normal, and the furious one about a typo got urgent.

  3. test set
    expectedv1v2v3
    Site down for allurgenturgent
    Calm: no one can payurgentnormal
    Feature idealowlow
    FURIOUS about typolowurgent
    Reports page slownormalnormal
    One user locked outurgenturgent
    version
    v2
    change
    define urgent

    3Hypothesis: "priority" is never defined, so the model goes by tone. Calm sounds minor, angry sounds urgent. v2 adds a definition: urgent means many users are affected or data is at risk; cosmetic issues are low.

  4. test set
    expectedv1v2v3
    Site down for allurgenturgenturgent
    Calm: no one can payurgentnormalurgent
    Feature idealowlowlow
    FURIOUS about typolowurgentlow
    Reports page slownormalnormalnormal
    One user locked outurgenturgentnormal
    version
    v2
    passed
    5 / 6

    4Both failures are fixed. But rerunning all six shows a new one: a single user locked out right before a payroll deadline is now normal, because "many users" does not cover them. Testing only the two broken tickets would have missed this.

  5. test set
    expectedv1v2v3
    Site down for allurgenturgenturgent
    Calm: no one can payurgentnormalurgent
    Feature idealowlowlow
    FURIOUS about typolowurgentlow
    Reports page slownormalnormalnormal
    One user locked outurgenturgentnormal
    version
    v3
    change
    blocked, not many

    5The definition was too narrow. v3 states the real goal: urgent if anyone is fully blocked from working or data is at risk, "however calm or angry the message sounds". The reason covers both the tone problem and the single-user case.

  6. test set
    expectedv1v2v3
    Site down for allurgenturgenturgenturgent
    Calm: no one can payurgentnormalurgenturgent
    Feature idealowlowlowlow
    FURIOUS about typolowurgentlowlow
    Reports page slownormalnormalnormalnormal
    One user locked outurgenturgentnormalurgent
    version
    v3
    passed
    6 / 6

    6All six pass. Three versions, each one change, each checked against the whole set. You know what fixed what, and that nothing that used to work broke on the way.

1 / 6

Notice what the fix was not: "If the ticket mentions payroll, label it urgent." That passes the test and teaches the model nothing about the next ticket, which will mention invoices. Fix the cause, not the case.

Finding the cause

Most prompt bugs come from a short list of causes. Match the symptom first.

SymptomLikely causeTry
Generic or off-target answerMissing contextSay who it is for and why (Prompting Basics)
Different readings of one wordUndefined termDefine it, with a reason (Be Clear and Specific)
Right content, wrong formatFormat described, not shownAdd an example or template (Show Examples)
Wrong answer to a multi-step problemNo room for workingWorking before answer (Give the Model Room to Think)
Ignores one rule, sometimesRules that conflict, or the rule is buriedSay which rule wins; move it up
Confident made-up detailsFacts not provided, no fallbackProvide the facts; say what to write when unsure

A useful trick when you cannot see the cause: ask the model. Add "Before the label, explain in one sentence which words in the ticket decided it." The explanation is not a reliable account of how the model really reached its label, but it often shows which part of your prompt or input it latched onto.

A tiny test runner

Once you have more than a few cases, run them with code. The runner below takes your cases and any function that returns a label. In real use that function sends the prompt and the ticket to a model; here a stand-in returns fixed answers so you can run it as is.

function runTests(cases, getLabel) {
  const failures = [];
  for (const c of cases) {
    const got = getLabel(c.ticket);
    if (got !== c.expected) failures.push(`${c.ticket}: expected ${c.expected}, got ${got}`);
  }
  console.log(`passed ${cases.length - failures.length} / ${cases.length}`);
  for (const f of failures) console.log("  " + f);
}
 
const cases = [
  { ticket: "Calm: no one can pay", expected: "urgent" },
  { ticket: "FURIOUS about typo", expected: "low" },
  { ticket: "Reports page slow", expected: "normal" },
];
// Stand-in for a real model call, behaving like prompt v1:
const v1 = { "Calm: no one can pay": "normal", "FURIOUS about typo": "urgent", "Reports page slow": "normal" };
runTests(cases, (ticket) => v1[ticket]); // passed 1 / 3
def run_tests(cases, get_label):
    failures = []
    for c in cases:
        got = get_label(c["ticket"])
        if got != c["expected"]:
            failures.append(f'{c["ticket"]}: expected {c["expected"]}, got {got}')
    print(f"passed {len(cases) - len(failures)} / {len(cases)}")
    for f in failures:
        print("  " + f)
 
cases = [
    {"ticket": "Calm: no one can pay", "expected": "urgent"},
    {"ticket": "FURIOUS about typo", "expected": "low"},
    {"ticket": "Reports page slow", "expected": "normal"},
]
# Stand-in for a real model call, behaving like prompt v1:
v1 = {"Calm: no one can pay": "normal", "FURIOUS about typo": "urgent", "Reports page slow": "normal"}
run_tests(cases, lambda ticket: v1[ticket])  # passed 1 / 3

Model replies vary from run to run, as Sampling and Temperature explains. So run each case a few times. A case that passes 2 times out of 3 is a real problem, even though a single run might have hidden it. Growing this runner into a proper test suite is the subject of Evals.

Try it yourself

Try it: Find the cause, then fix it

This prompt translates app text for a Spanish release. The product is called Bloom. Some replies come back with the name translated as "Florecer", which confused testers. Write one sentence naming the likely cause, then rewrite the prompt to fix the cause, not just this one word.

Show a sample answer

Cause: the prompt never says that some words are names, so "Bloom" looks like an ordinary English word to translate.

Rewritten promptIllustrative example. Real replies vary.
You
Translate the app text below into Spanish for users in Spain. Use the informal "tú". Do not translate names of products, features or buttons. Keep these exactly as written: Bloom, Sync Hub, Quick Note. Users search for them in our help pages by their English names, so a translated name sends them nowhere. Reply with the translation only. <text> Bloom keeps your notes in sync across devices. </text>

Why it works: it fixes the cause (names were not marked as names) with a general rule plus a list, and the reason lets the model treat other feature names the same way. After the change, rerun your other translation test cases too, to check the new rule did not stop ordinary words from being translated.

Common mistakes

Check yourself

Quick check

Your change fixes the failing case. What should you do before keeping it?

Tickets written angrily are labelled urgent even when the problem is minor. What is the most likely cause?

A case passes on one run and fails on the next with the same prompt. What does that tell you?

What to practise next

Some bad replies are not your prompt's fault at all: they come from text that someone else slipped into the input. That is Prompt Injection, the last lesson in this module.