BXUP Track progress & test yourself — free
Data Science & ML/AI · Module · ~14 min read
DATA SCIENCE & ML/AI · MODULE

LLMs as Data Infrastructure

This module installs the engineering that turns an LLM call into pipeline infrastructure: a schema as the contract, validators that re-ask the model with the error in context, constrained decoding as the backstop, and a schema-validity rate watched like an SLO.

  1. 02:14

    The page fires: the nightly job that extracts a structured record from every new support ticket has a 6% parse-failure rate — and 6% of a million rows is sixty thousand tickets that never made it into the table the churn model reads from at 6 a.m.

  2. TWO WEEKS EARLIER

    Karthik, who owns the pipeline, shipped it as a prompt-only json.loads(response) because it demoed perfectly on forty tickets. At pipeline scale, "perfectly on forty" became a recurring nightly page.

  3. THE NEXT MORNING

    Neha spots the shape of it: the failures cluster on tickets that mention a dollar figure, where the model helpfully invented an affected_customers count the ticket never stated — schema-perfect, factually fabricated.

  4. PRIYA, IN CHANNEL

    The only question that matters: "What's the contract, and what happens when the model breaks it?" There wasn't one.

  5. BY END OF WEEK

    The pipeline has a Pydantic schema, validators that re-ask the model with the error in context, a dead-letter queue for records that fail three informed attempts, and a schema-validity rate on a dashboard next to row count.

That is the shift this module is about. The Common Core taught you to call an LLM API and get JSON back. This module is about what changes when that call sits inside a data pipeline: when the output feeds a table, a feature matrix, or a downstream job that runs at 2 a.m. against a million rows. At that point the LLM stops being an assistant and becomes infrastructure — and infrastructure is judged by failure rates, unit costs, and contracts, not by demo quality.

Practitioner reports consistently describe double-digit parse-failure rates for prompt-only JSON extraction in production; at pipeline scale that is not an annoyance, it is a nightly page. The engineering answer is layered, boring, and entirely learnable: schemas as contracts, validation with retries, constrained decoding as the backstop, batch economics for scale, and hard guardrails wherever the model's output becomes executable.

Employers screen for exactly this — Pydantic is the de facto standard in the job descriptions this track was built against, and "structured outputs" has moved from blog topic to interview question. Everything here runs vendor-neutral: the worked code uses Anthropic and OpenAI APIs interchangeably, the patterns transfer to Gemini and open-weights, and every lab fits the track's sub-₹3,000 total API budget.

Key Takeaway:

Treat an LLM in a pipeline like any other unreliable distributed component: define the contract (schema), validate at the boundary, retry with feedback, constrain when you control decoding, and monitor the failure rate like an SLO.

What You'll Learn

  • The three-layer reliability stack for structured outputsvalidate → retry → constrain, with worked Pydantic, Instructor, and constrained-decoding code
  • Batch enrichment at scalebatch APIs at half price, prompt caching, model-tier routing, and sampling QA, with the cost math done in the open
  • LLM-derived features in classical modelshow to feed them in without leaking labels or silently changing feature definitions
  • Text-to-SQL with guardrailsschema retrieval, SQL validation, read-only execution, and the human gates that make it deployable
  • A free-path toolchainopen validators, free API tiers, and a lab budget of roughly ₹200–260 per exercise

The Three-Layer Reliability Stack for Structured Outputs

A schema is a contract; the stack below is how you enforce it with decreasing trust in the model and increasing machinery. Most production extraction needs layers 1 and 2. Layer 3 matters when you self-host or when even a 0.1% failure rate is too expensive.

Layer 1: Schema-native APIs with Pydantic validation

Both major providers now accept a schema and return validated output. Define the contract once, in Pydantic, and let the provider's structured-output mode plus client-side validation do the enforcement. Anthropic's SDK:

from typing import Literal
from pydantic import BaseModel, Field
import anthropic

class TicketRecord(BaseModel):
    product_area: Literal["billing", "auth", "api", "mobile", "other"]
    severity: Literal["p0", "p1", "p2", "p3"]
    summary: str = Field(description="One sentence, no PII")
    affected_customers: int | None = None
    churn_risk_mentioned: bool

client = anthropic.Anthropic()

