Module 1: Fundamentals of Prompt Engineering
5. Mental Models for Designing Prompts: CRISPE
Capsule overview
Designing prompts by intuition doesn't scale. Without a framework, every prompt is an isolated experiment: you try something, see if it works, tweak it, and repeat with no method. That works for personal exploration, but in production you need reproducible prompts that any team member can understand, modify, and version.
CRISPE (Capacity, Role, Insight, Statement, Personality, Experiment) is the mental model that turns prompt design into a systematic process. Instead of asking yourself "how do I write this prompt?", it asks you six concrete things about your task. Each answer is a component of the prompt. When you have all six, you have a complete prompt.
Why it matters: The following modules in this guide — zero-shot, few-shot, CoT, ReAct, composition — all benefit from having a reference framework. When a CoT prompt fails, CRISPE helps you diagnose which component is the problem. When you design a system prompt for an agent in module 8, CRISPE is the implicit structure behind the design.
The CRISPE framework
CRISPE is an acronym covering the six key dimensions of a professional prompt:
| Letter | Dimension | Question it answers |
|---|---|---|
| C | Capacity | What action/capability should the model execute? |
| R | Role | What role, expertise or persona does it adopt? |
| I | Insight | What context or background information does it need? |
| S | Statement | What is the concrete task and specific instruction? |
| P | Personality | What tone, style, constraints and guardrails? |
| E | Experiment | What output format? Few-shot examples? |
Analogy: If a prompt is a software spec, CRISPE is the requirements template. C is the operation type, R is the actor's context, I are the preconditions, S are the functional requirements, P the non-functional ones, and E the input/output contract.
Breaking down CRISPE with progressive examples
C — Capacity
What it is: The main capability or action the model must execute. It defines the verb of the task.
Most common Capacity verbs:
- Classify, categorize, label
- Extract, identify, detect
- Summarize, condense, synthesize
- Translate, adapt, rephrase
- Generate, create, produce
- Analyze, evaluate, compare
- Transform, convert, process
- Verify, validate, correct
Why it matters: Without a clear Capacity, the model can do something related but different. "Something with this text" vs "Classify and extract" are completely different instructions.
Progression:
❌ Vague: "Something with this review text"
⚠️ Better: "Analyze this text"
✅ Precise: "Classify the sentiment and extract the product aspects mentioned"
Rule: Capacity should be an action verb in the infinitive. If you use "something" or "process", Capacity is incomplete.
R — Role
What it is: The role, expertise, or persona the model adopts. It affects vocabulary, level of detail, implicit assumptions, and response style.
How Role changes the output:
from openai import OpenAI
client = OpenAI()
question = "How do I improve the performance of this Python function?"
# Role 1: Senior engineer
r_senior = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a senior Python engineer specialized in performance optimization."},
{"role": "user", "content": question}
],
temperature=0
)
# Role 2: Teacher for beginners
r_teacher = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a teacher who explains programming to students with no prior experience."},
{"role": "user", "content": question}
],
temperature=0
)
# r_senior → Talks about profiling, time complexity, async, caching
# r_teacher → Talks about basic loops, simplicity, avoids jargon
Roles by domain:
- Technical: "You are a senior Python engineer with 10 years of experience in distributed systems"
- Legal: "You are a lawyer specialized in international contracts under Spanish law"
- Education: "You are a teacher who explains complex concepts using real-world analogies"
- Analysis: "You are a data analyst with experience in e-commerce metrics"
- Support: "You are a level 2 technical support agent for enterprise software"
Rule: The Role must be specific, not generic. "You are a helpful assistant" is not a Role — it's the model's default. A real Role constrains and calibrates behavior.
I — Insight
What it is: Context, background information, domain definitions, or constraints the model needs to do the task well but that it CANNOT infer from the Statement or the Input.
Types of Insight:
- Domain definitions: "A 'critical' ticket is defined as any issue affecting more than 100 users simultaneously"
- Business constraints: "Only applies to users on the Enterprise plan (not Free or Pro)"
- System context: "This prompt operates in an inventory system. Product codes have the format SKU-XXXX where X are digits"
- Special rules: "If the total exceeds $1,000, the discount level changes to Tier-2"
- User background: "The user already restarted the device 3 times and has a stable internet connection"
What should NOT go in Insight:
- Things the model can infer from the Input
- Information that repeats the Statement
- Irrelevant context that adds tokens with no value
# Well-calibrated vs overloaded Insight
# ❌ Overloaded Insight (restates the obvious)
BAD_INSIGHT = """
This is a support ticket classification system.
Tickets are messages from users.
Users write in when they have problems.
Problems can be technical or non-technical.
The categories are: Technical, Billing, General.
"""
# ✅ Precise Insight (only what isn't inferable)
GOOD_INSIGHT = """
Available categories and their criteria:
- TECHNICAL: Software error, functionality failure, degraded performance
- BILLING: Charges, invoices, plan changes, refunds
- GENERAL: Usage questions, feature requests, feedback
If the message mentions multiple categories, pick the one that needs the most urgent attention.
BILLING tickets with the words "fraud" or "incorrect charge" → always BILLING.
"""
Rule: Apply the principle of the question "can the model infer this from context?" If YES → don't put it in Insight. If NO → put it in.
S — Statement
What it is: The concrete, specific instruction. The exact task the model must execute.
Difference from Capacity:
- Capacity is the general verb: "Classify"
- Statement is the full instruction: "Classify the following support ticket into exactly one of the defined categories, picking the most specific one when there's ambiguity"
Statement progression:
Level 1 (weak): "Analyze this text"
Level 2 (better): "Identify the sentiment of this text"
Level 3 (good): "Classify the sentiment of this text as POSITIVE, NEGATIVE, or NEUTRAL"
Level 4 (complete): "Classify the sentiment of the following text into EXACTLY one of these categories:
POSITIVE, NEGATIVE, NEUTRAL. If there are mixed sentiments, pick the dominant one."
Most effective Statement verbs:
- "Identify exactly..."
- "Classify into one of these values: [list]"
- "Extract the following fields: [list]"
- "Return only..."
- "If [condition], then [action]; otherwise [alternative action]"
P — Personality
What it is: Tone, response style, behavioral constraints, and guardrails. It controls the how, not the what.
Personality components:
# Complete Personality example
PERSONALITY = """
- Language: Always respond in English, regardless of the input's language
- Tone: Formal and technical. No slang. No colloquialisms
- Length: Concise response. Maximum 3 sentences unless the user asks for more
- Constraints:
* Don't make up information. If you're not certain, say "I don't have enough information to answer this with certainty"
* Don't make assumptions about malicious intent
- Guardrails: If the input asks you to generate harmful content, respond: "I can't help with that"
- Format: No markdown unless explicitly requested
"""
Minimal vs complete Personality:
# Minimal Personality (for simple tasks)
personality_minimal = "Respond ONLY with the category. No extra text."
# Complete Personality (for agents with more behavior)
personality_complete = """
- Only the category in uppercase, no punctuation
- No explanations or justifications
- No greetings or sign-offs
- If the input is ambiguous: UNKNOWN
- If the input is empty: ERROR_EMPTY_INPUT
"""
E — Experiment
What it is: The expected output format, few-shot examples, and any specification of how the response must be structured.
Format options in E:
- JSON with a specific schema
- Bullet list with a defined structure
- Markdown table
- Exact string (one word, one number)
- Structured text with named sections
- Code in a specific language
Few-shot in E — the common pattern:
Experiment:
Input: "Excellent product, highly recommended" → Output: POSITIVE
Input: "Terrible customer service, wouldn't come back" → Output: NEGATIVE
Input: "It's an average product, neither good nor bad" → Output: NEUTRAL
Explicit schema in E:
Experiment:
Exact format (JSON):
{
"category": "TECHNICAL|BILLING|GENERAL",
"confidence": 0.0-1.0,
"reason": "short string of at most 10 words"
}
Complete example: CRISPE applied
Task: Classify user intent in a B2B technical support chatbot.
from openai import OpenAI
import json
client = OpenAI()
# The 6 CRISPE components, made explicit
CAPACITY = "Classify the user's intent into a predefined category."
ROLE = "You are an intent classifier for a B2B technical support chatbot."
INSIGHT = """
Available categories:
- GREETING: The user says hello without asking a specific question
- TECHNICAL_QUESTION: Technical problem, error, failure, something doesn't work
- COMPLAINT: Dissatisfaction with the service or experience (may include a technical component, but the tone is frustration/emotional dissatisfaction)
- INFO_REQUEST: Request for information about product, pricing, features
- FAREWELL: The user says goodbye or closes the conversation
- OTHER: Anything that doesn't clearly fit the above
If there are multiple intents, pick the DOMINANT one (the one needing the most immediate attention).
Tell COMPLAINT apart from TECHNICAL_QUESTION by tone: emotional frustration → COMPLAINT; neutral technical question → TECHNICAL_QUESTION.
"""
STATEMENT = "Classify the following user message into exactly ONE of the defined categories."
PERSONALITY = """
- Respond ONLY with the category name (uppercase, no extra punctuation)
- No explanations
- No text before or after the category
"""
EXPERIMENT = """
Response format: a single word in uppercase.
Examples:
- "Hi, good afternoon" → GREETING
- "I can't access my account since Friday" → TECHNICAL_QUESTION
- "I've been waiting 3 days for a reply and nobody helps me, this is unacceptable" → COMPLAINT
- "How much does the Enterprise plan cost?" → INFO_REQUEST
- "Thanks a lot, talk soon" → FAREWELL
"""
# Assemble into a system prompt
SYSTEM = f"""## Capacity
{CAPACITY}
## Role
{ROLE}
## Insight
{INSIGHT}
## Statement
{STATEMENT}
## Personality
{PERSONALITY}
## Experiment
{EXPERIMENT}
"""
# Test with multiple representative inputs
test_inputs = [
"Good morning, I need help",
"The system has been down since 9am and I can't work",
"Do you have a Salesforce integration?",
"Sick of waiting, this is a disaster and my company is paying a lot of money",
"See you later, thanks for the help",
"I get a 500 error when logging in",
"I want to explore upgrade options"
]
print("=== Classification test with CRISPE ===\n")
for msg in test_inputs:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": msg}
],
temperature=0,
max_tokens=15
)
classification = response.choices[0].message.content.strip()
print(f"Input: {msg[:60]}")
print(f"Output: {classification}\n")
Expected output:
=== Classification test with CRISPE ===
Input: Good morning, I need help
Output: GREETING
Input: The system has been down since 9am and I can't work
Output: TECHNICAL_QUESTION
Input: Do you have a Salesforce integration?
Output: INFO_REQUEST
Input: Sick of waiting, this is a disaster and my company is paying a lot of money
Output: COMPLAINT
Input: See you later, thanks for the help
Output: FAREWELL
Input: I get a 500 error when logging in
Output: TECHNICAL_QUESTION
Input: I want to explore upgrade options
Output: INFO_REQUEST
The prompt as a software spec
Thinking of the prompt as a software spec changes how you design it. You're not "writing instructions" — you're defining a behavior contract.
| Software spec element | Prompt equivalent (CRISPE) |
|---|---|
| Operation type | Capacity (action verb) |
| Domain and actor | Role |
| Preconditions and context | Insight |
| Functional requirements | Statement |
| Non-functional requirements | Personality |
| I/O contract | Experiment |
Anti-pattern: "Do something useful with this" — with no spec, the model uses its defaults, which may not match your expectation.
Pattern: "Given text in any language (I), extract named entities (C+S) as an NLP engineer (R), without making things up, in JSON (P+E)."
Think in constraints, not wishes
The most important difference between a weak prompt and a strong one is in the constraints.
Wishes (vague, hard to evaluate):
- "Make it good"
- "Make it accurate"
- "Make it useful and complete"
Constraints (specific, verifiable):
- "Respond only with one of these 5 categories: [list]"
- "Maximum 100 words"
- "Don't include information that isn't in the input text"
- "JSON format with exactly these keys: a, b, c"
- "If the input is empty, return: {"error": "empty_input"}"
# Wish vs Constraint in code
# ❌ Wish: Vague, not evaluable
system_wish = """
You are a helpful assistant that gives accurate and complete information about products.
"""
# ✅ Constraint: Specific, evaluable, reproducible
system_constraint = """
You are a product information extractor.
CONSTRAINTS:
- Extract ONLY these fields: name, price, availability
- Format: JSON with these exact keys: {"name": str, "price": float|null, "availability": bool}
- If a field isn't in the text: use null (never make up values)
- If the text doesn't describe a product: {"error": "not_a_product"}
- No text outside the JSON
"""
# Test
def extract_product(text: str) -> dict:
from openai import OpenAI
import json
client = OpenAI()
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_constraint},
{"role": "user", "content": text}
],
temperature=0,
response_format={"type": "json_object"}
)
return json.loads(r.choices[0].message.content)
# Test cases
tests = [
"Dell XPS 13 laptop, price: $1,299, available in stock",
"This item is temporarily out of stock",
"The sky is clear today"
]
for t in tests:
print(f"Input: {t}")
print(f"Output: {extract_product(t)}\n")
Expected output:
Input: Dell XPS 13 laptop, price: $1,299, available in stock
Output: {'name': 'Dell XPS 13 laptop', 'price': 1299.0, 'availability': True}
Input: This item is temporarily out of stock
Output: {'name': None, 'price': None, 'availability': False}
Input: The sky is clear today
Output: {'error': 'not_a_product'}
Rule: Every constraint shrinks the space of possible responses → more predictability.
Iterative refinement with CRISPE
CRISPE isn't a form you fill in once. It's a refinement cycle:
1. First draft: Write the prompt with the 6 components
↓
2. Test with 10+ representative inputs (happy path + edge cases)
↓
3. Analyze failures: What kind of inputs fail?
↓
4. Diagnose: Which CRISPE component is responsible for the failure?
↓
5. Refine that component specifically
↓
6. Re-test. Repeat until acceptable consistency (>90%)
Diagnosis by component — real examples:
# Observed failure: COMPLAINT classified as TECHNICAL_QUESTION when there's technical frustration
# Diagnosis: Insight doesn't clearly separate the two categories
# Action: Improve the Insight
INSIGHT_V1 = """
- COMPLAINT: Dissatisfaction with the service
- TECHNICAL_QUESTION: Technical problem
"""
# After observing failures...
INSIGHT_V2 = """
- COMPLAINT: EMOTIONAL frustration or dissatisfaction toward the service.
May mention a technical problem, but the dominant tone is emotional/confrontational.
Signals: "fed up", "unacceptable", "disappointing", "never again", "disaster"
- TECHNICAL_QUESTION: NEUTRAL or informative description of a technical failure.
Signals: "error", "doesn't work", "I can't", questions about the cause of the problem
"""
# Observed failure: The model sometimes adds extra text before the category
# Diagnosis: Personality isn't restrictive enough
# Action: Improve the Personality and the Experiment
PERSONALITY_V1 = "Respond only with the category."
PERSONALITY_V2 = """
- Respond ONLY with the category name (e.g.: GREETING)
- No trailing punctuation
- No leading or trailing space
- No justification, explanation, or extra text
"""
Simplified CRISPE for simple tasks
For simple tasks, you don't always need all 6 components. Prioritize based on the task type:
| Task type | Minimum components needed |
|---|---|
| Simple classification | C + I (categories) + S + E (format) |
| Data extraction | C + S + E (schema) |
| Conversation with a persona | R + P + S |
| Creative generation | R + C + S + E (examples) |
| Complex analysis | All 6 |
# Example: partial CRISPE for language detection
# Rationale for the omissions:
# - Role: The task is so specific there's no ambiguity of perspective
# - Insight: There's no complex domain logic
# - Personality: No relevant tone constraints (only brevity, covered in E)
SYSTEM_SIMPLE = """
## Capacity
Detect the language of the text.
## Statement
Identify the language of the following text.
## Experiment
Respond only with the language's ISO 639-1 code (2 lowercase letters).
Examples: es, en, fr, pt, de, it, zh, ja, ar
If the text is too short or ambiguous: "und" (undetermined)
"""
from openai import OpenAI
client = OpenAI()
texts = [
"El prompt engineering es fundamental para sistemas de IA",
"Prompt engineering is fundamental for AI systems",
"Le prompt engineering est fondamental pour les systèmes d'IA",
"OK" # Ambiguous: too short
]
for text in texts:
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM_SIMPLE},
{"role": "user", "content": text}
],
temperature=0,
max_tokens=5
)
print(f"'{text[:45]}' → {r.choices[0].message.content.strip()}")
Expected output:
'El prompt engineering es fundamental para si' → es
'Prompt engineering is fundamental for AI sys' → en
'Le prompt engineering est fondamental pour l' → fr
'OK' → und
Lesson: CRISPE is a guide, not a rigid template. For simple tasks, 3-4 components can be enough. For production systems with edge cases, use all of them.
Comparison: with vs without CRISPE
import time
# Prompt without CRISPE
PROMPT_WITHOUT = "Classify this support ticket."
# Prompt with CRISPE (condensed for comparison)
PROMPT_WITH = """
Classify this support ticket from a B2B company.
Categories (pick exactly one):
- TECHNICAL: Software error, functionality failure
- BILLING: Charges, invoices, plans, refunds
- GENERAL: Usage questions, features, feedback
Respond ONLY with the category name in uppercase. Nothing else.
"""
ticket = "My invoice this month has an incorrect charge"
# Consistency test (5 calls each)
results_without = []
results_with = []
client = OpenAI()
for i in range(5):
r_without = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{PROMPT_WITHOUT}\n\nTicket: {ticket}"}],
temperature=0.5
)
results_without.append(r_without.choices[0].message.content.strip()[:50])
r_with = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": PROMPT_WITH},
{"role": "user", "content": ticket}
],
temperature=0,
max_tokens=10
)
results_with.append(r_with.choices[0].message.content.strip())
print("Without CRISPE (5 runs):")
for r in results_without:
print(f" '{r}'")
print("\nWith CRISPE (5 runs):")
for r in results_with:
print(f" '{r}'")
Typical output:
Without CRISPE (5 runs):
'The ticket is classified as a Billing issue.'
'Billing'
'This ticket corresponds to a billing or charges issue.'
'BILLING ISSUE'
'Billing (payment problem)'
With CRISPE (5 runs):
'BILLING'
'BILLING'
'BILLING'
'BILLING'
'BILLING'
Consistency: 20% vs 100%. Parseability by code: impossible vs trivial.
Connection to the project
In the Prompt Analyzer (capsule 08) you'll use CRISPE to:
- Identify which dimensions a given prompt has (present/absent/implicit)
- Detect the missing dimensions with specific suggestions for each one
- Score the prompt's "completeness" (e.g.: 4/6 CRISPE components present)
- Generate concrete suggestions: "Personality is missing: add format constraints and guardrails"
In Module 8 (Production Prompt System), when you register prompts in the registry, the metadata will include which CRISPE components each version has — useful for evolution and debugging.
Troubleshooting
Problem 1: The model ignores the Role
Cause: The Role is buried at the end of the system prompt, or it contradicts the Statement.
Fix:
# ✅ Role up front, clear and specific
You are a lawyer specialized in Mexican labor law.
You only answer questions about labor topics under Mexican law.
[Rest of the prompt]
# ❌ Role buried and weak
Analyze this contract. Consider legal aspects. The analysis must be professional. You are a lawyer.
Problem 2: Too many components create conflicts
Cause: CRISPE with very long text, or a Personality that contradicts the Statement, can confuse the model.
Fix: Be concise in each section. If the Insight goes past 300 words, ask yourself whether all that information is necessary. For simple tasks, use partial CRISPE (C + S + E minimum).
Problem 3: The Experiment (format) isn't followed consistently
Cause: The format has no concrete examples, or there's ambiguity in the schema.
Fix:
# ✅ Format with an explicit example and a counter-example
Response format (only this, nothing else):
{"sentiment": "POSITIVE|NEGATIVE|NEUTRAL", "confidence": 0.0-1.0}
Correct example: {"sentiment": "POSITIVE", "confidence": 0.92}
Incorrect example: "The sentiment is positive with high confidence"
Problem 4: The Insight makes the prompt too long and expensive
Cause: Including too much context that the model can infer or that isn't relevant to the task.
Fix: MECE principle (Mutually Exclusive, Collectively Exhaustive): the Insight covers everything necessary, without overlapping the Statement, without restating the obvious. Measure tokens with tiktoken before you deploy: len(tiktoken.encoding_for_model("gpt-4o-mini").encode(prompt)).
Problem 5: Full CRISPE for a simple task — overkill
Cause: Applying all 6 components to tasks that don't need them adds latency and cost with no benefit.
Fix: Use the task-type criterion (the Simplified CRISPE table). For simple classification tasks, C + I + S + E is usually enough.
Exercises
Exercise 1: CRISPE for a date extractor (Easy)
Apply full CRISPE to design a prompt that extracts dates from text and returns them in ISO format YYYY-MM-DD.
See solution
from openai import OpenAI
import json
client = OpenAI()
SYSTEM = """
## Capacity
Extract all explicit dates from a natural-language text.
## Role
You are a structured-data extractor specialized in temporal information.
## Insight
- Only explicit dates (not "yesterday", "last week", or dates inferred from context)
- ISO format: YYYY-MM-DD
- For ambiguous dates (e.g.: "03/04/2024"), use the interpretation of the text's language
(Spanish: DD/MM/YYYY; English: MM/DD/YYYY)
- Reference year if unspecified: 2025
## Statement
From the following text, identify and extract all the dates present.
## Personality
- Only valid JSON, no extra text
- If there are no dates, return an empty list (not null)
## Experiment
Format: {"dates": ["YYYY-MM-DD", ...]}
Examples:
- "Meeting on March 15, 2025" → {"dates": ["2025-03-15"]}
- "There are no dates here" → {"dates": []}
- "Due on 04/03/2025 and 04/15/2025" → {"dates": ["2025-04-03", "2025-04-15"]}
"""
test_cases = [
"The meeting is on March 15, 2025 at 3pm",
"The contract expires on 12/31/2025 and the renewal must happen before 12/01/2025",
"We don't have confirmed dates yet",
"See you tomorrow to review the project" # "tomorrow" is relative → not extracted
]
for text in test_cases:
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text}
],
temperature=0
)
result = json.loads(r.choices[0].message.content)
print(f"Input: {text}")
print(f"Output: {result}\n")
Expected output:
Input: The meeting is on March 15, 2025 at 3pm
Output: {'dates': ['2025-03-15']}
Input: The contract expires on 12/31/2025 and the renewal must happen before 12/01/2025
Output: {'dates': ['2025-12-31', '2025-12-01']}
Input: We don't have confirmed dates yet
Output: {'dates': []}
Input: See you tomorrow to review the project
Output: {'dates': []}
Explanation: The Insight explicitly states that "tomorrow" isn't extracted because it's a relative date. That's exactly the kind of domain rule that belongs in Insight: it isn't inferable from the Statement.
Exercise 2: Identify the missing dimension (Easy)
Analyze this prompt. Which CRISPE dimensions are missing? Fix it to make it production-ready.
"Summarize this article in 3 points."
See solution
Components present:
- Statement: ✅ "Summarize in 3 points" (clear instruction, though incomplete)
Missing components:
- Capacity: Summarize how? Extractive, abstractive, executive?
- Role: From what perspective? Technical editor? Generalist? It affects depth and vocabulary
- Insight: Which aspects to prioritize? Length of each point? Are there technical terms to keep?
- Personality: Tone? Maximum length per point? Language?
- Experiment: Exact format? Bullets? Numbered? Plain text only? JSON?
Fixed prompt:
SYSTEM = """
## Capacity
Summarize technical articles into actionable key points.
## Role
You are a technical editor who writes executive summaries for software engineers.
## Insight
- Prioritize: main findings, applicable methodology, conclusions with practical impact
- Keep domain-specific technical terms (don't simplify them)
- Ignore the generic introduction and the acknowledgements
## Statement
Summarize the following article in exactly 3 key points.
## Personality
- Concise: maximum 30 words per point
- Neutral, technical tone
- No subjective interpretations
- No introductory filler ("This article talks about...")
## Experiment
Format:
1. [First key point — what it does or demonstrates]
2. [Second key point — how it works or is implemented]
3. [Third key point — result, limitation, or practical implication]
"""
Quantifiable improvement: The original prompt can produce bullets of any length, in any language, with or without formatting, with or without introductory text. The fixed one has constraints that make it deterministic.
Exercise 3: Full CRISPE for review analysis (Medium)
Design a prompt with full CRISPE for this task: analyze restaurant reviews and extract sentiment (1-5 stars), aspects mentioned (food/service/ambiance/price), and whether the reviewer recommends the place.
See solution
from openai import OpenAI
import json
client = OpenAI()
SYSTEM = """
## Capacity
Analyze restaurant reviews and extract structured information.
## Role
You are a customer experience analyst specialized in the restaurant industry.
## Insight
Aspects and their criteria:
- food: quality, flavor, presentation, temperature, menu variety
- service: attentiveness, speed, friendliness, staff professionalism
- ambiance: decor, noise level, cleanliness, comfort, location
- price: value for money, perceived value, compared to expectations
Star scale:
1 = Very negative / Would never come back
2 = Negative / Below expectations
3 = Neutral / Acceptable but unremarkable
4 = Positive / Good experience
5 = Very positive / Excellent, would actively recommend it
## Statement
Analyze the following restaurant review and extract the specified fields.
## Personality
- Only valid JSON, no extra text
- If an aspect isn't mentioned in the review, use null (don't make it up)
- The recommendation is inferred from the overall tone and from whether the reviewer says they'd come back
## Experiment
Exact format:
{
"stars": 1-5,
"aspects": {
"food": "positive|negative|mixed|null",
"service": "positive|negative|mixed|null",
"ambiance": "positive|negative|mixed|null",
"price": "positive|negative|mixed|null"
},
"recommends": true|false,
"key_phrase": "direct quote of at most 10 words capturing the main sentiment"
}
"""
reviews = [
"The food was delicious but the service was painfully slow. I'll come back just for the pasta.",
"Horrible. We waited 40 minutes, the food arrived cold and the waiters were very rude. Never again.",
"Incredible ambiance for a romantic dinner. The prices are high but it's worth every penny."
]
for review in reviews:
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"}
)
result = json.loads(r.choices[0].message.content)
print(f"Review: {review[:60]}...")
print(f"Analysis: {json.dumps(result, ensure_ascii=False, indent=2)}\n")
Expected output:
{
"stars": 3,
"aspects": {"food": "positive", "service": "negative", "ambiance": null, "price": null},
"recommends": true,
"key_phrase": "food was delicious but the service was painfully slow"
}
Exercise 4: Simplified CRISPE with rationale (Medium)
For a simple task (detecting the language of a text), implement partial CRISPE. Write the rationale for which components you omitted.
See solution
# Partial CRISPE for language detection
# Components used: Capacity, Statement, Experiment
# Omitted (with rationale):
# - Role: The task is so specific that no ambiguity of perspective is possible
# - Insight: There's no complex domain logic beyond ISO 639-1
# - Personality: The behavioral constraints are covered in Experiment
SYSTEM = """
## Capacity
Detect the language of the text.
## Statement
Identify the language of the following text.
## Experiment
Respond only with the language's ISO 639-1 code (2 lowercase letters).
Examples: es, en, fr, pt, de, it, zh, ja, ar
If the text is ambiguous or too short to determine: "und" (undetermined)
"""
from openai import OpenAI
client = OpenAI()
texts = [
"El prompt engineering es fundamental para sistemas de IA",
"Prompt engineering is fundamental for AI systems",
"Le prompt engineering est fondamental pour les systèmes d'IA",
"OK", # Ambiguous
"Ciao" # Could be Italian or informal Spanish
]
for text in texts:
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text}
],
temperature=0,
max_tokens=5
)
print(f"'{text[:45]}' → {r.choices[0].message.content.strip()}")
Lesson: CRISPE is a guide, not a rigid template. For simple tasks, 3 components can be enough. For production systems with multiple edge cases, use all of them.
Exercise 5: Failure diagnosis with CRISPE (Hard)
You have this prompt, and it fails in ~30% of cases: the model sometimes adds "The ticket type is:" before the category. Diagnose which CRISPE component is the problem and fix it.
SYSTEM = """
Classify this support ticket:
- TECHNICAL: problems with the software
- BILLING: money and payment topics
- GENERAL: everything else
Respond with the category.
"""
See solution
Diagnosis:
- Failure: The model sometimes adds introductory text before the category
- Problematic component: Personality (too vague on format constraints) and Experiment (doesn't specify the exact format or give examples)
"Respond with the category" is a wish, not a constraint. It doesn't specify:
- Whether the category goes alone or with text
- In what case (uppercase, lowercase)
- Whether there's trailing punctuation or not
Fix:
CORRECTED_SYSTEM = """
## Capacity
Classify support tickets into a predefined category.
## Insight
Categories:
- TECHNICAL: software error, functionality failure, crash, degraded performance
- BILLING: charges, invoices, plan changes, refunds, incorrect charges
- GENERAL: usage questions, feature requests, feedback, general doubts
## Statement
Classify the following ticket into exactly ONE of the categories.
## Personality
- Respond ONLY with the category name in uppercase
- No text before or after
- No trailing punctuation
- No "The ticket is:", "Category:", or any prefix
## Experiment
Format: a single word.
Examples:
- "Login hasn't worked since this morning" → TECHNICAL
- "I'm being charged double this month" → BILLING
- "How do I export my data?" → GENERAL
"""
Key: The Experiment with explicit examples and the Personality with "or any prefix" eliminate the 30% of failures.
Summary
In this capsule you learned:
- CRISPE: Capacity (what it does), Role (from what perspective), Insight (what it needs to know about the domain), Statement (exact instruction), Personality (how it behaves), Experiment (format + examples)
- Prompt as a spec: You're not "writing instructions" — you're defining a behavior contract with functional and non-functional requirements
- Constraints > Wishes: "A single word" is a constraint. "Be concise" is a wish. Constraints are verifiable and produce deterministic prompts
- Iterative refinement: Draft → Test 10+ inputs → Diagnose by CRISPE component → Refine that specific component → Repeat
- Partial CRISPE: For simple tasks (direct classification, obvious extraction), C + S + E can be enough. For complex or production systems, use all of them
- Connection to the rest of the guide: CRISPE is the implicit framework in zero-shot (M2), few-shot (M2), structured output (M3), CoT (M4), and ReAct (M5)
Next capsule: A side-by-side comparison between a casual prompt and an engineered one with quantitative metrics — you'll see with data the difference CRISPE makes.
Additional resources
- CRISPE Framework Overview — Detailed description of the framework with more application examples
- Prompt Engineering Guide — OpenAI — Complementary techniques from OpenAI's perspective, including strategy patterns
- Anthropic Prompt Design — Anthropic's approach to effective design, especially useful for understanding Role and Personality
- Chain-of-Thought Prompting (Wei et al., 2022) — The original paper where CRISPE's components (especially CoT in Experiment) improve reasoning
- Prompt Engineering for Developers (DeepLearning.AI) — Andrew Ng's free course with a practical perspective on iterative refinement
- tiktoken — OpenAI's library for counting tokens in prompts, useful for optimizing Insight without blowing your token budget