Module 2: Zero-Shot and Few-Shot Prompting

2. Zero-Shot Prompting Patterns

Capsule overview

Zero-shot prompting is not "write whatever and hope it works". There are four proven patterns that maximize the consistency and quality of responses without adding a single example. In this capsule you'll learn each pattern with runnable code, when to use it, how to combine it with the others, and how to diagnose it when it fails.

The four patterns are: direct instructions (explicit action verbs), role-playing (anchoring perspective and tone), format specification (controlling the output), and constraint-based prompting (verifiable restrictions). Each one solves a different kind of consistency problem. Knowing which one to reach for — or how to combine several — is the difference between a prompt that works 60% of the time and one that works 95%.

Why it matters: These patterns are the foundation of everything else in the guide. The CoT prompts in Module 4 use direct instructions + format specification. The system prompts for agents in Module 8 use role-playing + constraint-based. Mastering them now gives you the vocabulary and the tools for the modules that follow.


Pattern 1: Direct Instructions

Concept

The instruction is explicit, imperative, and unambiguous. It answers "what to do" with concrete action verbs. The model doesn't have to infer the task — you tell it exactly what to execute.

Principle: Every degree of ambiguity in the instruction is a degree of variability in the output.

Anatomy of a good direct instruction

[ACTION VERB] [OBJECT] [CONSTRAINTS] [FORMAT]

Example:
EXTRACT   [people and organizations]  [explicit ones only]   [as JSON]
CLASSIFY  [the sentiment]             [into POSITIVE/NEGATIVE] [one word]
SUMMARIZE [the article]               [in 3 points]          [bullets]

Progressive examples

from openai import OpenAI
import json

client = OpenAI()

# Level 1: Basic — clear action, no additional constraints
def ner_basic(text: str) -> str:
    """Named Entity Recognition — basic version."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "Extract people and organizations from the text."
            },
            {"role": "user", "content": text}
        ],
        temperature=0
    )
    return response.choices[0].message.content

# Level 2: With format — action + output format
def ner_with_format(text: str) -> dict:
    """NER with structured JSON output."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
Extract people and organizations from the text.
Return JSON: {"people": ["name1", ...], "organizations": ["org1", ...]}
If there are none, use empty lists. Valid JSON only, no additional text.
"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)

