Prompt Engineering Fundamentals Every Developer Should Know

Table of Contents

You’re integrating an LLM into your application, and the outputs are inconsistent. Sometimes brilliant, sometimes garbage. The difference isn’t the model — it’s how you’re prompting it. Prompt engineering fundamentals separate developers who get reliable AI outputs from those fighting unpredictable responses.

This isn’t about finding magic phrases. It’s about understanding how LLMs process instructions and structuring your prompts accordingly.

System Prompts: Setting the Operating Context

The system prompt defines who the AI is and how it should behave. Think of it as the configuration layer that runs before every user interaction.

import anthropic

client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system="""You are a senior code reviewer for a Python development team.
    
RULES:
- Focus on security vulnerabilities and performance issues
- Ignore style preferences unless they impact readability
- Rate severity as: critical, warning, or suggestion
- Always explain WHY something is problematic""",
    messages=[
        {"role": "user", "content": "Review this function: def get_user(id): return db.execute(f'SELECT * FROM users WHERE id = {id}')"}
    ]
)

The system prompt above does several things right:

  • Establishes a specific role (code reviewer, not generic assistant)
  • Sets clear constraints on what to focus on
  • Defines output format expectations
  • Explains the reasoning requirement

If you’ve already built your first AI agent with Python and Claude, you’ve seen how system prompts shape agent behavior. For prompt engineering, this is your foundation.

Few-Shot Examples: Teaching by Demonstration

When you need consistent output formatting or want the model to follow a specific reasoning pattern, show it examples. Few-shot prompting works because LLMs are pattern-matching machines.

Convert user feedback into structured bug reports.

Example 1:
Input: "The app crashes when I click the submit button on the contact form"
Output:
{
  "component": "contact-form",
  "action": "submit",
  "issue_type": "crash",
  "reproduction": "Click submit button on contact form"
}

Example 2:
Input: "Loading takes forever on the dashboard after login"
Output:
{
  "component": "dashboard",
  "action": "page-load",
  "issue_type": "performance",
  "reproduction": "Login and navigate to dashboard"
}

Now convert this:
Input: "Profile picture upload fails with large images"

Three rules for effective few-shot examples:

  1. Use 2-4 examples — more isn’t always better
  2. Cover edge cases or variations you care about
  3. Keep examples structurally identical to what you expect

Chain-of-Thought: Making the Model Show Its Work

For complex reasoning tasks, explicitly asking the model to think step-by-step dramatically improves accuracy. This is chain-of-thought prompting.

Analyze this database query for potential issues. Think through each part step by step before giving your final assessment.

Query:
SELECT u.*, o.total 
FROM users u 
LEFT JOIN orders o ON u.id = o.user_id 
WHERE o.created_at > '2024-01-01'

The model will now walk through the query systematically rather than jumping to conclusions. You can make this even more explicit:

Before answering:
1. Identify what the query is trying to accomplish
2. Check for logical errors in the JOIN conditions
3. Consider performance implications
4. Look for data integrity issues

Then provide your assessment.

Chain-of-thought is especially useful when you’re building RAG pipelines where the model needs to reason over retrieved context.

XML Tags: Structured Input for Structured Output

XML tags help you clearly delineate different parts of your prompt. Claude in particular handles these well, but the technique works across models.

<context>
You are helping a developer debug a Laravel application. The application uses MySQL and Redis for caching.
</context>

<error_log>
[2026-05-07 09:15:32] production.ERROR: SQLSTATE[HY000] [2002] Connection refused
</error_log>

<recent_changes>
- Updated .env file with new database credentials
- Deployed new feature branch to production
- Cleared config cache
</recent_changes>

<task>
Identify the most likely cause of this error and provide specific debugging steps.
</task>

This structure does several things:

  • Separates context from the actual task
  • Makes it easy to swap out sections programmatically
  • Reduces ambiguity about what’s data vs. instruction

If you’re setting up Cursor rules for AI-assisted coding, you’ll use similar structural patterns to give the AI consistent context.

Output Format Control: Getting Parseable Responses

When you need to parse AI output programmatically, be explicit about format requirements.

Analyze the following API endpoint for RESTful design issues.

Endpoint: POST /api/getUserData

Respond in this exact JSON format:
{
  "issues": [
    {
      "type": "naming|method|structure",
      "description": "string",
      "suggestion": "string"
    }
  ],
  "overall_score": 1-10
}

Return ONLY valid JSON. No markdown code blocks. No explanations outside the JSON structure.

The key phrases here are:

  • “exact JSON format” — signals strict adherence
  • “Return ONLY valid JSON” — prevents preamble text
  • “No markdown code blocks” — stops the model from wrapping output in ```json

For production code, always validate the output. Models occasionally deviate even with strict instructions.

Putting It All Together

Here’s a complete example combining these techniques for a code documentation generator:

system_prompt = """You are a technical documentation writer specializing in PHP/Laravel codebases.

STYLE GUIDE:
- Write for intermediate developers
- Include parameter types and return types
- Add usage examples for non-obvious functions
- Keep descriptions under 3 sentences"""

user_prompt = """<function>
public function syncUserPermissions(User $user, array $permissions, bool $detach = true): void
{
    $permissionIds = Permission::whereIn('slug', $permissions)->pluck('id');
    $user->permissions()->sync($permissionIds, $detach);
    Cache::tags(['permissions', "user:{$user->id}"])->flush();
}
</function>

<output_format>
Return documentation in this structure:
{
  "summary": "One-line description",
  "parameters": [{"name": "", "type": "", "description": ""}],
  "returns": {"type": "", "description": ""},
  "example": "Code example string",
  "notes": ["Array of important considerations"]
}
</output_format>

Generate documentation for this function."""

Key Takeaways

  • System prompts set behavior rules that apply to every interaction — use them to establish role, constraints, and output expectations
  • Few-shot examples teach patterns more reliably than descriptions — show, don’t just tell
  • Chain-of-thought prompting improves accuracy on complex tasks — ask the model to reason step-by-step
  • XML tags provide clear structure that survives complex prompts — especially useful when mixing data with instructions
  • Output format control requires explicit instructions and validation — never assume the model will follow format perfectly

These fundamentals apply whether you’re running Llama locally or hitting cloud APIs. Master them, and your AI integrations become predictable tools rather than unpredictable experiments.