Lightning Lesson · Tue 30 June 2026 · 30 min · live

Your first LLM feature,
beyond the quickstart.

The quickstart gets you a working API call in ten minutes. Then real users arrive and it falls over. Tonight is the other 90%: the five things that break a first LLM feature, how to turn a vague prompt into a contract, and the one line that decides whether it ships, between the model and your code.

Ehsan Gazar
Principal Engineer · 16 years in production · 500+ mentees
Where we're going · 30 minutes

The run sheet.

0–3The reframethe demo lies, production is the 90%
3–14The five killerstokens, errors, cost, input, vague prompts
14–21Prompt → contractstructured output, constrained input
21–27Model vs codethe highest-leverage decision
27–30Recap and what's nextyour ship checklist + the cohort
By the end of tonight

You'll be able to…

1Name and pre-empt the five things that break a first LLM feature: token limits, error and rate-limit handling, context cost, raw user input, and vague prompts.
2Turn a vague prompt into a contract, with structured output and constrained input, so the model's job stays small, predictable, and testable.
3Draw the line between model and code: what to hand the LLM versus what to keep in plain deterministic code, the highest-leverage decision in any LLM feature.
The reframe

The API call is ten minutes.
The feature is everything else.

Over 60% of AI pilots never reach production, and it is almost never the model. A quickstart shows you a happy-path call with a clean string and one polite user. Production hands you long inputs, flaky networks, a real bill, hostile text, and prompts that quietly drift. The gap between a demo and a feature is engineering, not prompting.

💡 In plain English

The quickstart is a kid on a bike in an empty car park. Shipping is the same bike in rush-hour traffic. Same machine, completely different problem, and tonight is about the traffic.

The whole talk in one frame

Three parts.

1
The five killers
The same five things break almost every first feature. Spot them before users do.
what goes wrong
2
Prompt → contract
Shrink the model's job to something small, structured, and testable.
make it predictable
3
Model vs code
Decide what the LLM owns and what stays deterministic. The decision that decides everything.
where it ships or dies
Part 1 · what breaks a first LLM feature

The five killers.

1
Token limits
Inputs grow past the window and calls start failing on real data, not your test string.
2
Error & rate limits
No retry, no backoff, no timeout. One 429 or blip takes the feature down.
3
Context cost
You resend the whole history every turn. The bill and latency climb with each message.
4
Raw user input
Untrusted text goes straight into the prompt. Hello injection, hello garbage in.
5
Vague prompts
"Summarize this nicely" gives a different shape every call. Nothing downstream can rely on it.
Notice: four of the five are ordinary engineering, limits, retries, cost, untrusted input. Only the last is about the model.
Killers 1 & 2 · tokens and failure

Treat the call
like any flaky network call.

A model call is a slow request to a service that can be full, rate-limited, or down. The quickstart skips all of it. You don't get to.

Do this

Count tokens and truncate or chunk before you send. Wrap the call in a timeout, retry with backoff on 429 and 5xx, and have a fallback for when it still fails.

// the quickstart
const res = await client.messages.create({ model, messages });
return res.content;            // 🤞 hope it fits, hope it works

// beyond it — TypeScript, Anthropic SDK
const client = new Anthropic({ maxRetries: 4 }); // retries 429 / 5xx
const body = await clipToBudget(text, TICKET_TOKEN_BUDGET);
try {
  return await triage(body, { timeout: 20_000 }); // bound the call
} catch (err) {
  if (err instanceof Anthropic.RateLimitError)
    return queueForLater();   // honest fallback, not a crash
  throw err;
}
Killer 3 · context cost

You pay for the whole
history, every single turn.

The model has no memory, so most people resend the entire conversation each call. By message twenty you are paying for nineteen old ones to get one new reply, slower and pricier every turn.

Do this

Send only what this turn needs: a rolling window, a running summary, or just the few relevant facts. Context is a budget you spend on purpose, not a log you append to.

flowchart TB
  subgraph naive["Resend everything (cost grows)"]
    direction LR
    n1["msg 1..19"]:::old --> n2["+ new msg"]:::new --> n3["LLM"]:::model
  end
  subgraph lean["Send what's needed (cost flat)"]
    direction LR
    l1["running summary"]:::win --> l3["LLM"]:::model
    l2["last few + new"]:::new --> l3
  end
  classDef old fill:#FEE4E2,stroke:#0D1B33,color:#0D1B33;
  classDef new fill:#DCEBFE,stroke:#0D1B33,color:#0D1B33;
  classDef win fill:#FEF3C7,stroke:#0D1B33,color:#0D1B33;
  classDef model fill:#D6F5E3,stroke:#0D1B33,color:#0D1B33;
          
Killer 4 · raw user input

User text is untrusted,
even inside a prompt.

Concatenating raw input into your prompt is the LLM version of string-building a SQL query. A user can write "ignore the above and…" and now they are steering your system.

Do this

Keep instructions and user data in separate roles or clearly delimited blocks. Validate and size-check input first, and never let model output act on anything without a check in code.

