Prompt Engineering Patterns That Actually Work

Battle-tested prompt patterns for getting consistent, structured output from LLMs in production systems.

Prompt engineering isn’t about clever tricks — it’s about reliability. When you’re processing thousands of requests per day, you need prompts that produce consistent, parseable output every single time.

Pattern 1: The Structured Output Template

The most important pattern is forcing structured output. Never ask an LLM to “describe” something — ask it to fill in a schema.

@prompt """
Analyze the following code change and respond with ONLY valid JSON matching this schema:

{
  "risk_level": "low" | "medium" | "high" | "critical",
  "summary": "one sentence description",
  "concerns": ["list of specific concerns"],
  "suggestion": "one actionable suggestion or null"
}

Code change:
<diff>
<%= diff %>
</diff>
"""

The key insight: providing the exact schema in the prompt dramatically reduces parsing failures.

Pattern 2: Chain of Thought with Extraction

Sometimes you need the LLM to reason, but you only want the final answer. Use a two-phase approach:

@prompt """
<thinking>
Analyze the customer message step by step:
1. What is the customer's primary emotion?
2. What specific issue are they describing?
3. What urgency level does this warrant?
</thinking>

After your analysis, respond with ONLY this JSON:
{"emotion": "...", "issue": "...", "urgency": "low|medium|high"}

Customer message: <%= message %>
"""

The <thinking> block gives the model space to reason while the extraction format keeps your output clean.

Pattern 3: Few-Shot with Edge Cases

Don’t just show happy-path examples. Include the weird cases:

@examples """
Input: "I love your product!"
Output: {"sentiment": "positive", "actionable": false}

Input: "This is broken and I want a refund NOW"
Output: {"sentiment": "negative", "actionable": true}

Input: "Is the sky blue?"
Output: {"sentiment": "neutral", "actionable": false, "note": "off-topic"}

Input: ""
Output: {"sentiment": "unknown", "actionable": false, "note": "empty input"}
"""

Edge cases in few-shot examples teach the model how to handle the unexpected without crashing your pipeline.

Pattern 4: The Validation Loop

For critical workflows, retry with feedback:

defmodule LLM.ValidatedCall do
  def call_with_retry(prompt, validator, max_retries \\ 3) do
    Enum.reduce_while(1..max_retries, nil, fn attempt, _acc ->
      {:ok, response} = LLM.complete(prompt)

      case validator.(response) do
        {:ok, parsed} ->
          {:halt, {:ok, parsed}}

        {:error, reason} when attempt < max_retries ->
          corrected_prompt = """
          #{prompt}

          Your previous response was invalid: #{reason}
          Please try again, strictly following the output format.
          """
          {:cont, {:error, reason}}

        {:error, reason} ->
          {:halt, {:error, :max_retries_exceeded, reason}}
      end
    end)
  end
end

The 80/20 Rule

In practice, structured output templates solve 80% of prompt engineering problems. The other 20% requires careful few-shot examples and validation loops. Start simple, add complexity only when your error rates demand it.