# Level 3: With explicit constraints — action + format + rules
def ner_complete(text: str) -> dict:
    """NER with domain rules and edge case handling."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
Extract people and organizations from the text.

RULES:
- Only entities that appear EXPLICITLY (do not infer them from context)
- Full names when available (no nicknames)
- Organizations include companies, NGOs, governments, academic institutions
- If a name is ambiguous (person or organization), include it in both with the note [ambiguous]
- If the text has no entities: {"people": [], "organizations": []}

FORMAT: JSON with exact keys: people, organizations
VALID JSON ONLY. No explanations.
"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)

# Test with real text
text = "Maria Garcia, director of Google Spain, met with Telefonica's CEO in Madrid. The event was organized by the MIT Media Lab."

print("Basic:")
print(ner_basic(text))
print("\nWith format:")
print(ner_with_format(text))
print("\nComplete:")
print(json.dumps(ner_complete(text), ensure_ascii=False, indent=2))

Expected output:

Basic:
People: Maria Garcia
Organizations: Google Spain, Telefonica, MIT Media Lab

With format:
{'people': ['Maria Garcia'], 'organizations': ['Google Spain', 'Telefonica', 'MIT Media Lab']}

Complete:
{
  "people": ["Maria Garcia"],
  "organizations": ["Google Spain", "Telefonica", "MIT Media Lab"]
}

The difference between levels: The basic one works but is unparseable by code. The one with format is already structured. The complete one handles edge cases that the others ignore.

The most effective action verbs for direct instructions

Task typeEffective verbsVerbs to avoid
ExtractionExtract, identify, locate, detectFind, do something with
ClassificationClassify, categorize, label, assignAnalyze, review, evaluate
TransformationTranslate, convert, rephrase, adaptChange, modify
GenerationGenerate, create, write, produceMake, prepare
AnalysisAnalyze specifically X, evaluate criterion YAnalyze (with no criterion)

Pattern 2: Role-Playing

Concept

You assign the model a role ("You are an expert in X") to anchor tone, level of detail, perspective, and implicit assumptions. A specific role creates a consistent mental model of the "kind of response" you expect.

Why it works: Pre-training includes text written by experts across many domains. When you say "You are a lawyer specialized in contracts", the model reaches for the legal writing patterns it learned during training.

The role's impact on the output

from openai import OpenAI

client = OpenAI()

question = "What are the risks of using async/await in Python?"

# The same question, three different roles
roles = {
    "senior_engineer": "You are a senior Python engineer with 10 years in distributed systems.",
    "teacher_beginners": "You are a teacher who explains programming to students with no experience.",
    "technical_writer": "You are a technical writer who creates API documentation."
}

print(f"Question: {question}\n")
print("=" * 60)

for name, role in roles.items():
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": role},
            {"role": "user", "content": question}
        ],
        temperature=0,
        max_tokens=150
    )
    print(f"\n[{name}]")
    print(response.choices[0].message.content[:300])
    print("...")

Typical output:

[senior_engineer]
The main risks are: (1) Event loop blocking — if you have a CPU-bound operation
inside a coroutine, you block the entire event loop. Use asyncio.run_in_executor() for CPU-bound work.
(2) Complex error handling — exceptions in concurrent tasks require careful
handling with asyncio.gather(return_exceptions=True)...

[teacher_beginners]
Great question! async/await is useful but it gets confused with threads (which is a different thing).
The main risk is this: if you accidentally do something that "takes a long time" inside
an async function, everything else waits. It's like a coffee shop where the barista
falls asleep while making your coffee — everyone waits...

[technical_writer]
## Risks of async/await in Python

**Risk 1: CPU-bound blocking**
*Issue:* Blocking operations inside coroutines block the event loop.
*Mitigation:* Use `asyncio.run_in_executor()` for CPU-intensive tasks...

Same question, three audiences, three completely different styles. Without role-playing, the model falls back to its default (generally something close to the technical_writer style).

How to build an effective role

An effective role has three components:

# Components of an effective role
ROLE_TEMPLATE = """
You are {who}                   # Identity and expertise
specialized in {domain}         # Area of specialization
with experience in {context}    # Relevant specific context
"""

# Well-built examples
EFFECTIVE_ROLES = {
    "financial_analyst": "You are a financial analyst specialized in SaaS startups, with experience evaluating growth metrics and unit economics.",
    
    "support_agent": "You are a level 2 technical support agent for enterprise software. Your users are system administrators with technical knowledge.",
    
    "technical_editor": "You are a technical editor who writes for backend developers. Your style is concise, precise, and code-example driven.",
    
    "classifier": "You are an automatic classification system for {domain}. You only produce the category, with no explanations.",
}

# Role anti-patterns
INEFFECTIVE_ROLES = {
    "vague": "You are a helpful assistant.",  # Defines nothing specific
    "contradictory": "You are a technical expert who explains things for beginners.",  # Ambiguous
    "no_domain": "You are a professional.",  # No specialization
}

Role + direct instruction: a frequent combination

from openai import OpenAI
import json

client = OpenAI()

def analyze_review(text: str) -> dict:
    """Role-playing + direct instruction + format specification, combined."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
You are a customer experience analyst specialized in e-commerce.
Your job is to extract actionable insights from product reviews.

EXTRACT from the text:
1. sentiment: POSITIVE, NEGATIVE, MIXED
2. aspect: the most important topic mentioned (product/service/shipping/price)
3. action: what the company should do (1 sentence)

FORMAT: {"sentiment": "...", "aspect": "...", "action": "..."}
Valid JSON only.
"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)

