Prompt Engineering
Advanced prompting, JSON mode, strict tool use, chain-of-thought, few-shot, and validation loops
Advanced prompting, JSON mode, strict tool use, chain-of-thought, few-shot, and validation loops
Prompt engineering is the practice of designing inputs that reliably produce the outputs you need from Claude. On the CCA exam (Domain 4, 20% of the test), you need to know not just what techniques exist, but when and why to apply each one.
Clarity and explicitness
Claude responds to clear, specific instructions. Vague prompts produce vague results. State exactly what you want, including format, length, and constraints.
Structure your prompts
Organize into distinct sections: background context, specific instructions, and output description. Use XML tags or Markdown headers to delineate each section.
Iterative testing
Treat prompt development like a scientific experiment. Change one variable at a time, measure results, and refine systematically.
A well-structured prompt separates concerns with clear delimiters. XML tags are the recommended approach for Claude because they are unambiguous and nestable.
<background>
You are a code review assistant for a Python backend.
The codebase follows PEP 8 and uses type hints everywhere.
</background>
<instructions>
Review the following code for:
1. Security vulnerabilities
2. Performance issues
3. Style violations
Return findings as a JSON array.
</instructions>
<output_format>
[{ "line": number, "severity": "high"|"medium"|"low",
"issue": string, "suggestion": string }]
</output_format>
<code>
{{USER_CODE}}
</code>System prompts set Claude's role, personality, and constraints. They persist across all turns in a conversation, making them the foundation for consistent behavior in production applications.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="You are a senior security engineer. "
"Analyze code for OWASP Top 10 "
"vulnerabilities only. Respond in JSON "
"with fields: vulnerability, severity, "
"line_number, remediation.",
messages=[
{"role": "user",
"content": "Review this endpoint: ..."}
]
)# Customer support bot system prompt
system = """You are a helpful support agent
for Acme Cloud Platform.
Rules:
- Never reveal internal system details
- Escalate billing issues to humans
- Always confirm the customer's plan tier
before troubleshooting
- Response format: markdown with headers
Tone: professional, empathetic, concise."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=system,
messages=messages
)Few-shot prompting provides examples of desired input/output pairs so Claude learns the pattern. 2-5 diverse, canonical examples are usually sufficient. This approach produces more consistent formatting than zero-shot prompting alone.
Curate examples that cover edge cases and different categories. The examples should demonstrate both the format and the reasoning you expect.
messages = [
{"role": "user", "content":
"Classify this support ticket: "
"'My payment was charged twice'"},
{"role": "assistant", "content":
'{"category": "billing", '
'"priority": "high", '
'"sentiment": "frustrated"}'},
{"role": "user", "content":
"Classify this support ticket: "
"'How do I reset my password?'"},
{"role": "assistant", "content":
'{"category": "account", '
'"priority": "medium", '
'"sentiment": "neutral"}'},
{"role": "user", "content":
"Classify this support ticket: "
"'Love the new dashboard update!'"},
{"role": "assistant", "content":
'{"category": "feedback", '
'"priority": "low", '
'"sentiment": "positive"}'},
# Actual ticket to classify
{"role": "user", "content":
"Classify this support ticket: "
f"'{ticket_text}'"}
]Chain of thought (CoT) prompting asks Claude to reason step by step before producing a final answer. This dramatically improves accuracy for math, logic, and multi-step analysis. Each step can be a separate API call, which enables logging, branching, and cost control.
# Each step is a separate API call
# Enables logging, branching, and cost control
# Step 1: Analyze the problem
analysis = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content":
f"Analyze this bug report step by step. "
f"Identify the root cause.\n\n{bug_report}"}]
)
# Step 2: Generate a fix based on analysis
fix = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content":
f"Based on this analysis:\n"
f"{analysis.content[0].text}\n\n"
f"Write a fix. Think through edge cases "
f"before writing code."}]
)
# Step 3: Self-review the fix
review = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content":
f"Review this proposed fix for "
f"correctness and completeness:\n\n"
f"{fix.content[0].text}\n\n"
f"List any issues found."}]
)Claude offers multiple mechanisms to guarantee structured output. Each has different tradeoffs for flexibility, strictness, and token cost.
output_config.formatConstrains the entire response to valid, parseable JSON. Define a schema in the request and the response is guaranteed to conform. Ideal when you need structured data back from every call.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content":
"Extract entities from: 'Anthropic, "
"based in San Francisco, released "
"Claude 3.5 Sonnet in June 2024.'"}],
output_config={
"format": {
"type": "json_schema",
"json_schema": {
"name": "entity_extraction",
"schema": {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {
"type": "string",
"enum": ["person",
"org", "location",
"product", "date"]
}
},
"required": ["name", "type"]
}
}
},
"required": ["entities"]
}
}
}
}
)
# Response is guaranteed valid JSON
import json
data = json.loads(
response.content[0].text
)strict: trueTool definitions with strict schema validation guarantee that tool call inputs match the schema exactly. The schema is compiled to a grammar and cached for 24 hours. Supported types: object, array, string, number, boolean, null, enum, const, and $ref.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "create_ticket",
"description": "Create a support ticket",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"priority": {
"type": "string",
"enum": ["low", "medium",
"high", "critical"]
},
"assignee_id": {"type": "number"},
"labels": {
"type": "array",
"items": {"type": "string"}
},
"is_regression": {
"type": "boolean"
}
},
"required": ["title", "priority",
"assignee_id", "labels",
"is_regression"]
}
}],
tool_choice={"type": "tool",
"name": "create_ticket"},
messages=[{"role": "user", "content":
"File a critical bug: login page 500s "
"after latest deploy. Assign to user 42. "
"Labels: auth, production, regression."}]
)
# tool_use block inputs match schema exactly
tool_input = response.content[0].inputUse XML tags in prompts to structure Claude's output. Combine with stop_sequences set to the closing XML tag to save tokens and cost. More flexible than JSON mode when you need mixed content (prose + data).
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
stop_sequences=["</analysis>"],
messages=[{"role": "user", "content":
"Analyze this code for security issues. "
"Respond inside <analysis> tags with "
"<finding> sub-tags.\n\n"
f"{code_snippet}"}],
)
# Response stops at </analysis> โ saves tokens
# Parse XML to extract findingsDefine the exact shape of expected output using JSON Schema. Use required fields, optional fields, enums, nested objects, arrays, and $ref for reusable definitions. This is critical for production APIs where downstream systems depend on a precise contract.
{
"name": "api_response",
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["success", "error", "partial"]
},
"data": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": { "$ref": "#/$defs/user" }
},
"pagination": {
"type": "object",
"properties": {
"page": { "type": "number" },
"per_page": { "type": "number" },
"total": { "type": "number" }
},
"required": ["page", "per_page", "total"]
}
},
"required": ["users", "pagination"]
},
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": { "type": "string" },
"message": { "type": "string" },
"field": { "type": "string" }
},
"required": ["code", "message"]
}
}
},
"required": ["status", "data"],
"$defs": {
"user": {
"type": "object",
"properties": {
"id": { "type": "number" },
"email": { "type": "string" },
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"]
},
"permissions": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["id", "email", "role"]
}
}
}
}In production, always validate Claude's output against your schema. If validation fails, retry with the error context included in the prompt. Set a max retry count to prevent infinite loops. This is an expected production pattern on the CCA exam.
import json
from jsonschema import validate, ValidationError
def get_validated_response(
prompt: str,
schema: dict,
max_retries: int = 3
) -> dict:
messages = [{"role": "user", "content": prompt}]
for attempt in range(max_retries):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=messages
)
text = response.content[0].text
try:
data = json.loads(text)
validate(instance=data, schema=schema)
return data # Valid โ return it
except (json.JSONDecodeError,
ValidationError) as e:
# Include error in retry prompt
messages.append(
{"role": "assistant", "content": text}
)
messages.append(
{"role": "user", "content":
f"That response had a validation "
f"error:\n{str(e)}\n\n"
f"Please fix and return valid JSON "
f"matching the schema."}
)
raise RuntimeError(
f"Failed after {max_retries} retries"
)Define rubrics for Claude to evaluate against. This is essential for self-evaluation loops where Claude assesses its own output or scores content quality.
evaluation_prompt = """
Score this code review on each criterion (1-5):
<rubric>
<criterion name="completeness">
5: All issues found, nothing missed
3: Major issues found, minor ones missed
1: Critical issues missed entirely
</criterion>
<criterion name="actionability">
5: Every finding has a specific fix
3: Fixes are vague or partial
1: No remediation suggested
</criterion>
<criterion name="accuracy">
5: Zero false positives
3: 1-2 false positives
1: More false positives than real issues
</criterion>
</rubric>
<threshold>
PASS: All criteria >= 3 AND average >= 4
FAIL: Any criterion < 3 OR average < 3
</threshold>
<review_to_evaluate>
{review_text}
</review_to_evaluate>
Return JSON: {"scores": {...}, "pass": bool}
"""All Claude models have a 200K token context window. Understanding how to manage it is critical -- this bridges into Domain 5 (Extended Thinking and Context).
Pre-fill the assistant response to guide format and behavior. By starting Claude's response with specific text, you steer it toward the structure you want without additional instructions.
# Pre-fill forces Claude to continue in JSON
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content":
"List the top 3 security risks in "
"this code snippet.\n\n"
f"{code_snippet}"},
{"role": "assistant",
"content": "[{"}
]
)
# Claude continues from "[{" โ guarantees
# the response starts as a JSON array
result = json.loads("[{" + response.content[0].text)Framing Claude with a specific role adjusts its expertise, perspective, and level of detail. Combine role prompting with specific constraints for best results.
"You are a Senior Security Engineer..."
Focus on OWASP Top 10. Flag any user input that touches SQL or shell commands.
"You are a API Design Reviewer..."
Evaluate REST endpoints for consistency, versioning, and backwards compatibility.
"You are a Performance Engineer..."
Identify N+1 queries, unnecessary allocations, and missing indexes. Quantify impact.
Parameterized prompts with placeholders let you build reusable, versionable, and A/B-testable prompt libraries. Treat prompts as code: store them in version control, review changes, and test variants systematically.
from string import Template
# Template library โ version controlled
TEMPLATES = {
"code_review": Template("""
You are a $role reviewing $language code.
<context>$context</context>
<code>$code</code>
Focus on: $focus_areas
Output format: $output_format
"""),
"summarize": Template("""
Summarize the following $doc_type in $length.
Audience: $audience
Tone: $tone
<document>$content</document>
"""),
}
# Usage โ swap variables per request
prompt = TEMPLATES["code_review"].substitute(
role="senior backend engineer",
language="Python",
context="FastAPI microservice for payments",
code=user_code,
focus_areas="security, error handling",
output_format="JSON array of findings"
)
# A/B test: compare two prompt variants
variant_a = TEMPLATES["code_review"].substitute(
role="security engineer", ...
)
variant_b = TEMPLATES["code_review"].substitute(
role="senior developer", ...
)"Make it better" โ too vague, no criteria
โ Define specific criteria: "Improve readability by extracting functions over 20 lines"
Contradictory instructions in the same prompt
โ Review prompts for conflicting requirements before sending
Overloading a single prompt with 10+ distinct tasks
โ Break into separate, focused API calls (chain of thought)
Not specifying output format
โ Always include format in instructions or use JSON mode / strict tool use
Relying on implicit understanding of your codebase
โ Provide explicit context: language, framework, conventions, constraints
DOMAIN 4 โ 20% OF EXAM
JSON mode vs strict tool use
JSON mode (output_config.format) constrains the full response to JSON. Strict tool use (strict: true) constrains tool call inputs. Know when to use each.
Few-shot > zero-shot for formatting
When you need consistent output formatting, few-shot examples are more reliable than instructions alone.
Chain of thought as separate API calls
Each step as its own API call enables logging, branching, cost control, and debugging. This is the production pattern.
stop_sequences save tokens and cost
Set stop_sequences to a closing XML tag (e.g., "</analysis>") to prevent Claude from generating unnecessary content after the structured output.
Validation + retry is expected in production
Parse output, validate against schema, retry with error context if invalid. Always set a max retry count.
Prefill steers output format
Pre-filling the assistant message (e.g., starting with "[{") forces Claude to continue in that format without extra instructions.
System prompts persist across turns
They set role, personality, and constraints for the entire conversation. Use them for consistent behavior in multi-turn applications.