← Back to blog

LLM Structured Output in Practice: JSON Mode, Function Calling and Constrained Decoding (2026 Edition)

An LLM that "misbehaves" — JSON with an extra line of chatter, a misspelled field name, a hallucinated tool argument — is the wall most AI projects hit before production. This article gives a shipping-ready structured-output toolkit: a precision-vs-compatibility comparison of the three constraint mechanisms (JSON Mode / Function Calling / constrained decoding), six iron rules for schema design (flatten it, prefer enums, drive with examples), a four-step parse-and-validate defense chain (validator + feedback retry + degradation path), hallucination-proofing for agent tool calls (schema constraints + parameter whitelists + call audit), the high-frequency pitfalls of multilingual output and nested structures, and a pre-launch structured-output self-audit checklist.

The Three Walls LLMs Hit Before Production Are All Structured-Output Problems

AI demos dazzle; production ships break. Ten out of ten times it is the same place: the model output “misbehaves”. A support-ticket system asks the model to emit JSON for the database, and 3% of responses carry an extra line of “Sure, here is the ticket info:”. An agent calls a tool with a fabricated order_id, the tool errors, the workflow stalls. A content pipeline asks for category labels and gets “digital”, “digital products”, “3C digital” in the same day — downstream reports are full of dirty data.

Structured output is not “please output JSON” in the prompt. It is an engineering problem: constraint mechanism selection → schema design → parse and validate → failure fallback → monitoring. This article gives the complete toolkit we have validated in production.


1. Three Constraint Mechanisms: Precision vs. Compatibility

1.1 Comparison

MechanismGuaranteesDoes NOT guaranteeCompatibilityUse case
Pure prompt (“please output JSON”)NothingValidity, schema — nothingEverythingDeprecated, fallback only
JSON Mode (response_format)Output is valid JSONField names / types / enumsMost cloud APIsFallback for older models
Structured Outputs / Function CallingOutput 100% matches the JSON SchemaFactual correctness of argumentsOpenAI / Anthropic / Qwen / DeepSeek and other major APIsDefault for 99% of cases
Constrained decoding (guided decoding)Per-token grammar enforcementSame as abovePrivate vLLM / Ollama / llama.cppLocal deployments, offline environments

Three selection conclusions:

  1. Use structured outputs on every cloud API (OpenAI calls it structured outputs, Anthropic tool use, Qwen/DeepSeek a json_schema variant of response_format) — this is the 2026 baseline capability. Use it.
  2. Private deployments (vLLM etc.) use constrained decoding — vLLM natively supports guided_json / guided_choice (outlines-based grammar constraints); Ollama supports format: json (valid JSON only). In private environments the model has no “structured output switch” — the constraint must move down to the decoding layer.
  3. Small models (under 7B) get degraded handling — small models follow long schemas poorly. Keep the schema minimal (iron rules below) and always pair it with parse-retry fallback.

1.2 Key Insight: You Constrain Shape, Not Facts

A schema guarantees field names, types and enum membership — not that the value exists in your business. The model can output a perfectly formatted, semantically fabricated order_id: "ORD-2026-9981". This is not a flaw in the constraint mechanism; it is the nature of LLMs — they predict the next token, they do not query your database.

So the full definition of structured output is: constraint (the decoding layer guarantees shape) + validation (the execution layer guarantees semantics) — you need both.


2. Six Iron Rules for Schema Design

2.1 The Six Rules

#RuleAnti-patternDo this
1Nesting ≤2 levels, ≤8 fields per levelorder.items[].attributes[].valueFlat array + foreign key (item_id referencing the attributes table)
2Enum over open fieldsstatus: "已完成" (free text)status: ["pending","paid","refunded"] (enum)
3Self-explanatory names + descriptioncode (what code?)refund_reason + description explaining the values
41–2 complete examples in the promptSchema definition onlySchema + few-shot complete output examples
5Minimize requiredAll 12 fields required3 core fields required, rest optional with defaults
6No 2-D arraystags: [["A","B"],["C"]]tags: ["A","B","C"]; grouping via foreign key

2.2 A Production-Grade Schema Example

Support-ticket classification (simplified from a real delivery):

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "technical", "account", "other"],
      "description": "Primary ticket category, by the user’s underlying request"
    },
    "urgency": {
      "type": "string",
      "enum": ["low", "medium", "high"],
      "description": "high = affects money or a core function; medium = degraded with a workaround; low = inquiry"
    },
    "summary": { "type": "string", "maxLength": 100, "description": "One-sentence summary" },
    "action": {
      "type": "string",
      "enum": ["auto_reply", "human_agent", "create_ticket"],
      "description": "Recommended handling path"
    }
  },
  "required": ["category", "urgency", "action"],
  "additionalProperties": false
}

Three details matter: additionalProperties: false (no free-field invention — only truly enforced under structured outputs), the urgency description carries executable decision criteria (not filler like “urgency level”), and summary is the only open-text field, with a length cap.


3. Parse and Validate: the Four-Step Defense Chain

3.1 The Chain

