Skip to content
Chapter 105Lesson 1

When a feature needs an LLM

Opens the AI unit with a framework for deciding when a feature needs a language model rather than a cheaper query, form, or pipeline.

A stakeholder drops a line into the sprint: “Can we add AI to this?” The junior reflex is to open a branch and wire a chat box onto the dashboard.

That skips the only question that matters. “Add AI” is not a feature, it is a mood. A model is the most expensive, slowest, least predictable component you can bolt onto a product, and most of the time the stakeholder wants something a Server Component and a SQL query already return in two milliseconds without ever hallucinating. So before writing any code, ask whether this surface is really shaped for a probabilistic model, or whether you are paying tokens and latency for a worse version of a feature you could ship deterministically today.

Most 2026 SaaS still ships without an LLM-backed surface, and that is the correct call. Run your product through the four triggers below, find it hits none, and that is the expected outcome, not a failure. What you leave with is four product shapes that justify reaching for a model, the look-alikes that don’t, and one sentence to carry through all of it: the model never owns data; tools own data, the model orchestrates language.

A large language model earns its place on a surface only when that surface needs one of three things ordinary code cannot produce: open-ended natural-language input no form could have captured in advance, natural-language output that only prose can carry, or orchestration across steps whose shape depends on what the input turns out to be. A surface that needs none of those is a query and a component, nothing more.

The line is structural because a model is probabilistic : a WHERE status = 'overdue' clause returns the same rows every time, while a model asked “which ones are overdue” might miss one. That unpredictability powers language you couldn’t anticipate but disqualifies anything that must be exact. So the question under every trigger below is the same: is natural language essential here, or are you dressing up a lookup?

The four triggers that cross the threshold

Section titled “The four triggers that cross the threshold”

Four shapes, and for this course only four, call for a model. Each leads with why the deterministic path fails, then the example. If you can’t say why a form, a query, or a fixed pipeline can’t do the job, you haven’t found a trigger.

A user on an invoices dashboard wants to know which clients are overdue and trending worse than last quarter. The answer is entirely a function of structured data you already have, yet the deterministic path fails at both ends. Server-side SQL can’t read the question: there is no form field for “and trending worse than last quarter,” and you can’t pre-build a form for every question a human might phrase. A search box can’t compose the answer either; it returns rows, not a sentence that says “three clients slipped, and Acme is your biggest exposure.”

That two-sided gap, language in and prose out, is the signature of this trigger. The model reads the question, decides it needs real numbers, and calls a tool you wrote, say getInvoiceStats(...), to fetch them from Postgres, then writes the prose around those numbers. It never invents a balance or a due date because it doesn’t have your data: the tool owns the data, and the model orchestrates the language. The course builds this surface later.

Generation of structured artifacts from prompts

Section titled “Generation of structured artifacts from prompts”

A user types “monthly retainer, 40 hours at our standard rate, net 30” and expects a filled-in invoice line item, with description, quantity, unit price, and payment terms each in its typed slot. A form handles this when the input space is small. But once free text maps to a typed structure in too many ways to enumerate with dropdowns, you are no longer building a form; you are building a parser for natural language, and that is the model’s job.

The model’s task is narrow: map free text onto a known schema. It does not write the row to your database; your deterministic code does that, after validating it like any untrusted input. The deciding test is simple: if a dropdown and a number field would have captured the input, build the form. The model earns its place only when the input space is too varied for one.

Classification or extraction over unstructured text

Section titled “Classification or extraction over unstructured text”

Consider inbound support emails to route by intent, contract PDFs whose key terms must become structured metadata, or a salesperson’s free-form call notes that need tagging. The common thread is messy human text that no regex was going to parse reliably, so the model is the only reasonable tool for reading it.

What makes this trigger recognizable: the input is messy and open-ended, but the output is small and tidy, an enum, a short object, or a handful of tags. Deterministic code takes that clean output and does the rest: routes the ticket, files the metadata, updates the record. The model is a translator at the edge of your system, turning chaos into structure so the rest of your code never has to look at the chaos.

Agentic workflows the user couldn’t run by hand

Section titled “Agentic workflows the user couldn’t run by hand”

Consider the request: “Find every customer with an overdue invoice, draft a reminder for each in a tone that matches how we’ve talked to them before, and queue the drafts for a human to approve.” Each step is deterministic: query the overdue customers, fetch prior correspondence, generate a draft, write to a queue. What is not fixed is the path through them: how many customers there are, what each one’s history looks like, and which need a gentle nudge versus a firmer one. The model’s job is the orchestration, deciding what to do next based on what the previous step returned.

Here is the qualifier juniors over-reach on: this counts only when the steps genuinely vary by input. If you always run A, then B, then C regardless of the data, that is not an agent; it is a pipeline, and a pipeline is just a function you write. Wrapping a fixed sequence in a model pays a decision-maker to make a decision that was never in question.

All four reduce to one need a form, a query, and a fixed pipeline cannot meet: open-ended language coming in or going out, or orchestration whose shape depends on the input. Everything else has a cheaper, faster, more reliable default.