flowchart LR
  uIN["User input"]:::in --> uV{"Validate and delimit"}:::q
  uV -->|too long / junk| uR["Reject or trim"]:::warn
  uV -->|ok| uP["Data block, separate from instructions"]:::win
  sys["Your instructions"]:::ok --> uLLM["LLM"]:::model
  uP --> uLLM
  uLLM --> uC{"Check in code"}:::q
  uC -->|valid| uOUT["Use it"]:::ok
  uC -->|not| uR
  classDef in fill:#DCEBFE,stroke:#0D1B33,color:#0D1B33;
  classDef q fill:#FEF3C7,stroke:#0D1B33,color:#0D1B33;
  classDef win fill:#EEE6FF,stroke:#0D1B33,color:#0D1B33;
  classDef ok fill:#D6F5E3,stroke:#0D1B33,color:#0D1B33;
  classDef warn fill:#FEE4E2,stroke:#0D1B33,color:#0D1B33;
  classDef model fill:#D6F5E3,stroke:#0D1B33,color:#0D1B33;
          
Part 2 · killer 5, fixed

Turn the prompt
into a contract.

"Summarize this nicely" is a wish. A contract names the inputs, the exact output shape, and the rules, so the model's job is small and the same every time.

💡 In plain English

Stop asking the model to be clever. Ask it to fill in a form. A form you can validate, test, and trust the next line of code to read.

// a wish
"Summarize this ticket nicely."

// a contract — the model's whole job is to fill in this form
const TicketTriage = z.object({
  summary:  z.string(),        // one line, <= 200 chars
  category: z.enum(["billing", "bug", "account", "other"]),
  urgency:  z.union([z.literal(1), z.literal(2), z.literal(3)]),
});

// then, in code — parse + validate against the contract
const res = await client.messages.parse({
  output_config: { format: zodOutputFormat(TicketTriage) },
  messages: [{ role: "user", content: `<ticket>\n${body}\n</ticket>` }],
});
const triage = res.parsed_output; // typed & validated — safe to use
A contract has two sides

Constrain the input.
Pin the output.

Constrained input
Give the model a narrow, labelled set of fields, not a wall of free text. Delimit user data, drop what's irrelevant, and the smaller, cleaner job is far harder to get wrong.
Structured output
Demand a schema, JSON, an enum, a fixed set of keys, and validate it in code. If it doesn't parse, retry or fall back. Now downstream code can actually depend on it.
Why it pays

A small, structured job is testable. You can build a handful of example inputs with expected outputs and catch a regression when you tweak the prompt, the thing you simply cannot do with "nicely".

Part 3 · the line that decides everything

Model versus code:
give each what it's good at.

The jobHand it to…Why
Understand messy languagethe modelfuzzy, ambiguous, no fixed rules
Decide a category or intentthe modeljudgement over unstructured text
Math, dates, totals, lookupscodemust be exact and repeatable
Permissions, payments, writescodenever let probabilistic output act unchecked
Validation & control flowcodedeterministic, testable, cheap
The rule: the model proposes, code disposes. Use the LLM for judgement on language; keep truth, actions, and arithmetic in code.
Where it all lands

The shape of a feature that ships.

code
Validate & size input
untrusted text, token budget, delimit
model
The contract call
small job, structured output, retried
code
Parse & validate
schema check, retry or fall back
code
Act & observe
take the action, log cost & latency
One thin model call in the middle. A wrapper of plain, testable code around it. That's a first LLM feature that survives real users.
The same shape, in real TypeScript

Code · model · code.
The whole feature.

async function processTicket(raw: string) {
  // 1 · code — validate & size the untrusted input
  const input = validateInput(raw);
  if (!input.ok) return reject(input.reason);
  const body = await clipToBudget(input.value, TICKET_TOKEN_BUDGET);

  // 2 · model — the thin contract call (retried, structured)
  let t: TicketTriage;
  try { t = await triage(body); }
  catch (err) { return routeToHuman(err); }  // honest fallback

  // 3 · code — decide & act. The model proposes, code disposes.
  const { queue, sla, escalate } = route(t); // deterministic
  return dispatch(queue, sla, escalate);
}
One thin model call, plain testable code on both sides. This is processTicket from the demo, run the whole thing with npm run feature.
Recap · the ship checklist

Wrap a small model call
in code you can trust.

Before you ship: tokens budgeted, calls retried, context trimmed, input validated, prompt a contract, output schema-checked, and every action in code, not in the model. The quickstart was the easy 10%. Do these and you're in the 40% that reach production.

Go deeper · the cohort

Tonight was the first feature.
The cohort is the whole system.

Production-Ready Systems with LLMs and Agents: An Intensive for Engineers. Six weeks, twelve live sessions (Tue/Thu), five graded projects where you ship real LLM and agent systems, not notebooks. Jul 13 to Aug 23.

Code: AGENTS300
$300 off, so $1,200 instead of $1,500. The same code is in your recording email.
early-bird · capped at 12 on purpose
Enrol
maven.com/gazar/
production-ready-systems-with-llms-and-agents-an-intensive-for-engineers
link is in the chat
Ehsan Gazar
Principal Engineer · 500+ mentees · me@gazar.dev