response = client.messages.parse(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": f"Extract the record from this support ticket:\n\n{ticket_text}",
    }],
    output_format=TicketRecord,
)
record = response.parsed_output   # a validated TicketRecord instance

The OpenAI equivalent, same contract, different spelling:

from openai import OpenAI

client = OpenAI()
response = client.responses.parse(
    model="gpt-5.2",   # use your account's current model id
    input=[{"role": "user",
            "content": f"Extract the record from this support ticket:\n\n{ticket_text}"}],
    text_format=TicketRecord,
)
record = response.output_parsed

Two design decisions matter more than the SDK syntax:

  • Literal/enum fields everywhere a value set is closedfree-text severity fields are how "P1", "p1 ", and "high" end up as three categories in your GROUP BY.
  • Model the "cannot extract" case explicitlyan optional field, or a wrapper with extraction_confidence, rather than letting the model improvise nulls; a schema with no escape hatch forces confident fabrication.
Warning

Structured-output modes guarantee the shape, not the truth. A response can be schema-perfect and factually wrong — affected_customers: 4200 parsed cleanly from a ticket that never mentioned a number, exactly the fabrication that surfaced in Karthik's 2 a.m. page. Shape errors are layer-1's job; content errors are your eval set's job (Modules 5–6 industrialize this).

Layer 2: Validation-with-retry — the Instructor pattern

Semantic constraints — cross-field consistency, business rules, value sanity — live above what a JSON schema can express. The Instructor pattern runs Pydantic validators after parsing and, on failure, re-asks the model with the validation error in context, so the retry is informed rather than a blind re-roll:

import instructor
from pydantic import BaseModel, field_validator, model_validator

client = instructor.from_provider("anthropic/claude-opus-4-8")

class InvoiceRecord(BaseModel):
    invoice_id: str
    line_total: float
    tax: float
    grand_total: float

    @field_validator("invoice_id")
    @classmethod
    def id_format(cls, v: str) -> str:
        if not v.startswith("INV-"):
            raise ValueError("invoice_id must start with 'INV-' — copy it "
                             "verbatim from the document, do not invent one")
        return v

    @model_validator(mode="after")
    def totals_reconcile(self):
        if abs(self.line_total + self.tax - self.grand_total) > 0.01:
            raise ValueError("line_total + tax must equal grand_total; "
                             "re-read the document totals")
        return self

record = client.create(
    response_model=InvoiceRecord,
    max_retries=3,
    messages=[{"role": "user", "content": f"Extract the invoice:\n\n{doc_text}"}],
)

Note what the validators are doing: the error messages are written for the model — they say what to fix and where to look. This is the pattern's whole trick, and it is why hand-rolled while not valid: retry() loops underperform it. Engineering rules for production:

  • Cap retries at 2–3 and log every retry. Retries multiply cost and latency; a rising retry rate on a stable pipeline is your earliest drift signal — the input distribution changed before your dashboards noticed.
  • Route retry exhaustion to a dead-letter queue, never to a default value. A record that failed three informed attempts is exactly the record you want a human to see.
  • Keep validators deterministic and fast. A validator that calls another LLM turns your reliability layer into a reliability problem.

Layer 3: Constrained decoding — guarantee at the token level

When you control the inference stack (open-weights via vLLM, or local models), you can enforce the schema during generation: the decoder masks tokens that would violate the grammar, so invalid JSON is unrepresentable rather than merely retried away. With a vLLM OpenAI-compatible server:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
response = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",
    messages=[{"role": "user", "content": f"Extract:\n\n{ticket_text}"}],
    extra_body={"guided_json": TicketRecord.model_json_schema()},
)

Libraries like Outlines implement the same idea for local pipelines. Three trade-offs to know:

  • It fixes syntax, not semanticsyou still want layer-2 validators.
  • Aggressive constraints can degrade content qualitythe grammar forces the model down low-probability token paths.
  • It is only available where you control decodingwhich is one of the quieter arguments for the self-hosting skills in Module 9.
Pro Tip

Track schema-validity rate and retry rate as first-class pipeline metrics next to row counts and latency. A pipeline that silently absorbs a 4% failure rate by retrying is spending 4% more money and hiding a drift signal.

Batch Enrichment at Scale, with the Cost Math in the Open