reviews = [
    "The product is excellent but it took 3 weeks to arrive. Disappointed with the shipping.",
    "Incredible quality and a fair price. Customer service was very friendly. I'd buy again.",
    "Doesn't work as described. Returned."
]

for r in reviews:
    result = analyze_review(r)
    print(f"Review: {r[:60]}...")
    print(f"  Analysis: {result}\n")

Output:

Review: The product is excellent but it took 3 weeks to arrive....
  Analysis: {'sentiment': 'MIXED', 'aspect': 'shipping', 'action': 'Improve shipping times and update customer estimates'}

Review: Incredible quality and a fair price. Customer service was ...
  Analysis: {'sentiment': 'POSITIVE', 'aspect': 'product', 'action': 'Highlight quality and service in marketing campaigns'}

Review: Doesn't work as described. Returned....
  Analysis: {'sentiment': 'NEGATIVE', 'aspect': 'product', 'action': 'Review the product description and check quality control'}

Pattern 3: Format Specification

Concept

You explicitly specify the output format: JSON, XML, Markdown, a numbered list, a table, and so on. Without format specification, the model chooses how to answer — which leads to format variability between calls.

Rule: If your code needs to process the output, you have to specify the format. The model doesn't guess that you need JSON unless you ask for it.

Formats and when to use each one

FormatUse whenExample
JSONOutput processed by code, complex structures{"key": "value", "list": [...]}
Numbered listSequential steps, ranking1. First step\n2. Second step
BulletsItems with no specific order- Item 1\n- Item 2
Markdown tableComparisons, multiple attributes| Col1 | Col2 |
Plain textDirect human readingNormal paragraphs
Exact stringClassification, yes/noA single word

Format specification, step by step

from openai import OpenAI
import json

client = OpenAI()

article = """
Large language models (LLMs) have revolutionized natural language processing.
GPT-4 and Claude 3 lead the market with advanced reasoning capabilities.
However, API costs and privacy concerns are barriers to mass adoption.
Fine-tuning and carefully designed prompts can significantly improve results.
"""

# Level 1: Simple bullets
def summarize_bullets(text: str) -> str:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"Summarize in 3 bullets:\n{text}"
        }],
        temperature=0
    )
    return r.choices[0].message.content

# Level 2: JSON with a schema
def summarize_json(text: str) -> dict:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
Summarize the text with exactly this JSON:
{
  "key_points": ["point1", "point2", "point3"],
  "keywords": ["kw1", "kw2", "kw3"],
  "overall_sentiment": "positive|negative|neutral"
}
Valid JSON only.
"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(r.choices[0].message.content)

