Certification
    August 25, 2026

    CCA Developer · M2 — Production Prompting, Agents & Tool Use

    System prompts, XML tags, few-shot, extended thinking, tool schemas, the six-step tool-use loop, streaming events, context engineering, agent vs workflow decisions, memory scopes, and multimodal.

    Share

    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>
    NOTE

    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>
    TIP

    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:

    1. The output format is complex or unusual
    2. The model needs to apply a reasoning pattern you can demonstrate
    3. 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
    ]
    TIP

    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.

    NOTE

    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
    WARNING

    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
    NOTE

    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

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Free: CCA Foundations cheat-sheet (PDF)

    The domains, the 3 universal rules, core concepts, and exam-day shortcuts — one page. Enter your email and it's yours, plus my weekly AI-architecture notes.

    No spam. Unsubscribe any time.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Join engineers building production AI systems

    Comments