Classifying, normalizing, or extracting over large record sets is the workhorse GenAI use case for data teams — and the place where cost discipline is the difference between "approved" and "killed in review." Four levers, applied in order:

1. Batch APIs. Both major providers run asynchronous batch endpoints at roughly half the synchronous price, with results typically inside an hour and a 24-hour ceiling. Batch is the default for anything not latency-sensitive; interactive pricing is for interactive workloads. The shape of the Anthropic version:

import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

client = anthropic.Anthropic()

SYSTEM = [
    {"type": "text", "text": TAXONOMY_AND_RULES,          # shared, stable
     "cache_control": {"type": "ephemeral"}},              # cache the prefix
]

batch = client.messages.batches.create(
    requests=[
        Request(
            custom_id=f"rec-{row.id}",
            params=MessageCreateParamsNonStreaming(
                model="claude-haiku-4-5",        # cheap tier for bulk
                max_tokens=200,
                system=SYSTEM,
                messages=[{"role": "user", "content": row.text}],
            ),
        )
        for row in records
    ]
)

# Poll until ended, then collect — results arrive in ANY order:
for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        out[result.custom_id] = result.result.message
    else:
        dead_letter.append(result.custom_id)   # errored / expired: reprocess

Key by custom_id, never by position; handle errored and expired as re-drivable, not fatal.

2. Prompt caching. Your taxonomy, extraction rules, and few-shot examples are identical across every record — put them first, mark the cache breakpoint, and pay the ~0.1x cache-read rate instead of full price for the shared prefix on every call. The silent killer is a per-record value (timestamp, record ID) interpolated into the shared prefix, which invalidates the cache on every request; volatile content goes after the breakpoint, in the user message.

3. Model-tier routing. Run the bulk pass on the cheap tier; escalate only the records that earn it — validator failures, low-confidence extractions, records the cheap model flagged as ambiguous. A 95/5 split between a $1/M-input model and a $5–15/M-input model changes the cost curve more than any prompt optimization will.

