2.1 The Four Prompting Techniques
Production prompts are not guesswork. There are four techniques you apply systematically, depending on the symptom.
Diagnosis table — match the symptom to the missing technique:
| Symptom | Missing technique |
|---|---|
| Output is off-topic, too casual, or wrong persona | System prompt (role + constraints not set) |
| Instructions mixed up with user content, model confuses data for directives | XML tags (structure not applied) |
| Format wrong, model doesn't know what "good" looks like | Few-shot examples |
| Output is prose when you need JSON; inconsistent structure | Output constraints (schema or tool use) |
Treat these as independent levers. You can apply all four simultaneously.
2.2 System Prompts
The system prompt sets the operator-level context that persists across the conversation. It is where you define:
- Role and persona ("You are a senior code reviewer…")
- Capabilities and constraints ("You have access to the customer database via tools. Do not reveal raw SQL.")
- Output format requirements ("Always respond in JSON matching this schema…")
- Guardrails ("Never discuss competitor products. If asked, redirect politely.")
Structure matters. A long system prompt should use headers and XML tags internally:
<role>
You are a senior code reviewer for a financial services company.
Your job is to review Python code for correctness, security, and compliance with PCI-DSS.
</role>
<constraints>
- Never approve code that transmits raw card numbers outside the system
- Flag any use of MD5 or SHA-1 for password hashing as a critical issue
- All findings must be categorised: critical / major / minor
</constraints>
<output_format>
Respond in JSON:
{"findings": [{"severity": "...", "line": N, "description": "..."}], "approved": true/false}
</output_format>
System prompts are operator-level instructions — they cannot be overridden by user messages. But they are not a security boundary. A determined user can manipulate Claude's behaviour through the conversation. Authorization and access control must be enforced in your application code, not in system prompt instructions.
2.3 XML Tags
XML tags solve the "mixed content" problem: when instructions, examples, documents, and user input all appear in the same prompt, Claude can confuse which text it should treat as a directive and which as data.
Use XML tags to separate content types:
<document>
{{contract_text}}
</document>
<instructions>
Review the above contract for any clauses that would restrict our ability to use the software for AI-powered features. List each problematic clause with a plain-English explanation.
</instructions>
Custom tag names. You choose the tag names. Use descriptive names that match your domain:
<user_query>...</user_query>
<retrieved_context>...</retrieved_context>
<task>...</task>
XML tags are especially valuable when user content could contain text that looks like an instruction. Wrapping user input in <user_message> tags tells the model "treat this as data, not as a directive" — a first line of defence against prompt injection.
2.4 Few-Shot Examples
Few-shot examples show the model what "good" looks like rather than describing it in words. They are most effective when:
- The output format is complex or unusual
- The model needs to apply a reasoning pattern you can demonstrate
- The edge cases are important to get right
Example construction rules:
- Diversity over volume. Three examples covering different cases (normal, edge, tricky) beat ten examples of the same pattern.
- Cover the hard cases. Easy examples add no signal. Include examples where a naive approach would fail.
- Format consistency. The format of every example must match the format you expect in production.
- Negative examples. For classification tasks, include examples of each class, including the hard-to-distinguish ones.
<examples>
<example>
<input>Review: "Product stopped working after 2 days. Very disappointed."</input>
<output>{"sentiment": "negative", "confidence": "high"}</output>
</example>
<example>
<input>Review: "Does what it says, nothing more."</input>
<output>{"sentiment": "neutral", "confidence": "medium"}</output>
</example>
<example>
<input>Review: "Love it! Would be 5 stars but shipping took 2 weeks."</input>
<output>{"sentiment": "positive", "confidence": "medium"}</output>
</example>
</examples>
2.5 Output Constraints and Structured Output
For machine-readable output, don't ask for JSON in prose — enforce it structurally.
Method 1: Tool use for structured output. Define a tool that describes your schema. The model calls the tool instead of writing free text. This is the most reliable approach:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=[{
"name": "submit_classification",
"description": "Submit the sentiment classification result",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]},
"confidence": {"type": "string", "enum": ["high", "medium", "low"]},
"reasoning": {"type": "string"}
},
"required": ["sentiment", "confidence", "reasoning"]
}
}],
tool_choice={"type": "tool", "name": "submit_classification"},
messages=[{"role": "user", "content": f"Classify: {review}"}]
)
Setting tool_choice to a specific tool name forces the model to call exactly that tool — guaranteed structured output.
Method 2: Prefill the assistant turn. Start the assistant response with { to constrain the output to JSON:
messages = [
{"role": "user", "content": "Classify this review: ..."},
{"role": "assistant", "content": "{"} # assistant prefill
]
For complex schemas, tool use is more reliable than prefill. For simple schemas (1-3 fields), prefill is simpler and equally effective.
2.6 Extended Thinking
Extended thinking lets the model reason explicitly before producing an answer. Enable it per-request:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 8000},
messages=[{"role": "user", "content": "Design the optimal database schema for a multi-tenant SaaS billing system."}]
)
Thinking block in the response:
for block in response.content:
if block.type == "thinking":
print(f"[Reasoning] {block.thinking}")
elif block.type == "text":
print(f"[Answer] {block.text}")
The thinking block is for your diagnostics. Users typically see only the text block.
When using extended thinking in a multi-turn conversation, you must pass the thinking blocks back in the conversation history along with the text blocks. Omitting them causes the model to lose its reasoning context.
2.7 Tool Design
Tools are how Claude interacts with external systems. Each tool has three required fields:
{
"name": "get_customer_orders", # short snake_case identifier
"description": "Retrieve all orders for a customer. Use this when you need order history, order status, or order totals for a specific customer ID.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "The customer's unique identifier (UUID format)"
},
"limit": {
"type": "integer",
"description": "Maximum number of orders to return. Defaults to 20.",
"default": 20
}
},
"required": ["customer_id"]
}
}
The description is the routing signal. When the model decides which tool to call, it reads the description. A vague description ("Use this tool for orders") leads to wrong or missed tool calls. A specific description ("Use this when you need order history, order status, or order totals for a specific customer ID") reliably routes the right queries.
Description writing checklist:
- State when to use this tool (not just what it does)
- State what it returns (important for multi-tool setups where results overlap)
- Note required preconditions (e.g., "requires a valid session token")
- Keep it under 100 words; longer descriptions add noise
2.8 The Six-Step Tool-Use Loop
Tool use follows a deterministic six-step loop. Understanding every step prevents debugging confusion:
1. Application sends request → model receives messages + tool definitions
2. Model produces a tool_use block (name + input JSON)
3. stop_reason = "tool_use" — the model pauses and waits
4. Application receives the response, extracts the tool_use block
5. Application executes the tool, gets the result
6. Application appends tool_result to messages, sends again → back to step 2
Message block types in the API:
| Block type | Who produces it | Contains |
|---|---|---|
text |
Model | Natural language response |
tool_use |
Model | Tool name + input parameters |
tool_result |
Application | Tool output (string, JSON, or error) |
thinking |
Model (extended thinking) | Reasoning scratchpad |
Returning tool results:
# After executing the tool, append the result and continue
messages.append({
"role": "assistant",
"content": response.content # includes the tool_use block
})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps({"orders": [...], "total": 3})
}]
})
# Send again — model will continue from here
response = client.messages.create(model=model, messages=messages, tools=tools, max_tokens=1024)
Returning errors from tools. When a tool fails, return the error as a string in content and set is_error: true. The model will handle it gracefully:
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": "Error: customer_id not found in database",
"is_error": True
}
2.9 Streaming
Streaming returns tokens as they are generated. The event sequence for a streaming response is:
message_start → conversation metadata, usage counts
content_block_start → new block begins (type: text or tool_use)
content_block_delta → incremental content (text_delta or input_json_delta for tools)
content_block_stop → block complete
message_delta → final stop_reason and usage
message_stop → stream complete
Streaming with tool use. When the model calls a tool, the content_block_start event reports type: "tool_use" and subsequent content_block_delta events deliver the input_json_delta — the JSON arguments, streamed character by character. You accumulate them and parse when the block stops.
stop_reason values:
| stop_reason | Meaning |
|---|---|
end_turn |
Model finished naturally |
tool_use |
Model emitted a tool call, waiting for result |
max_tokens |
Output was truncated at max_tokens — truncation, not completion |
stop_sequence |
Hit a custom stop sequence |
stop_reason = "max_tokens" means the response is incomplete. If your pipeline treats this the same as end_turn, you'll silently deliver truncated answers. Always check stop_reason.
2.10 Context Engineering
Context engineering is the active management of what lives in the context window during a long-running task. It is not optional for agents — it is the primary lever for preventing context overflow.
Four strategies:
| Strategy | When to apply | Mechanism |
|---|---|---|
| Progressive truncation | Accumulating tool results that no longer need detail | Keep first N tokens + last N tokens; summarise the middle |
| Compaction | Long conversation history | Replace older turns with a model-generated summary |
| Clearing | Clean break between tasks | Reset the messages array; start fresh |
| Subagent handoffs | Long multi-stage workflows | Pass a structured summary (not full history) to the next subagent |
RAG vs live tool use — the key distinction:
- Retrieval (RAG): for stable knowledge (documents, policies, reference data) that was true yesterday and will be true tomorrow. Returns text chunks injected into context.
- Live tool use: for live state (database records, API values) that a system owns and changes independently. Returns current values at call time.
The symptom of wrong choice: If your RAG chunks are returning stale data that contradicts the database, you're retrieving something that should be a live tool call.
2.11 Agent vs Workflow — The Decision
The fundamental architectural decision: should this system be a workflow (fixed sequence) or an agent (dynamic decision loop)?
Use a workflow when:
- The steps and their order are known in advance
- You want deterministic, predictable behaviour
- Cost and latency are primary constraints
- Errors need to be caught at specific known points
Use an agent when:
- The steps cannot be enumerated ahead of time
- The task requires the model to decide what to do next based on intermediate results
- Genuine unpredictability in the task requires adaptive behaviour
Agents score worse than workflows on predictability, observability, cost, and latency. Use an agent only when the task genuinely demands it. Many tasks that feel agentic can be solved with a well-designed workflow.
Three wiring paths (from lowest to highest flexibility):
| Path | Description | When to use |
|---|---|---|
| Messages API (manual loop) | You manage the tool loop explicitly in your code | Full control; best for custom protocols |
| Agent SDK (Anthropic) | The SDK manages the loop for you with hook points | Standardised agents; most production use cases |
| Managed Agents | Fully server-side loop; you call an endpoint | Minimal infra; suitable for simple delegation |
Human-in-the-loop insertion points. Insert a human review gate at any step where the consequence of an error is irreversible or high-cost. Common patterns:
- Before any write operation that cannot be undone
- Before spending money (e-commerce, financial transactions)
- When the model's confidence is below a threshold
- After a fixed number of iterations (time-based escalation)
2.12 Memory Scopes
Four memory types, each with different persistence and retrieval:
| Scope | Persistence | Retrieval | Example |
|---|---|---|---|
| In-context | Single conversation | Automatic (it's in the prompt) | Conversation history, inline documents |
| External storage | Persistent across sessions | Explicit retrieval (search or lookup) | Vector DB, SQL, file system |
| In-weights | Permanent (training-time only) | Automatic (baked into model) | General knowledge, code patterns |
| KV cache | One session | Automatic (same prefix) | Repeated system prompt + document |
Only external storage gives you true cross-session memory that you control. In-context and KV cache are session-bounded. In-weights cannot be changed without fine-tuning.
2.13 Multimodal and Batch
Images. Pass images as base64-encoded content blocks:
import base64
with open("diagram.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}},
{"type": "text", "text": "Describe the architecture shown in this diagram."}
]
}]
)
PDFs. Claude supports PDF input natively via the document block type. The model processes the document's text and visual layout:
{
"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf_base64}
}
Image token cost. Images cost between 1,000–3,000 input tokens depending on resolution and detail level. For bulk image processing, use the Batches API.
M2 Checkpoint
- System prompt = operator-level constraints — not a security boundary; app code enforces authorization
- Description is the tool routing signal — vague descriptions cause wrong tool calls
stop_reason = "max_tokens"means truncation, not completion — always check- RAG for stable knowledge; live tools for live state — wrong choice causes stale data
- Workflow first; agent only when steps cannot be enumerated
- External storage is the only cross-session memory you control