Prompt Engineering Patterns for Production Applications
Systematic prompt engineering for production LLM applications: structured output extraction, chain-of-thought, few-shot calibration, versioning and evals.

Prompt engineering in production is nothing like chatting with an AI. You need deterministic output formats, consistent behavior across edge cases, version control for prompts, and evaluation pipelines that catch regressions before users do. The gap between a prompt that works in a playground and one that's reliable at scale is the same gap between a script and a production service.
These patterns treat prompts as code: versioned, tested, evaluated, and maintained with the same rigor as any other system component.
Structured Output Extraction
The most common production pattern: send unstructured input, get structured output. The prompt must constrain the model to produce parseable results.
// ❌ Hoping the model returns JSON
const prompt = `Extract the product info from this review:
"The new Sony WH-1000XM5 headphones are amazing at $349"
Return as JSON.`;
// Sometimes returns JSON, sometimes markdown, sometimes prose// ✅ Constrained structured output extraction
interface ProductExtraction {
name: string;
brand: string;
price: number | null;
currency: string;
sentiment: "positive" | "negative" | "neutral";
confidence: number;
}
function buildExtractionPrompt(
review: string
): string {
return `Extract product information from the customer review below.
RULES:
- Return ONLY a JSON object, no other text
- Use null for fields that cannot be determined
- Sentiment must be exactly one of: "positive", "negative", "neutral"
- Confidence is a number between 0 and 1
- Price should be a number without currency symbols
OUTPUT SCHEMA:
{
"name": "string - product name",
"brand": "string - manufacturer/brand",
"price": "number | null - price in local currency",
"currency": "string - ISO 4217 currency code",
"sentiment": "positive | negative | neutral",
"confidence": "number between 0 and 1"
}
REVIEW:
"""
${review}
"""
JSON:`;
}
// Parse and validate the response
function parseExtraction(
raw: string
): ProductExtraction | null {
try {
// Strip markdown code fences if present
const cleaned = raw
.replace(/```json\n?/g, "")
.replace(/```\n?/g, "")
.trim();
const parsed = JSON.parse(cleaned);
// Validate required fields
if (typeof parsed.name !== "string") return null;
if (typeof parsed.brand !== "string") return null;
if (
!["positive", "negative", "neutral"].includes(
parsed.sentiment
)
)
return null;
return parsed as ProductExtraction;
} catch {
return null;
}
}Few-Shot Calibration
Few-shot examples calibrate the model's behavior more reliably than instructions alone. The examples demonstrate edge cases and set expectations for output format and quality.
interface FewShotExample {
input: string;
output: string;
annotation?: string; // Why this example matters
}
function buildClassificationPrompt(
text: string,
examples: FewShotExample[]
): string {
const exampleSection = examples
.map(
(ex) =>
`Input: "${ex.input}"\nCategory: ${ex.output}`
)
.join("\n\n");
return `Classify the support ticket into exactly one category.
Categories: billing, technical, account, shipping, other
${exampleSection}
Input: "${text}"
Category:`;
}
// Curated examples covering edge cases
const classificationExamples: FewShotExample[] = [
{
input: "I was charged twice for my subscription",
output: "billing",
annotation: "Clear billing issue",
},
{
input: "The app crashes when I try to upload a photo",
output: "technical",
annotation: "Technical bug report",
},
{
input:
"I can't log in and I was also charged wrong",
output: "account",
annotation:
"Multi-issue: primary is account access, " +
"secondary is billing",
},
{
input: "When will my order arrive?",
output: "shipping",
annotation: "Shipping inquiry",
},
{
input:
"Your company is terrible and I want a refund " +
"on the broken thing you shipped",
output: "billing",
annotation:
"Emotional message — classify by actionable intent " +
"(refund = billing), not tone",
},
];The last example is critical—it teaches the model to handle emotionally charged inputs by focusing on actionable intent rather than surface tone. Edge case examples like this prevent the most impactful misclassifications.
Chain-of-Thought for Complex Reasoning
When the model needs to perform multi-step reasoning, chain-of-thought prompting improves accuracy by making intermediate steps explicit.
// ❌ Direct answer — model skips reasoning, makes errors
const directPrompt = `
Is this refund request eligible?
Customer purchased 45 days ago, item is opened,
total was $89. Our policy allows refunds within 30 days
for unopened items and 60 days for defective items.
Answer yes or no.`;
// ✅ Chain-of-thought — explicit reasoning steps
function buildRefundEligibilityPrompt(
request: RefundRequest
): string {
return `Determine if this refund request is eligible based on our policy.
POLICY:
1. Unopened items: full refund within 30 days of purchase
2. Opened items: exchange only within 14 days
3. Defective items: full refund within 60 days with proof
4. Digital items: no refunds after download
5. Orders over $500: manager approval required regardless
REQUEST DETAILS:
- Purchase date: ${request.purchaseDate}
- Days since purchase: ${request.daysSincePurchase}
- Item condition: ${request.condition}
- Item type: ${request.type}
- Order total: $${request.total}
- Reason: ${request.reason}
Think through each policy rule step by step, then provide your decision.
REASONING:
Step 1 - Check item type:
Step 2 - Check time window for condition:
Step 3 - Check special conditions:
Step 4 - Final decision:
DECISION: [eligible | not_eligible | needs_review]
REASON: [one sentence explanation]`;
}
interface RefundRequest {
purchaseDate: string;
daysSincePurchase: number;
condition: "unopened" | "opened" | "defective";
type: "physical" | "digital";
total: number;
reason: string;
}Prompt Versioning and Management
Prompts in production need versioning, A/B testing capability, and rollback support—just like application code.
interface PromptVersion {
id: string;
name: string;
version: string;
template: string;
variables: string[];
model: string;
temperature: number;
maxTokens: number;
createdAt: Date;
evaluationScore: number | null;
}
class PromptRegistry {
private versions: Map<string, PromptVersion[]> =
new Map();
private active: Map<string, string> = new Map();
register(prompt: PromptVersion): void {
const versions =
this.versions.get(prompt.name) ?? [];
versions.push(prompt);
this.versions.set(prompt.name, versions);
}
setActive(name: string, version: string): void {
const versions = this.versions.get(name);
if (
!versions?.some((v) => v.version === version)
) {
throw new Error(
`Version ${version} not found for ${name}`
);
}
this.active.set(name, version);
}
getActive(name: string): PromptVersion {
const version = this.active.get(name);
if (!version) {
throw new Error(`No active version for ${name}`);
}
const versions = this.versions.get(name) ?? [];
return versions.find(
(v) => v.version === version
)!;
}
render(
name: string,
variables: Record<string, string>
): string {
const prompt = this.getActive(name);
let rendered = prompt.template;
for (const [key, value] of Object.entries(variables)) {
rendered = rendered.replaceAll(`{{${key}}}`, value);
}
return rendered;
}
}
// Usage
const registry = new PromptRegistry();
registry.register({
id: "extract-v1",
name: "product-extraction",
version: "1.0.0",
template: buildExtractionPrompt("{{review}}"),
variables: ["review"],
model: "gpt-4",
temperature: 0,
maxTokens: 500,
createdAt: new Date("2024-01-15"),
evaluationScore: 0.92,
});
registry.setActive("product-extraction", "1.0.0");Automated Evaluation Pipeline
Prompts need regression tests. When you change a prompt, you need to know if it improved or degraded across your evaluation set.
interface EvalCase {
input: string;
expectedOutput: Record<string, unknown>;
tags: string[]; // Edge cases, normal, adversarial
}
interface EvalResult {
promptVersion: string;
totalCases: number;
passed: number;
failed: number;
accuracy: number;
latencyP50: number;
latencyP95: number;
failures: Array<{
input: string;
expected: unknown;
actual: unknown;
reason: string;
}>;
}
async function evaluatePrompt(
registry: PromptRegistry,
promptName: string,
evalSet: EvalCase[],
llmClient: LLMClient
): Promise<EvalResult> {
const prompt = registry.getActive(promptName);
const failures: EvalResult["failures"] = [];
const latencies: number[] = [];
for (const evalCase of evalSet) {
const rendered = registry.render(promptName, {
review: evalCase.input,
});
const start = performance.now();
const response = await llmClient.complete({
prompt: rendered,
model: prompt.model,
temperature: prompt.temperature,
maxTokens: prompt.maxTokens,
});
latencies.push(performance.now() - start);
const parsed = parseExtraction(response);
if (!parsed) {
failures.push({
input: evalCase.input,
expected: evalCase.expectedOutput,
actual: response,
reason: "Failed to parse output",
});
continue;
}
// Field-level comparison
for (const [key, expected] of Object.entries(
evalCase.expectedOutput
)) {
if (
parsed[key as keyof ProductExtraction] !== expected
) {
failures.push({
input: evalCase.input,
expected: { [key]: expected },
actual: {
[key]: parsed[key as keyof ProductExtraction],
},
reason: `Field ${key} mismatch`,
});
}
}
}
const sorted = [...latencies].sort((a, b) => a - b);
return {
promptVersion: prompt.version,
totalCases: evalSet.length,
passed: evalSet.length - failures.length,
failed: failures.length,
accuracy:
(evalSet.length - failures.length) / evalSet.length,
latencyP50: sorted[Math.floor(sorted.length * 0.5)] ?? 0,
latencyP95: sorted[Math.floor(sorted.length * 0.95)] ?? 0,
failures,
};
}Key Takeaways
Structured output prompts need explicit schema definitions, output format constraints, and post-response validation with fallback parsing for markdown code fences that models sometimes add despite instructions. Few-shot examples calibrate model behavior more reliably than instructions alone—curate examples that cover edge cases like multi-intent inputs and emotionally charged text, not just happy-path scenarios. Chain-of-thought prompting with explicit reasoning steps improves accuracy on multi-step decisions by forcing the model to evaluate each condition before reaching a conclusion. Prompt versioning with a registry pattern enables A/B testing, rollback, and audit trails—treat prompts as versioned artifacts with associated model parameters, not inline strings. Automated evaluation pipelines with tagged test cases (normal, edge case, adversarial) catch regressions when prompts change, measuring both accuracy and latency to ensure production reliability.


