Industry Context: A high-volume ticket routing system classifies 5,000,000 tickets/month using Claude 3.5 Sonnet ($75,000/month). Distill this narrow domain task into an open-weight Llama 3.1 8B model fine-tuned via LoRA, reducing operating costs to under $2,000/month while maintaining 95%+ agreement with the frontier teacher.
Deliverables
dataset_curation.py: Pipeline sampling 5,000 diverse production tickets, generating gold teacher labels via frontier LLM, and applying automated quality filters.train_lora.py: PEFT/LoRA training script using Hugging Facetransformers,peft, andtrl(SFTTrainer), with 4-bit quantization (QLoRA) and target modules (q_proj, v_proj).eval_canary.py: Evaluation suite proving >=95% agreement with the teacher model across a held-out test set, including a 5% canary shadow routing script.
Starter Code Scaffold
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]
)
# TODO: Initialize base model in 4-bit, attach LoRA adapter, train on curated dataset
100-Point Evaluation Rubric
- Criterion 1: Distillation Dataset Preparation & Quality Gate (25 pts) — Stratified sampling across 12 categories, teacher label verification, edge-case coverage.
- Criterion 2: LoRA / QLoRA Training Configuration (25 pts) — Efficient rank selection ($r=16$), learning rate warmup, evaluation loss tracking, avoiding catastrophic forgetting.
- Criterion 3: Agreement Benchmarking (25 pts) — Rigorous evaluation on held-out test data showing >=95% categorical agreement with the teacher model.
- Criterion 4: Production Deployment & Canary Routing (25 pts) — Gradual rollout strategy (5% canary), automated latency telemetry, and fallback circuit breakers.
Implementation Guide & Case Steps
"You're running classification at 5M tickets/month using Claude Sonnet ($75K/month). Distill it into a fine-tuned Llama 3 8B that runs for $1.5K/month. Walk through the full process."
Step 1 — Validate the economics
Current state:
- 5M tickets/month at Claude Sonnet rates (~$0.015 per ticket) = $75K/month.
Target state:
- 5M tickets/month on fine-tuned Llama 3 8B via Together AI ($0.20 per 1M input + output) ≈ $1K-$2K/month.
Savings: $73K/month. Fine-tuning cost: $100-500 one-time. Hosting (if Together): $0 (just per-token).
Break-even: < 1 week. Worth doing.
Step 2 — Generate the training data
Distillation: use the strong model to label data; train the small model on those labels.
import anthropic
claude = anthropic.Anthropic()
def label_with_claude(ticket_text):
response = claude.messages.create(
model="claude-sonnet-4-6",
messages=[{
"role": "user",
"content": f'''Classify this customer support ticket into ONE category:
- shipping, refund, product, account, billing, complaint, technical, feature_request, partnership, press, other, spam
Ticket: {ticket_text}
Category:'''
}],
max_tokens=20,
)
return response.content[0].text.strip().lower()
# Sample 10K real tickets, label each
import json
tickets = load_random_tickets(n=10000)
training_data = []
for t in tickets:
label = label_with_claude(t["text"])
training_data.append({"input": t["text"], "output": label})
if len(training_data) % 100 == 0:
print(f"Labeled {len(training_data)}/{len(tickets)}")
with open("training_data.jsonl", "w") as f:
for ex in training_data:
f.write(json.dumps(ex) + "\n")
Cost of labeling: 10K tickets × ~$0.015 = $150. Cheap.
Step 3 — Audit a sample manually
Don't trust Claude blindly. Sample 100-200 examples and verify labels:
import random
sample = random.sample(training_data, 100)
for ex in sample:
print(f"INPUT: {ex['input'][:200]}")
print(f"CLAUDE LABEL: {ex['output']}")
your_label = input("Correct? (y/n/<label>): ")
if your_label == "n":
new_label = input("Correct label: ")
ex["output"] = new_label
If Claude is >95% accurate on your sample, training data is good enough. If <90%, improve the labeling prompt before continuing.
Step 4 — Format for fine-tuning
Most fine-tuning services expect a specific format:
{"messages": [{"role": "user", "content": "Classify: ..."}, {"role": "assistant", "content": "shipping"}]}
{"messages": [{"role": "user", "content": "Classify: ..."}, {"role": "assistant", "content": "refund"}]}
Save in JSONL format:
with open("train.jsonl", "w") as f:
for ex in training_data:
f.write(json.dumps({
"messages": [
{"role": "user", "content": f"Classify this ticket: {ex['input']}"},
{"role": "assistant", "content": ex["output"]}
]
}) + "\n")
Split into train (90%) and validation (10%).
Step 5 — Run the fine-tune
Using Together AI (open model fine-tuning):
import together
together.api_key = os.environ["TOGETHER_API_KEY"]
# Upload training data
resp = together.Files.upload(file="train.jsonl", check=True)
train_file_id = resp["id"]
resp = together.Files.upload(file="val.jsonl", check=True)
val_file_id = resp["id"]
# Start fine-tuning
job = together.Finetune.create(
training_file=train_file_id,
validation_file=val_file_id,
model="meta-llama/Llama-3-8b-chat-hf",
n_epochs=3,
lora=True, # LoRA for efficiency
learning_rate=1e-5,
)
print(f"Fine-tune started: {job.id}")
Wait for completion (1-4 hours typical for 10K examples). Together returns a fine-tuned model name like llama-3-8b-acme-classifier.
Step 6 — Evaluate the fine-tuned model
Use the SAME eval set as before (100 manually-labeled tickets):
def eval_finetuned():
model = "llama-3-8b-acme-classifier"
results = []
for ex in eval_set:
response = together.Chat.completions.create(
model=model,
messages=[
{"role": "user", "content": f"Classify this ticket: {ex['ticket']}"}
],
max_tokens=20,
)
prediction = response.choices[0].message.content.strip().lower()
results.append({
"ticket": ex["ticket"],
"expected": ex["label"],
"predicted": prediction,
"correct": prediction == ex["label"],
})
return sum(r["correct"] for r in results) / len(results)
print(f"Fine-tuned accuracy: {eval_finetuned()}")
print(f"Original Claude Sonnet accuracy: 91%")
Typical result: fine-tuned 8B model gets 85-90% accuracy. Close to but not equal to Claude Sonnet's 91%.
If acceptable, proceed. If not, iterate:
- Add more training data.
- Increase epochs.
- Improve labeling quality (better Claude prompt or human review).
Step 7 — Deploy the fine-tuned model
# In production
def classify_ticket(ticket_text):
response = together.Chat.completions.create(
model="llama-3-8b-acme-classifier",
messages=[
{"role": "user", "content": f"Classify this ticket: {ticket_text}"}
],
max_tokens=20,
temperature=0, # deterministic
)
return response.choices[0].message.content.strip().lower()
Rollout strategy:
- Week 1: 5% canary, A/B against Claude Sonnet (shadow mode).
- Week 2: 50/50 split, compare metrics.
- Week 3: 100% on fine-tuned model, holdout 5% on Claude for ongoing comparison.
Step 8 — Monitor in production
-- Compare quality across the holdout split
SELECT
model_version,
COUNT(*) AS requests,
AVG(latency_ms) AS avg_latency,
AVG(cost) AS avg_cost,
SUM(cost) AS total_cost,
-- Match against eventually-labeled ground truth
AVG(CASE WHEN predicted = true_label THEN 1.0 ELSE 0.0 END) AS accuracy
FROM ticket_classifications c
LEFT JOIN ticket_labels l ON l.ticket_id = c.ticket_id
WHERE c.served_at >= NOW() - INTERVAL '7 days'
GROUP BY model_version;
Track over time. If fine-tuned model's accuracy degrades (data distribution shift), retrain.
Step 9 — Plan retraining cadence
Domain data shifts. Quarterly retraining is a reasonable default:
# .github/workflows/quarterly_retrain.yml
on:
schedule:
- cron: '0 0 1 1,4,7,10 *' # 1st of Jan, Apr, Jul, Oct
jobs:
retrain:
runs-on: ubuntu-latest
steps:
- run: python scripts/retrain_classifier.py
- run: python scripts/evaluate_new_classifier.py
- run: python scripts/promote_if_better.py
If retrained model is meaningfully better on eval, promote it. If not, keep current.
Result
- Cost: $75K/month → $1.5K/month (~50x reduction).
- Accuracy: 91% → 88% (3 points lower; acceptable for this use case).
- Latency: comparable (Together AI's 8B model is fast).
- Quality monitoring: ongoing via 5% holdout on Claude.
What this case teaches
- Distillation: frontier model labels → smaller model trains → cheap production.
- Manual data audit is essential — Claude isn't perfect.
- LoRA + Together = cheap, fast fine-tuning.
- Maintain a holdout on the original model for ongoing comparison.
- Quarterly retraining for distribution shift.