Skip to content

Foundations of Prompt Engineering

Prerequisites

Before this lesson you should be comfortable with:

  • LLM basics — what a language model is and how chat APIs work (Modules 01–02)
  • Python fundamentals — enough to read and run the code examples below
  • Basic API usage — calling client.chat.completions.create() with messages

You do not need prior prompt engineering experience. This lesson establishes the vocabulary and patterns used throughout the module.


What You'll Learn

Objective Time Difficulty
Explain what prompt engineering is and why it matters in production 10 min Intermediate
Decompose any prompt into six structural components 10 min Intermediate
Apply clarity, specificity, and structure principles to real tasks 10 min Intermediate
Run a minimal prompt comparison in Python 10 min Intermediate

Intuition First: The Briefing Room Analogy

Imagine you walk into a briefing room and tell a brilliant analyst: "Tell me about Python." They might talk about the snake, the programming language, or Monty Python — you gave them no constraints.

Now imagine you say: "You are a senior data engineer briefing a Java developer who is evaluating Python for a new ETL pipeline. Give me three concrete advantages of Python for data science, each with a five-line code example. Keep each advantage under 80 words."

Same analyst, radically different output. The second briefing specifies who they are, who the audience is, what to deliver, and how to format it.

That is prompt engineering. You are not programming the model's weights — you are programming its context window. Every token you add shifts the probability distribution over what comes next. Good prompts narrow that distribution to the output you actually need.


What Is Prompt Engineering?

Prompt engineering is the practice of designing inputs to language models that reliably produce high-quality, accurate outputs. It sits at the intersection of communication, programming, and understanding how LLMs process language.

Unlike traditional software where logic is explicit, LLM behavior is probabilistic. The same model can produce wildly different outputs depending on how you ask:

from openai import OpenAI

client = OpenAI()

# Vague prompt — unpredictable output
vague = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Tell me about Python"}],
)
print(vague.choices[0].message.content[:200])
# Could be about the snake, the programming language, or Monty Python

# Specific prompt — targeted output
specific = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{
        "role": "user",
        "content": (
            "Explain 3 key advantages of Python for data science, "
            "with a brief code example for each. Target audience: "
            "developers switching from Java."
        ),
    }],
)
print(specific.choices[0].message.content[:200])
# Focused, actionable, audience-appropriate response

In production, prompt quality is often the difference between a feature that ships and one that gets rolled back. A 200-token prompt change can move classification accuracy from 72% to 91% without touching model weights.


The Anatomy of a Great Prompt

Every effective prompt contains some or all of these six components:

┌─────────────────────────────────────────────┐
│  1. ROLE / PERSONA                          │
│     Who should the model act as?            │
│                                             │
│  2. CONTEXT                                 │
│     Background information needed           │
│                                             │
│  3. TASK / INSTRUCTION                      │
│     What exactly should the model do?       │
│                                             │
│  4. FORMAT / STRUCTURE                      │
│     How should the output look?             │
│                                             │
│  5. CONSTRAINTS                             │
│     Boundaries, limits, things to avoid     │
│                                             │
│  6. EXAMPLES (optional)                     │
│     Show what good output looks like        │
└─────────────────────────────────────────────┘

All Components Together

def build_code_review_prompt(code: str) -> str:
    return f"""You are a senior Python developer who specializes in code review.

Context:
I have a Flask API endpoint that handles user registration.
The code works but I'm concerned about security and best practices.

Task:
Review this code and provide:
1. Security vulnerabilities found
2. Performance improvements
3. A refactored version with fixes applied

Format:
- Use markdown headers for each section
- Include severity ratings (Critical/High/Medium/Low)
- Add inline code comments in the refactored version

Constraints:
- Keep the same API contract (don't change endpoint paths)
- Use only standard library + Flask (no new dependencies)
- Assume Python 3.11+

Here is the code:
{code}
"""

# Usage
prompt = build_code_review_prompt("""
@app.route('/register', methods=['POST'])
def register():
    data = request.json
    query = f"INSERT INTO users (email) VALUES ('{data['email']}')"
    db.execute(query)
    return {"status": "ok"}
""")

When a prompt fails in production, the fix usually maps to one of these six slots. "The model is too verbose" → tighten constraints. "It ignores my format" → strengthen format or add examples. Diagnose by component, not by guessing.


Core Principle 1: Be Specific

The biggest mistake in prompt engineering is being too vague. Specificity beats brevity.

Bad Prompt Good Prompt
"Summarize this" "Summarize this article in 3 bullet points, each under 20 words, focusing on business impact"
"Write code" "Write a Python function that validates email addresses using regex, returns True/False, and handles edge cases like '+' aliases"
"Explain AI" "Explain how transformer attention mechanisms work to a CS undergrad who understands matrix multiplication"

Specificity reduces the model's search space. Instead of generating from all possible "summaries," it generates from summaries that are exactly three bullets, each under 20 words, focused on business impact.


Core Principle 2: Provide Structure

LLMs follow structure remarkably well. Use XML tags, markdown headers, or numbered lists to separate concerns:

