Prompt Engineering Fundamentals
Master the art of communicating with language models through structured, intentional prompt design
Why Prompt Engineering Matters
The difference between a mediocre AI application and a production-grade one often comes down to prompt quality. A well-engineered prompt can eliminate 90% of the post-processing code you'd otherwise write, reduce hallucinations, and produce consistent, structured outputs.
Anatomy of a Perfect Prompt
Every production prompt should follow a consistent structure. Here's the flow from system-level instructions to final output format:
1. System Prompt
The system prompt defines the model's persona, capabilities, and boundaries. It persists across the entire conversation.
system_prompt = """You are a senior financial analyst at a Fortune 500 company.
CAPABILITIES:
- Analyze quarterly earnings reports
- Compare financial metrics across companies
- Identify trends and anomalies in financial data
CONSTRAINTS:
- Never provide investment advice or buy/sell recommendations
- Always cite specific numbers from the provided data
- If data is insufficient, say so explicitly
- Use professional, concise language"""
2. Context Block
Provide all relevant background information the model needs. Be explicit — don't assume the model "knows" your domain.
context = """COMPANY: Acme Corp (ACME)
QUARTER: Q3 2024
REVENUE: $4.2B (up 12% YoY)
NET INCOME: $890M (up 8% YoY)
OPERATING MARGIN: 21.2% (down from 22.1% in Q2)
HEADCOUNT: 45,200 (up 3,100 from Q2)
SECTOR AVERAGE MARGIN: 19.8%"""
3. Few-Shot Examples
Show the model exactly what you expect. Even 1-2 examples dramatically improve consistency.
4. Task Instruction
Be specific about what action the model should take. Use imperative verbs.
5. Output Format Specification
Define the exact structure of the expected response.
format_spec = """Respond in the following JSON format:
{
"summary": "2-3 sentence executive summary",
"metrics": [
{"name": "metric name", "value": "current value", "trend": "up|down|flat", "concern_level": "low|medium|high"}
],
"key_risks": ["risk 1", "risk 2"],
"outlook": "1-2 sentence forward-looking statement"
}"""
Role-Based Prompting
Assigning a specific role to the model activates relevant knowledge patterns and adjusts response style. The key is specificity:
Weak vs. Strong Role Assignment
| ❌ Weak | ✅ Strong |
|---|---|
| "You are a helpful assistant" | "You are a staff backend engineer at a fintech startup, specializing in Python microservices and PostgreSQL optimization" |
| "You are a writing expert" | "You are an editor at The Economist. Your style: concise, data-driven, slightly dry wit. Max sentence length: 25 words." |
| "You are a teacher" | "You are a CS professor teaching a sophomore algorithms class. Use analogies from everyday life. Always provide Big-O complexity." |
import openai
client = openai.OpenAI()
def create_expert_response(role: str, context: str, question: str) -> str:
"""Generate a response using role-based prompting."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": role
},
{
"role": "user",
"content": f"CONTEXT:\n{context}\n\nQUESTION:\n{question}"
}
],
temperature=0.3 # Low temp for analytical tasks
)
return response.choices[0].message.content
Temperature Tuning by Task Type
Temperature controls randomness in token selection. Choosing the right value is critical for production quality:
| Temperature | Use Cases | Behavior |
|---|---|---|
0.0 |
Classification, extraction, math, code generation | Deterministic, always picks highest-probability token |
0.1 – 0.3 |
Summarization, Q&A, analysis, translation | Mostly deterministic with slight variation |
0.4 – 0.7 |
Conversational chat, explanations, email drafting | Balanced creativity and coherence |
0.8 – 1.0 |
Creative writing, brainstorming, story generation | High variety, less predictable |
1.2 – 2.0 |
Poetry, artistic text, wild brainstorming | Very random, may lose coherence |
TEMPERATURE_PRESETS = {
"extraction": 0.0,
"classification": 0.0,
"summarization": 0.2,
"analysis": 0.3,
"conversation": 0.5,
"creative_writing": 0.8,
"brainstorming": 1.0,
}
def get_completion(task_type: str, prompt: str, **kwargs) -> str:
"""Route to appropriate temperature based on task type."""
temp = TEMPERATURE_PRESETS.get(task_type, 0.5)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=temp,
**kwargs
)
return response.choices[0].message.content
top_p (nucleus sampling). Setting temperature=0 with top_p=1 is deterministic. For most tasks, adjust temperature OR top_p, not both simultaneously.
Instruction Clarity Techniques
Ambiguous instructions produce ambiguous outputs. Here are battle-tested patterns for precise instructions:
The CLEAR Framework
- Concrete — Use specific numbers, lengths, formats ("Return exactly 3 bullet points")
- Limited — Define boundaries ("Only use information from the provided context")
- Explicit — State what to do AND what not to do
- Actionable — Use imperative verbs ("Analyze", "Extract", "Compare")
- Referenceable — Point to specific data ("Using the revenue figures in Table 2")
Before & After: Weak vs. Strong Prompts
"Summarize this article about AI."
Problems: No length constraint, no audience, no format, no focus area.
"Summarize this article in exactly 3 bullet points for a technical PM audience. Each bullet should: start with a bolded key term, be ≤ 25 words, and focus on business impact rather than technical details. Do not include information not present in the article."
"Write me some Python code for a web scraper."
"Write a Python async web scraper using aiohttp and BeautifulSoup that: 1) Takes a list of URLs from a CSV file, 2) Extracts all <h1> and <h2> headings, 3) Respects robots.txt via the robotparser module, 4) Rate-limits to 2 requests/second, 5) Outputs results as JSON Lines. Include error handling for timeouts and 4xx/5xx responses. Use Python 3.11+ syntax."
Output Format Specification
Reliable AI applications need predictable output formats. Here are patterns that work:
# Pattern 1: JSON with schema enforcement
json_prompt = """Extract entities from the text below.
OUTPUT FORMAT (strict JSON, no markdown):
{
"entities": [
{
"text": "exact text span",
"type": "PERSON | ORG | LOCATION | DATE | MONEY",
"confidence": 0.0-1.0
}
],
"entity_count": integer
}
TEXT: {user_text}"""
# Pattern 2: Structured markdown for human-readable output
markdown_prompt = """Analyze the code and provide a review.
FORMAT YOUR RESPONSE EXACTLY AS:
## Summary
[1-2 sentences]
## Issues Found
| # | Severity | Line | Description | Suggestion |
|---|----------|------|-------------|------------|
[table rows]
## Overall Score
[X/10] - [one sentence justification]"""
# Pattern 3: Delimiter-based for easy parsing
delimiter_prompt = """Generate 3 product descriptions.
Separate each description with ===SEPARATOR===
Each description must be exactly 2 sentences.
First sentence: feature highlight.
Second sentence: benefit to user."""
Common Failure Modes & Fixes
| Failure Mode | Symptom | Fix |
|---|---|---|
| Hallucination | Model invents facts not in context | Add: "Only use information from the provided context. If unsure, say 'insufficient data'." |
| Format drift | Output structure varies between calls | Provide a concrete example. Use JSON mode. Add: "Follow this format exactly." |
| Verbosity | Model adds unnecessary preamble/caveats | Add: "Respond with only the requested output. No preamble, no explanation." |
| Refusal | Model refuses safe requests | Rephrase to clarify intent. Add context about why the request is legitimate. |
| Instruction ignoring | Model skips constraints | Move critical instructions to end of prompt. Use ALL CAPS for must-follow rules. |
| Context confusion | Model mixes up entities in long context | Use XML tags or clear delimiters: <document id="1">...</document> |
🛠 Mini-Project: Prompt Testing Harness
Build a reusable harness that tests prompts against YAML-defined test cases and reports pass/fail rates.
Step 1: Define Test Cases (YAML)
# test_cases.yaml content (loaded as Python dict for demo)
test_config = {
"prompt_template": "Classify the sentiment of this review: {text}\nRespond with exactly one word: POSITIVE, NEGATIVE, or NEUTRAL",
"model": "gpt-4o",
"temperature": 0.0,
"test_cases": [
{
"input": {"text": "This product is amazing! Best purchase ever."},
"expected": "POSITIVE",
"tags": ["clear_positive"]
},
{
"input": {"text": "Terrible quality. Broke after one day."},
"expected": "NEGATIVE",
"tags": ["clear_negative"]
},
{
"input": {"text": "It's okay. Does what it says."},
"expected": "NEUTRAL",
"tags": ["subtle"]
},
{
"input": {"text": "Not bad, but not great either. Shipping was fast though."},
"expected": "NEUTRAL",
"tags": ["subtle", "mixed_signals"]
}
]
}
Step 2: Build the Harness
import openai
import yaml
import json
from dataclasses import dataclass
from typing import Optional
from pathlib import Path
client = openai.OpenAI()
@dataclass
class TestResult:
input_data: dict
expected: str
actual: str
passed: bool
latency_ms: float
tags: list[str]
def run_prompt_tests(config_path: str) -> list[TestResult]:
"""Run all test cases from a YAML config file."""
import time
with open(config_path) as f:
config = yaml.safe_load(f)
results = []
for case in config["test_cases"]:
# Format the prompt with test inputs
prompt = config["prompt_template"].format(**case["input"])
start = time.perf_counter()
response = client.chat.completions.create(
model=config["model"],
messages=[{"role": "user", "content": prompt}],
temperature=config["temperature"],
max_tokens=50
)
latency = (time.perf_counter() - start) * 1000
actual = response.choices[0].message.content.strip()
passed = actual.upper() == case["expected"].upper()
results.append(TestResult(
input_data=case["input"],
expected=case["expected"],
actual=actual,
passed=passed,
latency_ms=latency,
tags=case.get("tags", [])
))
return results
def print_report(results: list[TestResult]) -> None:
"""Print a formatted test report."""
total = len(results)
passed = sum(1 for r in results if r.passed)
print(f"\n{'='*60}")
print(f"PROMPT TEST REPORT")
print(f"{'='*60}")
print(f"Total: {total} | Passed: {passed} | Failed: {total - passed}")
print(f"Pass Rate: {passed/total*100:.1f}%")
print(f"Avg Latency: {sum(r.latency_ms for r in results)/total:.0f}ms")
print(f"{'-'*60}")
for i, r in enumerate(results, 1):
status = "✅" if r.passed else "❌"
print(f"{status} Test {i}: expected={r.expected}, got={r.actual}")
if not r.passed:
print(f" Input: {r.input_data}")
print(f" Tags: {r.tags}")
# Tag-level analysis
print(f"\n{'─'*60}")
print("RESULTS BY TAG:")
tag_results: dict[str, list[bool]] = {}
for r in results:
for tag in r.tags:
tag_results.setdefault(tag, []).append(r.passed)
for tag, passes in sorted(tag_results.items()):
rate = sum(passes) / len(passes) * 100
print(f" [{tag}]: {rate:.0f}% ({sum(passes)}/{len(passes)})")
# Run it
if __name__ == "__main__":
results = run_prompt_tests("test_cases.yaml")
print_report(results)
Step 3: Iterate on Your Prompt
Run the harness, identify failing cases, adjust your prompt, and re-run. Track pass rates over time to catch regressions.
📌 Key Takeaways
- Structure prompts with 5 layers: system → context → examples → task → format
- Specific roles activate better knowledge patterns than generic "helpful assistant"
- Match temperature to task type: 0 for extraction, 0.3 for analysis, 0.7+ for creative
- Use the CLEAR framework: Concrete, Limited, Explicit, Actionable, Referenceable
- Always specify output format — JSON schemas, markdown templates, or delimiters
- Test prompts systematically with structured test cases, not ad-hoc playground experiments