4. Sampling QA. At scale you cannot review outputs; you can review samples. Hold out a stratified 1–2% sample per run, score it (exact-match against a labeled slice where you have one, LLM-as-judge where you don't — calibrated in Module 6), and alert on movement. This is drift monitoring with the same logic you already apply to classical models.

The math for a concrete run — 100,000 support tickets, ~600 input tokens each (500 cached prefix + 100 ticket), ~80 output tokens, cheap tier at $1/M input and $5/M output:

ComponentNaive syncBatch + cache + routing
Input (60M tokens)$6010M uncached × $0.50 + 50M cache-read ≈ $7.50
Output (8M tokens)$408M × $2.50 = $20
Escalation (5% to premium tier)—≈ $3
Total~$100~$30

The lab version runs the same pipeline on 5,000 records for roughly $2–3 (about ₹200–260), which is the design budget for every API-consuming lab in this track. Free inference tiers (Gemini's free tier, Groq and OpenRouter free allowances) cover the prompt-development iterations before you spend anything at all.

Note

Always run a 200-record pilot before the full batch. Every parse failure, taxonomy gap, and prompt bug you find at pilot scale costs a thousandth of what it costs at production scale — and the pilot doubles as your first QA sample.

LLM-Derived Features in Classical Models

The bridge pattern for experienced DS teams: the LLM does not replace your GBM — it manufactures features your GBM could never see. Sentiment, complaint topics, urgency, named entities, "customer threatens to cancel" — extracted with the layer-1/2 stack, typed by the schema, and joined into the feature matrix like any other column:

class TextFeatures(BaseModel):
    sentiment: Literal["negative", "neutral", "positive"]
    topic: Literal["pricing", "reliability", "support", "feature_gap", "other"]
    cancellation_intent: bool
    competitor_mentioned: bool

# batch-extract over the training corpus, join on record id,
# one-hot / ordinal encode, then business as usual:
model = XGBClassifier().fit(X_tabular.join(X_llm_features), y)

Three disciplines keep this honest:

  • Leakage rules apply with full force. The extraction prompt must not see the label or any post-outcome text. If churn_label comes from month N+3 and the ticket text you extract from was written in month N+2, you have built a time machine. Every LLM feature gets the same "knowable at prediction time?" interrogation as any other feature — Module 1's discipline, unchanged.
  • The prompt is part of the feature definition. Change the prompt, the schema, or the model version and you have changed the feature — training/serving skew by another name. Version prompts in git alongside the feature code, pin model versions, and re-extract training data when any of them change.
  • Bake off before you commit. For classification tasks, run the honest three-way comparison: zero/few-shot LLM classifier vs. TF-IDF + GBM vs. GBM-with-LLM-features, scored on quality and cost per 1,000 predictions and p95 latency. The frontier is often surprising — the hybrid frequently wins on quality while the TF-IDF baseline wins on cost by two orders of magnitude, and the right answer depends on which margin your product actually needs. Present the frontier, not a winner; that framing is also exactly what interview loops for senior DS roles now probe.

Embedding-based features belong in the same toolkit — cluster support verbatims with sentence-transformers + HDBSCAN and let an LLM label the clusters — but the retrieval-grade treatment of embeddings starts in Module 3.

Remember:

An LLM feature pipeline is a feature pipeline. Everything your team already enforces — point-in-time correctness, versioned definitions, training/serving parity, drift monitoring — applies verbatim; the only new part is that the "transformation" has a per-call price and a failure rate.

Text-to-SQL with Guardrails

A text-to-SQL copilot over the warehouse is the highest-demand internal LLM product for data teams and the easiest to build irresponsibly. The model is not the hard part; the guardrails are. Layer them the same way you layered extraction:

Schema retrieval, not schema dumping. Beyond a handful of tables, retrieve the relevant subset — table and column descriptions, sample values, join keys — rather than pasting the whole warehouse DDL. A semantic layer (dbt models with descriptions, or a metrics layer) is the highest-leverage input here: it turns "revenue" from a guess over five candidate columns into a governed definition.

Validate before execution. Parse, don't grep:

import sqlglot
from sqlglot import exp

ALLOWED_TABLES = {"analytics.orders", "analytics.customers", "analytics.sessions"}
MAX_ROWS = 10_000

def validate_sql(query: str, dialect: str = "snowflake") -> str:
    tree = sqlglot.parse_one(query, dialect=dialect)   # raises on invalid SQL

    if not isinstance(tree, exp.Select):
        raise ValueError("Only SELECT statements are allowed")

    tables = {t.sql(dialect=dialect).lower() for t in tree.find_all(exp.Table)}
    if not tables <= ALLOWED_TABLES:
        raise ValueError(f"Disallowed tables: {tables - ALLOWED_TABLES}")

    if not tree.args.get("limit"):
        tree = tree.limit(MAX_ROWS)                    # inject a ceiling

    return tree.sql(dialect=dialect)

Execute in a sandbox. The copilot's database role is read-only, scoped to the allowed schemas, with a statement timeout and a bytes-scanned cap. This is defense in depth: the validator should make a destructive statement impossible, and the role makes it impossible again. Prompt injection through user questions ("ignore previous instructions and...") is neutralized by the same two layers — the model can emit whatever it likes; only validated SELECTs against permitted tables ever execute.

Verify the answer, not just the query. A query that parses, passes the allowlist, and executes can still be semantically wrong — the Module 1 SQL failure mode, industrialized. Production text-to-SQL systems add: EXPLAIN-based cost rejection, a reconciliation check where a known control total exists, returning the SQL alongside the result so analysts can audit it, and a feedback control ("wrong answer" button) that feeds the eval set you will build in Module 5. For decision-grade numbers, the copilot drafts and a human approves; full autonomy is earned by measured accuracy on your query distribution, not assumed.

Exam Tip

Guardrail-ordering questions are a favorite: validation happens on the parsed AST before execution, the database role enforces read-only independently of validation, and row/cost limits are injected, not requested politely in the prompt. Any answer that relies on the prompt alone ("instruct the model to only write SELECTs") is the wrong answer.

Tools and the Free Path

  • Contracts and validation: Pydantic + Instructor. Alternatives: provider-native structured-output modes alone (layer 1 only), Outlines for constrained decoding.
  • Provider access: Anthropic / OpenAI / Gemini SDKs; LiteLLM if you want one client surface over all of them for routing and spend tracking.
  • SQL machinery: sqlglot for parsing/validation; dbt or a semantic layer for governed definitions.
  • Free path: Gemini's free tier and Groq/OpenRouter free allowances for development iterations; open-weights + vLLM for constrained decoding without per-token cost; every lab budgeted at $2–3 (~₹200–260) using cheap-tier models, with the full track designed under ₹3,000 total API spend.
Pro Tip

Your first 15 minutes Monday: find the one place your team already does prompt-only extraction into a table (there is always one), and add two dashboard tiles next to its row count — schema-validity rate and retry rate. You cannot fix a failure rate you are not watching, and a rising retry rate is your earliest drift signal, just as it would have caught Karthik's job before 2 a.m.

Check yourself: Your extraction call uses a provider structured-output mode, and every response parses and validates against the Pydantic schema. The pipeline reports 100% schema-validity. Is the data trustworthy?

▸Show answeroptional deep dive — click to expand

No. Structured-output modes guarantee the shape, not the truth — a response can be schema-perfect and factually wrong, like affected_customers: 4200 extracted cleanly from a ticket that never stated a number. Layer 1 (schema-native validation) only catches shape errors; content errors are the job of your eval set (Modules 5–6) and, at the boundary, layer-2 semantic validators (cross-field consistency, business rules) whose error messages are written for the model so the retry is informed. Treat the LLM like any unreliable distributed component: define the contract, validate at the boundary, retry with feedback, and route retry-exhaustion to a dead-letter queue for a human — never to a silent default. A 100% parse rate over fabricated numbers is a false sense of security, not a quality guarantee.

Exercises

  1. Three-layer extraction pipeline. Build the ticket-extraction pipeline against 200 real (or realistic) support tickets: layer-1 schema with Literal fields and an explicit cannot-extract path, layer-2 Instructor validators with model-directed error messages, retries capped at 2, dead-letter queue. Report: schema-validity rate at layer 1 alone vs. with layer 2, retry rate, and the dead-letter contents — with one sentence on what each dead-lettered record teaches.
  2. Batch cost engineering. Run the same pipeline on 5,000 records through the batch API with a cached shared prefix and haiku-tier routing. Produce the cost table: actual spend vs. the naive-synchronous counterfactual, cache-read token share, and escalation rate. Hard budget: $3. If you are over, find it in the levers, not the record count.
  3. Feature bake-off. Pick a text classification task with labels (sentiment sets and support-ticket datasets on Kaggle work). Run the three-way bake-off — few-shot LLM, TF-IDF + GBM, GBM with LLM features — and present the frontier: quality metric, cost per 1,000 predictions, p95 latency. Write the one-paragraph recommendation memo for a hypothetical PM, including the conditions under which your recommendation flips.
  4. Leakage hunt. Take your bake-off's LLM-feature pipeline and deliberately introduce a temporal leak (extract from text that post-dates the label window). Measure the offline lift the leak produces. Keep the number — it is your personal calibration for "too good to be true" on any future LLM-feature result.
  5. Guardrailed text-to-SQL. Build the validator above against a DuckDB or Postgres sandbox with three tables and a read-only role. Attack your own system with ten adversarial questions (injection attempts, DML smuggled into CTEs, cross-schema reads, unbounded scans). Document which layer caught each attack — and fix anything that reached the database.

Your playbooks & labs

This module's playbooks package the working assets: the three-layer structured-output decision card with the Anthropic/OpenAI/Instructor code recipes, the validator-message style guide (errors written for the model), the batch-enrichment cost worksheet with the four levers and pilot-run checklist, the LLM-feature governance card (prompt-as-feature-definition, leakage interrogation, bake-off template), and the text-to-SQL guardrail stack with the sqlglot validator and sandbox role definitions. Do this at work this week → pb-14 Bulletproof Structured Outputs: Schema-Enforced LLM Extraction (Karthik's fix, as the reusable three-layer recipe), pb-16 Batch Enrichment at Scale: Cost and Latency Engineering for Millions of Records (the four cost levers with the pilot-run checklist), and pb-19 Text-to-SQL Copilot with Schema Retrieval and Guarded Execution (the parse-before-execute guardrail stack). Two graded labs anchor the module: L3 is the production extraction pipeline — graded on validity rate, cost against a hard budget, and dead-letter handling; L4 is the guardrailed text-to-SQL copilot — graded on an adversarial attack suite your instructor runs against your validator, not on the happy path.

Further Reading

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in