Skip to main content
LLM Engineering beginner Lesson 3 of 12

Prompt Engineering

Write effective prompts using few-shot examples, chain-of-thought reasoning, role prompting, and structured output techniques.

Real-World Scenario

An ML engineer at a legal-tech startup needs to extract contract clauses, classify risk levels, and generate plain-language summaries from 10,000 contracts. The accuracy of a naive prompt is 62%. After applying few-shot examples, chain-of-thought reasoning, and output format constraints, accuracy reaches 91% — without changing the model or fine-tuning. Prompt engineering is free performance.

Zero-Shot Prompting

The simplest approach: describe the task clearly and let the model generalize.

import anthropic

client = anthropic.Anthropic()

def zero_shot(task_description: str, input_text: str) -> str:
    return client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": f"{task_description}\n\nInput: {input_text}"
        }]
    ).content[0].text

# Classification
result = zero_shot(
    task_description="Classify the sentiment of the following review as POSITIVE, NEGATIVE, or NEUTRAL.",
    input_text="The battery life is amazing but the screen has terrible glare."
)
print(result)  # MIXED or NEUTRAL (depends on model judgment)

# Extraction
result = zero_shot(
    task_description="Extract the product name, price, and availability from the following text.",
    input_text="The Sony WH-1000XM5 headphones are currently in stock at $349.99."
)
print(result)

Few-Shot Prompting

Show the model worked examples to constrain the output format and improve accuracy.

import anthropic

client = anthropic.Anthropic()

FEW_SHOT_SYSTEM = """You classify customer support tickets into categories.

Categories: BILLING, TECHNICAL, SHIPPING, RETURNS, ACCOUNT, OTHER

Examples:
Input: "I was charged twice for my order #12345"
Output: BILLING

Input: "My app keeps crashing when I try to open the dashboard"
Output: TECHNICAL

Input: "My package hasn't arrived — it was supposed to be delivered yesterday"
Output: SHIPPING

Input: "I want to return the item I bought last week, it doesn't fit"
Output: RETURNS

Reply with only the category name, nothing else."""

def classify_ticket(ticket: str) -> str:
    return client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=20,
        system=FEW_SHOT_SYSTEM,
        messages=[{"role": "user", "content": ticket}]
    ).content[0].text.strip()


tickets = [
    "I can't log in — my password reset email never arrived",
    "The invoice shows $200 but I was charged $250",
    "How do I export my data before I cancel?",
]

for ticket in tickets:
    category = classify_ticket(ticket)
    print(f"{category:12s} | {ticket}")

Chain-of-Thought Reasoning

For complex reasoning tasks, asking the model to think step-by-step dramatically improves accuracy — the model gets to “show its work” before committing to an answer.

import anthropic

client = anthropic.Anthropic()

STANDARD_PROMPT = """
A company had 240 employees at the start of Q1. 
They hired 15% more in Q1, laid off 20 people in Q2, 
and grew 10% in Q3.
How many employees do they have now?
Answer with just the number.
"""

COT_PROMPT = """
A company had 240 employees at the start of Q1. 
They hired 15% more in Q1, laid off 20 people in Q2, 
and grew 10% in Q3.
How many employees do they have now?

Think through each step carefully before giving your final answer.
Format your response as:
Step 1: [calculation]
Step 2: [calculation]
...
Final answer: [number]
"""

def ask(prompt: str) -> str:
    return client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}]
    ).content[0].text

print("Standard:", ask(STANDARD_PROMPT))
print("\nChain-of-thought:")
print(ask(COT_PROMPT))

Role Prompting

Assigning the model a specific expert persona shapes the style, depth, and focus of responses.

import anthropic

client = anthropic.Anthropic()

PERSONAS = {
    "security_engineer": """You are a senior security engineer with 10 years of experience.
When reviewing code, you focus exclusively on security vulnerabilities:
SQL injection, XSS, authentication flaws, insecure deserialization, 
secret exposure, and OWASP Top 10. Be direct, specific, and actionable.
Rate severity as CRITICAL, HIGH, MEDIUM, or LOW.""",

    "staff_engineer": """You are a Staff Engineer with expertise in system design and code quality.
When reviewing code, focus on: scalability, maintainability, performance,
and architectural soundness. Avoid nitpicking style issues — focus on
decisions that will matter at 10x scale.""",

    "junior_tutor": """You are a patient programming tutor helping beginners.
Use simple language, avoid jargon, explain every concept from scratch,
and always include analogies to everyday life.""",
}

