Reliable outputs & tool use
Making models produce things downstream systems can trust.
A model that is right 95% of the time will break a workflow that calls it a hundred times. This module is about closing that gap. It turns a stochastic text generator into a component whose outputs and actions downstream systems can trust.
- Structured output — getting valid, schema-conformant data out, with validation, repair loops, and fallback chains for when you don't.
- Function calling — reliable tool invocation: contracts, argument validation, and idempotency so retries are safe.
- Agent guardrails — loop budgets, tool budgets, and termination conditions so an agent can't run away.
- Model routing — choosing models per request and degrading gracefully instead of failing hard.
The throughline: never trust model output as if it were a typed return value. Treat it as untrusted input to be validated, bounded, and made safe to act on. This is the harness doing its job.
Connects to other tracks
- Tool calling — the same contract discipline at product-decision altitude, plus permissions and blast radius.
- Tools & function calling — the agent's view of the same tool contracts.
- Tool engineering in the harness — building reliable tools by hand.
- Service integration & error handling (Flowable) — the same idempotency and retry discipline in a process engine.
📌 Close out the module: Recap & real-world examples — war stories from production plus the key takeaways.
Agent budget simulator — what stops a runaway?
Structured output: validation, repair loops, and fallback chains
TL;DR
When a downstream system expects JSON, or any schema, "usually valid" is a production incident waiting to happen. Reliable structured output is a pipeline, not a prompt: constrain generation where you can, validate every output against a schema, repair failures by feeding the error back, and fall back through progressively safer options so a malformed response never reaches your business logic. The goal is a component that returns valid data or a clean, typed error, never garbage.
🎯 For the AI-native PM
Why it matters — Most "the AI broke the workflow" incidents are a malformed output that a downstream system trusted. This is reliability your users — and your integrations — feel immediately.
What it changes in your decisions — Your integration/API commitments, your error budget, and what "done" means for an AI feature that feeds other systems.
Ask your eng team — "When the model returns invalid output, does it crash, retry, or degrade gracefully?"
Product risk if ignored — One parse error takes down an entire automated workflow and erodes trust in the whole feature.
Mental model
Model output is untrusted input. You wouldn't JSON.parse() a request
body and use it without validation. Don't do it with a model either. The
pipeline:
generate ──▶ parse ──▶ validate(schema) ──▶ ✅ typed object
▲ │ │
│ └─ fail ─────┤
│ ▼
└───── repair loop ◀── error fed back (bounded retries)
│ still failing
▼
fallback chain ──▶ safer model / stricter mode / default / typed error
Layer 1 — Constrain generation (prevent, don't just detect)
The cheapest invalid output is the one that can't be produced.
- Constrained / structured decoding — grammar- or schema-guided, such as JSON-schema modes, GBNF grammars, "JSON mode," or tool/function schemas — masks the token distribution so the model can emit only schema-valid tokens. This makes syntactic validity near-certain.
- Caveat: constrained decoding guarantees the output parses and fits
the schema, not that it's semantically correct. A grammar can force
{"age": 200}to be valid JSON, but it can't make 200 a sensible age. You still need validation. - Constrained decoding can interact with quality — over-tight grammars can push the model into awkward continuations. Pair it with clear schema descriptions.
Layer 2 — Validate (always, even with constrained decoding)
Validate every output against a real schema (Pydantic, JSON Schema, zod, protobuf):
- Syntactic: does it parse and match types and required fields?
- Semantic: are values in range, are enums legal, do references resolve,
do invariants hold? (
end_date > start_date,total == sum(items), ids that exist.) - Validation produces a precise, machine-readable error, which is the fuel for the repair loop.
Layer 3 — Repair loop (bounded)
On validation failure, send the model the invalid output plus the specific error and ask it to fix only what's wrong:
for attempt in 1..MAX_REPAIRS: # MAX_REPAIRS is small (1–2)
out = model(prompt + last_output + validation_error)
if validate(out): return out
return fallback(...) # don't loop forever
Discipline:
- Bound it. Allow one or two repairs, then fall back. Unbounded repair is an agent runaway and a cost leak.
- Be specific. "Field
prioritymust be one of [low, med, high]; goturgent" repairs far better than "invalid JSON." - Track repair rate in observability — a rising rate signals prompt, model, or schema drift.
Layer 4 — Fallback chain
When repair fails, degrade deliberately instead of throwing:
- Stricter generation — re-run with constrained decoding or lower temperature.
- A more capable model — escalate via model routing for the hard case.
- A safe default or partial result — for example, return the fields you could validate and flag the rest.
- A clean typed error — surface a structured failure the caller can handle and that degraded-mode UX can render. Never a stack trace or raw model text.
Tradeoffs
| Lever | Buys | Costs |
|---|---|---|
| Constrained decoding | Near-certain valid syntax | Possible quality skew; not all providers/grammars |
| More repair attempts | Higher success rate | Latency + tokens + cost |
| Escalate to bigger model | Rescues hard cases | $$ and latency; route carefully |
| Strict schema | Safety, clear contracts | More repair churn if model struggles |
Failure modes
- Trusting
JSON.parsewith no schema — a stray markdown fence or trailing comma crashes the workflow. Strip fences defensively, and still validate. - Unbounded repair loops — a persistently malformed case retries forever, burning cost. See budgets.
- Valid-but-wrong — the schema passes but values are nonsense. Only semantic validation and evals catch this.
- Quantization-induced malformation — aggressive quantization degrades strict-format adherence, so watch JSON validity after model swaps.
- Silent schema drift — you tightened the schema and repair rate spiked, but nobody noticed because it wasn't monitored.
Practitioner checklist
- Is every model output validated against an explicit schema before use?
- Do you validate semantics (ranges, enums, invariants), not just syntax?
- Is the repair loop bounded (1–2 tries) with specific error feedback?
- Is there a fallback chain ending in a clean typed error, never a crash?
- Do you track parse-failure and repair rates as metrics?
- Did structured-output validity hold after your last model/quantization change?
Related lessons
- Function calling reliability
- Agent guardrails
- Model routing & degraded-mode UX
- Quantization formats
- Production failure modes
Function calling reliability, tool contracts, argument validation, and idempotency
TL;DR
Function calling is where a model stops generating text and starts causing effects — writing to databases, sending messages, moving money. That raises the stakes from "wrong answer" to "wrong action." Reliability comes from treating tool calls like an API boundary with an untrusted client: tight contracts, strict argument validation, idempotency so retries are safe, and authorization so the model can't do what the user couldn't. The model proposes; your harness disposes.
🎯 For the AI-native PM
Why it matters — This is where the AI stops talking and starts doing — moving money, sending email, editing records. The blast radius of a wrong action is far larger than a wrong sentence.
What it changes in your decisions — Which actions you let the AI take autonomously vs. behind a confirmation, and your audit/compliance posture.
Ask your eng team — "If the model calls a tool with a hallucinated argument, what stops it from actually acting on it?"
Product risk if ignored — A double-charge, a wrong-customer email, or an unauthorized change becomes a real-world incident, not a chat mistake.
Mental model
The model is an untrusted client of your tools. Its function call is a request, not a command you execute blindly:
model proposes call ──▶ validate args (schema + semantics)
──▶ authorize (can THIS user/tenant do this?)
──▶ execute idempotently (safe to retry)
──▶ return structured result (incl. errors the model can act on)
Every arrow can reject. A hallucinated or malformed call should fail validation, not hit your database.
Tool contracts
A tool contract is the schema, semantics, and guarantees of a callable:
- Crisp schema. Typed, required versus optional, enums, formats, ranges. This is the same structured-output discipline applied to arguments.
- Descriptions are part of the contract. The model picks and fills tools from their names and descriptions. Vague descriptions cause wrong tool selection and bad arguments. Write them like API docs, with units and examples.
- Narrow surface area. Fewer, well-scoped tools beat many overlapping
ones, because the model confuses similar tools. Don't expose a raw
run_sqlwhen you meanget_orders_by_customer. - Errors are contractual. Return structured, actionable errors, such as "customer_id not found," so the model can recover — not opaque 500s.
Argument validation
Never pass model-provided arguments straight through:
- Syntactic and semantic validation against the schema — types, enums, ranges, referential existence.
- Hallucinated arguments are common — plausible-looking IDs, dates, and enum values that don't exist. Validate against the real system of record before acting.
- On invalid args, feed the error back for a bounded repair rather than executing or crashing.
Idempotency — the keystone for safe retries
Models retry, networks fail, agents re-issue calls. If a tool isn't idempotent, a retry can double-charge a card or send two emails.
- Idempotency keys. Have the caller supply, or the harness derive, a stable key per logical operation. The tool dedupes on it, so repeats are no-ops that return the prior result.
- Prefer idempotent designs.
set_status(order, shipped)is naturally idempotent;increment_balance(+10)is not. Model it instead asapply_transaction(txn_id, +10). - Separate read from write. Reads are freely retryable; writes need keys. Mark which tools have side effects so the harness knows what's safe to repeat.
- This is what makes agent guardrails and retry/fallback logic safe: you can re-run a step without fear.
Authorization & boundaries
The model must never be able to do something the user couldn't:
- Enforce permissions in the tool, keyed on the real session or tenant — not on arguments the model supplies. A model told "you are admin" by a prompt injection must still be blocked by the tool's own authz.
- Scope every call to the tenant or user context to prevent cross-tenant access.
- Least privilege. Give tools the minimum scope they need. Dangerous actions get confirmation or human approval.
Reliability patterns
- Parallel versus sequential calls — validate them, and order them where they have side effects. Watch for the model issuing conflicting parallel writes.
- Timeouts and retries with backoff — but only retry idempotent operations automatically.
- Circuit breakers — if a tool is failing, stop calling it and degrade rather than hammering it.
- Result shaping — return concise, structured results. Dumping a 10k-token API response back into context recreates a context-engineering problem.
Failure modes
- Hallucinated tool call — the model invents a tool or an argument. Catch this by validating tool names and args against the real contract or system.
- Non-idempotent retry — a double-send or double-charge after a timeout retry.
- Confused-deputy / injection — retrieved or tool-returned content tells the model to call a dangerous tool. Block this with tool-side authz, not by trusting the model.
- Wrong tool selected — overlapping or vague tools cause this. Fix descriptions and narrow the surface.
- Context blowup — verbose tool results crowd out the task. Shape and truncate them.
Practitioner checklist
- Does every tool have a typed schema and clear, example-rich descriptions?
- Are all model-supplied arguments validated against the real system before use?
- Is every side-effecting tool idempotent (keys) or modeled to be?
- Is authorization enforced in the tool on the real session — never on model claims?
- Are only idempotent operations auto-retried?
- Are tool errors structured and actionable for recovery?
- Is the tool surface narrow and least-privilege?
Related lessons
- Structured output
- Agent guardrails
- Safety engineering
- Multi-tenant isolation
- Production failure modes
- Tool calling: the product surface — this contract, one level up, at product-decision altitude.
Agent guardrails: loop budgets, tool budgets, and termination conditions
TL;DR
An agent is a loop: think, act, observe, repeat. Loops need brakes. Without explicit budgets — max iterations, max tool calls, max tokens, max wall-clock, max cost — and clear termination conditions, an agent can spin forever, thrash a tool, or quietly run up a large bill. Each step looks locally reasonable, so nobody notices until the invoice or the incident. Guardrails make the worst case bounded and observable instead of open-ended.
🎯 For the AI-native PM
Why it matters — Agents can loop, thrash, and run up cost with no upper bound. Budgets are how you make worst-case behavior — and worst-case spend — predictable enough to ship.
What it changes in your decisions — Whether you ship autonomous agents at all, your per-task cost ceilings, and the UX for "I couldn't finish that."
Ask your eng team — "What's the most this agent can cost — or do — on a single request?"
Product risk if ignored — A runaway agent burns budget or takes too many actions, causing bill shock or a trust-destroying incident.
Mental model
Treat the agent loop like any unbounded recursion in production: it needs a base case and a depth limit. The model decides what to do next. Your harness decides whether it's still allowed to.
while not done:
if over_any_budget(): terminate_with_partial_result_or_escalate()
action = model.decide(state)
if action == STOP or goal_met(state): return result
observe = execute(action) # validated, idempotent tools
state = update(state, observe)
The budgets
| Budget | Caps | Prevents |
|---|---|---|
| Loop / iteration | Max think-act cycles | Infinite reasoning loops |
| Tool | Total calls, per-tool calls | Thrashing one API; repeated side effects |
| Token | Cumulative input+output tokens | Context blowup, runaway cost |
| Cost | Dollars per request/session | Bill shock |
| Wall-clock | Total elapsed time | Hung requests, bad UX |
| Depth | Sub-agent / recursion depth | Fan-out explosions |
Set defaults conservatively, and raise them per-task only with justification. The harness should enforce budgets, not request them of the model in the prompt — a model under injection or confusion won't respect a polite "please stop after 5 steps."
Termination conditions
An agent should stop on the first of these:
- Goal met — a checkable success condition (output validates, task verified), not the model's self-assessment alone.
- Explicit stop — the model emits a terminal action or answer.
- Budget exceeded — any cap above.
- No progress — repeated identical actions, oscillation, or repeated errors. Detect loops by breaking when the same tool and args repeat N times.
- Unrecoverable error — a tool hard-fails in a way retries won't fix.
Critically, define what happens at termination: return the best partial result, escalate to a human, fall back to a simpler path, or return a clean typed error. Never just hang or dump raw state.
Detecting "no progress"
Runaway agents often aren't infinite — they're circular. Here are some cheap detectors:
- Hash (action, args). If the same hash repeats K times, stop.
- Track whether state or goal-distance is changing. If not, stop.
- Cap consecutive tool errors.
These catch the common "agent keeps calling search with the same query" failure that a raw iteration cap would let run to the limit.
Observability for agents
Budgets are only safe if you can see them. Every agent run should emit a trace with one span per step: action, args, tool latency, tokens, cost, and which budget, if any, terminated it. Aggregate:
- the distribution of steps-per-task — a rising tail means a degrading agent,
- budget-hit rate — how often you terminate on a cap versus success,
- cost-per-task by feature/tenant.
Tradeoffs
- Tight budgets are safe and cheap, but they may cut off legitimately hard tasks — premature termination. Loose budgets solve more tasks but risk runaways and cost. Tune with evals: measure success rate and cost as you vary caps.
- Budgets interact with model routing: a cheap model may need more steps. Escalate hard tasks rather than letting a weak model loop.
Failure modes
- Runaway agent — no cap, circular reasoning, large bill. This is the canonical production failure.
- Tool thrash — the same call repeats; non-idempotent versions cause repeated side effects (see idempotency).
- Premature termination — the budget is too tight, so hard tasks fail. This looks like "the agent is dumb" when it's actually "the agent was cut off."
- Silent budget hits — terminating on a cap without surfacing it hides a quality problem as a "completed" run.
Practitioner checklist
- Are iteration, tool, token, cost, and time budgets all enforced in the harness?
- Is there a no-progress / loop detector beyond the raw iteration cap?
- Are termination conditions explicit, including what happens at termination?
- Are tool calls idempotent so a re-issued step is safe?
- Do agent traces record per-step action, tokens, cost, and the stop reason?
- Do you monitor budget-hit rate and steps-per-task over time?
Related lessons
- Function calling reliability & idempotency
- Structured output
- Model routing
- Observability
- Cost attribution
- Production failure modes
Model routing, graceful fallback logic, and degraded-mode UX
TL;DR
Don't hard-wire one model to every request. Routing sends each request to the right model for its difficulty, latency budget, and cost: a small, cheap model for easy cases, a large, expensive one for hard ones. Fallback keeps the system up when a model times out, errors, rate-limits, or returns low-confidence output, by trying another path. Degraded-mode UX makes the fallback honest and usable for the user, instead of a silent quality drop or a hard failure. Together they decouple your product's reliability from any single model's reliability.
🎯 For the AI-native PM
Why it matters — Routing decouples your product's reliability and cost from any single model. It's how you survive a provider outage and how you hit margin without tanking quality.
What it changes in your decisions — Your vendor strategy, the SLA you can credibly offer, and your degraded-mode UX.
Ask your eng team — "If our main provider has a bad hour, what do our users experience?"
Product risk if ignored — Single-vendor dependence means their outage is your outage. Silent quality drops on fallback quietly erode trust.
Mental model
Your provider or model is a dependency with its own latency, error rate, and outages. Production systems don't bet availability on a single dependency — they route and fall back. The same applies here:
request ──▶ classify (difficulty / cost ceiling / latency SLO / privacy)
──▶ pick model (cheap → capable)
──▶ call with timeout
──▶ on error / timeout / rate-limit / low-confidence:
├─ retry (backoff, idempotent only)
├─ failover to alt provider / model
├─ degrade (smaller model, cached, simpler answer)
└─ clean typed error + degraded-mode UX
Routing strategies
- Difficulty-based (cascade). Try a cheap, fast model first, and escalate to a stronger one only when needed — low confidence, validation failure, an explicit "I'm not sure." Most traffic is easy, so cascades cut cost dramatically, but they add latency on escalated requests. This pairs naturally with structured-output fallback.
- Classifier / router model. A small upfront classifier predicts which model can handle the request, routing in one hop with no escalation latency, at the cost of router accuracy.
- Capability-based. Route by need: vision goes to a multimodal model, code goes to a code model, long context goes to a long-context model, cheap bulk work goes to a small model.
- Constraint-based. Honor latency SLOs (a fast model for interactive use), cost budgets, and data-residency and privacy rules — sensitive data goes only to an on-prem or approved model, a safety boundary.
A market note that settles the "is routing worth it?" debate: in August 2025, the biggest product in the industry shipped it as the default architecture. GPT-5's headline design is a real-time router deciding per request whether a fast model or a deliberate reasoning model answers. When routing is how the frontier lab spends its own margin, treating it as an optional optimization in your stack leaves the same economics on the table.
Graceful fallback logic
Fail over on the right signals, in order of cost:
- Transient errors (timeout, 5xx, rate-limit) — retry with backoff, only for idempotent operations, then fail over to an alternate provider or model.
- Quality signals (validation failure, low confidence, refusal) — escalate to a stronger model or stricter generation.
- Hard outage — serve from a semantic cache, a smaller local model, or a non-LLM fallback such as a templated or rule-based response.
Principles:
- Multi-provider removes single-vendor outage risk. But you must keep prompts and evals portable, since models behave differently.
- Circuit breakers: when a model is failing, stop routing to it for a cooldown rather than piling on.
- Idempotency and budgets: fallback chains can multiply calls, so bound them like any agent loop.
Degraded-mode UX
A fallback that silently lowers quality is a trust bug. Make degradation legible:
- Be honest. "We're experiencing high load — here's a quick answer; ask for more detail to retry" beats a confidently worse answer presented as normal.
- Preserve the core job. Drop nice-to-haves — citations, rich formatting, long reasoning — before dropping the actual answer.
- Offer a path back. Let the user retry the full-quality path when it recovers.
- Never expose internals. No stack traces, raw model text, or "provider X 503." Surface a clean, typed, user-appropriate message instead — the end of the structured-output fallback chain.
Tradeoffs
| Strategy | Buys | Costs |
|---|---|---|
| Cheap-first cascade | Big cost savings | Added latency on escalations |
| Upfront router | Low latency, one hop | Router errors mis-route |
| Multi-provider failover | Outage resilience | Prompt/eval portability burden |
| Degraded mode | Stays up under stress | Lower quality; must be honest |
Failure modes
- No fallback — the provider has a bad hour, and your whole feature is down.
- Silent degradation — quality quietly drops on fallback. Users lose trust, and evals don't catch it because they only test the happy path.
- Routing misclassification — an easy task goes to the expensive model (a cost problem), or a hard task goes to the weak model (a quality problem). Monitor per-route success and cost.
- Fallback amplification — chained retries and failovers explode cost and latency without budgets.
- Portability gaps — the failover model gives differently-shaped output that breaks downstream parsing. Validate every route against the same schema and evals.
Practitioner checklist
- Does every model call have a timeout and a defined fallback path?
- Do you route by difficulty/capability/cost/latency, not one-model-fits-all?
- Is there a second provider/model for outage failover?
- Are retries idempotent and the whole fallback chain budget-bounded?
- Is degraded mode honest, job-preserving, and free of raw internals?
- Do you monitor per-route success rate, latency, and cost?
- Are all routes validated against the same output schema and evals?
Related lessons
- Structured output & fallback chains
- Agent guardrails
- Cost attribution
- Evals
- Inference-stack tradeoffs
Recap & real-world examples
Real-world examples & war stories
The $1 Chevy Tahoe (December 2023). Users prompt-injected a Chevrolet dealership's ChatGPT-powered website bot into "agreeing" to sell a Tahoe for $1 and stating the offer was "legally binding, no takesies-backsies." It went viral. 🎯 PM takeaway: never let raw model output be authoritative for a commitment or action. That's guardrails plus tool-side authority, and it's exactly what adversarial evals exist to catch before launch.
DPD's bot swears at a customer (January 2024). After a system update, users goaded the parcel company's chatbot into swearing and writing a poem about how terrible DPD is. The clip spread widely. 🎯 PM takeaway: a model update is a change that needs regression and adversarial testing and live guardrails. "It worked before the update" is not a safety property.
Air Canada must honor its bot's invented policy (2024). A tribunal held the airline responsible after its chatbot fabricated a bereavement-refund policy. 🎯 PM takeaway: ungrounded, unvalidated output is a liability, not just a bad answer. Push toward structured, grounded responses and own what the AI says.
The industry standardizes structured output. Providers shipped JSON mode, function-calling schemas, and constrained/structured decoding precisely because "usually-valid JSON" kept breaking real workflows. 🎯 PM takeaway: schema validation, repair, and fallback are now table stakes, not a nice-to-have. See structured output.
Module recap
| Lesson | The one idea | The decision it drives |
|---|---|---|
| Structured output | Treat output as untrusted: validate → repair → fall back | Integration commitments; error budget |
| Function calling | Tools cause effects; demand contracts + idempotency | What the AI may do autonomously |
| Agent guardrails | Loops need budgets and termination | Whether to ship agents; cost ceilings |
| Model routing | Decouple reliability/cost from any one model | Vendor strategy; the SLA you can offer |
The through-line: never trust model output as if it were a typed return value. Validate it, bound it, authorize it, and have a fallback. Reliability is a property your harness provides. The model is a brilliant, manipulable, occasionally-wrong component in the middle.
Walk-away question: "When the model is wrong — and it will be — what catches it before the user, the database, or the press does?"
← Back to module index · → Next module: 03 · RAG & Retrieval
↑ back to top