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.
On this page
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
- 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.
- Run them all and record which pass.
- Read the failures next to the prompt, and guess why. Name one cause.
- Change one thing that targets that cause.
- 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.
- test set
expected v1 v2 v3 Site down for all urgent Calm: no one can pay urgent Feature idea low FURIOUS about typo low Reports page slow normal One user locked out urgent - 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.
- test set
expected v1 v2 v3 Site down for all urgent urgent Calm: no one can pay urgent normal Feature idea low low FURIOUS about typo low urgent Reports page slow normal normal One user locked out urgent urgent - 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 goturgent. - test set
expected v1 v2 v3 Site down for all urgent urgent Calm: no one can pay urgent normal Feature idea low low FURIOUS about typo low urgent Reports page slow normal normal One user locked out urgent urgent - 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.
- test set
expected v1 v2 v3 Site down for all urgent urgent urgent Calm: no one can pay urgent normal urgent Feature idea low low low FURIOUS about typo low urgent low Reports page slow normal normal normal One user locked out urgent urgent normal - 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. - test set
expected v1 v2 v3 Site down for all urgent urgent urgent Calm: no one can pay urgent normal urgent Feature idea low low low FURIOUS about typo low urgent low Reports page slow normal normal normal One user locked out urgent urgent normal - 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.
- test set
expected v1 v2 v3 Site down for all urgent urgent urgent urgent Calm: no one can pay urgent normal urgent urgent Feature idea low low low low FURIOUS about typo low urgent low low Reports page slow normal normal normal normal One user locked out urgent urgent normal urgent - 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.
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.
| Symptom | Likely cause | Try |
|---|---|---|
| Generic or off-target answer | Missing context | Say who it is for and why (Prompting Basics) |
| Different readings of one word | Undefined term | Define it, with a reason (Be Clear and Specific) |
| Right content, wrong format | Format described, not shown | Add an example or template (Show Examples) |
| Wrong answer to a multi-step problem | No room for working | Working before answer (Give the Model Room to Think) |
| Ignores one rule, sometimes | Rules that conflict, or the rule is buried | Say which rule wins; move it up |
| Confident made-up details | Facts not provided, no fallback | Provide 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 / 3def 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 / 3Model 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
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.
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
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.