# Level 3: Markdown table for comparisons
def compare_in_table(items: list[str], criteria: list[str]) -> str:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""
Compare the items against the given criteria.
FORMAT: a Markdown table with columns: Item | {" | ".join(criteria)}
Only the table, no additional text.
"""
            },
            {
                "role": "user",
                "content": f"Compare: {', '.join(items)}"
            }
        ],
        temperature=0
    )
    return r.choices[0].message.content

# Level 4: Exact output for classification
def classify_exact_string(text: str, categories: list[str]) -> str:
    cats = ", ".join(categories)
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"Classify into one of: {cats}. Only the exact name of the category. Nothing else."
            },
            {"role": "user", "content": text}
        ],
        temperature=0,
        max_tokens=10
    )
    return r.choices[0].message.content.strip()

# Tests
print("=== Bullets ===")
print(summarize_bullets(article))

print("\n=== JSON ===")
print(json.dumps(summarize_json(article), ensure_ascii=False, indent=2))

print("\n=== Table ===")
print(compare_in_table(
    ["GPT-4o", "Claude 3.5", "Gemini 1.5"],
    ["Speed", "Reasoning quality", "Price"]
))

print("\n=== Exact string ===")
print(classify_exact_string(
    "Error connecting to the database",
    ["TECHNICAL", "BILLING", "GENERAL"]
))

Expected output:

=== Bullets ===
• LLMs have transformed natural language processing
• GPT-4 and Claude 3 lead with advanced reasoning capabilities
• Costs and privacy are barriers to mass adoption

=== JSON ===
{
  "key_points": [
    "LLMs have revolutionized NLP",
    "GPT-4 and Claude 3 lead the market, with cost as a barrier",
    "Fine-tuning and prompts improve results significantly"
  ],
  "keywords": ["LLMs", "GPT-4", "fine-tuning"],
  "overall_sentiment": "neutral"
}

=== Table ===
| Item | Speed | Reasoning quality | Price |
|------|-------|-------------------|-------|
| GPT-4o | High | Excellent | High |
| Claude 3.5 | High | Excellent | Medium |
| Gemini 1.5 | High | Very good | Low |

=== Exact string ===
TECHNICAL

Pattern 4: Constraint-Based Prompting

Concept

You add explicit restrictions: maximum length, language, what to avoid, allowed values, edge case handling. Constraints are verifiable — you can check programmatically whether the output meets them.

The difference between wishes and constraints:

  • Wish: "Keep it concise" → The model decides what concise means
  • Constraint: "50 words maximum" → Verifiable: len(output.split()) <= 50

Types of constraints, with examples

from openai import OpenAI

client = OpenAI()

# Length constraint
prompt_length = """
Translate into Spanish (Latin American Spanish).
CONSTRAINTS:
- 100 words maximum in the translation
- Technical terms (API, endpoint, JSON) are not translated
- No translator's notes, no explanations
"""

# Allowed-values constraint
prompt_values = """
Classify the urgency of the ticket.
CONSTRAINTS:
- Only these values: HIGH, MEDIUM, LOW, CRITICAL
- If you can't determine the urgency: MEDIUM (default)
- No additional text
"""

# Edge case constraint
prompt_edge_cases = """
Extract the email from the text.
CONSTRAINTS:
- If there are multiple emails: return them all in a list
- If there is no email: return an empty list []
- Only emails that appear explicitly, not inferred ones
- Format: {"emails": ["email1@...", "email2@..."]}
"""

# Behavior constraint
prompt_behavior = """
Answer the user's question.
CONSTRAINTS:
- Answer ONLY based on the provided context
- If the answer isn't in the context: "I don't have that information in the context"
- Do not invent data, statistics, or claims that the context doesn't back up
- 3 sentences maximum
"""

# A complete example with multiple constraints
def translate_documentation(text: str, target_language: str = "Spanish") -> str:
    """
    Translates technical documentation with strict constraints.
    """
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""
You are a technical translator specialized in software documentation.
Translate into {target_language}.

MANDATORY CONSTRAINTS:
1. Technical terms: DO NOT translate (API, endpoint, JSON, HTTP, REST, SDK, CLI, etc.)
2. Code: DO NOT modify — anything in backticks or code blocks stays as it is
3. Function/variable names: DO NOT translate
4. Length: keep a length similar to the original (±20%)
5. Tone: technical and formal
6. If a translation is ambiguous: use the one most common in official documentation
"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0
    )
    return r.choices[0].message.content

technical_text = """
Create a REST endpoint using FastAPI. The endpoint should accept a JSON body with the `user_id` field 
and return the user's data from the database. Use `async/await` for the queries.
"""

translation = translate_documentation(technical_text)
print(translation)

Expected output:

Crea un endpoint REST usando FastAPI. El endpoint debe aceptar un JSON body con el campo `user_id` 
y devolver los datos del usuario desde la base de datos. Usa `async/await` para las consultas.

Note: API, FastAPI, JSON, user_id, async/await come through untranslated — the constraints work.


Combining the 4 Patterns

In production prompts, all four patterns work together:

from openai import OpenAI
import json

client = OpenAI()

# Example: a complete system prompt for a support classifier
SYSTEM_FULL_CLASSIFIER = """
## Role (Role-Playing)
You are an automatic classifier of technical support tickets for a B2B SaaS company.

