The most common production pattern for experienced DS teams is not "replace the model with an LLM" — it is LLM-extracted features feeding a gradient-boosted model, and an honest bake-off deciding when a zero/few-shot LLM classifier actually beats TF-IDF + GBM.
Tier: Core · Workflow area: Hybrid Classical + LLM Modeling
This playbook runs both: extract typed features from text at scale, then compare four modeling options on a single cost-quality frontier.
scikit-learn, xgboost (or LightGBM), pydantic, an LLM API key, and the structured-extraction function from pb-14.Fix the evaluation contract first. One held-out test split, one metric (macro-F1 or PR-AUC for imbalanced targets), and a cost-per-1k-predictions column in every result table. Freeze the split before any LLM call so no option gets tuned on test data.
Contender A — classical baseline. TF-IDF + linear model and TF-IDF + XGBoost. This takes 20 minutes and is the bar everything else must clear:
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
baseline = make_pipeline(TfidfVectorizer(ngram_range=(1,2), min_df=3),
LogisticRegression(max_iter=1000, class_weight="balanced"))
Contender B — LLM features → GBM. Define a feature schema and batch-extract it over the text column (use the Batch API from pb-16 for anything above a few thousand rows):
class TextFeatures(BaseModel):
sentiment: Literal["negative", "neutral", "positive"]
mentions_competitor: bool
urgency_1to5: int
topics: list[Literal["billing", "quality", "delivery", "support", "other"]]
One-hot/ordinal-encode these alongside your existing tabular features and train XGBoost. Extract features for train and test with the same model, prompt, and schema version.
Contender C — zero/few-shot LLM classifier. Direct classification via structured output (Literal over your label set), zero-shot and with 8–16 few-shot examples selected per-class. Run it on the cheap tier and the frontier tier — the delta is often smaller than the price gap.
Contender D — small fine-tuned encoder (optional stretch). A DistilBERT/ModernBERT-class model fine-tuned on your training set; the strongest classical-adjacent contender when you have ≥5k labels. Fits free Kaggle GPU hours.
Plot the frontier. One scatter: x = cost per 1k predictions (include amortized extraction cost for B and training cost for D), y = test metric, one point per contender/tier. Add inference-latency as point labels. The decision usually falls out immediately.
Bootstrap the test set (1,000 resamples) and put confidence intervals on every point — a 0.01 F1 gap between contenders is usually noise, and the cheaper option wins ties.
client.messages.parse(..., output_format=Model); use a cheap tier (e.g. claude-haiku-4-5) vs a frontier tier (e.g. claude-opus-4-8) as your two price points — tier routing is the subject here, so measure both.client.responses.parse(..., text_format=Model); mirror the same two-tier comparison.usage token counts in API responses, not guesses.