model output → ① pydantic validation → ② on failure: feed the error back and retry (≤2) → ③ still failing: business degradation
                   ↓ pass
             ④ business execution (re-validate argument existence before tool calls)

① Validate fully: json.loads alone is not validation. Compile the schema into a validator (pydantic for Python, zod for TS) — missing fields, wrong types and out-of-enum values are all caught.

② Retry with feedback: a blind retry (same prompt again) only succeeds ~60% of the time. Feed back the validation error — “your last output was missing the required field urgency; please re-emit” — and retry success climbs above 95%. Cap retries at 2; a third attempt almost certainly fails, so degrade instead.

③ Pre-set degradation: before each scenario goes live, decide what “still failing” means:

ScenarioDegradation path
Ticket classificationDefault other + human_agent (route to human — slow rather than wrong)
Data extractionSkip the field, null it, queue for manual backfill
Agent tool argumentFail the step, roll back one step and re-plan
Content generationFall back to a human template

④ Prevent truncation: set max_tokens to “schema maximum possible length × 1.5”. JSON truncated at max_tokens is unrecoverable (missing tokens cannot be patched) — only a full retry works. Budget it; do not starve max_tokens to save tokens.

3.2 Monitoring

Parse-failure rate on the dashboard, three rules:

  1. Rate >5% (baseline is typically <1%) — a regression signal after any prompt/model/schema change.
  2. Rate spikes 3× — top cause: a silent model version upgrade (vendor-side canary); second: a prompt change.
  3. Single scenario >10% — that scenario’s schema or few-shot examples need rework.

4. Agent Tool Calls: Three Layers Against Hallucinated Arguments

4.1 The Three Layers

LayerMechanismBlocks
1. SchemaTool arguments defined as JSON Schema + descriptionsWrong types, missing args, out-of-enum values
2. ValidationVerify argument existence in the data layer before executionHallucinated arguments (right format, nonexistent)
3. GranularitySplit big tools into small ones, ≤4 arguments per toolArgument “padding” behavior (more args = more fabrication)

Layer 2 is the key one. Example (order lookup tool in a ticket system):

def get_order(order_id: str) -> dict:
    order = db.orders.find(order_id)
    if not order:
        # Do not raise — feed "does not exist" back to the model so it can retry with a different argument
        return {
            "error": f"order_id '{order_id}' does not exist",
            "hint": "Extract the order number the user provided in the conversation, or call search_orders",
            "suggestions": db.orders.recent(user_id, limit=3)  # offer candidates
        }
    return order.data

Tool-error design principle: the error message is written for the model — it must contain “why it failed + what to do next + candidate values”, not a human-facing stack trace.

4.2 Tool Descriptions Matter More Than the Schema

The model decides “whether to call it, and with what” by reading the tool description. Two lines that work:

Use when the user asks about order status or requests a refund (do NOT use for inquiries).
Argument order_id: the order number extracted from the user’s message, format ORD-YYYY-NNNN;
if the user has not provided one, call search_orders first — never guess.

“When to call” (trigger condition) + “where the argument comes from” (argument source) + “what if missing” (fallback action) — with all three, misfire rates drop by an order of magnitude.


5. High-Frequency Pitfalls: Multilingual and Long-Text Output

5.1 Mixed Languages

Let the JSON “structure” speak English and the “content” speak the user’s language:

  • Field names and enum values: fixed English (status: "paid"); downstream code matches on English.
  • Content fields (summary, reply, …): the schema description says “this field is in Chinese”.
  • Anti-pattern: unconstrained, the model emits status: "已完成", the database stores two representations, and reporting is dirty.

5.2 Long-Structure Generation: Two-Phase Calls

Cramming a full report into one output field = guaranteed truncation + retry costs. The correct design is skeleton-then-fill:

Call 1: emit the skeleton
  { "sections": [ {"id": 1, "title": "...", "outline": "key points for this section"}, ... ] }
Calls 2..N: fill each section (parallelizable)
  input = document requirements + skeleton + this section’s outline
  output = this section’s Markdown
Assembly: the program concatenates (section id as anchor)

Bonus: a failed section re-runs in isolation; sections fill in parallel, so total latency is often shorter; and the skeleton is reviewable — a human can restructure sections before filling starts, instead of waiting for the whole document to realize the direction was wrong.


6. Pre-Launch Self-Audit Checklist

CheckPass criteria
Constraint mechanismstructured outputs on cloud APIs / guided decoding on private; not reliant on prompt alone
Schema flatnessnesting ≤2 levels, ≤8 fields per level, no 2-D arrays
Enum coverageevery closed-value field is an enum; descriptions carry executable criteria
additionalPropertiesset to false (only truly enforced under structured outputs)
Few-shot examples1–2 complete output examples in the prompt, fully consistent with the schema
max_tokens budgetschema max length × 1.5, with truncation-rate monitoring
Validatorpydantic/zod full-schema validation, not json.loads
Retry mechanismfailure feedback retry ≤2 times, retry success rate instrumented
Degradation pathpre-set per scenario (human / skip / re-plan), no unbounded retries
Tool argument validationexistence check in the data layer before execution; error message carries “why + next step + candidates”
Tool granularity≤4 arguments per tool; description covers trigger + argument source + fallback
Monitoringparse-failure dashboard + 3 rules (>5% / 3× spike / single scenario >10%)