## Task (Direct Instruction)
Classify each ticket into ONE category and assign a priority.

## Categories (Direct Instruction + Format)
Available categories:
- ACCESS: trouble getting into the system
- ERROR: crashes, errors, broken functionality
- PERFORMANCE: slowness, timeouts
- INTEGRATION: connection with external APIs or services
- BILLING: charges, invoices, plans
- DATA: export, import, data loss
- OTHER: anything that doesn't fit

Priorities: CRITICAL (system down), HIGH (functionality blocked), MEDIUM (inconvenience), LOW (question)

## Constraints (Constraint-Based)
- If you're torn between two categories: pick the more specific one
- CRITICAL priority: only if the user indicates the system is down for multiple users
- If the ticket is vague or incomplete: category OTHER, priority MEDIUM
- If it mentions a business deadline: raise the priority one level

## Output Format (Format Specification)
Exact JSON:
{
  "category": "CATEGORY_IN_UPPERCASE",
  "priority": "CRITICAL|HIGH|MEDIUM|LOW",
  "reason": "10 words maximum"
}
JSON only. No additional text.
"""

def classify_ticket(text: str) -> dict:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_FULL_CLASSIFIER},
            {"role": "user", "content": text}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(r.choices[0].message.content)

tickets = [
    "The dashboard hasn't loaded for anyone for 2 hours",
    "How do I export the reports to PDF?",
    "Error 403 when trying to connect to the Salesforce API",
    "I need the sales report for tomorrow — meeting with the executives",
]

for ticket in tickets:
    result = classify_ticket(ticket)
    print(f"Ticket: {ticket[:60]}")
    print(f"  {result}\n")

Output:

Ticket: The dashboard hasn't loaded for anyone for 2 hours
  {'category': 'PERFORMANCE', 'priority': 'CRITICAL', 'reason': 'system down for multiple users'}

Ticket: How do I export the reports to PDF?
  {'category': 'DATA', 'priority': 'LOW', 'reason': 'question about how a feature works'}

Ticket: Error 403 when trying to connect to the Salesforce API
  {'category': 'INTEGRATION', 'priority': 'HIGH', 'reason': 'integration blocked by authentication error'}

Ticket: I need the sales report for tomorrow — meeting with the executives
  {'category': 'DATA', 'priority': 'HIGH', 'reason': 'business deadline detected, priority raised'}

Pattern Comparison by Use Case

PatternBest forAvoid in
Direct instructionsExtraction, classification, transformationOpen-ended creative tasks
Role-playingAnalysis, recommendations, a specific toneTechnical tasks where the role doesn't matter
Format specificationOutput that code has to parseConversational responses
Constraint-basedFine control, edge cases, productionQuick prototyping

Rule for combining: In production, use all four. In prototyping, start with direct instructions + format specification and add the others as you need them.


Connection to the Project

In the Few-Shot Classification System (capsule 08), the zero-shot component uses:

  • Direct instructions: "Classify into one of: [categories]. Only the category."
  • Format specification: Output as an exact string so code can parse it
  • Constraint-based: "If you can't classify with confidence, return OTHER"
  • Role-playing: "You are an automatic classifier for [domain]"

The system compares this zero-shot against few-shot and measures when each pattern is enough.


Troubleshooting

Problem 1: The model ignores the specified format

Cause: The format is buried in the prompt or competing with a lot of text.

Fix:

# ✅ Prominent format at the end
system = """
[Main instruction here]