debug_prompt = """
<context>
You are helping a user debug a React application.
The app uses Next.js 15 with the App Router.
</context>

<error_message>
TypeError: Cannot read properties of undefined (reading 'map')
at ProductList (./components/ProductList.tsx:12:24)
</error_message>

<task>
1. Identify the root cause of this error
2. Explain why it happens in simple terms
3. Provide the fix with before/after code
</task>

<format>
Use markdown. Start with a one-line diagnosis, then details.
</format>
"""

Structure also makes prompts testable. You can assert that rendered templates contain required sections before sending them to the API.


Core Principle 3: Use the System Message

Most chat APIs support a system message that sets persistent behavior across the conversation:

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": (
                "You are a database expert. Always suggest the most "
                "performant query. When writing SQL, include EXPLAIN "
                "ANALYZE output estimates. If the user's approach has "
                "a better alternative, mention it proactively."
            ),
        },
        {
            "role": "user",
            "content": (
                "How do I find duplicate emails in a users table "
                "with 10M rows?"
            ),
        },
    ],
)

System Message Best Practices

Do Don't
Define the role and expertise Leave system message empty
Set output format preferences Put conversation history in system
Specify constraints and boundaries Make it extremely long (wastes tokens)
Include domain-specific instructions Contradict the system message in user messages

The system message is your persistent contract with the model. User messages carry the variable data; the system message carries the rules that should not change turn to turn.


Core Principle 4: Iterate and Refine

Prompt engineering is iterative. Start simple, measure output quality, then refine.

Iteration 1: "Translate this to Spanish"
  Problem: Too formal for a casual chat app

Iteration 2: "Translate this to casual Latin American Spanish"
  Problem: Sometimes uses region-specific slang

Iteration 3: "Translate this to casual Spanish suitable for a
  general Latin American audience. Avoid country-specific slang.
  Keep the tone friendly and conversational."
  Result: Consistent, appropriate translations

Treat prompts like code: version them, test them, and review failures systematically. Later lessons cover eval sets and A/B testing; the mindset starts here.


Runnable Example: Prompt Builder

from dataclasses import dataclass
from openai import OpenAI

client = OpenAI()

@dataclass
class PromptParts:
    role: str
    context: str
    task: str
    format_spec: str
    constraints: str

def render_prompt(parts: PromptParts) -> str:
    return f"""Role: {parts.role}

Context: {parts.context}

Task: {parts.task}

Output Format: {parts.format_spec}

Constraints: {parts.constraints}
"""

def call_llm(prompt: str, system: str = "") -> str:
    messages = []
    if system:
        messages.append({"role": "system", "content": system})
    messages.append({"role": "user", "content": prompt})
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.2,
    )
    return response.choices[0].message.content

# Build a structured PR review prompt
review = PromptParts(
    role="Senior Node.js developer and security expert",
    context="Reviewing a PR for an authentication microservice using Express.js",
    task="Identify bugs, security issues, and suggest improvements",
    format_spec="Markdown table with columns: Issue, Severity, Line, Fix",
    constraints="Focus on OWASP Top 10 vulnerabilities. Max 10 findings.",
)

prompt = render_prompt(review)
# print(call_llm(prompt))  # Uncomment with API key configured

Production Connection

In production, prompt anatomy directly affects reliability and cost:

  • Version every prompt — store prompts as named artifacts (summarize_v3), not inline strings. When output quality shifts, you need to know which prompt version was live.
  • Log prompt hash + version with every LLM call so you can correlate failures to prompt changes.
  • A/B test structural changes — adding a constraint section vs. adding examples often produces different accuracy/cost tradeoffs. Measure both.
  • Failure recovery — when output violates format constraints, retry with a stricter format instruction before falling back to a simpler model or default response.

Teams that treat prompts as first-class configuration — with the same rigor as database schemas — ship faster and debug less.


Edge Cases & Common Misconceptions

Misconception 1: Longer prompts are always better. Extra tokens cost money and can dilute key instructions. A focused 150-token prompt often beats a rambling 800-token one. Put the most important constraints first.

Misconception 2: The model "understands" implied requirements. If you need JSON, say JSON. If you need three bullets, say three bullets. Implicit expectations become production bugs.

Misconception 3: Prompt engineering replaces eval. Good structure helps, but you still need test cases. A prompt that works on five examples may fail on edge cases you never considered.

Misconception 4: One prompt fits all models. The anatomy stays the same, but optimal phrasing varies by provider. Lesson 9 covers model-specific adaptations.


Key Takeaways

  • Prompt engineering programs the context window, not the model weights — specificity narrows the output distribution.
  • Every effective prompt has up to six components: role, context, task, format, constraints, and optional examples.
  • Specificity beats brevity; vague prompts produce vague (or wrong) outputs.
  • Structure (XML tags, headers, numbered lists) makes prompts easier for models to follow and for engineers to debug.
  • Use the system message for persistent rules; use the user message for variable task data.
  • Prompt engineering is iterative — start simple, identify failure patterns, refine by component.
  • In production, version prompts, log which version ran, and retry with stricter instructions on format failures.
  • Diagnose prompt failures by asking which anatomy component is missing or weak.

Next Lesson

Lesson 2: Few-Shot and Chain-of-Thought Prompting — Learn how examples and step-by-step reasoning dramatically improve model output on classification and logic tasks.