Structured Output Is the “Data Contract” of an LLM Application

Latency, cost and quality are the three engineering metrics of an LLM application — but structured output is the precondition for all three. If output cannot be parsed, cost cannot be computed (you do not know whether the call succeeded or failed), quality cannot be scored (no automatic grading), and agents cannot run (every tool waits for valid arguments).

In the AI customer-service, data-extraction and agent-workflow projects we have delivered, structured-output parse success stays above 99%. That is not because we “picked a better-behaved model” — it is this defense chain: constraint selection (structured outputs / guided decoding) → six schema iron rules → four-step parse defense → three-layer tool hallucination-proofing. If your AI project is stuck on “the model will not listen”, bring us your schema and your failure samples — we will first do a structured-output health check (failure attribution + defense-chain gap analysis), then talk implementation.


Further reading:

Need a structured-output design, an agent toolchain, or parse-success-rate optimization? Contact us for a free assessment.

FAQ

What is the difference between JSON Mode, Function Calling, and constrained decoding? How do I choose?

Constraint strength increases, compatibility decreases: ① JSON Mode (response_format: json_object) — the model guarantees the output is valid JSON, but not that it matches your schema: field names can be misspelled, extra or missing fields still happen. ② Function Calling / structured outputs (OpenAI structured_outputs, Anthropic tool use) — output is constrained by a JSON Schema; field names, types and enum values are enforced. Every major API supports it; it is the default for 99% of cases. ③ Constrained decoding (outlines / guidance / lm-format-enforcer, grammar enforced at the inference engine level) — masks illegal tokens one by one, guaranteeing 100% conformance. Use it for private deployments on local engines (vLLM/Ollama) that are OpenAI-compatible but lack structured outputs. Decision path: cloud API → structured outputs directly; private vLLM → constrained decoding (vLLM has native guided decoding); old or small models (under 7B) → prompt + JSON Mode + parse-retry fallback.

I already use Function Calling — why does the model still invent arguments?

The schema constrains shape, not facts. The model will happily fill in a value that is type-valid but semantically fabricated (an order_id that matches the format but does not exist in your database). A schema cannot block hallucination, only format errors. Defend in two layers: ① parameter whitelist validation — before the tool executes, verify the argument against your data layer (the order_id must exist); if invalid, reject it and feed the model back "this parameter does not exist; available candidates are …" so it can regenerate. ② narrow tool granularity — split one big tool into small ones (never a universal get_record(id, field); use get_order(id) / get_customer(id) instead). Fewer arguments means less to hallucinate. Note: a tool description that spells out "when to call it" and "where the argument comes from" reduces misfires more than the schema itself.

What parse-failure rate should I expect, and how do I handle failures in production?

With frontier models plus structured outputs, parse failure is under 0.5% (mostly truncation on over-long outputs); pure prompt + JSON Mode runs 2–8% (15%+ on small models). Four-step defense chain: ① validate — pydantic/zod against the full schema, never just json.loads; ② automatic retry — feed the validation error plus the original input back to the model, at most 2 times, with the retry prompt stating exactly "your last output was missing field X / had the wrong type"; ③ truncation handling — set max_tokens with headroom (estimate the schema’s maximum length × 1.5); truncated JSON is unrecoverable and must be retried wholesale; ④ degradation path — if retries still fail, the business degrades (skip the step / route to a human / return a default) instead of retrying forever and burning money. Monitor the parse-failure rate on a dashboard; a 3× spike is an alert (usually a model version upgrade or a prompt change).

How do I design a schema the model can reliably follow?

Six iron rules: ① flatten — no more than 2 levels of nesting, no more than 8 fields per level (deep nesting is where small models collapse); ② prefer enums over open fields — if the value set is closed, make it an enum (status is ["pending","paid","refunded"], never free text); ③ self-explanatory names — business-semantic names (refund_reason, not code) plus a description on every field; ④ drive with examples — 1–2 complete few-shot output examples in the prompt beat the schema description alone; ⑤ minimize required — only core business fields are required, the rest optional with defaults; ⑥ no arrays of arrays — nested arrays are the reliability cliff of structured output; use a flat array plus a foreign key instead of a 2-D array.

How do I handle multilingual output and long-structure generation (e.g. a full report)?

Two high-frequency pitfalls: ① mixed-language fields — keep JSON field names and enum values fixed in English, and allow Chinese only in "content" fields (the schema description says "this field is in Chinese; all other values in English"). Otherwise downstream enum matching breaks: "已完成" never equals "completed". ② long-text truncation — cramming a whole report into one output field guarantees hitting max_tokens. The correct design is two-phase: call 1 emits a structured skeleton (section array + per-section outline); call 2..N fills each section (one call per section, input = skeleton + section requirements); the program assembles. Bonus: a failed section re-runs in isolation, not the whole document.

This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?

📡 Also published on: CSDN 知乎

Subscribe to Updates

Get notified when new articles are published. No spam, occasional updates only.

Subscribe →