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.