RESPONSE FORMAT (MANDATORY):
{"result": "..."}
JSON only. Nothing else.
"""

# Better still: use response_format for guaranteed JSON (OpenAI)
response_format={"type": "json_object"}

Problem 2: Role-playing produces responses that are too verbose

Cause: The role implies "expert", and experts tend to be wordy. With no length constraint, the model justifies its expertise with more text.

Fix:

# Add a length constraint to the role
role = "You are a senior analyst. Concise answers: 3 sentences or 50 words maximum."

Problem 3: Direct instructions fail on ambiguous tasks

Cause: "Classify" with no categories, or "analyze" with no specific criteria. Ambiguity in the instruction = variability in the output.

Fix:

# ❌ Ambiguous
"Classify the ticket."

# ✅ Specific
"Classify into ONE of these categories: ACCESS, ERROR, PERFORMANCE, OTHER.
If it's none of them: OTHER."

Problem 4: The constraints get ignored for unusual inputs

Cause: The model "decides" the input is a special case that warrants breaking the constraint.

Fix:

# Make the constraints more explicit, with emphasis
CONSTRAINTS = """
ABSOLUTE RULES (no exceptions):
- ALWAYS respond in JSON, even if the text is rude or irrelevant
- If the input is incomprehensible: {"error": "incomprehensible_input"}
- NEVER add text before or after the JSON
"""

Problem 5: Inconsistent output with temperature > 0

Cause: Temperature > 0 introduces variability on purpose.

Fix: For classification and extraction, always use temperature=0. For generation where you want variety, accept the format inconsistency and control it with format specification.


Exercises

Exercise 1: Turn a vague instruction into a direct one (Easy)

Transform this vague instruction into a direct instruction with a specified format:

"Do something useful with these customer comments."
See solution
# Direct instruction + format specification
SYSTEM = """
For each customer comment: extract the sentiment and the main topic mentioned.

JSON FORMAT:
[{"comment": "original text", "sentiment": "POSITIVE|NEGATIVE|NEUTRAL", "topic": "product|service|price|shipping|other"}]

RULES:
- If the topic is ambiguous: use the most prominent one
- If the comment is very short (< 5 words): sentiment NEUTRAL
- Valid JSON only. No additional text.
"""

The key difference: The direct version specifies exactly what to extract, which values are valid for each field, and how to handle edge cases — all without adding a single example.


Exercise 2: Design role-playing for technical support (Easy)

Design a role for a level 1 technical support agent at a software company. The role has to calibrate: tone, audience, and the limits of what it can answer.

See solution
ROLE = """
You are a level 1 technical support agent at SoftwareCo.

Your audience: non-technical end users at mid-sized companies (50-500 employees).

YOU CAN:
- Answer basic questions about using the software
- Guide people through standard configurations
- Provide links to official documentation

YOU CANNOT:
- Access real user accounts
- Promise resolution dates
- Discuss legal or data privacy matters

If the problem requires level 2 (code, database, complex integrations):
"This problem requires specialized support. I'll escalate it to the technical team.
Can I get your contact email?"

Tone: professional, empathetic, no technical jargon.
"""

This role defines identity, audience, capabilities, limitations, and escalation behavior — every component of a production role.


Exercise 3: Add constraints to an extraction prompt (Medium)

Given this basic prompt, add at least 4 constraints that make it production-ready:

"Extract the email from the text."
See solution
SYSTEM = """
Extract the email from the text.

CONSTRAINTS:
1. If there are multiple emails: return them all in a list
2. If there is no email: return an empty list []
3. Only emails that appear EXPLICITLY (not ones inferred from context)
4. Mandatory format: {"emails": ["email@domain.com", ...]}
5. Only the domain is case-insensitive; the local part (before the @) keeps its original case
6. If the text is empty or only whitespace: {"emails": [], "error": "empty_input"}

JSON ONLY. No additional text.
"""

The 4 minimum constraints solve: multiple emails, no email at all, implicit emails, and the exact format. Constraints 5 and 6 are extra improvements for production edge cases.


Exercise 4: Implement a classifier with all 4 patterns (Medium)

Implement a Python function that uses the four patterns (role-playing, direct instruction, format specification, constraint-based) to classify mobile app reviews into: BUG_REPORT, FEATURE_REQUEST, GENERAL_FEEDBACK.

See solution
import json
from openai import OpenAI

client = OpenAI()

SYSTEM = """
## Role
You are an automatic classifier of mobile app reviews for the product team.