The most expensive beginner mistake is reaching for a model on a workload a query or a form already solves: slower, more expensive, and occasionally wrong where the deterministic version is always right. Here are the five most common impostors, each paired with the cheaper default you already know how to build.

Smart search over your own data. “AI-powered search” across the invoices, customers, and notes almost always means the user wants good search, and Postgres already ships it. Full-text search and pg_trgm trigram similarity cover the overwhelming majority of the workload, at zero per-query cost and zero hallucination risk.

Form auto-fill from one typed field. A country code fills in the currency; a chosen customer populates the billing address. This is an onChange handler and a deterministic lookup, a map or a single query: faster than any model, free per keystroke, and incapable of guessing wrong.

“Make the UI feel modern.” Here “add AI” means “the demo would look impressive with a chat box in it” — a pitch-deck decision, not a product one. The user pays the latency, you pay the tokens, and the surface is worse than the buttons and filters it replaced.

Categorizing a fixed, knowable enum. Sorting transactions into one of five accounting buckets defined at design time is a switch statement, a set of rules, or at most a tiny purpose-trained classifier; a general-purpose model is wildly overpowered for choosing among five known options. This one sits right next to the extraction trigger, so ask one question: are the categories knowable at design time? If you can write the complete list of buckets today and a rule could plausibly assign them, use the switch or the classifier. If the input is open-ended text you have to actually understand, like a messy support email or a freeform contract, it’s the extraction trigger and the model earns its place.

Replacing existing search and filter with chat. “What if, instead of filters, users just ask the dashboard what they want?” Chat is a poor primary affordance for scanning a list: slower, less precise, and it hides the structure users rely on. Add chat alongside the affordances people already know; never replace them. Take away the filter bar and you’ve shipped a regression with a token bill attached.

Now make the cut yourself. For each request below, decide whether a model genuinely earns its weight or whether a deterministic default does the job better and cheaper. Drag each into its bucket, then check.

For each request, decide whether the surface is genuinely LLM-shaped, or whether a deterministic default is the better tool. Drag each item into the bucket it belongs to, then press Check.

LLM earns it Open-ended language or input-varying orchestration
Deterministic default A query, form, or fixed rule does it better
Triage incoming support emails into the team that should handle each one
Pull the renewal date, party names, and total value out of an uploaded vendor contract
Let a finance manager type a question about this quarter’s revenue and get a written answer
Draft a personalized win-back message for each churned customer based on their account history
Suggest a city as the user types the first few letters into an address field
Show only invoices that are unpaid and older than 30 days
Sort each expense into one of the four tax categories the accountant defined
Find customers whose company name nearly matches what the user typed, ignoring typos

The two worth pausing on are the contract extraction and the tax-category sort: both look like “pull structure out of a document,” yet the contract is open-ended text you have to read, while the tax categories were knowable at design time.

The triggers and anti-triggers are two separate lists; asking the questions in a fixed order turns them into one procedure. Walk the funnel below, picking the true answer for a feature you’re weighing, and the leaf you land on is the verdict.

Run a feature through the filter

The shape of the walk carries the rule: every branch has an exit to a cheaper default, and the four model verdicts sit at the end of the longest paths. The model is where you arrive when nothing cheaper will carry the language, never where you start.

Why the Vercel AI SDK is the Next.js default

Section titled “Why the Vercel AI SDK is the Next.js default”

Once a feature lands one of the four triggers, the question is what you reach for. For a Next.js team in 2026, the answer is the Vercel AI SDK , for three reasons.

It owns the React 19 streaming model. When a model generates a long answer, you want the text to appear as it’s produced, not a spinner until the last word arrives. The SDK streams those pieces and partial objects to compose with Suspense and Server Components by design, not as something you bolt on afterward.

Provider abstraction is first-class. The model behind a feature sits behind a single identifier, so swapping from one provider to another is a one-line change rather than a rewrite of every call site. That matters when a stronger model ships from a different company, which happens often.

The surface is tight. You work with five primitives: streamText, generateText, generateObject, streamObject, and the useChat / useCompletion hooks. That’s the whole vocabulary, with no forty-concept framework to learn before you ship one feature.

Two alternatives exist, and since you will see them elsewhere it’s worth knowing why the course doesn’t pick them. Calling a provider’s SDK directly, OpenAI’s or Anthropic’s own client, welds every call site to that one vendor and loses the unified streaming shape; reach for it only for a brand-new provider feature the AI SDK hasn’t surfaced yet. Hosting LangChain on the server brings a heavier model of chains, agents, and retrievers, plus a streaming primitive that fights the App Router; it earns its place for research-style multi-agent orchestration off the request path, not the user-facing surfaces this course is about. For a Next.js web app, the AI SDK is the pick.