def review_with_persona(code: str, persona: str) -> str:
    return client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=PERSONAS[persona],
        messages=[{"role": "user", "content": f"Review this code:\n\n```python\n{code}\n```"}]
    ).content[0].text

code = """
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return db.execute(query).fetchone()
"""

print("Security review:")
print(review_with_persona(code, "security_engineer"))

Output Format Constraints

Forcing a specific structure makes LLM output programmatically reliable.

import anthropic
import json
from typing import TypedDict

client = anthropic.Anthropic()

class ContractAnalysis(TypedDict):
    contract_type: str
    parties: list[str]
    effective_date: str | None
    key_obligations: list[str]
    termination_clauses: list[str]
    risk_level: str    # LOW | MEDIUM | HIGH
    risk_reasons: list[str]


def analyze_contract(contract_text: str) -> ContractAnalysis:
    system = """You are a contract lawyer. Analyze contracts and return ONLY valid JSON.
The JSON must exactly match this schema — no extra fields, no markdown, no explanation:
{
  "contract_type": "string",
  "parties": ["string"],
  "effective_date": "YYYY-MM-DD or null",
  "key_obligations": ["string"],
  "termination_clauses": ["string"],
  "risk_level": "LOW | MEDIUM | HIGH",
  "risk_reasons": ["string"]
}"""

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=system,
        messages=[{"role": "user", "content": contract_text}]
    )

    return json.loads(response.content[0].text)


sample_contract = """
SERVICE AGREEMENT
Between Acme Corp ("Client") and TechVendor Inc ("Provider")
Effective Date: March 1, 2025

Provider agrees to deliver software development services at $150/hour.
Client shall pay within 30 days of invoice. Provider may terminate with 
14 days notice. Liability is limited to fees paid in the prior 3 months.
Auto-renews annually unless 60 days notice is given.
"""

analysis = analyze_contract(sample_contract)
print(json.dumps(analysis, indent=2))

Prompt Templates

from string import Template
import anthropic

client = anthropic.Anthropic()

# Use string templates for reusable prompts with variable injection
SUMMARIZE_TEMPLATE = Template("""
Summarize the following $content_type for a $audience audience.

Constraints:
- Maximum $max_words words
- Use bullet points for key findings
- End with one actionable recommendation

Content:
$content
""")

def summarize(content: str, content_type: str, audience: str, max_words: int = 100) -> str:
    prompt = SUMMARIZE_TEMPLATE.substitute(
        content_type=content_type,
        audience=audience,
        max_words=max_words,
        content=content,
    )
    return client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}]
    ).content[0].text


article = """
A new study from MIT found that developers who write tests before code (TDD) 
produce 40% fewer bugs and spend 15% less time on debugging. The study tracked 
120 engineers across 6 months. However, TDD teams initially shipped 20% slower 
before catching up by month 3.
"""

print(summarize(article, "research paper", "engineering manager", max_words=75))

Common Mistakes

1. Vague instructions — the model guesses your intent:

# Bad
"Write something about Python."

# Good
"Write a 3-sentence explanation of Python's GIL for a Java developer
who is evaluating Python for a high-concurrency web service."

2. No output format specification:

# Bad: returns a paragraph, hard to parse programmatically
"Extract the price from this text: ..."

# Good: constrains the format
"Extract the price from this text and return ONLY the number with no currency symbol or text.
If no price is found, return null.
Text: ..."

3. Contradictory instructions:

# Bad: model has to choose between brief and thorough
"Write a brief and thorough explanation of neural networks."

# Good: be explicit about the tradeoff
"Write a 3-paragraph explanation of neural networks. Cover what they are,
how they learn, and one real-world use case. Skip mathematical notation."

Frequently Asked Questions

What is the difference between zero-shot and few-shot prompting?
Zero-shot prompting gives the model only instructions and the task — no examples. Few-shot prompting includes 2–5 worked examples before the actual input. Few-shot improves accuracy on tasks where the output format is strict or the task is novel, because examples show the model exactly what you expect.
Does prompt length affect quality?
Longer prompts are not always better. A clear, concise instruction often outperforms a padded one. What matters is precision: unambiguous task description, relevant context, explicit output format, and a small number of high-quality examples if needed. Irrelevant context dilutes the signal.