Prompt engineering is the practice of designing and optimizing inputs to large language models (LLMs) to get reliable, accurate, and useful outputs. A well-crafted prompt can turn a mediocre LLM response into a production-ready answer — without changing the model or fine-tuning.
Quick reference
| Technique | When to use | Complexity |
|---|---|---|
| Zero-shot | Simple, well-defined tasks | Low |
| Few-shot | Pattern matching, formatting | Low |
| Chain-of-thought | Math, logic, multi-step reasoning | Medium |
| Role prompting | Persona, tone, expertise level | Low |
| System prompt | Set model behaviour across all turns | Low |
| Structured output | JSON, tables, typed responses | Medium |
| Prompt chaining | Tasks too complex for one call | Medium |
| ReAct | Agents that need tool use + reasoning | High |
| Self-consistency | High-stakes, verify answers | High |
| Tree of Thoughts | Complex planning, creative exploration | High |
What is prompt engineering?
A prompt is everything you send to an LLM before it generates a response: the system instruction, conversation history, user message, examples, and any retrieved context.
Prompt engineering is the skill of structuring that input so the model:
- Understands the task precisely
- Stays on topic and format
- Avoids common failure modes (hallucination, verbosity, wrong format)
- Behaves consistently across many inputs
It matters more than most people think. GPT-4 and Claude 3 Opus given a bad prompt will underperform GPT-3.5 with a good one.
The anatomy of a prompt
┌─────────────────────────────────────────┐
│ SYSTEM PROMPT │
│ "You are a senior software engineer..." │
├─────────────────────────────────────────┤
│ CONTEXT / RETRIEVED DATA (optional) │
│ "Here is the relevant documentation..." │
├─────────────────────────────────────────┤
│ EXAMPLES (optional, for few-shot) │
│ "Input: ... Output: ..." │
├─────────────────────────────────────────┤
│ USER MESSAGE │
│ "Refactor this Python function..." │
├─────────────────────────────────────────┤
│ OUTPUT CONSTRAINTS (optional) │
│ "Respond only with valid JSON." │
└─────────────────────────────────────────┘
Core techniques
Zero-shot prompting
Ask the model to perform a task with no examples. Works well for tasks the model was trained on extensively.
import openai
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Translate 'Hello, how are you?' to French."}
]
)
print(response.choices[0].message.content)
# Bonjour, comment allez-vous ?
When to use: Translation, summarization, simple classification, grammar correction — tasks with clear universal patterns.
When it fails: Novel formatting requirements, domain-specific conventions, ambiguous tasks.
Few-shot prompting
Provide 2–8 input/output examples before the actual task. The model infers the pattern from the examples.
system = """You extract the main action verb from support tickets.
Examples:
Ticket: "My account was locked and I can't log in."
Verb: locked
Ticket: "The payment was declined at checkout."
Verb: declined
Ticket: "I accidentally deleted my project."
Verb: deleted"""
user = "Ticket: 'My subscription was cancelled without notice.'"
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}
]
)
# Verb: cancelled
Rules for good few-shot examples:
| Rule | Why it matters |
|---|---|
| 3–5 examples is usually enough | More adds cost without benefit |
| Examples should cover edge cases | One tricky example prevents many failures |
| Format must be consistent | Model copies the exact format |
| Shuffle example order | Recency bias can affect output |
| Use real data when possible | Synthetic examples can introduce biases |
Chain-of-thought (CoT) prompting
Ask the model to reason step by step before giving the final answer. Dramatically improves performance on math, logic, and multi-step tasks.
Standard CoT — add "Think step by step":
user = """A store sells apples for $0.50 each and oranges for $0.75 each.
Sarah buys 6 apples and 4 oranges. She pays with a $10 bill.
How much change does she receive?
Think step by step."""
Zero-shot CoT trigger phrases (in order of effectiveness):
| Phrase | Notes |
|---|---|
"Think step by step." |
Most reliable, works on all models |
"Let's work through this carefully." |
Slightly more deliberate |
"Before answering, reason through the problem." |
Useful for ambiguous tasks |
"Show your work." |
Good for math/code |
Few-shot CoT — include reasoning in examples:
system = """Solve word problems by reasoning step by step.
Problem: A train travels 60 km/h for 2 hours, then 80 km/h for 1 hour. Total distance?
Reasoning:
- First leg: 60 km/h × 2 h = 120 km
- Second leg: 80 km/h × 1 h = 80 km
- Total: 120 + 80 = 200 km
Answer: 200 km"""
When CoT helps most:
| Task type | Improvement |
|---|---|
| Arithmetic / math | Large — often doubles accuracy |
| Multi-step logic | Large |
| Code debugging | Medium |
| Factual lookup | Small (may hallucinate reasoning) |
| Simple classification | None — adds unnecessary tokens |
Role prompting
Give the model a persona with a specific expertise, tone, or perspective.
system = """You are a senior security engineer with 15 years of experience.
You review code for vulnerabilities with a focus on OWASP Top 10.
You are direct, concise, and prioritize critical issues first.
You always suggest the fix, not just the problem."""
Effective role patterns:
| Pattern | Example |
|---|---|
| Expert persona | "You are a board-certified cardiologist..." |
| Audience-specific | "Explain this as if the reader is a 12-year-old..." |
| Tone persona | "You respond in the style of a Socratic tutor..." |
| Domain constraint | "You only discuss topics related to Python and Linux." |
| Anti-pattern persona | "You are a devil's advocate who challenges every claim." |
Avoid: Vague roles like "You are a helpful assistant" — they add nothing. Be specific about expertise and behaviour.
System prompt design
The system prompt sets persistent behaviour across the entire conversation. It's your most powerful lever.
Structure your system prompt with sections:
You are [ROLE].
## Capabilities
- [What you can do]
- [What you cannot do]
## Tone & Style
- [How to respond]
- [What to avoid]
## Output Format
- [Specific formatting rules]
- [Length guidelines]
## Knowledge
- [Domain context]
- [Special terminology]
Real example — code review assistant:
system = """You are a code reviewer for a Python backend team.
## Capabilities
- Review Python, SQL, and Dockerfile code
- Identify bugs, security issues, and performance problems
- Suggest idiomatic Python rewrites
## Tone & Style
- Be direct and concise (max 3 sentences per issue)
- Label issues as CRITICAL / HIGH / MEDIUM / LOW
- Never suggest stylistic changes as HIGH or CRITICAL
## Output Format
- Group findings by severity
- Always include the line number reference
- End with a one-line summary verdict
## Knowledge
- The team uses Python 3.11, FastAPI, SQLAlchemy 2.0, and PostgreSQL
- PEP 8 is enforced via ruff; do not flag style issues ruff catches automatically"""
Advanced techniques
Structured output (JSON mode)
Force the model to return valid JSON — critical for production applications.
With OpenAI JSON mode:
import json
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": """Extract order information from the user message.
Return valid JSON with this schema:
{
"product": string,
"quantity": integer,
"urgency": "low" | "medium" | "high"
}"""
},
{
"role": "user",
"content": "I urgently need 5 units of Model X laptops by Friday."
}
]
)
order = json.loads(response.choices[0].message.content)
# {"product": "Model X laptops", "quantity": 5, "urgency": "high"}
With Anthropic Claude + Pydantic:
import anthropic
from pydantic import BaseModel
class OrderInfo(BaseModel):
product: str
quantity: int
urgency: str
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=f"""Extract order information and respond with valid JSON matching:
{OrderInfo.model_json_schema()}
Respond with JSON only, no other text.""",
messages=[{
"role": "user",
"content": "I urgently need 5 units of Model X laptops by Friday."
}]
)
order = OrderInfo.model_validate_json(response.content[0].text)
Structured output tips:
| Tip | Why |
|---|---|
| Include a schema in the prompt | Model matches your schema more reliably |
Use json_object mode (OpenAI) |
Guarantees valid JSON syntax |
| Add an example JSON in the prompt | Reduces schema interpretation errors |
| Validate with Pydantic | Catches type mismatches at runtime |
Handle null explicitly |
Models often omit optional fields |
Prompt chaining
Break a complex task into a pipeline of smaller prompts. Each step's output feeds the next.
def chain(steps: list[dict], initial_input: str) -> str:
"""Run a sequence of prompts, passing output as input."""
value = initial_input
for step in steps:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": step["system"]},
{"role": "user", "content": step["user_template"].format(input=value)}
]
)
value = response.choices[0].message.content
return value
steps = [
{
"system": "You extract the core technical problem from bug reports.",
"user_template": "Bug report:\n{input}\n\nCore problem in one sentence:"
},
{
"system": "You generate 3 hypotheses for a given technical problem.",
"user_template": "Problem: {input}\n\nList 3 possible root causes:"
},
{
"system": "You write a systematic debugging plan.",
"user_template": "Hypotheses:\n{input}\n\nDebugging steps:"
}
]
result = chain(steps, bug_report_text)
When to chain vs single prompt:
| Use chaining when… | Use single prompt when… |
|---|---|
| Task has distinct phases | Task is simple and coherent |
| You need to validate between steps | Latency matters more than quality |
| Different roles/expertise per step | Token context is sufficient |
| You want to log intermediate results | Cost is a concern |
ReAct (Reasoning + Acting)
The model alternates between reasoning about what to do and taking actions (calling tools). Basis for most modern AI agents.
system = """You have access to these tools:
- search(query): Search the web for current information
- calculate(expression): Evaluate a math expression
- get_weather(city): Get current weather
To use a tool, write:
Thought: [your reasoning]
Action: tool_name(argument)
When you have the final answer, write:
Thought: I now have enough information.
Answer: [final answer]"""
user = "What is the current temperature in Tokyo in Fahrenheit?"
# Model response might be:
# Thought: I need to get Tokyo's current weather.
# Action: get_weather(Tokyo)
# [system injects: "22°C, partly cloudy"]
# Thought: I need to convert 22°C to Fahrenheit.
# Action: calculate(22 * 9/5 + 32)
# [system injects: "71.6"]
# Thought: I have the answer.
# Answer: The current temperature in Tokyo is 71.6°F (22°C), partly cloudy.
Self-consistency
Run the same prompt multiple times, then take the most common answer. Significantly improves accuracy on reasoning tasks.
from collections import Counter
def self_consistent_answer(prompt: str, n: int = 5) -> str:
"""Get the most consistent answer across n runs."""
answers = []
for _ in range(n):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.7 # Increase variation between runs
)
answers.append(response.choices[0].message.content.strip())
# Return most common answer
return Counter(answers).most_common(1)[0][0]
# For classification tasks, parse the label before counting
Self-consistency tradeoffs:
| Aspect | Value |
|---|---|
| Accuracy improvement | +5–15% on reasoning tasks |
| Cost multiplier | n× (5 runs = 5× cost) |
| Latency | Can parallelize all n calls |
| Best for | Math, logic, factual classification |
| Worst for | Creative tasks, open-ended generation |
Practical patterns for developers
Classification with confidence
system = """Classify customer support tickets.
Categories: billing | technical | account | general
Confidence: high | medium | low
Return JSON:
{"category": "...", "confidence": "...", "reason": "..."}"""
Extraction with fallback
system = """Extract the requested fields from the text.
If a field is not present, use null.
Never hallucinate values that are not in the text.
Return JSON: {"name": string|null, "email": string|null, "phone": string|null}"""
Summarization with length control
system = """Summarize the following text.
Rules:
- Maximum 3 bullet points
- Each bullet maximum 15 words
- Start each bullet with an action verb
- Do not include information not in the text"""
Code generation with constraints
system = """You generate Python functions.
Rules:
- Use type hints on all parameters and return values
- Include a docstring with Args and Returns sections
- Handle edge cases with clear ValueError messages
- Write idiomatic Python 3.11+
- Do not use deprecated APIs"""
Translation with terminology
system = """You are a technical translator from English to German.
Rules:
- Preserve all code snippets exactly (do not translate code)
- Keep technical terms in English unless they have a standard German translation
- Use formal register (Sie, not du)
- Preserve markdown formatting"""
Model comparison for prompting
| Model | Instruction following | JSON output | CoT strength | Context window |
|---|---|---|---|---|
| GPT-4o | Excellent | Native JSON mode | Strong | 128k |
| GPT-4o mini | Good | JSON mode | Moderate | 128k |
| Claude Opus 4.5 | Excellent | Reliable | Very strong | 200k |
| Claude Sonnet 4.6 | Excellent | Reliable | Strong | 200k |
| Claude Haiku 4.5 | Good | Reliable | Moderate | 200k |
| Gemini 1.5 Pro | Good | Requires prompting | Strong | 1M |
| Llama 3.1 70B | Moderate | Requires prompting | Moderate | 128k |
Measuring prompt quality
Never ship a prompt without evaluation. Even a 5-example manual test catches most regressions.
test_cases = [
{"input": "...", "expected": "..."},
{"input": "...", "expected": "..."},
]
def evaluate_prompt(system: str, test_cases: list[dict]) -> float:
correct = 0
for case in test_cases:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": case["input"]}
]
)
output = response.choices[0].message.content.strip()
if output == case["expected"]:
correct += 1
return correct / len(test_cases)
Evaluation metrics by task type:
| Task | Metric |
|---|---|
| Classification | Accuracy, F1 |
| Extraction | Exact match, token F1 |
| Summarization | ROUGE, human rating |
| Code generation | Test pass rate |
| Open-ended | LLM-as-judge (GPT-4 grades outputs) |
Common mistakes table
| Mistake | Problem | Fix |
|---|---|---|
| Vague instructions | "Write a good summary" produces inconsistent results | Specify length, format, and what "good" means |
| Overloading one prompt | Complex tasks have too many failure points | Use prompt chaining |
| No output format | Model chooses its own structure | Always specify format and provide an example |
| Ignoring temperature | High temp for structured output → invalid JSON | Use temperature=0 for structured output |
| Not testing edge cases | Prompt works for common inputs, fails on edge cases | Include edge-case examples in few-shot |
| Prompt injection risk | User input overrides system instructions | Separate system and user content clearly |
| Negative-only instructions | "Don't mention X" is less reliable than "Only discuss Y" | Reframe as positive constraints |
| No fallback handling | API errors or bad output crash the app | Validate output; retry with simplified prompt |
Prompt engineering vs fine-tuning
| Prompt engineering | Fine-tuning | |
|---|---|---|
| Setup cost | Zero | High (data + compute) |
| Expertise needed | Low | High (ML knowledge) |
| Iteration speed | Minutes | Hours to days |
| Best for | New tasks, low data | High-volume, consistent format |
| Portability | Stays with your code | Tied to model version |
| Output consistency | Variable | High |
| When to switch | — | When prompt engineering plateaus |
Rule of thumb: Exhaust prompt engineering before fine-tuning. Fine-tuning is often 10× more effort than a well-engineered prompt.
Frequently asked questions
What is the difference between zero-shot and few-shot prompting?
Zero-shot gives the model only the task description. Few-shot includes 2–8 input/output examples before the actual input. Few-shot is more reliable for unusual formats or domain-specific tasks; zero-shot works well for tasks the model was extensively trained on.
How many few-shot examples should I include?
Start with 3, test with 5, and rarely go beyond 8. More examples increase cost and token usage. If accuracy plateaus after 5 examples, consider fine-tuning instead.
When should I use chain-of-thought prompting?
Use CoT for multi-step reasoning tasks: arithmetic, logic puzzles, debugging, and anything requiring intermediate steps. It has little benefit — and wastes tokens — on simple classification or lookup tasks.
How do I prevent prompt injection attacks?
Clearly separate system instructions from user content. Use structured formats (XML or JSON delimiters) to demarcate untrusted input. Validate outputs and never pass raw LLM output directly to sensitive systems (databases, file systems, APIs).
What temperature should I use?
Use temperature=0 for deterministic tasks (JSON extraction, classification, code generation). Use 0.3–0.7 for creative tasks. Use 0.7–1.0 for brainstorming or when you want diverse outputs. Never use high temperature with structured output requirements.
Is prompt engineering still relevant now that models are smarter?
Yes. Smarter models follow better instructions — which means prompting skill matters more, not less. The gap between a carefully engineered prompt and a vague one widens with model capability. Additionally, structured output, latency, and cost optimization require deliberate prompt design regardless of model quality.