Once you start building, you will search for help, and much of what you find, including code a model generates from older training data, will hand you AI SDK v4 shapes. The SDK was redesigned in v5, stable since mid-2025, so v4 shapes aren’t just dated, they’re wrong against the current SDK. You don’t need the v5 APIs yet, only the ability to spot a v4 fingerprint on sight: a flat .content string on each message, append and reload to send and retry, a hook that owns the input state for you, or maxSteps on the client to bound an agent loop. Those shapes come from the next two chapters in their v5 form; for now they only need to set off the alarm when a search result looks wrong.

The architectural shape: server calls, client streams

Section titled “The architectural shape: server calls, client streams”

Every LLM call runs on the server, the Client Component subscribes to the resulting stream through the SDK’s hooks, and the provider key never reaches the browser. This is the same server-seam rule you learned for authedAction and authedRoute: privileged work lives behind a server boundary, and the client only ever talks to that boundary. The AI SDK enforces this by shape, because its React hooks talk to a server endpoint rather than a provider, so there is nowhere to put a key on the client.

flowchart LR
  client["<b>Client Component</b><br/><code>useChat</code><br/><i>no key — runs in the browser</i>"]
  server["<b>Server route handler</b><br/>auth + provider key<br/>🔑 <i>key lives only here</i>"]
  provider["<b>Provider</b><br/><i>OpenAI · Anthropic · …</i>"]

  client -- "request" --> server
  server -- "model call" --> provider
  server -. "streamed response" .-> client

  class client browser
  class server seam
  class provider external
  classDef browser fill:#dbeafe,stroke:#1d4ed8,color:#111,stroke-width:2px
  classDef seam fill:#bbf7d0,stroke:#15803d,color:#111,stroke-width:3px
  classDef external fill:#1f2937,stroke:#94a3b8,color:#f8fafc,stroke-width:2px
Every model call goes through the server seam; the provider key lives only there, never in the browser.

Cost guards and provider configuration both live inside that middle box.

An LLM-backed surface passes the three-test rule for when a decision deserves an ADR . It touches multiple parts of the system: a route handler, a schema, a client component, environment config, and likely billing. Real alternatives exist: ship no model, pick a different provider, or lean on a hosted service. And reversal is expensive, because a live surface has already dragged in a rate limiter, a per-user quota, audit-log events, and possibly Stripe metering, none of which unwind in one commit. So when this shape lands in a real codebase, write the ADR: capture which trigger justified it, which alternatives you weighed, and what reversing it would cost.

The worked surface this course builds later ships no ADR, because the habit attaches to the decision, not to this project.

“Can we add AI?” is a how, not a what. Ask back what user problem it solves and whether that problem hits one of the four triggers. If it doesn’t, the professional answer is the cheaper deterministic surface, not a chat box that satisfies the literal request.

Two traps catch teams under deadline pressure. The first is “we’ll worry about cost later”: the moment the surface is public, every authenticated user can spend your money in tokens as fast as they type, so abuse is a day-one problem the next lesson is entirely about bounding. The second is reaching for a provider’s SDK because a launch blog used it; that welds your call site to one vendor, which is exactly what the AI SDK exists to keep reversible.

Each request below arrives the way they actually do: some hit a trigger, some are impostors in a trigger’s clothes, and one hits nothing and should ship no model. The deciding question is always which of the four shapes, if any, this really is.

A teammate pitches it as the headline AI feature: a single box where a user types anything — a half-remembered company name, a typo’d contact, a partial domain — and the matching customer surfaces instantly. The ticket title is literally “AI customer search.” Before you size it, what shape is this actually?

A forgiving lookup over rows you already own — pg_trgm similarity ranks the closest matches and ships today with no per-query bill.
Trigger 1: the user is typing free text, so the model has to read the query and answer it.
Trigger 3: the model extracts the intended customer name out of the messy input.
An agentic flow: the model decides whether to search by name, email, or domain each time.

Support is drowning in long inbound emails. The ask: as each one arrives, automatically produce a two-line gist plus a short list of suggested next actions, so an agent grasps the ticket without reading the whole thread. Which of the four triggers, if any, does this land?

Trigger 3 — the input is unbounded human prose and the output is a small, tidy structure the downstream UI consumes.
No trigger — every email routes to one of a fixed set of teams, so a rules table covers it.
Trigger 4 — reading then summarizing then listing actions is three steps, so the model must orchestrate them.
No trigger — a summary is just the first few sentences truncated, which is plain string work.

Sales leadership wants the dashboard to “feel like AI.” Concretely: take the exact revenue and overdue-invoice figures the page already renders and, on load, restate each KPI as a friendly sentence — “Revenue is up 4% this month” instead of a bare number. No new question is being asked; the inputs are the same metrics as today. Which trigger justifies a model here?

None — keep it a query and a component. Pretty-printing numbers nobody asked a question about is the “make it modern” impulse, not a trigger; the right move is to ship no model.
Trigger 1 — it emits prose about your app’s data, which is grounded Q&A.
Trigger 2 — it generates the sentence artifacts from the underlying figures.
Trigger 3 — it reads the KPIs and classifies each into a sentence form.

If those felt like judgment calls rather than recall, that’s the point: the filter only earns its keep on inputs you’ve never seen phrased this way.