## Instruction
Classify each review into ONE of the categories, based on its main content.

## Categories
- BUG_REPORT: The user reports an error, crash, malfunction or unexpected behavior
- FEATURE_REQUEST: The user asks for a new feature or an improvement to an existing one
- GENERAL_FEEDBACK: A general opinion, satisfaction/dissatisfaction with no error report and no feature request

## Constraints
- If the review has a bug + a feature: classify it as BUG_REPORT (more urgent)
- If the review is very short (< 5 words): GENERAL_FEEDBACK
- If you can't classify with confidence: GENERAL_FEEDBACK
- Ignore the language (it can be any)

## Format
{"category": "BUG_REPORT|FEATURE_REQUEST|GENERAL_FEEDBACK", "confidence": 0.0-1.0}
Valid JSON only.
"""

def classify_review(review: str) -> dict:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": review}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(r.choices[0].message.content)

reviews = [
    "The app crashes every time I try to attach a photo",
    "It would be great to export the data straight to Excel",
    "5 stars, the best app I've ever used",
    "Excellent app but it needs dark mode",  # Mixed: feedback + feature
    "No"  # Very short
]

for r in reviews:
    result = classify_review(r)
    print(f"Review: {r}")
    print(f"  {result}\n")

Teaching note: This exercise integrates all 4 patterns. The role anchors the perspective (product team). The direct instruction defines what to do. The categories with descriptions reduce ambiguity. The constraints handle edge cases. The format guarantees a parseable output.


Exercise 5: Diagnose a failing prompt (Hard)

This prompt fails ~40% of the time — the model sometimes includes explanations, uses lowercase, or gives multiple categories. Identify which pattern(s) it's missing and fix it.

"What is the sentiment of this comment: positive, negative or neutral?"
See solution

Diagnosis:

  • Missing Format specification: the question allows answers like "It's positive", "Positive (though with a nuance...)", "it could be positive or neutral"
  • Missing Constraint-based: no "a single word", "no explanations", "no punctuation"
  • Missing Role (minor): with no role, the model can turn conversational

Corrected prompt:

SYSTEM = """
Classify the sentiment of the comment.
Respond ONLY with one of these exact words: POSITIVE, NEGATIVE, NEUTRAL.
No punctuation, no explanations, no additional text.
For mixed sentiments: use the dominant one.
"""

Why the original fails: It's an open question, not an instruction. Questions invite conversational answers. Direct instructions in the imperative ("classify", "respond with") produce more controlled outputs.


Summary

In this capsule you learned:

  • Direct instructions: Action verbs in the imperative (extract, classify, summarize), explicit restrictions. Remove ambiguity = remove variability
  • Role-playing: "You are an X specialized in Y" anchors tone, vocabulary and perspective. Specific > generic
  • Format specification: Define the output format explicitly. JSON mode for parseable output. The format goes at the end or as a prominent constraint
  • Constraint-based: Verifiable restrictions (length, allowed values, edge cases). The difference between wishes and constraints: constraints can be verified programmatically
  • Combination: In production, the 4 patterns work together. The system prompt in Module 8 uses all four at once

Next capsule: Few-shot prompting — choosing examples: how many to use, how to pick them, what order to put them in, and when an example does more harm than good.


Further resources

  1. OpenAI Prompt Engineering — Tactics — OpenAI's official tactics line up exactly with these 4 patterns
  2. Anthropic Prompt Engineering — Claude-specific guide with an emphasis on role-playing and constraints
  3. Prompt Engineering Guide (DAIR.AI) — Zero-Shot — Comparison of techniques with accuracy benchmarks across different tasks
  4. Learn Prompting: Structuring Prompts — Format specification examples with different output formats
  5. OpenAI JSON Mode Documentation — How to use response_format to guarantee valid JSON (the basis of Module 3)
  6. OpenAI Cookbook: Techniques to improve reliability — A collection of real techniques, with code, for improving prompt consistency