Building AI Applications
Architecture patterns, APIs, and integrating AI into products
From Experiments to Production
Building AI-powered applications is different from traditional software development. You're integrating systems that are probabilistic (not deterministic), working with API rate limits and costs, handling longer latencies, and managing models that can fail in unexpected ways. This lesson shows you how to architect, build, and deploy AI applications that are reliable, scalable, and maintainable in production.
AI Application Architecture Patterns
AI applications typically follow one of several architectural patterns. Choose based on your requirements for latency, cost, control, and complexity:
1. Direct API Integration (Simplest)
Your application calls AI APIs (OpenAI, Anthropic, etc.) directly for each request.
┌──────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │─────>│ Your Backend │─────>│ AI API │
│ (User) │<─────│ (Flask/ │<─────│ (OpenAI/ │
└──────────┘ │ FastAPI) │ │ Anthropic) │
└──────────────┘ └─────────────┘
Flow:
1. User sends request to your API
2. Your backend calls AI API
3. AI API returns response
4. Your backend processes and returns to user
Pros:
✓ Simple to implement
✓ No infrastructure to manage
✓ Always latest model versions
✓ Minimal code
Cons:
✗ Dependent on external API availability
✗ Latency (network + model inference)
✗ Cost per request
✗ Limited customizationBest for:
- Prototypes and MVPs
- Low to medium volume applications
- When you need the most capable models
- When development speed matters more than cost
2. Cached API Integration (Optimized)
Add caching layer to reduce API calls for similar requests.
┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌─────────┐
│ Client │─────>│ Your Backend │─────>│ Cache │─────>│ AI API │
└──────────┘ └──────────────┘ │ (Redis) │ └─────────┘
└─────────┘
Flow:
1. Check cache for similar prompt (semantic similarity)
2. If cache hit: return cached response (fast, free)
3. If cache miss: call AI API, cache result
4. Return response to user
Example:
- "What is Python?" and "What's Python?" → same cache entry
- Saves ~50-80% of API costs for repeated queriesBest for:
- Applications with repeated similar queries
- Cost optimization (reduce API calls by 50-80%)
- Improving response times
- Customer support chatbots, FAQ systems
3. Self-Hosted Models (Full Control)
Run open-source models (Llama, Mistral, etc.) on your own infrastructure.
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ Client │─────>│ Your Backend │─────>│ Your GPU Server │
└──────────┘ └──────────────┘ │ (Llama 3, etc.) │
└──────────────────┘
Pros:
✓ No per-request costs (only infrastructure)
✓ Full data privacy
✓ Low latency (no network to external API)
✓ Can fine-tune models for your domain
✓ No rate limits
Cons:
✗ Complex infrastructure (GPUs, orchestration)
✗ Model quality usually lower than commercial APIs
✗ You manage updates, scaling, monitoring
✗ High upfront and maintenance costsBest for:
- High-volume applications (where API costs exceed infrastructure costs)
- Sensitive data that can't leave your infrastructure
- When you need custom fine-tuned models
- When you have GPU infrastructure expertise
4. Hybrid Architecture (Best of Both)
Use different models for different tasks based on requirements.
Strategy: - Simple tasks → Fast, cheap local model (classification, extraction) - Complex tasks → Powerful API model (reasoning, generation) Example routing: ┌────────────────────────┐ │ Is this spam? │ → Local model (fast, cheap) ├────────────────────────┤ │ Classify sentiment │ → Local model (good enough) ├────────────────────────┤ │ Write marketing copy │ → API model (quality matters) ├────────────────────────┤ │ Complex code review │ → API model (needs reasoning) └────────────────────────┘ Result: Optimize cost, speed, and quality per use case
Best for:
- Large-scale applications with varied AI needs
- When you need to optimize cost/quality trade-offs
- Applications with both simple and complex AI tasks
Working with AI APIs
Let's build a production-ready AI integration with proper error handling, retries, and cost management.
# Complete AI API Integration with Best Practices
import anthropic
from typing import Optional, Dict, Any
import time
import logging
from functools import wraps
from datetime import datetime
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class AIClient:
"""Production-ready AI API client with error handling and monitoring."""
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
self.request_count = 0
self.total_tokens = 0
self.total_cost = 0.0
def _calculate_cost(self, input_tokens: int, output_tokens: int) -> float:
"""Calculate API cost based on token usage."""
# Claude Sonnet pricing (example, check current pricing)
INPUT_COST_PER_1M = 3.00 # $3 per 1M input tokens
OUTPUT_COST_PER_1M = 15.00 # $15 per 1M output tokens
input_cost = (input_tokens / 1_000_000) * INPUT_COST_PER_1M
output_cost = (output_tokens / 1_000_000) * OUTPUT_COST_PER_1M
return input_cost + output_cost
@staticmethod
def _retry_with_backoff(max_retries: int = 3, base_delay: float = 1.0):
"""Decorator for exponential backoff retry logic."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except anthropic.RateLimitError as e:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
logger.warning(f"Rate limited. Retry {attempt + 1}/{max_retries} after {delay}s")
time.sleep(delay)
except anthropic.APIError as e:
logger.error(f"API error: {e}")
if attempt == max_retries - 1:
raise
time.sleep(base_delay)
return wrapper
return decorator
@_retry_with_backoff(max_retries=3)
def generate(
self,
prompt: str,
system_prompt: Optional[str] = None,
max_tokens: int = 1000,
temperature: float = 0.7,
timeout: int = 30
) -> Dict[str, Any]:
"""
Generate response from AI with full error handling and monitoring.
Args:
prompt: User prompt
system_prompt: System instructions
max_tokens: Max response length
temperature: Creativity (0-1)
timeout: Request timeout in seconds
Returns:
Dict with response, tokens, cost, and metadata
"""
start_time = time.time()
try:
# Build messages
messages = [{"role": "user", "content": prompt}]
# Make API call
response = self.client.messages.create(
model=self.model,
max_tokens=max_tokens,
temperature=temperature,
system=system_prompt if system_prompt else anthropic.NOT_GIVEN,
messages=messages,
timeout=timeout
)
# Extract metrics
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
cost = self._calculate_cost(input_tokens, output_tokens)
# Update counters
self.request_count += 1
self.total_tokens += input_tokens + output_tokens
self.total_cost += cost
# Calculate latency
latency = time.time() - start_time
# Log metrics
logger.info(
f"AI Request #{self.request_count}: "
f"tokens={input_tokens + output_tokens}, "
f"cost={cost:.4f}, "
f"latency={latency:.2f}s"
)
return {
"response": response.content[0].text,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"cost": cost,
"latency": latency,
"model": self.model,
"timestamp": datetime.now().isoformat()
}
except anthropic.APITimeoutError:
logger.error(f"Request timeout after {timeout}s")
raise
except anthropic.AuthenticationError:
logger.error("Invalid API key")
raise
except anthropic.BadRequestError as e:
logger.error(f"Bad request: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise
def get_stats(self) -> Dict[str, Any]:
"""Get usage statistics."""
return {
"total_requests": self.request_count,
"total_tokens": self.total_tokens,
"total_cost": round(self.total_cost, 4),
"avg_cost_per_request": round(self.total_cost / max(self.request_count, 1), 4)
}
# Example usage
if __name__ == "__main__":
import os
# Initialize client
client = AIClient(api_key=os.getenv("ANTHROPIC_API_KEY"))
# Example 1: Simple generation
result = client.generate(
prompt="Explain machine learning in one sentence.",
temperature=0.3
)
print(f"Response: {result['response']}")
print(f"Cost: {result['cost']:.4f}")
print(f"Tokens: {result['total_tokens']}")
# Example 2: With system prompt
result = client.generate(
prompt="Review this code for security issues: [code here]",
system_prompt="You are a security expert. Be thorough and specific.",
max_tokens=1500
)
# Check statistics
stats = client.get_stats()
print(f"\nTotal API usage:")
print(f" Requests: {stats['total_requests']}")
print(f" Cost: {stats['total_cost']}")
print(f" Avg cost/request: {stats['avg_cost_per_request']}")- Automatic retry with exponential backoff for rate limits
- Comprehensive error handling for all API error types
- Cost tracking per request and total
- Latency monitoring
- Configurable timeouts
- Detailed logging for debugging and monitoring
Adding a Caching Layer
Caching can reduce your AI API costs by 50-80% for applications with repeated or similar queries.
import redis
import hashlib
import json
from typing import Optional, Dict, Any
class CachedAIClient(AIClient):
"""AI client with Redis caching to reduce API calls and costs."""
def __init__(
self,
api_key: str,
redis_url: str = "redis://localhost:6379",
cache_ttl: int = 3600, # 1 hour
model: str = "claude-sonnet-4-20250514"
):
super().__init__(api_key, model)
self.redis_client = redis.from_url(redis_url)
self.cache_ttl = cache_ttl
self.cache_hits = 0
self.cache_misses = 0
def _generate_cache_key(
self,
prompt: str,
system_prompt: Optional[str],
temperature: float,
max_tokens: int
) -> str:
"""Generate cache key from request parameters."""
# Combine all parameters that affect the response
cache_input = f"{prompt}|{system_prompt}|{temperature}|{max_tokens}|{self.model}"
return f"ai_cache:{hashlib.sha256(cache_input.encode()).hexdigest()}"
def generate(
self,
prompt: str,
system_prompt: Optional[str] = None,
max_tokens: int = 1000,
temperature: float = 0.7,
use_cache: bool = True,
timeout: int = 30
) -> Dict[str, Any]:
"""
Generate with caching support.
Args:
use_cache: If True, check cache before calling API
(other args same as parent class)
"""
# Generate cache key
cache_key = self._generate_cache_key(prompt, system_prompt, temperature, max_tokens)
# Check cache if enabled
if use_cache:
try:
cached_result = self.redis_client.get(cache_key)
if cached_result:
self.cache_hits += 1
result = json.loads(cached_result)
result['from_cache'] = True
logger.info(f"Cache HIT. Saved {result['cost']:.4f}")
return result
except redis.RedisError as e:
logger.warning(f"Cache check failed: {e}. Falling back to API.")
# Cache miss - call API
self.cache_misses += 1
result = super().generate(
prompt=prompt,
system_prompt=system_prompt,
max_tokens=max_tokens,
temperature=temperature,
timeout=timeout
)
# Store in cache
if use_cache:
try:
result_copy = result.copy()
result_copy['from_cache'] = False
self.redis_client.setex(
cache_key,
self.cache_ttl,
json.dumps(result_copy)
)
except redis.RedisError as e:
logger.warning(f"Failed to cache result: {e}")
result['from_cache'] = False
return result
def get_stats(self) -> Dict[str, Any]:
"""Get statistics including cache performance."""
stats = super().get_stats()
total_requests = self.cache_hits + self.cache_misses
cache_hit_rate = (self.cache_hits / max(total_requests, 1)) * 100
# Estimate savings
avg_cost = stats['avg_cost_per_request']
estimated_savings = self.cache_hits * avg_cost
stats.update({
'cache_hits': self.cache_hits,
'cache_misses': self.cache_misses,
'cache_hit_rate': f"{cache_hit_rate:.1f}%",
'estimated_savings': f"{estimated_savings:.4f}"
})
return stats
def clear_cache(self, pattern: str = "ai_cache:*"):
"""Clear cached responses matching pattern."""
keys = self.redis_client.keys(pattern)
if keys:
self.redis_client.delete(*keys)
logger.info(f"Cleared {len(keys)} cache entries")
# Example usage
if __name__ == "__main__":
import os
# Initialize cached client
client = CachedAIClient(
api_key=os.getenv("ANTHROPIC_API_KEY"),
cache_ttl=3600 # Cache for 1 hour
)
# First request - cache miss, calls API
result1 = client.generate("What is Python?")
print(f"First request - Cost: {result1['cost']:.4f}, Cached: {result1['from_cache']}")
# Second identical request - cache hit, no API call
result2 = client.generate("What is Python?")
print(f"Second request - Cost: {result2['cost']:.4f}, Cached: {result2['from_cache']}")
# Check savings
stats = client.get_stats()
print(f"\nCache Statistics:")
print(f" Hit rate: {stats['cache_hit_rate']}")
print(f" Savings: {stats['estimated_savings']}")
print(f" Total cost: {stats['total_cost']}")- Set appropriate TTL based on how often data changes
- For FAQ/support: long TTL (hours to days)
- For dynamic content: short TTL (minutes)
- Implement cache versioning for model updates
- Monitor cache hit rates - low rates suggest cache isn't helping
Complete Example: AI-Powered Code Review Service
Let's build a production-ready REST API that reviews code for security issues, bugs, and best practices.
Architecture Overview
Components: 1. FastAPI REST API (Python web framework) 2. CachedAIClient (our AI client with Redis caching) 3. Background task queue (for async processing) 4. Rate limiting (protect from abuse) 5. Authentication (API keys) 6. Monitoring and logging
# app.py - Complete AI-powered code review service
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, Header
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, List
import os
from datetime import datetime
import uuid
# Import our cached AI client
from ai_client import CachedAIClient
# Initialize FastAPI app
app = FastAPI(
title="AI Code Review Service",
description="AI-powered code review for security and best practices",
version="1.0.0"
)
# Initialize AI client (singleton)
ai_client = CachedAIClient(
api_key=os.getenv("ANTHROPIC_API_KEY"),
redis_url=os.getenv("REDIS_URL", "redis://localhost:6379"),
cache_ttl=3600
)
# Request/Response models
class CodeReviewRequest(BaseModel):
"""Request body for code review."""
code: str = Field(..., description="Code to review", min_length=1)
language: str = Field("python", description="Programming language")
focus: str = Field(
"all",
description="Review focus: security, performance, quality, or all"
)
model_config = ConfigDict(json_schema_extra={
"examples": [{
"code": "def login(username, password):\n query = f\"SELECT * FROM users WHERE name='{username}'\"",
"language": "python",
"focus": "security"
}]
})
class Finding(BaseModel):
"""Individual review finding."""
severity: str # critical, high, medium, low
category: str # security, performance, quality
line: Optional[int]
issue: str
recommendation: str
class CodeReviewResponse(BaseModel):
"""Response from code review."""
review_id: str
summary: str
score: int # 1-10
findings: List[Finding]
language: str
from_cache: bool
processing_time: float
cost: float
# Authentication
async def verify_api_key(x_api_key: str = Header(...)):
"""Verify API key from header."""
valid_keys = os.getenv("API_KEYS", "").split(",")
if x_api_key not in valid_keys:
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
# Build review prompt
def build_review_prompt(code: str, language: str, focus: str) -> tuple[str, str]:
"""Build system and user prompts for code review."""
system_prompt = f"""You are an expert {language} code security and quality reviewer.
Provide thorough, actionable feedback. Always cite specific line numbers.
Be direct about issues but constructive in recommendations."""
focus_instructions = {
"security": """Focus on security vulnerabilities:
- Injection attacks (SQL, command, XSS)
- Authentication/authorization issues
- Data exposure risks
- Input validation problems
- Cryptographic weaknesses""",
"performance": """Focus on performance issues:
- Time complexity problems
- Memory usage issues
- Inefficient algorithms
- Unnecessary operations
- Scalability concerns""",
"quality": """Focus on code quality:
- Readability and maintainability
- Error handling
- Code organization
- Best practices
- Documentation""",
"all": """Perform comprehensive review covering:
- Security vulnerabilities
- Performance issues
- Code quality and readability
- Best practices
- Error handling"""
}
user_prompt = f"""{focus_instructions.get(focus, focus_instructions['all'])}
Return a JSON object with this structure:
{{
"summary": "Brief overall assessment",
"score": <1-10 score>,
"findings": [
{{
"severity": "critical|high|medium|low",
"category": "security|performance|quality",
"line": <line number or null>,
"issue": "Clear description of the issue",
"recommendation": "Specific fix or improvement"
}}
]
}}
Code to review:
```{language}
{code}
```
Return only valid JSON, no additional text."""
return system_prompt, user_prompt
# Main endpoint
@app.post("/review", response_model=CodeReviewResponse, dependencies=[Depends(verify_api_key)])
async def review_code(request: CodeReviewRequest):
"""
Review code for security issues, bugs, and best practices.
Returns detailed findings with severity ratings and recommendations.
"""
import time
import json
start_time = time.time()
review_id = str(uuid.uuid4())
try:
# Build prompts
system_prompt, user_prompt = build_review_prompt(
request.code,
request.language,
request.focus
)
# Call AI (with caching)
result = ai_client.generate(
prompt=user_prompt,
system_prompt=system_prompt,
temperature=0.3, # Low temp for consistent analysis
max_tokens=2000
)
# Parse JSON response
try:
# Extract JSON from response
response_text = result['response']
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_text = response_text[start_idx:end_idx]
review_data = json.loads(json_text)
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=500,
detail=f"Failed to parse AI response: {str(e)}"
)
# Build response
processing_time = time.time() - start_time
return CodeReviewResponse(
review_id=review_id,
summary=review_data.get('summary', 'No summary provided'),
score=review_data.get('score', 5),
findings=[Finding(**f) for f in review_data.get('findings', [])],
language=request.language,
from_cache=result['from_cache'],
processing_time=round(processing_time, 2),
cost=result['cost']
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Review failed: {str(e)}")
# Health check endpoint
@app.get("/health")
async def health_check():
"""Check service health and AI API connectivity."""
try:
# Test AI API with minimal request
result = ai_client.generate(
prompt="Say 'OK'",
max_tokens=10,
temperature=0
)
return {
"status": "healthy",
"ai_api": "connected",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
return {
"status": "unhealthy",
"ai_api": "disconnected",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
# Statistics endpoint
@app.get("/stats", dependencies=[Depends(verify_api_key)])
async def get_stats():
"""Get API usage statistics."""
return ai_client.get_stats()
# Clear cache endpoint (admin only)
@app.delete("/cache", dependencies=[Depends(verify_api_key)])
async def clear_cache():
"""Clear the response cache."""
ai_client.clear_cache()
return {"message": "Cache cleared successfully"}
# Run with: uvicorn app:app --reload
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)- ✓ Authentication with API keys
- ✓ Comprehensive error handling
- ✓ Request/response validation with Pydantic
- ✓ Redis caching to reduce costs
- ✓ Health check endpoint for monitoring
- ✓ Usage statistics endpoint
- ✓ Structured JSON responses
- ✓ Proper HTTP status codes
- ✓ API documentation (FastAPI auto-generates)
Testing the API:
# Install dependencies
pip install fastapi uvicorn redis anthropic
# Set environment variables
export ANTHROPIC_API_KEY="your-key-here"
export REDIS_URL="redis://localhost:6379"
export API_KEYS="test-key-123,prod-key-456"
# Run the server
uvicorn app:app --reload
# Test with curl
curl -X POST "http://localhost:8000/review" \
-H "Content-Type: application/json" \
-H "X-API-Key: test-key-123" \
-d '{
"code": "def get_user(id): return db.execute(f\"SELECT * FROM users WHERE id={id}\").fetchone()",
"language": "python",
"focus": "security"
}'
# Response:
{
"review_id": "a1b2c3d4-...",
"summary": "Critical SQL injection vulnerability detected",
"score": 2,
"findings": [
{
"severity": "critical",
"category": "security",
"line": 1,
"issue": "SQL injection: user input directly in query",
"recommendation": "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id=?', (id,))"
}
],
"language": "python",
"from_cache": false,
"processing_time": 1.23,
"cost": 0.0045
}
# Second identical request uses cache (from_cache: true, cost: 0.0)Best Practices for Production AI Applications
1. Always Have Fallbacks
AI APIs can fail. Have graceful degradation: fallback to cached responses, simpler models, or default behavior. Never let an AI API failure break your entire application.
2. Implement Rate Limiting
Protect your application from abuse and runaway costs. Limit requests per user, per API key, and globally. Set budget alerts on your AI API provider.
3. Monitor Everything
Track: costs per request, latency, error rates, cache hit rates, token usage, and user satisfaction. Set up alerts for anomalies (sudden cost spikes, high error rates).
4. Validate AI Outputs
Never trust AI output blindly. Validate JSON structure, check for required fields, sanitize text output, and have fallbacks for malformed responses. AI can generate invalid or unexpected outputs.
5. Optimize for Cost
Use caching aggressively. Choose appropriate models (don't use GPT-4 for simple tasks). Limit max_tokens. Use streaming for long responses. Monitor and optimize prompts to reduce token usage.
6. Handle Sensitive Data Carefully
Don't send sensitive data (passwords, credit cards, PII) to AI APIs unless absolutely necessary. If you must, use providers with zero data retention policies and enterprise agreements. Consider anonymizing data first.
7. Test AI Responses Thoroughly
Build test suites with diverse inputs: normal cases, edge cases, adversarial inputs, malformed data. AI models can behave unpredictably. Regression test after model updates.
Handling Non-Deterministic AI Systems
Unlike traditional software, AI systems are non-deterministic, the same input can produce different outputs. This creates unique challenges for reliability, testing, and idempotency. Here are proven techniques to make non-deterministic systems behave predictably.
1. Enforce Output Schemas with Validation
Never trust that AI will return valid JSON or include all required fields. Always validate and retry on schema violations.
from pydantic import BaseModel, Field, ValidationError
from typing import List, Optional
import json
class ArticleOutput(BaseModel):
"""Strict schema for AI-generated article."""
title: str = Field(..., min_length=10, max_length=200)
author: str = Field(..., min_length=2)
summary: str = Field(..., min_length=50)
content: str = Field(..., min_length=500)
tags: List[str] = Field(..., min_items=3, max_items=10)
category: str = Field(..., pattern="^(tech|business|science)$")
# Template requirements
has_introduction: bool = Field(default=False)
has_conclusion: bool = Field(default=False)
word_count: int = Field(..., ge=500)
def generate_article_with_validation(
ai_client,
topic: str,
max_retries: int = 3
) -> ArticleOutput:
"""
Generate article with automatic validation and retry.
Returns validated article or raises after max retries.
"""
system_prompt = """You are a professional article writer.
CRITICAL: You must return ONLY valid JSON with this exact structure:
{
"title": "string (10-200 chars)",
"author": "string (2+ chars)",
"summary": "string (50+ chars)",
"content": "string (500+ chars)",
"tags": ["string", "string", "string"], // 3-10 tags
"category": "tech|business|science",
"has_introduction": true,
"has_conclusion": true,
"word_count": 500
}
The article content MUST have:
- An introduction section (start with "Introduction:")
- A conclusion section (end with "Conclusion:")
- At least 500 words"""
user_prompt = f"""Write a complete article about: {topic}
Return only the JSON object, no additional text."""
for attempt in range(max_retries):
try:
# Call AI
result = ai_client.generate(
prompt=user_prompt,
system_prompt=system_prompt,
temperature=0.7,
max_tokens=2000
)
# Extract JSON (AI might wrap it in markdown or text)
response_text = result['response']
# Try to find JSON in response
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
if start_idx == -1 or end_idx == 0:
raise ValueError("No JSON found in response")
json_text = response_text[start_idx:end_idx]
data = json.loads(json_text)
# Validate structure with Pydantic
article = ArticleOutput(**data)
# Additional content validation
if "Introduction:" not in article.content:
raise ValueError("Missing introduction section in content")
if "Conclusion:" not in article.content:
raise ValueError("Missing conclusion section in content")
# Success!
logger.info(f"Valid article generated on attempt {attempt + 1}")
return article
except (json.JSONDecodeError, ValidationError, ValueError) as e:
logger.warning(
f"Attempt {attempt + 1}/{max_retries} failed: {type(e).__name__}: {str(e)}"
)
if attempt == max_retries - 1:
# Final attempt failed
raise ValueError(
f"Failed to generate valid article after {max_retries} attempts. "
f"Last error: {str(e)}"
)
# Add feedback to next prompt for better results
user_prompt += f"\n\nPREVIOUS ATTEMPT FAILED: {str(e)}. Please fix and try again."
# Should never reach here due to raise above
raise RuntimeError("Unexpected end of retry loop")
# Example usage
if __name__ == "__main__":
client = AIClient(api_key=os.getenv("ANTHROPIC_API_KEY"))
try:
article = generate_article_with_validation(
ai_client=client,
topic="The Impact of AI on Software Engineering",
max_retries=3
)
print(f"✓ Valid article generated!")
print(f" Title: {article.title}")
print(f" Category: {article.category}")
print(f" Tags: {', '.join(article.tags)}")
print(f" Word count: {article.word_count}")
print(f" Has intro: {article.has_introduction}")
print(f" Has conclusion: {article.has_conclusion}")
except ValueError as e:
print(f"✗ Failed to generate valid article: {e}")- Pydantic validates types, ranges, patterns automatically
- Retry loop gives AI multiple chances to get it right
- Error feedback helps AI fix mistakes in next attempt
- Explicit template checks ensure all required sections present
2. Ensure Idempotency with Request IDs & Caching
When your service depends on AI, identical requests should return identical results. This is critical for reliability and avoiding duplicate charges.
import hashlib
from datetime import datetime, timedelta
from typing import Optional
class IdempotentAIService:
"""
AI service that guarantees idempotency using request IDs and result caching.
Ensures:
1. Same request_id always returns same result
2. Duplicate requests don't call AI API again
3. Results are stable for configured TTL
"""
def __init__(self, ai_client, redis_client, result_ttl: int = 86400):
self.ai_client = ai_client
self.redis = redis_client
self.result_ttl = result_ttl # 24 hours default
def _generate_request_id(
self,
operation: str,
user_id: str,
input_data: dict
) -> str:
"""
Generate deterministic request ID from inputs.
Same inputs → same request_id → same cached result
"""
# Canonicalize input (sorted JSON for consistency)
canonical_input = json.dumps(input_data, sort_keys=True)
# Hash to create stable ID
hash_input = f"{operation}:{user_id}:{canonical_input}"
request_id = hashlib.sha256(hash_input.encode()).hexdigest()
return request_id
def generate_summary(
self,
user_id: str,
document: str,
max_length: int = 200,
request_id: Optional[str] = None
) -> dict:
"""
Generate document summary with idempotency guarantee.
Args:
user_id: User making request
document: Document to summarize
max_length: Max summary length
request_id: Optional explicit request ID (for client retry)
Returns:
Dict with summary and metadata, guaranteed identical for same request_id
"""
# Generate or use provided request_id
if request_id is None:
request_id = self._generate_request_id(
operation="summarize",
user_id=user_id,
input_data={
"document_hash": hashlib.sha256(document.encode()).hexdigest(),
"max_length": max_length
}
)
# Check if we already processed this exact request
cache_key = f"idempotent_result:{request_id}"
cached_result = self.redis.get(cache_key)
if cached_result:
result = json.loads(cached_result)
result['from_cache'] = True
result['request_id'] = request_id
logger.info(f"Idempotent cache hit for request {request_id[:8]}")
return result
# Check if request is currently processing (prevent duplicate processing)
processing_key = f"processing:{request_id}"
if self.redis.exists(processing_key):
# Wait for ongoing processing to complete
for _ in range(30): # Wait up to 30 seconds
time.sleep(1)
cached_result = self.redis.get(cache_key)
if cached_result:
result = json.loads(cached_result)
result['from_cache'] = True
result['request_id'] = request_id
return result
raise TimeoutError(
f"Request {request_id[:8]} is still processing after 30s"
)
# Mark as processing
self.redis.setex(processing_key, 60, "processing") # 60s timeout
try:
# Call AI with temperature=0 for more deterministic output
ai_result = self.ai_client.generate(
prompt=f"Summarize this document in max {max_length} words:\n\n{document}",
system_prompt="You are a precise summarizer. Be concise and accurate.",
temperature=0.0, # More deterministic
max_tokens=max_length * 2 # Words to tokens rough estimate
)
# Build result
result = {
'request_id': request_id,
'summary': ai_result['response'],
'user_id': user_id,
'timestamp': datetime.now().isoformat(),
'cost': ai_result['cost'],
'from_cache': False
}
# Store result with TTL
self.redis.setex(
cache_key,
self.result_ttl,
json.dumps(result)
)
logger.info(
f"Processed new request {request_id[:8]}, "
f"cached for {self.result_ttl}s"
)
return result
finally:
# Always clear processing lock
self.redis.delete(processing_key)
# Example usage
if __name__ == "__main__":
import os
import redis
ai_client = AIClient(api_key=os.getenv("ANTHROPIC_API_KEY"))
redis_client = redis.from_url(os.getenv("REDIS_URL"))
service = IdempotentAIService(
ai_client=ai_client,
redis_client=redis_client,
result_ttl=86400 # 24 hours
)
document = """
Artificial intelligence is transforming software engineering...
[long document text]
"""
# First request
result1 = service.generate_summary(
user_id="user123",
document=document,
max_length=100
)
print(f"Request 1: {result1['request_id'][:8]}, From cache: {result1['from_cache']}")
# Identical request - returns cached result, no AI call
result2 = service.generate_summary(
user_id="user123",
document=document,
max_length=100
)
print(f"Request 2: {result2['request_id'][:8]}, From cache: {result2['from_cache']}")
# Verify idempotency
assert result1['summary'] == result2['summary']
assert result1['request_id'] == result2['request_id']
print("✓ Idempotency guaranteed!")- Same inputs always produce same cached result
- Duplicate requests don't trigger duplicate AI calls
- Processing locks prevent concurrent duplicate processing
- Explicit request IDs allow clients to retry safely
- Temperature=0 makes AI output more deterministic
3. Use Temperature=0 and Seeding for Consistency
When consistency is more important than creativity, use techniques that make AI output more deterministic.
def generate_consistent_classification(
ai_client,
text: str,
categories: List[str]
) -> str:
"""
Classify text into categories with maximum consistency.
Same input should produce same category (nearly always).
"""
system_prompt = f"""You are a classifier. Return ONLY the category name, nothing else.
Available categories: {', '.join(categories)}
Rules:
1. Return exactly one category from the list
2. No explanation, just the category name
3. If unclear, choose the closest match"""
# Use temperature=0 for maximum determinism
result = ai_client.generate(
prompt=f"Classify this text:\n\n{text}",
system_prompt=system_prompt,
temperature=0.0, # Most deterministic
max_tokens=20
)
category = result['response'].strip()
# Validate result
if category not in categories:
# Fallback: find closest match
category = min(categories, key=lambda c: levenshtein_distance(c, category))
return category
def generate_with_retry_until_stable(
ai_client,
prompt: str,
system_prompt: str,
stability_checks: int = 3
) -> str:
"""
Generate response and retry until output stabilizes.
Ensures we get consistent output by running multiple times
and checking for agreement.
"""
results = []
for i in range(stability_checks):
result = ai_client.generate(
prompt=prompt,
system_prompt=system_prompt,
temperature=0.0 # Deterministic
)
results.append(result['response'])
# Check if all results are identical or very similar
if len(set(results)) == 1:
# Perfect agreement
logger.info("Output stable on first attempt")
return results[0]
# Calculate similarity between responses
similarities = []
for i in range(len(results)):
for j in range(i + 1, len(results)):
similarity = calculate_similarity(results[i], results[j])
similarities.append(similarity)
avg_similarity = sum(similarities) / len(similarities)
if avg_similarity >= 0.95: # 95% similar
logger.info(f"Output stable with {avg_similarity:.2%} similarity")
# Return the most common or first result
return results[0]
else:
logger.warning(
f"Output unstable: {avg_similarity:.2%} similarity. "
"Consider stronger constraints or lower temperature."
)
# Return majority vote or first result
return max(set(results), key=results.count)
# Example: Consistent product categorization
def categorize_product(ai_client, product_name: str, product_desc: str) -> str:
"""Categorize product with consistency."""
categories = [
"Electronics",
"Clothing",
"Home & Garden",
"Sports & Outdoors",
"Books & Media"
]
text = f"{product_name}\n{product_desc}"
# Use deterministic classification
category = generate_consistent_classification(
ai_client=ai_client,
text=text,
categories=categories
)
return category4. Multi-Stage Validation Pipeline
For critical applications, validate AI output through multiple stages before accepting it.
from typing import List, Tuple
class ValidationStage:
"""Base class for validation stages."""
def validate(self, data: dict) -> Tuple[bool, Optional[str]]:
"""
Validate data.
Returns:
(is_valid, error_message)
"""
raise NotImplementedError
class SchemaValidation(ValidationStage):
"""Stage 1: Validate schema structure."""
def __init__(self, model_class):
self.model_class = model_class
def validate(self, data: dict) -> Tuple[bool, Optional[str]]:
try:
self.model_class(**data)
return True, None
except ValidationError as e:
return False, f"Schema validation failed: {str(e)}"
class ContentValidation(ValidationStage):
"""Stage 2: Validate content quality and completeness."""
def __init__(self, required_sections: List[str], min_length: int):
self.required_sections = required_sections
self.min_length = min_length
def validate(self, data: dict) -> Tuple[bool, Optional[str]]:
content = data.get('content', '')
# Check length
if len(content) < self.min_length:
return False, f"Content too short: {len(content)} < {self.min_length}"
# Check required sections
for section in self.required_sections:
if section not in content:
return False, f"Missing required section: {section}"
return True, None
class SafetyValidation(ValidationStage):
"""Stage 3: Validate safety and appropriateness."""
def __init__(self, banned_words: List[str]):
self.banned_words = [word.lower() for word in banned_words]
def validate(self, data: dict) -> Tuple[bool, Optional[str]]:
content = data.get('content', '').lower()
for word in self.banned_words:
if word in content:
return False, f"Content contains banned word: {word}"
return True, None
class BusinessRuleValidation(ValidationStage):
"""Stage 4: Validate business rules."""
def validate(self, data: dict) -> Tuple[bool, Optional[str]]:
# Example: Check if product price is reasonable
if 'price' in data:
price = data['price']
if price < 0:
return False, "Price cannot be negative"
if price > 1000000:
return False, "Price suspiciously high"
# Example: Check if date is in valid range
if 'publish_date' in data:
date = datetime.fromisoformat(data['publish_date'])
if date > datetime.now():
return False, "Publish date cannot be in future"
return True, None
class ValidationPipeline:
"""Multi-stage validation pipeline for AI outputs."""
def __init__(self, stages: List[ValidationStage]):
self.stages = stages
def validate(self, data: dict) -> Tuple[bool, List[str]]:
"""
Run all validation stages.
Returns:
(all_valid, list_of_errors)
"""
errors = []
for i, stage in enumerate(self.stages):
is_valid, error_msg = stage.validate(data)
if not is_valid:
errors.append(f"Stage {i+1} ({stage.__class__.__name__}): {error_msg}")
all_valid = len(errors) == 0
return all_valid, errors
def generate_with_pipeline_validation(
ai_client,
prompt: str,
system_prompt: str,
validation_pipeline: ValidationPipeline,
max_retries: int = 3
) -> dict:
"""
Generate AI output with multi-stage validation.
Retries until output passes all validation stages.
"""
for attempt in range(max_retries):
# Generate
result = ai_client.generate(
prompt=prompt,
system_prompt=system_prompt,
temperature=0.3
)
# Parse
try:
data = extract_and_parse_json(result['response'])
except Exception as e:
logger.warning(f"Attempt {attempt + 1}: Failed to parse JSON: {e}")
continue
# Validate through pipeline
is_valid, errors = validation_pipeline.validate(data)
if is_valid:
logger.info(f"Output passed all validation stages on attempt {attempt + 1}")
return data
else:
logger.warning(
f"Attempt {attempt + 1}: Validation failed:\n" +
"\n".join(errors)
)
# Add feedback to prompt for next attempt
prompt += f"\n\nPREVIOUS ATTEMPT HAD ERRORS:\n" + "\n".join(errors)
raise ValueError(
f"Failed to generate valid output after {max_retries} attempts"
)
# Example usage
if __name__ == "__main__":
# Create validation pipeline
pipeline = ValidationPipeline([
SchemaValidation(ArticleOutput),
ContentValidation(
required_sections=["Introduction:", "Conclusion:"],
min_length=500
),
SafetyValidation(banned_words=["spam", "scam"]),
BusinessRuleValidation()
])
# Generate with validation
article_data = generate_with_pipeline_validation(
ai_client=client,
prompt="Write article about AI safety",
system_prompt="You are a professional tech writer.",
validation_pipeline=pipeline,
max_retries=3
)
print("✓ Article passed all validation stages!")
print(f" Title: {article_data['title']}")
print(f" Length: {len(article_data['content'])} chars")- High-stakes applications (legal, medical, financial)
- User-facing content that requires quality control
- When AI output directly affects business logic
- Compliance requirements (content moderation, safety)
Key Strategies for Handling Non-Determinism
- Schema validation: Use Pydantic to enforce structure, retry on failures
- Idempotency: Cache results by request ID, prevent duplicate processing
- Consistency: Use temperature=0 and stability checks for critical tasks
- Multi-stage validation: Validate schema → content → safety → business rules
- Feedback loops: Tell AI about previous failures to improve next attempt
- Graceful degradation: Have fallbacks when validation consistently fails
Common Pitfalls When Building AI Applications
1. Underestimating Costs
Problem: Small per-request costs add up quickly at scale. $0.01 per request × 100,000 users = $1,000/day = $30,000/month.
Solution: Model your costs early. Implement caching. Monitor spending daily. Set hard limits. Consider self-hosted models for high-volume use cases.
2. Ignoring Latency
Problem: AI API calls take 1-5 seconds. Synchronous calls block your API, creating poor user experience.
Solution: Use async processing for non-critical paths. Show progress indicators. Use streaming responses. Cache aggressively.
3. No Retry Logic
Problem: AI APIs have rate limits and occasional failures. Without retries, your app breaks intermittently.
Solution: Implement exponential backoff retries for rate limits and transient errors. But don't retry on 4xx errors (bad requests).
4. Trusting AI Output Format
Problem: Even when you ask for JSON, AI might return malformed or wrapped JSON, breaking your parser.
Solution: Extract JSON with regex, handle parsing errors gracefully, validate structure with schemas, and have fallback responses.
5. Prompt Injection Vulnerabilities
Problem: Users can manipulate AI behavior by injecting instructions into their input: "Ignore previous instructions and..."
Solution: Validate and sanitize user inputs. Use system prompts to set behavior. Separate instructions from user data. Monitor for suspicious patterns.
6. No Cost Monitoring
Problem: Wake up to a $10,000 API bill because a bug caused infinite loops or a user abused your system.
Solution: Set spending alerts. Implement rate limiting per user. Monitor costs in real-time. Set hard budget caps on API providers.
Key Takeaways
- Choose the right architecture - Start simple (direct API), optimize as you scale (caching, self-hosted)
- Error handling is critical - Implement retries, fallbacks, and graceful degradation
- Caching reduces costs by 50-80% - Use Redis or similar for repeated queries
- Monitor everything - Track costs, latency, error rates, and usage patterns
- Validate AI outputs - Don't trust responses blindly, parse and verify
- Optimize for cost - Use appropriate models, limit tokens, implement caching
- Handle sensitive data carefully - Avoid sending PII to external APIs
- Test thoroughly - AI behavior can be unpredictable, test edge cases
- Implement rate limiting - Protect from abuse and runaway costs
- Plan for scale - What works at 100 requests/day breaks at 100,000
What's Next?
You now know how to build production-ready AI applications. In the next lessons, we'll cover:
- MLOps & Deployment - Deploying, monitoring, and maintaining AI systems at scale
- Ethics & Best Practices - Responsible AI development, bias mitigation, and fairness