
LLM Architect Skill
- 119 installs
- 404kidwiz/claude-supercode-skills
Design and architecture for LLM-based systems, prompt engineering, and agentic workflows.
About
LLM architect provides strategic expertise on designing LLM-powered systems and agentic applications. Essential for builders creating sophisticated AI systems with proper architecture and integration patterns.
- System architecture
- Prompt engineering
- Agent design
- LLM integration patterns
Llm Architect by the numbers
- 119 all-time installs (skills.sh)
- Ranked #3,958 of 16,575 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/404kidwiz/claude-supercode-skills --skill llm-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 119 |
|---|---|
| Repository | 404kidwiz/claude-supercode-skills ↗ |
What it does
Design and architecture for LLM-based systems, prompt engineering, and agentic workflows.
Files
LLM Architect
Purpose
Provides expert large language model system architecture for designing, deploying, and optimizing LLM applications at scale. Specializes in model selection, RAG (Retrieval Augmented Generation) pipelines, fine-tuning strategies, serving infrastructure, cost optimization, and safety guardrails for production LLM systems.
When to Use
- Designing end-to-end LLM systems from requirements to production
- Selecting models and serving infrastructure for specific use cases
- Implementing RAG (Retrieval Augmented Generation) pipelines
- Optimizing LLM costs while maintaining quality thresholds
- Building safety guardrails and compliance mechanisms
- Planning fine-tuning vs RAG vs prompt engineering strategies
- Scaling LLM inference for high-throughput applications
Quick Start
Invoke this skill when:
- Designing end-to-end LLM systems from requirements to production
- Selecting models and serving infrastructure for specific use cases
- Implementing RAG (Retrieval Augmented Generation) pipelines
- Optimizing LLM costs while maintaining quality thresholds
- Building safety guardrails and compliance mechanisms
Do NOT invoke when:
- Simple API integration exists (use backend-developer instead)
- Only prompt engineering needed without architecture decisions
- Training foundation models from scratch (almost always wrong approach)
- Generic ML tasks unrelated to language models (use ml-engineer)
Decision Framework
Model Selection Quick Guide
| Requirement | Recommended Approach |
|---|---|
| Latency <100ms | Small fine-tuned model (7B quantized) |
| Latency <2s, budget unlimited | Claude 3 Opus / GPT-4 |
| Latency <2s, domain-specific | Claude 3 Sonnet fine-tuned |
| Latency <2s, cost-sensitive | Claude 3 Haiku |
| Batch/async acceptable | Batch API, cheapest tier |
RAG vs Fine-Tuning Decision Tree
Need to customize LLM behavior?
│
├─ Need domain-specific knowledge?
│ ├─ Knowledge changes frequently?
│ │ └─ RAG (Retrieval Augmented Generation)
│ └─ Knowledge is static?
│ └─ Fine-tuning OR RAG (test both)
│
├─ Need specific output format/style?
│ ├─ Can describe in prompt?
│ │ └─ Prompt engineering (try first)
│ └─ Format too complex for prompt?
│ └─ Fine-tuning
│
└─ Need latency <100ms?
└─ Fine-tuned small model (7B-13B)Architecture Pattern
[Client] → [API Gateway + Rate Limiting]
↓
[Request Router]
(Route by intent/complexity)
↓
┌────────┴────────┐
↓ ↓
[Fast Model] [Powerful Model]
(Haiku/Small) (Sonnet/Large)
↓ ↓
[Cache Layer] ← [Response Aggregator]
↓
[Logging & Monitoring]
↓
[Response to Client]Core Workflow: Design LLM System
1. Requirements Gathering
Ask these questions:
- Latency: What's the P95 response time requirement?
- Scale: Expected requests/day and growth trajectory?
- Accuracy: What's the minimum acceptable quality? (measurable metric)
- Cost: Budget constraints? ($/request or $/month)
- Data: Existing datasets for evaluation? Sensitivity level?
- Compliance: Regulatory requirements? (HIPAA, GDPR, SOC2, etc.)
2. Model Selection
def select_model(requirements):
if requirements.latency_p95 < 100: # milliseconds
if requirements.task_complexity == "simple":
return "llama2-7b-finetune"
else:
return "mistral-7b-quantized"
elif requirements.latency_p95 < 2000:
if requirements.budget == "unlimited":
return "claude-3-opus"
elif requirements.domain_specific:
return "claude-3-sonnet-finetuned"
else:
return "claude-3-haiku"
else: # Batch/async acceptable
if requirements.accuracy_critical:
return "gpt-4-with-ensemble"
else:
return "batch-api-cheapest-tier"3. Prototype & Evaluate
# Run benchmark on eval dataset
python scripts/evaluate_model.py \
--model claude-3-sonnet \
--dataset data/eval_1000_examples.jsonl \
--metrics accuracy,latency,cost
# Expected output:
# Accuracy: 94.3%
# P95 Latency: 1,245ms
# Cost per 1K requests: $2.154. Iteration Checklist
- [ ] Latency P95 meets requirement? If no → optimize serving (quantization, caching)
- [ ] Accuracy meets threshold? If no → improve prompts, fine-tune, or upgrade model
- [ ] Cost within budget? If no → aggressive caching, smaller model routing, batching
- [ ] Safety guardrails tested? If no → add content filters, PII detection
- [ ] Monitoring dashboards live? If no → set up Prometheus + Grafana
- [ ] Runbook documented? If no → document common failures and fixes
Cost Optimization Strategies
| Strategy | Savings | When to Use |
|---|---|---|
| Semantic caching | 40-80% | 60%+ similar queries |
| Multi-model routing | 30-50% | Mixed complexity queries |
| Prompt compression | 10-20% | Long context inputs |
| Batching | 20-40% | Async-tolerant workloads |
| Smaller model cascade | 40-60% | Simple queries first |
Safety Checklist
- [ ] Content filtering tested against adversarial examples
- [ ] PII detection and redaction validated
- [ ] Prompt injection defenses in place
- [ ] Output validation rules implemented
- [ ] Audit logging configured for all requests
- [ ] Compliance requirements documented and validated
Red Flags - When to Escalate
| Observation | Action |
|---|---|
| Accuracy <80% after prompt iteration | Consider fine-tuning |
| Latency 2x requirement | Review infrastructure |
| Cost >2x budget | Aggressive caching/routing |
| Hallucination rate >5% | Add RAG or stronger guardrails |
| Safety bypass detected | Immediate security review |
Quick Reference: Performance Targets
| Metric | Target | Critical |
|---|---|---|
| P95 Latency | <2x requirement | <3x requirement |
| Accuracy | >90% | >80% |
| Cache Hit Rate | >60% | >40% |
| Error Rate | <1% | <5% |
| Cost/1K requests | Within budget | <150% budget |
Additional Resources
- Detailed Technical Reference: See REFERENCE.md
- RAG implementation workflow
- Semantic caching patterns
- Deployment configurations
- Code Examples & Patterns: See EXAMPLES.md
- Anti-patterns (fine-tuning when prompting suffices, no fallback)
- Quality checklist for LLM systems
- Resilient LLM call patterns
LLM Architect - Examples & Patterns
Anti-Patterns
Anti-Pattern: Fine-Tuning When Prompting Would Suffice
What it looks like:
# User wants to change response format or style
# Jumps straight to fine-tuning a custom model
train_model(
base_model="llama2-7b",
dataset="responses_formatted_nicely.jsonl",
epochs=3
)Why it fails:
- Costs thousands in compute for what good prompting achieves
- Maintenance burden (need to retrain on updates)
- Slower iteration cycles (days vs minutes)
Correct approach:
# Try prompt engineering first (90% of cases this works)
prompt = """Output JSON in this exact format:
{
"answer": "your response here",
"confidence": 0.95,
"sources": ["source1", "source2"]
}"""
# Only fine-tune if:
# - Prompting fails after extensive iteration
# - Need <50ms latency (fine-tuned model smaller/faster)
# - Highly domain-specific behavior required---
Anti-Pattern: No Fallback Strategy
What it looks like:
# Single model, no error handling
response = claude.messages.create(
model="claude-3-opus",
messages=[{"role": "user", "content": prompt}]
)
# If Claude API is down → your app is downWhy it fails:
- API outages happen (99.9% uptime = 43 minutes downtime/month)
- Rate limits can be hit unexpectedly
- Single point of failure
Correct approach:
import asyncio
async def resilient_llm_call(prompt):
# Strategy 1: Retry with exponential backoff
for attempt in range(3):
try:
return await call_primary_llm(prompt)
except RateLimitError:
await asyncio.sleep(2 ** attempt) # 1s, 2s, 4s
except APIError as e:
logger.warning(f"Attempt {attempt+1} failed: {e}")
# Strategy 2: Fallback to alternative model
try:
return await call_fallback_llm(prompt)
except Exception:
pass
# Strategy 3: Degrade gracefully
return {
"response": "Service temporarily unavailable, queued for processing",
"queued": True
}---
Anti-Pattern: Ignoring Context Window Limits
What it looks like:
# Stuffing entire document into prompt
prompt = f"Summarize this: {entire_100k_document}"
# API error: context length exceededWhy it fails:
- Models have token limits (4K-200K depending on model)
- Costs increase linearly with input size
- Quality degrades with very long contexts
Correct approach:
def chunk_and_summarize(document: str, max_chunk_tokens: int = 4000) -> str:
chunks = split_into_chunks(document, max_chunk_tokens)
# Map: Summarize each chunk
summaries = []
for chunk in chunks:
summary = await call_llm(f"Summarize this section:\n{chunk}")
summaries.append(summary)
# Reduce: Combine summaries
if len(summaries) == 1:
return summaries[0]
combined = "\n\n".join(summaries)
return await call_llm(f"Combine these summaries into one:\n{combined}")---
Anti-Pattern: Treating LLMs as Databases
What it looks like:
# Asking model to recall specific facts from training
prompt = "What is customer ID 12345's current order status?"Why it fails:
- Models don't have deterministic memory
- Hallucinate plausible-sounding but false information
- Training data is outdated (months to years old)
Correct approach:
# Fetch from database, use LLM for synthesis/presentation
order_data = database.query("SELECT * FROM orders WHERE customer_id = 12345")
prompt = f"Summarize this order status for customer: {order_data}"
# LLM transforms data → user-friendly response
# Database provides facts (deterministic, accurate, up-to-date)
# LLM provides presentation (natural language, helpful)---
Anti-Pattern: No Output Validation
What it looks like:
# Trusting LLM output blindly
response = await call_llm("Generate a JSON config")
config = json.loads(response) # Crashes if invalid JSONCorrect approach:
import json
from pydantic import BaseModel, ValidationError
class ConfigOutput(BaseModel):
setting_a: str
setting_b: int
enabled: bool
async def get_validated_config(prompt: str) -> ConfigOutput:
for attempt in range(3):
response = await call_llm(prompt)
try:
# Extract JSON from response
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
data = json.loads(json_match.group())
return ConfigOutput(**data)
except (json.JSONDecodeError, ValidationError) as e:
# Retry with error feedback
prompt = f"{prompt}\n\nPrevious attempt failed: {e}. Please fix."
raise ValueError("Failed to get valid output after 3 attempts")---
Quality Checklist
Use this checklist before marking an LLM system implementation complete:
Architecture & Design
- [ ] Latency requirements documented and validated (P50, P95, P99)
- [ ] Cost projections calculated for expected traffic ($/1K requests)
- [ ] Model selection justified with trade-off analysis
- [ ] Fallback strategy implemented and tested
- [ ] Scaling strategy defined (horizontal/vertical, triggers)
Performance
- [ ] Benchmark results on representative dataset (>1000 examples)
- [ ] Accuracy/quality metrics meet minimum thresholds
- [ ] Latency <2x requirement across P95
- [ ] Throughput tested at 2x expected peak load
- [ ] Cache hit rate measured (if caching implemented)
Cost Optimization
- [ ] Caching strategy implemented and verified
- [ ] Prompt optimization applied (compression, templates)
- [ ] Multi-model routing configured (if applicable)
- [ ] Cost monitoring dashboards created
- [ ] Budget alerts configured (>110% expected spend)
Safety & Compliance
- [ ] Content filtering tested against adversarial examples
- [ ] PII detection and redaction validated
- [ ] Prompt injection defenses in place
- [ ] Output validation rules implemented
- [ ] Audit logging configured for all requests
- [ ] Compliance requirements documented and validated
Monitoring & Observability
- [ ] Latency metrics tracked (P50, P95, P99)
- [ ] Cost metrics tracked ($/day, $/1K requests)
- [ ] Quality metrics tracked (accuracy, user ratings)
- [ ] Error rate tracked and alerted (>5% error rate)
- [ ] Dashboards created for stakeholders
Operational Readiness
- [ ] Runbook documented with common failure scenarios
- [ ] On-call escalation paths defined
- [ ] Rollback procedure tested
- [ ] A/B testing framework configured (if needed)
- [ ] Model versioning strategy implemented
Documentation
- [ ] Architecture diagram created and reviewed
- [ ] API documentation published (if exposing APIs)
- [ ] Configuration documentation complete
- [ ] Decision log maintained (why this model, why this approach)
- [ ] Known limitations documented
---
Prompt Engineering Patterns
Chain of Thought
prompt = """Solve this step by step:
Question: If a train travels at 60 mph for 2.5 hours, how far does it travel?
Let me think through this:
1. First, I'll identify the formula: distance = speed × time
2. Speed = 60 mph
3. Time = 2.5 hours
4. Distance = 60 × 2.5 = 150 miles
The train travels 150 miles.
Now solve this:
Question: {user_question}
Let me think through this:"""Few-Shot Examples
prompt = """Classify the sentiment of these reviews:
Review: "The product arrived broken and customer service was unhelpful."
Sentiment: Negative
Review: "Exactly what I needed! Fast shipping too."
Sentiment: Positive
Review: "It works okay, nothing special but does the job."
Sentiment: Neutral
Review: "{user_review}"
Sentiment:"""Structured Output
prompt = """Extract information from this text and return as JSON:
Text: "John Smith, CEO of Acme Corp, announced Q3 revenue of $5.2M"
Output the following JSON structure:
{
"person": "name of the person mentioned",
"role": "their job title",
"company": "company name",
"metric": "any financial figures mentioned"
}
JSON:"""LLM Architect - Technical Reference
RAG System Implementation
Use case: Build retrieval-augmented generation for document Q&A
1. Document Processing Pipeline
from langchain.text_splitter import RecursiveCharacterTextSplitter
from datetime import datetime
# Step 1: Load documents
documents = load_documents("./knowledge_base/") # PDFs, HTML, MD, etc.
# Step 2: Chunking strategy (critical for retrieval quality)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # Tokens, not characters
chunk_overlap=200, # Overlap to preserve context
separators=["\n\n", "\n", ". ", " ", ""] # Preserve semantic boundaries
)
chunks = text_splitter.split_documents(documents)
# Step 3: Add metadata (enables filtering)
for chunk in chunks:
chunk.metadata["source_file"] = chunk.metadata.get("source")
chunk.metadata["chunk_index"] = chunks.index(chunk)
chunk.metadata["created_at"] = datetime.now().isoformat()2. Embedding & Vector Store
from pinecone import Pinecone, ServerlessSpec
from openai import OpenAI
# Initialize Pinecone
pc = Pinecone(api_key="YOUR_API_KEY")
index = pc.Index("llm-rag-demo")
# Embedding model options:
# - OpenAI text-embedding-3-small: $0.02/1M tokens, 1536 dims, fast
# - Cohere embed-english-v3.0: $0.10/1M tokens, 1024 dims, high quality
# - Local BAAI/bge-large-en-v1.5: Free, 1024 dims, self-hosted
client = OpenAI()
def embed_chunks(chunks, batch_size=100):
embeddings = []
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i+batch_size]
texts = [chunk.page_content for chunk in batch]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
embeddings.extend([r.embedding for r in response.data])
return embeddings
# Upload to vector store
embeddings = embed_chunks(chunks)
index.upsert(vectors=zip(ids, embeddings, metadatas))3. Retrieval with Hybrid Search
from sentence_transformers import CrossEncoder
def retrieve_context(query, top_k=5):
# Step 1: Dense retrieval (semantic similarity)
query_embedding = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
dense_results = index.query(
vector=query_embedding,
top_k=top_k * 2, # Get more candidates
include_metadata=True
)
# Step 2: Rerank with cross-encoder (optional, +50% relevance)
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
candidates = [(r.metadata['text'], r.score) for r in dense_results.matches]
reranked = reranker.rank(query, [c[0] for c in candidates])
# Return top-k after reranking
return reranked[:top_k]4. Generation with Retrieved Context
import anthropic
anthropic_client = anthropic.Anthropic()
def generate_answer(query, context_chunks):
# Build prompt with retrieved context
context = "\n\n".join([
f"[Source {i+1}]: {chunk['text']}"
for i, chunk in enumerate(context_chunks)
])
prompt = f"""Answer the question using ONLY the provided sources.
If the sources don't contain enough information, say "I don't have enough information to answer this."
Sources:
{context}
Question: {query}
Answer:"""
# Generate with Claude
response = anthropic_client.messages.create(
model="claude-3-sonnet-20240229",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
# Full RAG pipeline
def rag_query(query):
context_chunks = retrieve_context(query, top_k=5)
answer = generate_answer(query, context_chunks)
return {
"answer": answer,
"sources": context_chunks, # Return for citation
"confidence": calculate_confidence(context_chunks)
}5. Evaluation & Iteration
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
results = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, answer_relevancy]
)
# Expected baselines:
# - Faithfulness (answer matches sources): >90%
# - Answer relevancy: >85%
# - Retrieval precision@5: >70%---
Semantic Caching Layer
When to use: Repeated or similar queries (60%+ hit rate achievable)
import hashlib
import time
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
class SemanticCache:
def __init__(self, embedding_model):
self.client = QdrantClient(":memory:")
self.client.create_collection(
collection_name="llm_cache",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
)
self.embedding_model = embedding_model
self.ttl_seconds = 86400 # 24 hours
async def get(self, query, similarity_threshold=0.95):
# Embed query
query_vec = self.embedding_model.encode(query)
# Search for similar cached queries
results = self.client.search(
collection_name="llm_cache",
query_vector=query_vec,
limit=1
)
if results and results[0].score >= similarity_threshold:
# Check TTL
cached_time = results[0].payload["timestamp"]
if time.time() - cached_time < self.ttl_seconds:
return results[0].payload["response"] # CACHE HIT
return None # Cache miss
async def set(self, query, response):
query_vec = self.embedding_model.encode(query)
cache_id = hashlib.md5(query.encode()).hexdigest()
self.client.upsert(
collection_name="llm_cache",
points=[{
"id": cache_id,
"vector": query_vec,
"payload": {
"query": query,
"response": response,
"timestamp": time.time()
}
}]
)Expected savings: 40-80% cost reduction if 60%+ queries are cacheable
---
Kubernetes Deployment
# kubernetes/llm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-service
spec:
replicas: 3
template:
spec:
containers:
- name: vllm-server
image: vllm/vllm-openai:latest
resources:
limits:
nvidia.com/gpu: 1
env:
- name: MODEL_NAME
value: "mistralai/Mistral-7B-Instruct-v0.2"
- name: TENSOR_PARALLEL_SIZE
value: "1"
- name: MAX_MODEL_LEN
value: "4096"
ports:
- containerPort: 8000
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 10---
Multi-Model Routing
from enum import Enum
from typing import Literal
class ModelTier(Enum):
FAST = "haiku" # Simple queries, <500ms
BALANCED = "sonnet" # Medium complexity
POWERFUL = "opus" # Complex reasoning
def classify_query_complexity(query: str) -> ModelTier:
"""Route queries to appropriate model tier"""
# Simple heuristics (replace with classifier in production)
word_count = len(query.split())
if word_count < 20 and "?" in query:
return ModelTier.FAST
complex_indicators = [
"explain", "analyze", "compare", "evaluate",
"step by step", "reasoning", "implications"
]
if any(indicator in query.lower() for indicator in complex_indicators):
return ModelTier.POWERFUL
return ModelTier.BALANCED
async def route_and_call(query: str) -> str:
tier = classify_query_complexity(query)
model_map = {
ModelTier.FAST: "claude-3-haiku-20240307",
ModelTier.BALANCED: "claude-3-sonnet-20240229",
ModelTier.POWERFUL: "claude-3-opus-20240229"
}
response = await anthropic_client.messages.create(
model=model_map[tier],
max_tokens=1024,
messages=[{"role": "user", "content": query}]
)
return response.content[0].text---
Safety Guardrails
Content Filtering
from typing import Tuple
async def check_safety(content: str) -> Tuple[bool, str]:
"""Check content for safety issues"""
# PII detection
pii_patterns = [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
r'\b\d{16}\b', # Credit card
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' # Email
]
import re
for pattern in pii_patterns:
if re.search(pattern, content):
return False, "PII detected"
# Prompt injection detection
injection_patterns = [
"ignore previous instructions",
"disregard all prior",
"you are now",
"new instructions:"
]
content_lower = content.lower()
for pattern in injection_patterns:
if pattern in content_lower:
return False, "Potential prompt injection"
return True, "OK"
async def safe_llm_call(query: str) -> str:
# Check input
is_safe, reason = await check_safety(query)
if not is_safe:
return f"Request blocked: {reason}"
# Get response
response = await call_llm(query)
# Check output
is_safe, reason = await check_safety(response)
if not is_safe:
return "Response filtered for safety reasons"
return response---
Monitoring Setup
Key Metrics to Track
import prometheus_client as prom
# Define metrics
llm_request_latency = prom.Histogram(
'llm_request_latency_seconds',
'LLM request latency',
['model', 'endpoint'],
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)
llm_request_tokens = prom.Counter(
'llm_request_tokens_total',
'Total tokens processed',
['model', 'type'] # type: input/output
)
llm_cache_hits = prom.Counter(
'llm_cache_hits_total',
'Cache hit count',
['cache_type'] # exact/semantic
)
llm_errors = prom.Counter(
'llm_errors_total',
'LLM error count',
['model', 'error_type']
)
# Usage in request handler
async def handle_llm_request(query: str):
with llm_request_latency.labels(model='sonnet', endpoint='/chat').time():
response = await call_llm(query)
llm_request_tokens.labels(model='sonnet', type='input').inc(len(query.split()))
llm_request_tokens.labels(model='sonnet', type='output').inc(len(response.split()))
return responseFine-Tuning Guide
Overview
Fine-tuning adapts pre-trained models to specific tasks or domains, improving performance on specialized data.
Methods
1. Full Fine-Tuning
Updates all model parameters.
Pros:
- Maximum performance gains
- No architectural changes
- Works for any model
Cons:
- High computational cost
- Large storage requirements
- Risk of catastrophic forgetting
Use case:
- Critical tasks requiring maximum accuracy
- Sufficient compute resources
- Domain-specific models
2. LoRA (Low-Rank Adaptation)
Adds trainable rank decomposition matrices.
Pros:
- 100-1000x faster training
- Small storage (1-100MB)
- No performance degradation
- Easy to switch between adapters
Cons:
- Slightly lower performance than full fine-tuning
- Requires LoRA-supporting codebase
Use case:
- Most production use cases
- Multiple task adapters
- Resource-constrained environments
Configuration:
lora_config = LoraConfig(
r=8, # Rank (4-16 typical)
lora_alpha=32, # Scaling factor
target_modules=["q_proj", "v_proj"], # Target layers
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)3. Prefix Tuning
Learns continuous prompt embeddings.
Pros:
- No gradient through model
- Very efficient
Cons:
- More complex to implement
- Lower performance than LoRA
4. P-Tuning (Prompt Tuning)
Learns soft prompts.
Pros:
- Simple implementation
- Parameter efficient
Cons:
- Limited to prompt-based learning
Data Preparation
Dataset Format
[
{
"text": "The capital of France is Paris."
},
{
"text": "Machine learning enables computers to learn from data."
}
]Data Quality Requirements
1. Relevance: Data must match target task 2. Diversity: Cover edge cases and variations 3. Size: 100-10,000+ examples depending on task 4. Balance: Balanced classes for classification 5. Cleanliness: Remove duplicates and errors
Data Augmentation
def augment_data(text):
variations = []
# Paraphrasing
variations.append(paraphrase(text))
# Back-translation
variations.append(back_translate(text))
# Synonym replacement
variations.append(replace_synonyms(text))
return variationsTraining Process
Step 1: Prepare Data
from finetune_model import ModelFinetuner, FinetuningConfig
config = FinetuningConfig(
model_name="gpt2",
output_dir="./finetuned_model",
train_file="train_data.json",
validation_file="validation_data.json",
use_lora=True,
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=2e-4
)
finetuner = ModelFinetuner(config)
finetuner.load_model()Step 2: Train
finetuner.train()Step 3: Evaluate
# Use validation set
validation_loss = finetuner.evaluate()
# Or test on held-out examples
test_results = finetuner.test(test_data)Step 4: Save Model
finetuner.save_model()Hyperparameter Tuning
Key Parameters
| Parameter | Typical Range | Impact |
|---|---|---|
| Learning Rate | 1e-5 to 5e-4 | High impact |
| Batch Size | 1-16 | Moderate impact |
| Epochs | 1-10 | Depends on data size |
| LoRA Rank (r) | 4-32 | Low-Moderate impact |
| LoRA Alpha | 16-128 | Low impact |
| Warmup Steps | 100-1000 | Moderate impact |
Tuning Strategy
# Grid search
learning_rates = [1e-5, 5e-5, 1e-4]
batch_sizes = [4, 8]
for lr in learning_rates:
for bs in batch_sizes:
config.learning_rate = lr
config.per_device_train_batch_size = bs
finetuner.train()
results.append(evaluate())Evaluation
Metrics
Language Generation:
- Perplexity
- BLEU score
- ROUGE score
- Human evaluation
Classification:
- Accuracy
- F1 score
- Precision/Recall
- Confusion matrix
Question Answering:
- Exact match
- F1 score (token-level)
- Semantic similarity
Evaluation Framework
from evaluate_model import ModelEvaluator
evaluator = ModelEvaluator()
# Add metrics
evaluator.add_metric(EvaluationMetric(
name="perplexity",
metric_type=MetricType.CUSTOM,
description="Model perplexity"
))
# Run evaluation
report = evaluator.evaluate_model(
model_name="finetuned_model",
generate_func=generate_with_model,
dataset=test_dataset
)Deployment
Loading Fine-Tuned Models
finetuner.load_finetuned_model("path/to/model")
# Generate
response = finetuner.generate(
prompt="Your prompt here",
max_new_tokens=100,
temperature=0.7
)Serving Fine-Tuned Models
from serve_model import ModelServer
server = ModelServer(config)
server.load_model("finetuned_model", "path/to/model")
server.start()Multi-Adapter Deployment
# Switch between adapters
model.set_adapter("adapter_1")
output_1 = model.generate(prompt)
model.set_adapter("adapter_2")
output_2 = model.generate(prompt)Advanced Techniques
Instruction Tuning
# Format: instruction + input + output
data = [
{
"instruction": "Summarize the following text.",
"input": "Long text here...",
"output": "Summary here..."
}
]Multi-Task Learning
# Train on multiple tasks simultaneously
tasks = ["summarization", "translation", "qa"]
for task in tasks:
task_data = load_task_data(task)
finetuner.train(task_data)Continual Learning
# Learn new tasks without forgetting
for task in new_tasks:
finetuner.train(task_data, replay_buffer=old_data)Common Issues
Catastrophic Forgetting
- Symptom: Performance on old tasks degrades
- Solution: Use replay buffer, elastic weight consolidation
Overfitting
- Symptom: Training loss decreases, validation loss increases
- Solution: Reduce epochs, increase dropout, add regularization
Underfitting
- Symptom: Both training and validation loss high
- Solution: Increase model capacity, train longer
Poor Convergence
- Symptom: Loss oscillates or doesn't decrease
- Solution: Adjust learning rate, check data quality
Best Practices
1. Start with LoRA: Most cases don't need full fine-tuning 2. Monitor validation loss: Stop before overfitting 3. Use checkpoints: Save best performing model 4. Evaluate on held-out data: Don't trust training metrics alone 5. Test on edge cases: Validate model robustness 6. Document hyperparameters: Enable reproducibility 7. Version control models: Track model iterations
Model Selection Guide
Model Comparison Matrix
| Model | Context | Parameters | Input Cost | Output Cost | Strengths |
|---|---|---|---|---|---|
| GPT-4 | 8K | Unknown | $0.03/K | $0.06/K | Complex reasoning, coding |
| GPT-4 Turbo | 128K | Unknown | $0.01/K | $0.03/K | Long context, vision |
| GPT-3.5 Turbo | 16K | Unknown | $0.0005/K | $0.0015/K | Fast, cheap coding |
| Claude 3.5 Sonnet | 200K | Unknown | $0.003/K | $0.015/K | Long context, safety |
| Claude 3 Opus | 200K | Unknown | $0.015/K | $0.075/K | Highest quality |
| Llama 2 70B | 4K | 70B | Free | Free | Open source |
Selection Framework
Task-Based Selection
Simple Tasks:
- Text completion: GPT-3.5 Turbo, Llama 2 7B
- Classification: Fine-tuned BERT, RoBERTa
- Summarization: BART, T5
Complex Tasks:
- Reasoning: GPT-4, Claude 3 Opus
- Code generation: GPT-4, Claude 3.5 Sonnet
- Multi-step tasks: GPT-4 Turbo
Long Context:
- Document analysis: Claude 3.5 Sonnet (200K)
- Codebase analysis: GPT-4 Turbo (128K)
- Research: Claude 3 Opus (200K)
Cost-Based Selection
def select_model_by_budget(task_complexity, budget_per_request):
if budget_per_request < 0.01:
if task_complexity == 'simple':
return 'gpt-3.5-turbo'
else:
return 'llama-2-7b'
elif budget_per_request < 0.10:
if task_complexity == 'complex':
return 'claude-3-5-sonnet-20241022'
else:
return 'gpt-4-turbo'
else:
return 'gpt-4'Latency-Based Selection
- <100ms: Local models, TinyLlama
- 100-500ms: GPT-3.5 Turbo, small open-source
- 500ms-2s: GPT-4 Turbo, Claude 3.5 Sonnet
- >2s: GPT-4, Claude 3 Opus
Benchmarking
Metrics to Track
1. Accuracy: How correct are the outputs? 2. Latency: How fast is the response? 3. Cost: How much does it cost? 4. Token Usage: Efficient use of context? 5. Consistency: Reproducible results?
Benchmark Framework
from benchmark_models import ModelBenchmarker
benchmarker = ModelBenchmarker(models)
# Define evaluation function
def evaluate_task(model_name, test_case):
result = generate_with_model(model_name, test_case)
return evaluate_result(result)
# Run benchmarks
benchmarker.benchmark_task(
task_name="summarization",
task_func=evaluate_task,
test_data=test_cases,
ground_truth=references
)
# Get best model
best = benchmarker.get_best_model_for_task(
"summarization",
metric="accuracy"
)Model Specialization
Coding Models
- Best: GPT-4, Claude 3.5 Sonnet
- Budget: GPT-3.5 Turbo, StarCoder
- Local: CodeLlama, DeepSeek Coder
Writing Models
- Creative: Claude 3 Opus, GPT-4
- Technical: Claude 3.5 Sonnet, GPT-4 Turbo
- Concise: Fine-tuned models
Analysis Models
- Data Analysis: GPT-4 Turbo, Claude 3.5 Sonnet
- Document Analysis: Claude 3.5 Sonnet (long context)
- Research: Claude 3 Opus
Multimodal Models
- Vision: GPT-4 Turbo, Claude 3.5 Sonnet
- Audio: Whisper
- Video: GPT-4 Turbo (frame-by-frame)
Hybrid Approaches
Cascade Selection
def cascade_model_selection(prompt):
# Try cheapest first
result = try_model('gpt-3.5-turbo', prompt)
# If confidence low, escalate
if result['confidence'] < 0.7:
result = try_model('claude-3-5-sonnet-20241022', prompt)
# If still low confidence, use best
if result['confidence'] < 0.9:
result = try_model('gpt-4', prompt)
return resultEnsemble Methods
- Combine outputs from multiple models
- Use voting for classification
- Average for numerical outputs
- Best-of-N for quality
Deployment Considerations
API-Based Models
- No infrastructure overhead
- Scalable
- Requires internet
- Data privacy concerns
Self-Hosted Models
- Full control
- Privacy guaranteed
- Requires GPU resources
- Higher maintenance
Hybrid Deployment
- Simple tasks: Local models
- Complex tasks: API models
- Fallback to API when local fails
Best Practices
1. Benchmark before deploying: Always test on your specific task 2. Monitor performance: Track metrics over time 3. Budget alerts: Set cost limits and monitoring 4. Fallback models: Have backup options 5. Regular re-evaluation: New models appear frequently 6. A/B testing: Compare models on production data 7. Quality checks: Validate outputs before use
Serving Infrastructure Guide
Overview
Production model serving requires careful consideration of performance, scalability, and reliability.
Serving Options
1. API-Based Serving
Use provider APIs (OpenAI, Anthropic, etc.)
Pros:
- Zero infrastructure
- Automatic scaling
- Built-in monitoring
- Regular updates
Cons:
- Ongoing costs
- Data privacy concerns
- Rate limits
- Dependency on external services
Best for:
- Proof of concept
- Low to medium traffic
- No ML infrastructure team
- Rapid prototyping
2. Self-Hosted Serving
Deploy models on your own infrastructure
Pros:
- Full control
- Data privacy
- Predictable costs
- Custom optimizations
Cons:
- Infrastructure setup
- Maintenance overhead
- Scaling complexity
- Higher initial cost
Best for:
- High volume production
- Sensitive data
- Custom models
- Cost optimization at scale
Serving Frameworks
vLLM
High-throughput serving with PagedAttention.
Installation:
pip install vllmUsage:
python -m vllm.entrypoints.api_server \
--model meta-llama/Llama-2-7b-hf \
--port 8000 \
--tensor-parallel-size 4Pros:
- 10-20x higher throughput
- Low latency
- Continuous batching
- OpenAI-compatible API
Cons:
- Newer, less battle-tested
- Limited model support
Text Generation WebUI (Oobabooga)
Feature-rich web interface for model serving.
Features:
- Web UI
- Multiple model support
- Extensions ecosystem
- API access
Setup:
git clone https://github.com/oobabooga/text-generation-webui
cd text-generation-webui
python server.py --model-path /path/to/model --listenLocalAI
OpenAI-compatible API for local models.
Setup:
docker run -p 8080:8080 \
-v /models:/models \
localai/localai \
--models-path /modelsUse OpenAI client:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="not-needed"
)Deployment Strategies
Docker Deployment
Dockerfile:
FROM python:3.10-slim
WORKDIR /app
RUN pip install fastapi uvicorn transformers accelerate torch
COPY model.py .
COPY ./models ./models
CMD ["uvicorn", "model:app", "--host", "0.0.0.0", "--port", "8000"]Build and Run:
docker build -t model-server .
docker run -p 8000:8000 --gpus all model-serverKubernetes Deployment
Deployment YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-server
spec:
replicas: 3
selector:
matchLabels:
app: model-server
template:
metadata:
labels:
app: model-server
spec:
containers:
- name: model-server
image: model-server:latest
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: 1Service:
apiVersion: v1
kind: Service
metadata:
name: model-server
spec:
selector:
app: model-server
ports:
- port: 80
targetPort: 8000
type: LoadBalancerServerless Deployment
AWS Lambda:
import json
def lambda_handler(event, context):
prompt = event['prompt']
response = generate_with_model(prompt)
return {
'statusCode': 200,
'body': json.dumps({'output': response})
}Performance Optimization
Batch Processing
@app.post("/batch_generate")
async def batch_generate(requests: List[GenerationRequest]):
outputs = []
for request in requests:
output = generate(request)
outputs.append(output)
return outputsCaching
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_generate(prompt_hash):
return generate(original_prompt)Quantization
from transformers import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(
load_in_8bit=True
)
model = AutoModelForCausalLM.from_pretrained(
model_path,
quantization_config=quantization_config
)Stream Responses
async def stream_generate(prompt):
for token in model.generate_stream(prompt):
yield tokenMonitoring
Key Metrics
1. Throughput: Requests per second 2. Latency: P50, P95, P99 response times 3. Error Rate: Failed requests 4. GPU Utilization: Compute efficiency 5. Memory Usage: VRAM consumption
Prometheus Integration
from prometheus_client import Counter, Histogram
request_counter = Counter('model_requests_total', 'Total requests')
latency_histogram = Histogram('model_latency_seconds', 'Request latency')
@app.post("/generate")
async def generate(request: GenerationRequest):
with latency_histogram.time():
output = generate_with_model(request)
request_counter.inc()
return outputHealth Checks
@app.get("/health")
async def health():
return {
"status": "healthy",
"gpu_available": torch.cuda.is_available(),
"memory_used": torch.cuda.memory_allocated()
}Scaling
Horizontal Scaling
Add more instances to handle increased load.
Kubernetes HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: model-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: model-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80Vertical Scaling
Increase resources per instance.
GPU Types:
- A100 (40GB/80GB): Best performance
- V100 (16GB/32GB): Good balance
- T4 (16GB): Cost-effective
- L4 (24GB): Newer option
Load Balancing
upstream model_servers {
least_conn;
server server1:8000;
server server2:8000;
server server3:8000;
}
server {
listen 80;
location / {
proxy_pass http://model_servers;
}
}Best Practices
1. Use FastAPI: Async, type-safe, automatic docs 2. Implement rate limiting: Prevent abuse 3. Add authentication: Secure endpoints 4. Log everything: Debugging and monitoring 5. Version models: Easy rollbacks 6. Graceful shutdown: Handle connections properly 7. Health checks: Kubernetes ready 8. Resource limits: Prevent memory leaks 9. Request validation: Use Pydantic models 10. Monitor continuously: Detect issues early
"""
Model Benchmarking and Selection
Compares different models on tasks and metrics
"""
import time
import logging
from typing import Dict, List, Any, Callable, Optional
from dataclasses import dataclass, field
import json
import yaml
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class BenchmarkResult:
model_name: str
task_name: str
accuracy: float
latency: float
cost: float
token_usage: int
additional_metrics: Dict[str, Any] = field(default_factory=dict)
@dataclass
class ModelSpec:
name: str
context_window: int
input_price_per_1k: float
output_price_per_1k: float
capabilities: List[str]
class ModelBenchmarker:
def __init__(self, models: List[ModelSpec]):
self.models = {m.name: m for m in models}
self.results: List[BenchmarkResult] = []
def benchmark_task(
self,
task_name: str,
task_func: Callable,
test_data: List[Any],
ground_truth: Optional[List[Any]] = None
):
logger.info(f"Benchmarking task: {task_name}")
for model_name, model_spec in self.models.items():
logger.info(f" Testing model: {model_name}")
accuracies = []
latencies = []
costs = []
token_usages = []
for i, test_case in enumerate(test_data):
start_time = time.time()
try:
result = task_func(model_name, test_case)
latency = time.time() - start_time
latencies.append(latency)
if 'usage' in result:
token_usages.append(result['usage'].get('total_tokens', 0))
cost = self._calculate_cost(model_spec, result['usage'])
costs.append(cost)
if ground_truth and i < len(ground_truth):
accuracy = self._evaluate_accuracy(result, ground_truth[i])
accuracies.append(accuracy)
except Exception as e:
logger.error(f" Error on test case {i}: {e}")
continue
if accuracies:
avg_accuracy = sum(accuracies) / len(accuracies)
else:
avg_accuracy = 0.0
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
avg_cost = sum(costs) / len(costs) if costs else 0.0
avg_tokens = sum(token_usages) / len(token_usages) if token_usages else 0
result = BenchmarkResult(
model_name=model_name,
task_name=task_name,
accuracy=avg_accuracy,
latency=avg_latency,
cost=avg_cost,
token_usage=int(avg_tokens),
additional_metrics={
'test_cases': len(test_data),
'successful_cases': len(latencies)
}
)
self.results.append(result)
logger.info(f" Result: accuracy={avg_accuracy:.2%}, latency={avg_latency:.2f}s")
def _calculate_cost(self, model_spec: ModelSpec, usage: Dict[str, int]) -> float:
input_cost = (usage.get('prompt_tokens', 0) / 1000) * model_spec.input_price_per_1k
output_cost = (usage.get('completion_tokens', 0) / 1000) * model_spec.output_price_per_1k
return input_cost + output_cost
def _evaluate_accuracy(self, result: Dict[str, Any], ground_truth: Any) -> float:
if isinstance(ground_truth, str):
return 1.0 if result.get('output', '').strip() == ground_truth.strip() else 0.0
elif isinstance(ground_truth, list):
output = result.get('output', [])
correct = sum(1 for o, g in zip(output, ground_truth) if o == g)
return correct / max(len(ground_truth), 1)
else:
return 1.0 if result.get('output') == ground_truth else 0.0
def get_best_model_for_task(
self,
task_name: str,
metric: str = 'accuracy'
) -> Optional[BenchmarkResult]:
task_results = [r for r in self.results if r.task_name == task_name]
if not task_results:
return None
if metric == 'accuracy':
return max(task_results, key=lambda r: r.accuracy)
elif metric == 'latency':
return min(task_results, key=lambda r: r.latency)
elif metric == 'cost':
return min(task_results, key=lambda r: r.cost)
else:
return task_results[0]
def get_comparison_table(self, task_name: str) -> str:
task_results = [r for r in self.results if r.task_name == task_name]
if not task_results:
return f"No results for task: {task_name}"
lines = []
lines.append(f"\n{'Model':<25} {'Accuracy':<10} {'Latency':<10} {'Cost':<10} {'Tokens':<10}")
lines.append("-" * 65)
for r in sorted(task_results, key=lambda x: x.accuracy, reverse=True):
lines.append(
f"{r.model_name:<25} "
f"{r.accuracy:>9.2%} "
f"{r.latency:>9.2f}s "
f"${r.cost:>8.4f} "
f"{r.token_usage:>9d}"
)
return "\n".join(lines)
def export_results(self, filepath: str):
data = {
'results': [
{
'model': r.model_name,
'task': r.task_name,
'accuracy': r.accuracy,
'latency': r.latency,
'cost': r.cost,
'token_usage': r.token_usage,
'additional_metrics': r.additional_metrics
}
for r in self.results
]
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Exported results to {filepath}")
# Default model specifications
DEFAULT_MODELS = [
ModelSpec(
name="gpt-4",
context_window=8192,
input_price_per_1k=0.03,
output_price_per_1k=0.06,
capabilities=["reasoning", "coding", "analysis"]
),
ModelSpec(
name="gpt-4-turbo",
context_window=128000,
input_price_per_1k=0.01,
output_price_per_1k=0.03,
capabilities=["reasoning", "coding", "vision"]
),
ModelSpec(
name="gpt-3.5-turbo",
context_window=16384,
input_price_per_1k=0.0005,
output_price_per_1k=0.0015,
capabilities=["coding", "text-generation"]
),
ModelSpec(
name="claude-3-5-sonnet-20241022",
context_window=200000,
input_price_per_1k=0.003,
output_price_per_1k=0.015,
capabilities=["reasoning", "coding", "vision"]
),
]
def sample_task(model_name: str, test_case: str) -> Dict[str, Any]:
"""Sample task function for demonstration"""
return {
'output': test_case.upper(),
'usage': {
'prompt_tokens': len(test_case.split()),
'completion_tokens': len(test_case.split()),
'total_tokens': len(test_case.split()) * 2
}
}
def main():
benchmarker = ModelBenchmarker(DEFAULT_MODELS)
test_data = ["hello", "world", "test"]
ground_truth = ["HELLO", "WORLD", "TEST"]
benchmarker.benchmark_task(
task_name="uppercase_conversion",
task_func=sample_task,
test_data=test_data,
ground_truth=ground_truth
)
print(benchmarker.get_comparison_table("uppercase_conversion"))
best = benchmarker.get_best_model_for_task("uppercase_conversion", metric="accuracy")
print(f"\nBest model: {best.model_name}")
if __name__ == "__main__":
main()
"""
Prompt Engineering and Optimization
Tests and optimizes prompts for better performance
"""
import logging
from typing import Dict, List, Any, Optional, Callable
from dataclasses import dataclass, field
from statistics import mean, stdev
import json
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class PromptVariant:
name: str
template: str
description: str = ""
@dataclass
class TestCase:
input_data: Dict[str, Any]
expected_output: Any
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class EvaluationResult:
prompt_variant: str
test_case: str
output: str
score: float
metrics: Dict[str, float]
latency: float
class PromptOptimizer:
def __init__(self, llm_func: Callable):
self.llm_func = llm_func
self.variants: List[PromptVariant] = []
self.test_cases: List[TestCase] = []
self.results: List[EvaluationResult] = []
def add_variant(self, variant: PromptVariant):
self.variants.append(variant)
logger.info(f"Added prompt variant: {variant.name}")
def add_test_case(self, test_case: TestCase):
self.test_cases.append(test_case)
logger.info(f"Added test case: {test_case.metadata.get('name', 'unnamed')}")
def evaluate(
self,
scoring_func: Callable[[str, Any], float],
progress_callback: Optional[Callable] = None
) -> Dict[str, Any]:
logger.info(f"Evaluating {len(self.variants)} prompt variants on {len(self.test_cases)} test cases")
import time
total_evaluations = len(self.variants) * len(self.test_cases)
current = 0
for variant in self.variants:
variant_scores = []
variant_latencies = []
for test_case in self.test_cases:
try:
prompt = self._render_template(variant.template, test_case.input_data)
start_time = time.time()
output = self.llm_func(prompt)
latency = time.time() - start_time
score = scoring_func(output, test_case.expected_output)
result = EvaluationResult(
prompt_variant=variant.name,
test_case=test_case.metadata.get('name', 'unnamed'),
output=output,
score=score,
metrics={'latency': latency},
latency=latency
)
self.results.append(result)
variant_scores.append(score)
variant_latencies.append(latency)
current += 1
if progress_callback:
progress_callback(current, total_evaluations)
except Exception as e:
logger.error(f"Error evaluating {variant.name} on {test_case.metadata.get('name')}: {e}")
return self._generate_report()
def _render_template(self, template: str, variables: Dict[str, Any]) -> str:
try:
return template.format(**variables)
except KeyError as e:
logger.error(f"Missing variable in template: {e}")
raise
def _generate_report(self) -> Dict[str, Any]:
variant_stats = {}
for variant in self.variants:
variant_results = [r for r in self.results if r.prompt_variant == variant.name]
if variant_results:
scores = [r.score for r in variant_results]
latencies = [r.latency for r in variant_results]
variant_stats[variant.name] = {
'mean_score': mean(scores),
'std_score': stdev(scores) if len(scores) > 1 else 0,
'mean_latency': mean(latencies),
'best_score': max(scores),
'worst_score': min(scores),
'total_tests': len(variant_results)
}
best_variant = max(
variant_stats.items(),
key=lambda x: x[1]['mean_score']
) if variant_stats else None
return {
'variant_stats': variant_stats,
'best_variant': best_variant[0] if best_variant else None,
'best_score': best_variant[1]['mean_score'] if best_variant else 0,
'total_evaluations': len(self.results)
}
def ab_test(
self,
variant_a: PromptVariant,
variant_b: PromptVariant,
test_cases: List[TestCase],
scoring_func: Callable,
sample_size: Optional[int] = None
) -> Dict[str, Any]:
"""Perform A/B test between two variants"""
logger.info(f"A/B testing: {variant_a.name} vs {variant_b.name}")
if sample_size:
test_cases = test_cases[:sample_size]
self.variants = [variant_a, variant_b]
self.test_cases = test_cases
results = self.evaluate(scoring_func)
stats_a = results['variant_stats'].get(variant_a.name, {})
stats_b = results['variant_stats'].get(variant_b.name, {})
return {
'variant_a': variant_a.name,
'variant_b': variant_b.name,
'score_a': stats_a.get('mean_score', 0),
'score_b': stats_b.get('mean_score', 0),
'winner': variant_a.name if stats_a.get('mean_score', 0) > stats_b.get('mean_score', 0) else variant_b.name,
'improvement': abs(stats_a.get('mean_score', 0) - stats_b.get('mean_score', 0)),
'is_significant': self._check_significance(
[r.score for r in self.results if r.prompt_variant == variant_a.name],
[r.score for r in self.results if r.prompt_variant == variant_b.name]
)
}
def _check_significance(self, scores_a: List[float], scores_b: List[float]) -> bool:
"""Simple significance check using z-test approximation"""
try:
from scipy import stats
_, p_value = stats.ttest_ind(scores_a, scores_b)
return p_value < 0.05
except ImportError:
logger.warning("scipy not available for significance testing")
return False
def export_results(self, filepath: str):
data = {
'results': [
{
'variant': r.prompt_variant,
'test_case': r.test_case,
'output': r.output,
'score': r.score,
'latency': r.latency,
'metrics': r.metrics
}
for r in self.results
],
'summary': self._generate_report()
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Exported results to {filepath}")
def create_prompt_variants() -> List[PromptVariant]:
"""Create example prompt variants"""
return [
PromptVariant(
name="basic",
template="Summarize: {text}",
description="Basic summarization prompt"
),
PromptVariant(
name="detailed",
template="Provide a detailed summary of the following text:\n\n{text}\n\nInclude key points and main ideas.",
description="Detailed summarization prompt"
),
PromptVariant(
name="structured",
template="Summarize the following text in 3 bullet points:\n\n{text}",
description="Structured output prompt"
),
PromptVariant(
name="role_based",
template="You are a professional editor. Create a concise summary of:\n\n{text}",
description="Role-based prompt"
)
]
def create_test_cases() -> List[TestCase]:
"""Create example test cases"""
sample_text = "Machine learning is a subset of artificial intelligence that enables computers to learn and improve from experience without being explicitly programmed."
return [
TestCase(
input_data={'text': sample_text},
expected_output="Machine learning enables computers to learn from experience without explicit programming.",
metadata={'name': 'simple_summary'}
),
TestCase(
input_data={'text': sample_text},
expected_output="Machine learning: a subset of AI for learning from experience.",
metadata={'name': 'concise_summary'}
)
]
def mock_llm(prompt: str) -> str:
"""Mock LLM function for demonstration"""
return f"Generated response for: {prompt[:50]}..."
def mock_scoring(output: str, expected: str) -> float:
"""Mock scoring function - simple word overlap"""
output_words = set(output.lower().split())
expected_words = set(expected.lower().split())
overlap = len(output_words & expected_words)
total = len(expected_words)
return overlap / total if total > 0 else 0
def main():
optimizer = PromptOptimizer(mock_llm)
variants = create_prompt_variants()
for variant in variants:
optimizer.add_variant(variant)
test_cases = create_test_cases()
for tc in test_cases:
optimizer.add_test_case(tc)
results = optimizer.evaluate(mock_scoring)
print("\n=== Prompt Optimization Results ===")
print(f"Best variant: {results['best_variant']}")
print(f"Best score: {results['best_score']:.2%}")
print("\n--- Variant Statistics ---")
for name, stats in results['variant_stats'].items():
print(f"{name}:")
print(f" Mean score: {stats['mean_score']:.2%}")
print(f" Mean latency: {stats['mean_latency']:.3f}s")
if __name__ == "__main__":
main()