Explicit Output Requirements
Reducing Ambiguity With Explicit Output Requirements
Lessons 5 and 6 each introduced a version of the same underlying principle applied to a specific task type: state the exact valid output shape rather than describing it loosely, and let the model infer the rest. This lesson generalizes that principle into a standalone skill, because it is the single highest-leverage technique for making any prompt's output reliable enough for application code to depend on, regardless of task type. An ambiguous prompt does not fail loudly — it produces output that is usually fine, which is often worse than output that fails consistently, because the occasional divergence is what breaks a parser, a downstream field, or a user-facing display in production.
Why Ambiguity Is the Root Cause of Most Prompt Reliability Problems
When an instruction leaves a decision unspecified, the model still has to make that decision on every single call — it cannot leave a field blank in the way a person filling out a form might. Some of those implicit decisions come out consistent across calls; others do not, especially for decisions near a genuine boundary case. The practical consequence is that ambiguity in a prompt does not show up as an obvious bug during initial testing (where a handful of typical inputs all look fine) — it shows up later, as a low but nonzero rate of malformed output once the prompt runs against the full diversity of real production input.
Consider a deceptively simple prompt:
# Ambiguous: several unstated decisions
instructions = "Extract the person's name and age from the text."
This leaves unstated: What format should the output be in — a sentence, a JSON object, a comma-separated pair? What happens if age is not mentioned? What happens if multiple people are named? What happens if age is given as a range ("in her thirties") rather than a specific number? Each of these is a real decision the model must make on every call, and without explicit guidance, it will make each one via a plausible-sounding but uncontrolled default that can silently change between calls, between inputs, or between model versions.
The General Technique: Enumerate Every Output Decision
The fix is systematic: before finalizing a prompt, list every decision the output format leaves open, and answer each one explicitly in the instructions.
# Explicit: same task, every output decision answered
instructions = """Extract the person's name and age from the text.
Respond with a JSON object with exactly two keys: "name" and "age".
- "name": the person's full name as a string, or null if not mentioned.
- "age": the person's age as an integer, or null if not stated as a
specific number (do not estimate from vague descriptions like "in her
thirties" -- use null in that case).
If multiple people are mentioned, extract only the first person named."""
Every ambiguity identified above now has an explicit answer: format is JSON with named keys; missing values become null; vague ages become null rather than an estimate; multiple people resolve to "the first one." None of these answers are objectively "correct" in the abstract — a different application might legitimately want an estimated age from a vague description, or might want all people extracted as a list. The point is not that this specific set of answers is universal; it is that some explicit answer to each question must be given, because leaving it unanswered does not mean the model skips the decision — it means the model decides inconsistently instead of your application deciding consistently.
A Practical Checklist for Common Ambiguities
Across tasks, certain categories of ambiguity recur often enough to check systematically:
| Ambiguity category | Question to answer explicitly | Example resolution |
|---|---|---|
| Output format | What exact structure — prose, JSON, list, single word? | "Respond with only a JSON object with keys X, Y." |
| Missing data | What if the requested information isn't present? | "Use null for any field not found in the text." |
| Multiple matches | What if there are several valid candidates? | "Extract only the first occurrence." |
| Boundary/edge cases | What about inputs at the edge of the task's scope? | "If the text is not in English, respond with an empty object." |
| Precision | How exact must a value be (rounding, units, format)? | "Round to two decimal places. Use ISO 8601 for dates." |
| Length | How long or short should output be? | "Respond in exactly one sentence, under 25 words." |
| Explanation | Should reasoning accompany the answer, or only the answer? | "Respond with only the category name, no explanation." |
Not every prompt needs every row addressed — a summarization prompt has little use for "multiple matches," and a classification prompt has little use for "precision." The checklist's value is in the systematic pass: reading down the list while reviewing a prompt catches ambiguities that are easy to overlook when writing the prompt for the first time with only the typical case in mind.
Worked Example: Applying the Checklist
Take a realistic prompt before and after a checklist pass:
# Before: ambiguous on nearly every axis
BEFORE = "Find the deadline mentioned in this email and tell me if it's urgent."
# After: checklist applied
AFTER = """Find the deadline mentioned in the email below.
Respond with a JSON object with these keys:
- "deadline": the deadline as a string in YYYY-MM-DD format, or null if
no specific date is mentioned.
- "is_urgent": true if the deadline is within 3 days of the email's own
date (stated or implied), false otherwise, or null if deadline is null.
If more than one deadline is mentioned, use the earliest one.
Do not include any explanation, only the JSON object."""
Walking through what changed: output format went from free text to a named JSON structure (format ambiguity resolved); a missing deadline now maps to null rather than an undefined response (missing data resolved); "urgent" was previously a subjective judgment call left entirely to the model's own notion of urgency — now it has a concrete, checkable definition ("within 3 days") that application code could even second-guess or recompute independently if needed (precision resolved); multiple deadlines now resolve to "the earliest" (multiple matches resolved); and an explicit "no explanation" line prevents the model from prefacing the JSON with a sentence of commentary that would break naive json.loads parsing (explanation ambiguity resolved).
When Looser Instructions Are the Right Choice
Explicit constraints are not free — they add prompt length, and an overly rigid specification can sometimes prevent the model from handling a genuinely novel input sensibly (a rule that assumed one deadline format may fumble when the real text uses an unanticipated one). Favor looser instructions when:
- The task is genuinely open-ended and any reasonable output is acceptable (creative writing, brainstorming, general Q&A) — over-constraining these can make output feel mechanical or miss useful variety.
- The application has a robust human review step before output is used for anything consequential, reducing the cost of occasional format drift.
- Early prototyping, where the goal is to understand what the model naturally produces before deciding which behaviors need to be pinned down.
Favor explicit constraints whenever output feeds directly into code — parsing, routing, storage, another API call — because unparseable or inconsistent output there causes an application-level failure, not just a slightly-off response a human reader can mentally correct for.
Verifying That an Instruction Actually Reduced Ambiguity
Adding an explicit instruction is not automatically effective — the model must actually follow it. The only way to know is to test the prompt against a range of inputs, especially edge cases, and check the output mechanically:
import json
from openai import OpenAI
client = OpenAI()
def extract_deadline(email_text: str) -> dict:
response = client.responses.create(
model="gpt-5.6-terra",
instructions=AFTER,
input=email_text,
)
return json.loads(response.output_text)
def check_output_shape(result: dict) -> list[str]:
problems = []
if set(result.keys()) != {"deadline", "is_urgent"}:
problems.append(f"unexpected keys: {result.keys()}")
if result["deadline"] is not None and not isinstance(result["deadline"], str):
problems.append("deadline is not a string or null")
if result["is_urgent"] not in (True, False, None):
problems.append("is_urgent is not a boolean or null")
return problems
check_output_shape is a mechanical validator, not a judgment of correctness — it checks that the shape of the output matches what the instructions promised, independent of whether the extracted deadline is the right one. Running this against a batch of representative inputs (Lesson 9 covers building and using such a dataset systematically) turns "I made the instructions more explicit" from a hopeful guess into a measured, checkable claim: if check_output_shape returns problems on 2% of a representative sample, that is a concrete, trackable defect rate rather than an unknown one.
Testing the Validator Itself
def test_check_output_shape_flags_wrong_keys():
problems = check_output_shape({"deadline": "2027-01-01", "extra": True})
assert any("unexpected keys" in p for p in problems)
print("PASS: extra key detected as a shape problem")
def test_check_output_shape_accepts_valid_output():
problems = check_output_shape({"deadline": None, "is_urgent": None})
assert problems == []
print("PASS: valid null-filled output produces no problems")
def test_check_output_shape_flags_bad_type():
problems = check_output_shape({"deadline": "2027-01-01", "is_urgent": "yes"})
assert any("is_urgent" in p for p in problems)
print("PASS: non-boolean is_urgent value flagged")
test_check_output_shape_flags_wrong_keys()
test_check_output_shape_accepts_valid_output()
test_check_output_shape_flags_bad_type()
These tests validate the validator using hand-constructed dictionaries rather than live model output — cheap, fast, and precisely targeted at the shape-checking logic itself, which is exactly the kind of bug (an overly strict or overly lenient check) that would otherwise only surface once real, messier model output started flowing through it.
Common Mistakes
Assuming a prompt is unambiguous because it worked on the first few test inputs. Ambiguity in an instruction usually surfaces as inconsistent behavior only on edge cases and less common inputs, which a handful of manual tests during development are unlikely to include.
Over-specifying a genuinely open-ended task. Applying the "answer every ambiguity" checklist to a creative or exploratory prompt can produce mechanical, overly rigid output where variety was actually desirable — this technique is for cases where output feeds directly into code, not for every prompt.
Adding a constraint without verifying the model follows it. Writing a more explicit instruction is only half the work; the other half is checking, against real or representative inputs, that the output actually conforms — an instruction the model ignores provides no more reliability than no instruction at all.
Best Practices
Run the ambiguity checklist deliberately before finalizing any prompt whose output feeds application code. Treat format, missing data, multiple matches, edge cases, precision, length, and explanation as a standing list to check against, not something to notice only after a production incident.
Pair every explicit output requirement with a mechanical validator in code. A check_output_shape-style function turns "the model should now do X" into something measurable, and can be reused for both spot-checks during development and systematic evaluation (Lesson 9).
Match the level of constraint to how the output will be used. Tight, explicit constraints for anything parsed or routed by code; looser, more open instructions for genuinely open-ended tasks reviewed by a human.