Episode 4 — Generative AI Engineering / 4.5 — Generating JSON Responses from LLMs
4.5.e — Building Structured Profile Analysis
In one sentence: This is the capstone — a complete, end-to-end working example that combines JSON mode, schema-based prompting, validation, and retry logic to build a profile analysis feature that reliably returns
{ compatibility_score, strengths, weaknesses, suggested_openers }.
Navigation: ← 4.5.d — Validating Returned Structure · 4.5 Overview
1. What We're Building
A function that takes two dating profiles and returns a structured compatibility analysis:
const analysis = await analyzeCompatibility(profileA, profileB);
// Returns:
{
compatibility_score: 85,
strengths: [
"Both are into hiking and outdoor activities",
"Similar sense of humor based on conversation style",
"Both value work-life balance"
],
weaknesses: [
"Different music tastes might cause friction",
"One is a morning person, the other is a night owl"
],
suggested_openers: [
"Ask about their favorite hiking trail — you both love the outdoors!",
"Mention the comedy special you both referenced in your profiles"
]
}
This is the target JSON from the section overview. Let's build every piece from scratch.
2. The Target JSON Schema
Before writing any code, define the exact contract:
/**
* Target JSON structure for compatibility analysis.
*
* TypeScript equivalent:
* type CompatibilityAnalysis = {
* compatibility_score: number; // Integer 0-100
* strengths: string[]; // 2-5 positive compatibility factors
* weaknesses: string[]; // 0-4 potential friction points
* suggested_openers: string[]; // 2-3 personalized conversation starters
* }
*/
const SCHEMA_DESCRIPTION = {
compatibility_score: 'Integer 0-100. Higher = more compatible.',
strengths: 'Array of 2-5 strings. Specific things these two have in common or that complement each other.',
weaknesses: 'Array of 0-4 strings. Potential friction points or differences. Empty array if none apparent.',
suggested_openers: 'Array of 2-3 strings. Personalized opening messages one could send to the other, referencing shared interests.'
};
3. System Prompt Design
The system prompt is the most important piece. It must communicate:
- Role — what the AI is
- Task — what it does
- Schema — the exact JSON structure
- Rules — constraints and edge cases
const SYSTEM_PROMPT = `You are a dating app compatibility analyzer. Given two user profiles, analyze their compatibility and return a structured JSON assessment.
## Output Format
Return ONLY a valid JSON object with this exact structure:
{
"compatibility_score": <integer 0-100>,
"strengths": [<string>, <string>, ...],
"weaknesses": [<string>, <string>, ...],
"suggested_openers": [<string>, <string>, ...]
}
## Field Rules
- "compatibility_score" (integer, REQUIRED):
- 0-20: Very low compatibility
- 21-40: Below average
- 41-60: Moderate compatibility
- 61-80: Good compatibility
- 81-100: Excellent compatibility
- Base your score on shared interests, values, lifestyle compatibility, and communication style
- "strengths" (array of strings, REQUIRED):
- Include 2-5 specific positive compatibility factors
- Be specific: "Both enjoy trail running in nature" NOT "Both like exercise"
- Reference actual details from both profiles
- "weaknesses" (array of strings, REQUIRED):
- Include 0-4 potential friction points
- Use empty array [] if no obvious concerns
- Be constructive, not judgmental: "Different sleep schedules may require compromise" NOT "incompatible schedules"
- Only mention genuine differences, not minor preferences
- "suggested_openers" (array of strings, REQUIRED):
- Include 2-3 personalized conversation starters
- Each should reference something specific from the other person's profile
- Keep them casual, friendly, and non-generic
- Format: direct messages the user could send
## Important
- Return ONLY the JSON object — no explanation, no markdown, no extra text
- All arrays must contain strings only
- The compatibility_score must be an integer, not a decimal
- Do not invent details not present in the profiles`;
Why each part matters
| Section | Purpose |
|---|---|
| Role declaration | Sets the model's persona and domain expertise |
| Output format example | Shows the exact JSON shape with placeholders |
| Field rules | Defines types, ranges, count constraints, and content guidelines |
| Score range guide | Anchors the 0-100 scale so the model doesn't cluster all scores around 50-70 |
| Specificity instructions | Prevents generic, useless outputs |
| "Important" section | Reinforces critical constraints the model must not violate |
4. Making the API Call with JSON Mode
import OpenAI from 'openai';
const openai = new OpenAI();
async function callCompatibilityAPI(profileA, profileB) {
const userMessage = `Analyze the compatibility of these two profiles:
Profile A:
${profileA}
Profile B:
${profileB}`;
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0, // Deterministic for consistent scoring
max_tokens: 1024, // Enough for the response, not wasteful
response_format: { type: 'json_object' }, // Guarantee valid JSON
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userMessage }
],
});
return {
raw: response.choices[0].message.content,
finishReason: response.choices[0].finish_reason,
usage: response.usage,
};
}
Parameter choices explained
| Parameter | Value | Why |
|---|---|---|
temperature: 0 | Deterministic | Same profiles should get similar scores each time |
max_tokens: 1024 | Generous but bounded | Our JSON is ~200-400 tokens; 1024 gives room without risk of runaway output |
response_format: json_object | JSON mode | Guarantees valid JSON syntax |
model: 'gpt-4o' | Latest capable model | Best at following complex schema instructions |
5. Parsing and Validating the Response
function parseAndValidate(apiResult) {
// Step 1: Check for truncation
if (apiResult.finishReason === 'length') {
return {
success: false,
error: 'Response was truncated — increase max_tokens',
errorType: 'TRUNCATED'
};
}
// Step 2: Parse JSON
let data;
try {
data = JSON.parse(apiResult.raw);
} catch (parseError) {
return {
success: false,
error: `JSON parse failed: ${parseError.message}`,
errorType: 'PARSE_ERROR',
raw: apiResult.raw
};
}
// Step 3: Validate schema
const errors = [];
// compatibility_score: required integer 0-100
if (typeof data.compatibility_score !== 'number') {
errors.push(`compatibility_score must be a number, got ${typeof data.compatibility_score}`);
} else if (!Number.isInteger(data.compatibility_score)) {
// Auto-fix: round to integer
data.compatibility_score = Math.round(data.compatibility_score);
}
if (typeof data.compatibility_score === 'number') {
if (data.compatibility_score < 0 || data.compatibility_score > 100) {
errors.push(`compatibility_score must be 0-100, got ${data.compatibility_score}`);
}
}
// strengths: required array of 2-5 strings
if (!Array.isArray(data.strengths)) {
errors.push('strengths must be an array');
} else {
if (data.strengths.length < 2) {
errors.push(`strengths must have at least 2 items, got ${data.strengths.length}`);
}
if (data.strengths.length > 5) {
data.strengths = data.strengths.slice(0, 5); // Auto-trim, not an error
}
if (!data.strengths.every(s => typeof s === 'string')) {
errors.push('All strengths items must be strings');
}
}
// weaknesses: required array of 0-4 strings
if (!Array.isArray(data.weaknesses)) {
errors.push('weaknesses must be an array');
} else {
if (data.weaknesses.length > 4) {
data.weaknesses = data.weaknesses.slice(0, 4); // Auto-trim
}
if (!data.weaknesses.every(s => typeof s === 'string')) {
errors.push('All weaknesses items must be strings');
}
}
// suggested_openers: required array of 2-3 strings
if (!Array.isArray(data.suggested_openers)) {
errors.push('suggested_openers must be an array');
} else {
if (data.suggested_openers.length < 2) {
errors.push(`suggested_openers must have at least 2 items, got ${data.suggested_openers.length}`);
}
if (data.suggested_openers.length > 3) {
data.suggested_openers = data.suggested_openers.slice(0, 3); // Auto-trim
}
if (!data.suggested_openers.every(s => typeof s === 'string')) {
errors.push('All suggested_openers items must be strings');
}
}
if (errors.length > 0) {
return {
success: false,
error: errors.join('; '),
errorType: 'VALIDATION_ERROR',
data: data // Include data in case caller wants to use it despite errors
};
}
return { success: true, data };
}
6. Error Handling and Retries
async function analyzeWithRetry(profileA, profileB, maxRetries = 3) {
let messages = [
{ role: 'system', content: SYSTEM_PROMPT },
{
role: 'user',
content: `Analyze the compatibility of these two profiles:\n\nProfile A:\n${profileA}\n\nProfile B:\n${profileB}`
}
];
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Make the API call
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
max_tokens: 1024,
response_format: { type: 'json_object' },
messages: messages,
});
const apiResult = {
raw: response.choices[0].message.content,
finishReason: response.choices[0].finish_reason,
usage: response.usage,
};
// Parse and validate
const result = parseAndValidate(apiResult);
if (result.success) {
return {
success: true,
data: result.data,
metadata: {
attempt,
model: 'gpt-4o',
inputTokens: apiResult.usage.prompt_tokens,
outputTokens: apiResult.usage.completion_tokens,
}
};
}
// Validation failed — prepare retry
console.warn(`Attempt ${attempt}/${maxRetries} failed: ${result.error}`);
if (attempt < maxRetries) {
messages = [
...messages,
{ role: 'assistant', content: apiResult.raw },
{
role: 'user',
content: `Your JSON response had validation errors:\n${result.error}\n\nPlease return the corrected JSON object. Remember:\n- compatibility_score: integer 0-100\n- strengths: array of 2-5 strings\n- weaknesses: array of 0-4 strings\n- suggested_openers: array of 2-3 strings`
}
];
}
} catch (apiError) {
console.error(`Attempt ${attempt}/${maxRetries} API error:`, apiError.message);
// Don't retry on certain errors
if (apiError.status === 401) throw apiError; // Auth error — won't help to retry
if (apiError.status === 400) throw apiError; // Bad request — fix the code
// Retry on transient errors (429, 500, 503)
if (attempt === maxRetries) throw apiError;
// Exponential backoff for rate limits
if (apiError.status === 429) {
const waitMs = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Waiting ${waitMs}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
}
}
return {
success: false,
error: `Failed after ${maxRetries} attempts`,
data: null,
};
}
7. The Complete End-to-End Module
Here's everything combined into a clean, production-ready module:
import OpenAI from 'openai';
// ─────────────────────────────────────────────────
// Configuration
// ─────────────────────────────────────────────────
const openai = new OpenAI();
const SYSTEM_PROMPT = `You are a dating app compatibility analyzer. Given two user profiles, analyze their compatibility and return a structured JSON assessment.
Return ONLY a valid JSON object with this exact structure:
{
"compatibility_score": <integer 0-100>,
"strengths": [<string>, ...],
"weaknesses": [<string>, ...],
"suggested_openers": [<string>, ...]
}
Rules:
- compatibility_score: integer 0-100 (0-20 very low, 21-40 below average, 41-60 moderate, 61-80 good, 81-100 excellent)
- strengths: 2-5 specific positive compatibility factors referencing actual profile details
- weaknesses: 0-4 potential friction points (empty array if none). Be constructive, not judgmental
- suggested_openers: 2-3 personalized conversation starters referencing the other person's profile
- All array items must be strings
- Do not invent details not present in the profiles
- Return ONLY the JSON object`;
const CONFIG = {
model: 'gpt-4o',
temperature: 0,
maxTokens: 1024,
maxRetries: 3,
};
// ─────────────────────────────────────────────────
// Validation
// ─────────────────────────────────────────────────
function validateAnalysis(data) {
const errors = [];
// compatibility_score
if (typeof data.compatibility_score !== 'number') {
errors.push('compatibility_score must be a number');
} else {
if (!Number.isInteger(data.compatibility_score)) {
data.compatibility_score = Math.round(data.compatibility_score);
}
if (data.compatibility_score < 0 || data.compatibility_score > 100) {
data.compatibility_score = Math.max(0, Math.min(100, data.compatibility_score));
}
}
// strengths
if (!Array.isArray(data.strengths)) {
errors.push('strengths must be an array');
} else if (data.strengths.length < 2) {
errors.push('strengths must have at least 2 items');
} else if (!data.strengths.every(s => typeof s === 'string' && s.length > 0)) {
errors.push('All strengths must be non-empty strings');
}
// weaknesses
if (!Array.isArray(data.weaknesses)) {
errors.push('weaknesses must be an array');
} else if (!data.weaknesses.every(s => typeof s === 'string')) {
errors.push('All weaknesses must be strings');
}
// suggested_openers
if (!Array.isArray(data.suggested_openers)) {
errors.push('suggested_openers must be an array');
} else if (data.suggested_openers.length < 2) {
errors.push('suggested_openers must have at least 2 items');
} else if (!data.suggested_openers.every(s => typeof s === 'string' && s.length > 0)) {
errors.push('All suggested_openers must be non-empty strings');
}
return { valid: errors.length === 0, errors, data };
}
function cleanAnalysis(data) {
return {
compatibility_score: Math.round(
Math.max(0, Math.min(100, data.compatibility_score))
),
strengths: data.strengths
.filter(s => typeof s === 'string' && s.length > 0)
.slice(0, 5),
weaknesses: data.weaknesses
.filter(s => typeof s === 'string' && s.length > 0)
.slice(0, 4),
suggested_openers: data.suggested_openers
.filter(s => typeof s === 'string' && s.length > 0)
.slice(0, 3),
};
}
// ─────────────────────────────────────────────────
// Core Analysis Function
// ─────────────────────────────────────────────────
async function analyzeCompatibility(profileA, profileB) {
let messages = [
{ role: 'system', content: SYSTEM_PROMPT },
{
role: 'user',
content: `Analyze the compatibility between these two profiles:\n\nProfile A:\n${profileA}\n\nProfile B:\n${profileB}`
}
];
for (let attempt = 1; attempt <= CONFIG.maxRetries; attempt++) {
try {
const response = await openai.chat.completions.create({
model: CONFIG.model,
temperature: CONFIG.temperature,
max_tokens: CONFIG.maxTokens,
response_format: { type: 'json_object' },
messages,
});
// Check for truncation
if (response.choices[0].finish_reason === 'length') {
throw new Error('Response truncated');
}
// Parse
const data = JSON.parse(response.choices[0].message.content);
// Validate
const validation = validateAnalysis(data);
if (!validation.valid) {
throw new Error(`Validation: ${validation.errors.join('; ')}`);
}
// Clean and return
return {
success: true,
data: cleanAnalysis(validation.data),
metadata: {
attempt,
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
totalTokens: response.usage.total_tokens,
}
};
} catch (error) {
console.warn(`[Attempt ${attempt}/${CONFIG.maxRetries}] ${error.message}`);
if (attempt === CONFIG.maxRetries) {
return {
success: false,
error: error.message,
data: getDefaultAnalysis(),
};
}
// Add error context for retry
messages.push({
role: 'user',
content: `Error: ${error.message}. Please return the corrected JSON.`
});
}
}
}
function getDefaultAnalysis() {
return {
compatibility_score: 50,
strengths: ['Unable to fully analyze — please try again'],
weaknesses: [],
suggested_openers: ['Hi! I noticed we might have some things in common.', 'Hey there! What are you most passionate about?'],
};
}
// ─────────────────────────────────────────────────
// Usage Example
// ─────────────────────────────────────────────────
const profileA = `Name: Maya, 27
Location: San Francisco
Bio: Software engineer who lives for the weekends. You'll find me on a hiking trail, at a rock climbing gym, or trying to perfect my sourdough recipe. Big fan of indie films, spicy food, and philosophical debates over coffee. Looking for someone who can keep up on the trail and in conversation.
Interests: Hiking, Rock Climbing, Cooking, Indie Films, Philosophy, Coffee`;
const profileB = `Name: Alex, 29
Location: Oakland
Bio: Product designer by day, amateur chef by night. I spend my free time exploring Bay Area trails, experimenting with fusion recipes, and binge-watching sci-fi shows. Morning person who's usually at the farmers market by 8am. Would love to find someone to share a meal and a sunset hike with.
Interests: Trail Running, Cooking, Farmers Markets, Sci-Fi, Design, Hiking`;
const result = await analyzeCompatibility(profileA, profileB);
if (result.success) {
console.log('Compatibility Score:', result.data.compatibility_score);
console.log('Strengths:');
result.data.strengths.forEach(s => console.log(` - ${s}`));
console.log('Weaknesses:');
result.data.weaknesses.forEach(w => console.log(` - ${w}`));
console.log('Suggested Openers:');
result.data.suggested_openers.forEach(o => console.log(` - ${o}`));
console.log(`\n[Tokens: ${result.metadata.totalTokens}, Attempt: ${result.metadata.attempt}]`);
} else {
console.error('Analysis failed:', result.error);
console.log('Using default analysis:', result.data);
}
Expected output
Compatibility Score: 85
Strengths:
- Both are outdoor enthusiasts — Maya hikes and climbs, Alex trail runs and hikes
- Strong shared passion for cooking — Maya does sourdough, Alex does fusion cuisine
- Both are in the Bay Area with compatible locations (SF and Oakland)
- Similar lifestyle values — both enjoy active weekends and exploring local food scenes
Weaknesses:
- Maya is likely a night owl (philosophical debates over coffee) while Alex is a self-described morning person
- Different entertainment preferences — Maya loves indie films while Alex prefers sci-fi
Suggested Openers:
- "I saw you're into fusion cooking — have you ever tried making a sourdough-based fusion dish? I'd love to swap recipes!"
- "Fellow Bay Area hiker! What's your favorite trail? I've been exploring a lot around Marin lately."
- "A product designer who cooks — that's a creative combo! Do you bring any design thinking into your recipes?"
[Tokens: 847, Attempt: 1]
8. Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│ COMPATIBILITY ANALYSIS PIPELINE │
│ │
│ Input: Profile A + Profile B (strings) │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Build Messages │ System prompt (schema + rules) │
│ │ │ User message (both profiles) │
│ └──────────┬───────────┘ │
│ │ │
│ ┌────────▼────────────────────────────────────────────┐ │
│ │ RETRY LOOP (max 3 attempts) │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ API Call │ GPT-4o + JSON mode + temp 0 │ │
│ │ └──────┬──────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Parse JSON │ JSON.parse() with error handling │ │
│ │ └──────┬──────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Validate │ Schema + types + ranges │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ┌────┴─────┐ │ │
│ │ ▼ ▼ │ │
│ │ Valid? Invalid? │ │
│ │ │ │ │ │
│ │ │ Add error feedback │ │
│ │ │ to messages │ │
│ │ │ │ │ │
│ │ │ ◄────┘ (retry) │ │
│ │ ▼ │ │
│ └────┤ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Clean & Normalize │ Round score, trim arrays, strip extras │
│ └──────────┬───────────┘ │
│ ▼ │
│ Output: { compatibility_score, strengths, weaknesses, suggested_openers}│
│ + Metadata: { attempt, tokens, success } │
└─────────────────────────────────────────────────────────────────────────┘
9. Extending the Pattern
Adding more analysis fields
The same pattern extends to richer analysis. Update the schema, system prompt, and validation:
// Extended schema
const EXTENDED_SYSTEM_PROMPT = `...
{
"compatibility_score": <integer 0-100>,
"strengths": [<string>, ...],
"weaknesses": [<string>, ...],
"suggested_openers": [<string>, ...],
"shared_interests": [<string>, ...],
"personality_match": {
"summary": <string>,
"energy_level": "similar" | "complementary" | "mismatched",
"communication_style": "similar" | "complementary" | "mismatched"
},
"date_ideas": [<string>, <string>, <string>]
}
...`;
Using with Anthropic (Claude)
async function analyzeCompatibilityClaude(profileA, profileB) {
const anthropic = new Anthropic();
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
temperature: 0,
system: SYSTEM_PROMPT,
messages: [
{
role: 'user',
content: `Analyze the compatibility between these two profiles:\n\nProfile A:\n${profileA}\n\nProfile B:\n${profileB}`
},
{
role: 'assistant',
content: '{' // Prefill to force JSON
}
],
});
const jsonString = '{' + response.content[0].text;
const data = JSON.parse(jsonString);
const validation = validateAnalysis(data);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join('; ')}`);
}
return cleanAnalysis(validation.data);
}
Using with function calling (alternative approach)
async function analyzeCompatibilityWithTools(profileA, profileB) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
messages: [
{
role: 'system',
content: 'You are a dating app compatibility analyzer. Use the provided tool to return your analysis.'
},
{
role: 'user',
content: `Analyze compatibility:\n\nProfile A:\n${profileA}\n\nProfile B:\n${profileB}`
}
],
tools: [{
type: 'function',
function: {
name: 'submit_compatibility_analysis',
description: 'Submit the compatibility analysis results',
parameters: {
type: 'object',
properties: {
compatibility_score: {
type: 'integer',
description: 'Compatibility score 0-100',
minimum: 0,
maximum: 100
},
strengths: {
type: 'array',
items: { type: 'string' },
description: '2-5 positive compatibility factors',
minItems: 2,
maxItems: 5
},
weaknesses: {
type: 'array',
items: { type: 'string' },
description: '0-4 potential friction points',
maxItems: 4
},
suggested_openers: {
type: 'array',
items: { type: 'string' },
description: '2-3 personalized conversation starters',
minItems: 2,
maxItems: 3
}
},
required: ['compatibility_score', 'strengths', 'weaknesses', 'suggested_openers'],
additionalProperties: false
}
}
}],
tool_choice: { type: 'function', function: { name: 'submit_compatibility_analysis' } }
});
const args = JSON.parse(response.choices[0].message.tool_calls[0].function.arguments);
return cleanAnalysis(args);
}
10. Testing the Module
// Test 1: Normal case — two compatible profiles
async function testNormalCase() {
const result = await analyzeCompatibility(profileA, profileB);
console.assert(result.success === true, 'Should succeed');
console.assert(result.data.compatibility_score >= 0 && result.data.compatibility_score <= 100, 'Score in range');
console.assert(result.data.strengths.length >= 2, 'Has strengths');
console.assert(result.data.suggested_openers.length >= 2, 'Has openers');
console.log('Test 1 passed: Normal case');
}
// Test 2: Very different profiles
async function testLowCompatibility() {
const introvert = 'Name: Sam. Bio: Homebody who loves reading, quiet evenings, and cats. Introvert to the core.';
const extrovert = 'Name: Jordan. Bio: Life of the party! Clubs, concerts, festivals every weekend. 500+ friends.';
const result = await analyzeCompatibility(introvert, extrovert);
console.assert(result.success === true, 'Should succeed');
console.assert(result.data.compatibility_score < 60, 'Should have lower score');
console.assert(result.data.weaknesses.length > 0, 'Should identify weaknesses');
console.log('Test 2 passed: Low compatibility');
}
// Test 3: Minimal profiles (edge case)
async function testMinimalProfiles() {
const result = await analyzeCompatibility('Name: X. Age: 25.', 'Name: Y. Age: 26.');
console.assert(result.success === true, 'Should succeed even with minimal data');
console.log('Test 3 passed: Minimal profiles');
}
// Test 4: Verify validation catches bad data
function testValidation() {
const badData = { compatibility_score: 'high', strengths: 'good match', weaknesses: [], suggested_openers: [] };
const result = validateAnalysis(badData);
console.assert(result.valid === false, 'Should fail validation');
console.assert(result.errors.length > 0, 'Should have errors');
console.log('Test 4 passed: Validation catches bad data');
}
// Run all tests
await testNormalCase();
await testLowCompatibility();
await testMinimalProfiles();
testValidation();
11. Cost Analysis
System prompt: ~350 tokens (fixed per call)
User message: ~200 tokens (varies with profile length)
Model output: ~200-400 tokens (the JSON response)
Total per call: ~750-950 tokens
GPT-4o pricing (example):
Input: $2.50 per 1M tokens → ~550 tokens × $2.50/1M = $0.001375
Output: $10.00 per 1M tokens → ~300 tokens × $10.00/1M = $0.003
Total per analysis: ~$0.004 (less than half a cent)
10,000 analyses/day: ~$40/day
With 10% retry rate (1.1x): ~$44/day
If using retries:
Each retry adds another API call (~$0.004)
Average cost = $0.004 × (1 + 0.1 × 1 + 0.01 × 2) ≈ $0.0045
12. Key Takeaways
- The system prompt is the foundation — invest time in clear role definition, exact schema, field rules, and edge case handling.
- JSON mode + schema prompt + validation is the trifecta for reliable structured output.
- Always validate and clean — round scores, trim arrays, strip unexpected fields, handle missing data.
- Build retry logic that feeds validation errors back to the model — success rate goes from ~90% to ~99%+ across 3 attempts.
- Default/fallback responses ensure your application never crashes even when AI fails completely.
- Temperature 0 is essential for consistency — the same profiles should get similar scores.
- The same pattern (system prompt + JSON mode + validate + retry + clean) works for any structured AI output, not just compatibility scoring.
- Test with edge cases — minimal data, very different profiles, long profiles, missing fields.
Explain-It Challenge
- Walk through what happens step-by-step when the model returns
{ "score": 85, "pros": [...] }instead of the expected keys. How does the pipeline handle it? - Why do we use
temperature: 0for compatibility scoring? When might you intentionally use a higher temperature for this feature? - A product manager asks: "Can we guarantee the compatibility score is always consistent for the same two profiles?" What's your honest answer, and what do you do to get as close as possible?
Navigation: ← 4.5.d — Validating Returned Structure · 4.5 Overview