Strukturoidut vasteet (structured outputs) on LLM-rajapinnan tila, jossa malli pakotetaan tuottamaan vain JSON Schema -määritelmän mukaista JSON:ia. Sitä ei pyydetä kohteliaasti promptissa, vaan tokenien generointi rajoitetaan tuottamaan skeeman mukainen vaste yli 99,9 % osumatarkkuudella. Vuonna 2026 tämä on function calling -putkien tuotantooletus OpenAI:n strict: true -lipulla, Anthropicin structured-outputs-2025-11-13-beetalla ja Google Geminin responseSchema-konfiguraatiolla. Olen ajanut kaikki kolme tuotannossa viimeisen vuoden aikana, ja tässä oppaassa käyn vendorierot läpi kolmen ajettavan koodiesimerkin kanssa. Näytän myös, miksi skeema pitää suunnitella ennen promptia, ei sen jälkeen.
JSON-tila takaa vain sen, että vaste jäsentyy. Strict-tila puolestaan takaa skeeman noudattamisen constrained decoding -tasolla. Ero mittauksissa on 2–5 % vs. alle 0,1 % skeemarikkomus.
OpenAI:n response_format palauttaa argumentit JSON-merkkijonona, kun taas Anthropic ja Gemini palauttavat jo jäsennetyn objektin. SDK-koodin pitää käsitellä ero.
Claude jättää huomiotta strict-parametrin ilman marraskuun 2025 beetaheaderia. Anthropicin SDK poistaa lisäksi minimum-, maximum- ja pattern-rajoitteet ja siirtää ne description-kenttään.
Geminin responseSchema tukee suppeampaa JSON Schema -alijoukkoa: yli 3 tason syvyys ja monimuotoiset union-tyypit epäonnistuvat hiljaisesti.
Reasoning-kenttä ensin -kuvio (chain-of-thought skeeman sisällä) parantaa luokittelutehtävien tarkkuutta merkittävästi, koska LLM generoi vasemmalta oikealle.
Refusalit (Anthropicin stop_reason, OpenAI:n finish_reason) ohittavat skeeman kokonaan. Ne pitää käsitellä ensiluokkaisina virheinä, eikä olettaa jäsennetyn vasteen olemassaoloa.
Mikä on structured outputs -tila ja miten se eroaa JSON-tilasta?
Structured outputs -tila rajoittaa mallin dekoodauksen niin, että jokainen generoitu token noudattaa etukäteen määriteltyä JSON Schemaa. JSON-tila (type: "json_object") sen sijaan takaa vain sen, että vaste on jäsennettävissä JSON:ksi. Kenttien nimet, tyypit ja enumerointien arvot ovat kaikki mallin harkinnan varassa. Käytännössä JSON-tila tuottaa 2–5 %:ssa vasteista skeemarikkomuksen, kun taas strict-tila jää alle 0,1 %:iin. Kun ajat päivässä 100 000 kutsua, ero on 2 000–5 000 uudelleenyritystä vs. alle 100. (Meidän tapauksessamme kustannussäästö oli noin tuhannen dollarin luokkaa kuukaudessa pelkän retry-liikenteen vähenemisenä.)
Constrained decoding tarkoittaa, että LLM-runtime ylläpitää tila-automaattia, joka sallii vain skeemaan sopivat tokenit. Jos skeema vaatii, että seuraava kenttä on "confidence" ja sen arvo on 0.0–1.0-alueen liukuluku, malli ei fyysisesti pysty tuottamaan mitään muuta. Tokenien todennäköisyysjakauma leikataan sallittuihin. Tämä on infrastruktuuritakuu, ei promptaustemppu. OpenAI julkaisi tekniikan elokuussa 2024, Google laajensi sen Geminiin samana vuonna, ja Anthropic siirtyi beetasta yleiseen saatavuuteen alkuvuodesta 2026.
Käytännön seuraus: skeeman noudattaminen ei enää ole promptaustyön ongelma. Sen sijaan skeeman suunnittelu on koko työn ydin. Jos skeema vaatii pakollisen kentän, jolle ei ole dataa lähdetekstissä, malli täyttää sen jollain, ja se joku on hallusinaatio. Tämän vuoksi LLM-sovellusten arviointi tuotannossa on siirtynyt syntaksitarkistuksesta semanttiseen validointiin: jakaumat, ei vain tyypit.
OpenAI, Anthropic ja Gemini: strukturoitujen vasteiden vertailutaulukko
Näiden kolmen vendorin toteutukset näyttävät päällisin puolin samalta. Anna skeema, saa skeeman mukainen vaste. Käytännössä pakotusmekanismi, tuettu skeeman alijoukko ja vasteen muoto poikkeavat riittävästi, että sama pyyntökoodi ei toimi kaikilla ilman muutoksia.
Ei rekursiota, ei numeerisia rajoitteita, max 24 valinnaista
Kapein: union-tyypit ja >3 tason syvyys epäluotettavia
Natiivi JSON-tila
Kyllä (json_object, legacy)
Ei, tool-pohjainen kiertotie
Kyllä (responseMimeType)
strict: true
Pakolliseksi tuotannossa
Vaatii beetaheaderin, muuten ohitetaan
Ei suoraa vastinetta, koska skeema itsessään on tiukka
Rinnakkaiset työkalukutsut
Oletuksena päällä
Kyllä, oma tool_use-blokki jokaiselle
Kyllä, useita functionCall-osia
OpenAI response_format ja strict: true käytännössä
OpenAI on kypsin ekosysteemi function callingille: SDK-tuki on paras, framework-integraatiot (LangChain, LlamaIndex, CrewAI) ovat oletuksena OpenAI-yhteensopivia, ja strict: true takaa 99,9+ % skeemanoudattamisen. GPT-4o osuu 90–95 % accuracy-tasolle pelkän function calling -benchmarkin kohdalla, ja strict-tila sulkee jäljelle jäävän skeemadriftin lähes kokonaan.
Alla oleva esimerkki hakee vastauksen tukilipulle, jossa vaaditaan kategorisointi ja luottamusarvio. Reasoning-kenttä on skeeman ensimmäisenä tarkoituksella. Se pakottaa mallin ajattelemaan ennen kategorian sitoutumista (tästä lisää alempana).
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
client = OpenAI()
class TicketTriage(BaseModel):
reasoning: str = Field(
description="Perustelu kategorian ja prioriteetin valinnalle. 1-3 lausetta."
)
category: Literal["billing", "bug", "feature_request", "account"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float = Field(ge=0.0, le=1.0)
requires_human_review: bool
response = client.chat.completions.parse(
model="gpt-4o-2024-11-20",
messages=[
{"role": "system", "content": "Luokittele tukilippu annetun skeeman mukaan."},
{"role": "user", "content": "Kortiltani veloitettiin kahdesti eiliselta tilaukselta."},
],
response_format=TicketTriage, # strict: true otetaan kayttoon automaattisesti
)
triage = response.choices[0].message.parsed
if response.choices[0].message.refusal:
raise RuntimeError(f"Malli kieltaytyi: {response.choices[0].message.refusal}")
print(triage.category, triage.confidence) # billing 0.98
Huomaa, että parsed-kentän lisäksi refusal on olemassa ja se pitää tarkistaa ennenparsed-kentän lukemista. Jos malli kieltäytyy turvallisuussyistä, parsed on None ja koodisi kaatuu tyyppivirheeseen, ellet käsittele tätä. (Osuin tähän itse kolmannella tuotantopäivälläni, ja se on ilmiö, jonka näin toistuvasti myös muissa tiimeissä.) Katso yksityiskohdat OpenAI:n virallisesta structured outputs -oppaasta.
Anthropic Claude: tool use ja marraskuun 2025 structured outputs -beeta
Claudessa ei ole globaalia JSON-tilaa. Sen sijaan määritellään työkalu input_schema-kentällä ja pakotetaan sen käyttö tool_choice-parametrilla. Malli palauttaa tool_use-blokin, jonka input-kenttä on jo jäsennetty JavaScript-objekti (ei merkkijono kuten OpenAI:lla). Marraskuussa 2025 Anthropic julkaisi lisäksi natiivin structured outputs -beetan, joka tuo tekstivastaukseen samat constrained decoding -takuut kuin tool useen. Beetan käyttö vaatii anthropic-beta: structured-outputs-2025-11-13 -otsakkeen.
Kriittinen yksityiskohta: strict: true yksin, ilman beetaotsaketta, jätetään huomiotta tool use -kutsuissa. Anthropicin SDK muokkaa lisäksi skeemaasi hiljaisesti niin, että minimum, maximum, minLength ja pattern siirretään description-kenttään pelkäksi tekstiksi. Constrained decoder ei kykene niitä pakottamaan, joten SDK validoi ne generoinnin jälkeen ja yrittää uudelleen. Sinun näkökulmastasi tämä näyttää lisäviiveeltä ja korkeammalta virhefrekvenssiltä.
Claude-mallit menestyvät erityisen hyvin monivaiheisissa työkalukutsuketjuissa, käytännössä agenttien pääsilmukoissa. Kun rakennat MCP-palvelinta, joka altistaa työkalut Claudelle, tämä tool use -kuvio on sama myös MCP-tasolla: skeema on kontrahti, ja constrained decoding tekee siitä sitovan. Rajoitukset kannattaa tuntea etukäteen: rekursiiviset skeemat eivät ole tuettuja, ja skeeman käännösaika on rajattu 180 sekuntiin. Yksityiskohdat löytyvät Anthropicin tool use -dokumentaatiosta.
Google Gemini responseSchema ja sen sudenkuopat
Geminin responseSchema istuu OpenAI:n ja Anthropicin välissä. Konfiguraatio hyväksyy JSON Schema -alijoukon, päättelykerros validoi vasteen palvelinpuolella, ja rikkomus palautuu virhekehyksenä eikä rikkinäisenä JSON-merkkijonona. Takaus on selkeä: vaste on joko skeeman mukainen tai eksplisiittisesti hylätty. Tämä on lievempi kuin OpenAI:n HTTP 400 -reaktio, joka tapahtuu jo skeemaa käännettäessä.
Tunnetut karikot: monijäseniset oneOf/union-tyypit epäonnistuvat hiljaisesti (malli valitsee ensimmäisen ja jatkaa), yli 3 tason nestaus nostaa virhefrekvenssiä, ja tuettu skeeman alijoukko on kapein kolmesta. Käytännössä skeema, joka toimii gpt-4o:lla, ei välttämättä ole ilmaistavissa gemini-2.5-prolla. Testaa siis sama skeema kummallakin ennen kuin sitoudut vendor-agnostisiin kirjastoihin.
from google import genai
from google.genai import types
from pydantic import BaseModel
from typing import Literal
class TicketTriage(BaseModel):
reasoning: str
category: Literal["billing", "bug", "feature_request", "account"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float
requires_human_review: bool
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="Kortiltani veloitettiin kahdesti eiliselta tilaukselta.",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=TicketTriage,
system_instruction="Luokittele tukilippu annetun skeeman mukaan.",
),
)
triage: TicketTriage = response.parsed
print(triage.category, triage.confidence)
Geminin vahvuus on 2 miljoonan tokenin konteksti. Jos työkalumäärittelysi ovat isot (kuten laaja OpenAPI-spesifikaatio työkaluna), Gemini on ainoa provider, johon ne mahtuvat kokonaisuudessaan. Katso tuki-alijoukon rajat Geminin structured output -dokumentaatiosta.
Schema-first-suunnittelu Pydanticilla ja Zodilla
Vuoden 2026 vallitseva kuvio on schema-first. Määritellään Pydantic- (Python) tai Zod-skeema (TypeScript) ensin, ja rakennetaan promptit sen ympärille. Skeema on sekä LLM-rajapinnan kontrahti että sovelluslogiikan tyyppijärjestelmä. Sama malli, joka lähetetään mallin skeemakenttään, generoi API-vastaustyypit ja tietokantasarakemäärittelyt.
Pydanticin Field(description=...) ei ole vain dokumentaatiota. Kuvausteksti kääntyy JSON Schemaan ja päätyy mallin näkyviin. Käytännössä se on prompt-engineeringiä skeeman sisällä. Yksi hyvin muotoiltu kuvaus vaikuttaa tarkkuuteen enemmän kuin puolen sivun lisäys pääpromptiin, koska se ankkuroituu juuri siihen kohtaan generointia, missä kenttä täytetään.
from pydantic import BaseModel, ConfigDict, Field
from typing import Literal, Optional
class ExtractedInvoice(BaseModel):
model_config = ConfigDict(extra="forbid") # Hylkaa tuntemattomat kentat
reasoning: str = Field(
description="Miksi valitsit nama arvot? Viittaa lahdetekstin kohtaan."
)
invoice_number: str = Field(
description="Laskun numero. Yleensa 'INV-' tai 'LASKU-' etuliite."
)
total_eur: float = Field(
description="Loppusumma euroina. Sisaltaa ALV:n jos mainittu."
)
due_date: Optional[str] = Field(
default=None,
description="ISO 8601 -paiva (YYYY-MM-DD) tai None jos ei mainittu."
)
currency: Literal["EUR", "USD", "GBP"] = "EUR"
Kolme sääntöä, jotka toistuvat jokaisessa käyttökelpoisessa skeemassa. Ensinnäkin, extra="forbid" torjuu mallin sivuraiteille lipsahtavat lisäkentät. Toiseksi, valinnaiset kentät (Optional[...] = None) estävät hallusinaatiot silloin, kun lähdetekstissä ei ole dataa. Ja kolmanneksi, skeemat versioidaan (InvoiceV2, InvoiceV3) niin, että vanhat parserit eivät hajoa uusia kenttiä lisättäessä. Näistä konventioista Pydanticin JSON Schema -dokumentaatio antaa täydellisen referenssin.
Reasoning ensin ja muut skeeman suunnittelusäännöt
Vaikuttavin yksittäinen kuvio: laita reasoning-kenttä skeeman ensimmäiseksi. LLM generoi tokenit vasemmalta oikealle, joten jos category tulee ensin, malli valitsee kategorian ja rationalisoi sen jälkikäteen. Jos reasoning tulee ensin, malli ajattelee ongelman läpi ja sitoutuu vasta sen jälkeen kategoriaan. Käytännössä tämä on chain-of-thought paistettuna skeemaan, ilman erillistä system-promptin CoT-ohjeistusta.
Nestauksen ja skeeman koon rajoittaminen
Pidä nestaus kahdessa tai kolmessa tasossa. Syvempi nestaus ei ole tuettu tasapuolisesti kaikilla providereilla, ja skeeman käännösaika kasvaa räjähdysmäisesti. Jos yrität poimia 50+ kenttää yhdellä kutsulla, jaa se useaan uuttokutsuun: ensin karkea kategoria, sitten kategoriaan sidottu tarkempi skeema. OpenAI:n strict-tilassa on käytännön kokoraja, ja isot skeemat degradoivat vasteen laatua vaikka rakenne olisi validi.
Semanttiset validaattorit tyyppitarkistuksen päälle
Varoittava tarina: sentimenttiluokittelija validoi kaksi viikkoa täydellisesti. Joka tietue oli validia JSON:ia, oikein tyypitettyjä, oikein enumeroituja. Sitten huomattiin, että confidence oli 0,99 kaikella, mukaan lukien satunnainen roska syötteessä. Rakenne validi, sisältö merkityksetön. Ratkaisu: koodissa ajettavat semanttiset validaattorit, jotka tarkistavat jakaumat eivätkä pelkkiä tyyppejä. Tämä yhdistyy suoraan LLM-sovellusten arviointi-CI/CD-pipelinen rakentamiseen: evals ennen deployta, ei sen jälkeen.
Refusalit, max_tokens ja muut tuotannon karikot
Strukturoidut vasteet eivät suojaa refusalilta. Jos malli kieltäytyy turvallisuussyistä, se ei palauta skeeman mukaista objektia, vaan refusal-blokin, ja koodisi kaatuu tyyppivirheeseen ellet tarkista finish_reason-kenttää (OpenAI) tai stop_reason-kenttää (Anthropic) ensin. Tämä on ehkä yleisin tuotantobugi, kun tiimi siirtyy JSON-tilasta strict-tilaan olettaen, että "nyt vastaus on aina validi". Kaatoin itse tuotanto-endpointin viime vuonna juuri tästä syystä, ja siitä opitusta läksystä lisäsin refusalin ensiluokkaiseksi virhepoluksi jokaisessa skeemakutsussa.
Toinen usein toistuva karikko on max_tokens. Jos raja tulee vastaan kesken skeeman generoinnin, JSON katkeaa keskeltä kenttää ja constrained decoder sallii sen. Se ei voi ennustaa etukäteen, mahtuuko täydellinen skeema jäljellä oleviin tokeneihin. Tulos on skeeman kannalta ehjä alkuosa, joka ei jäsenny. Tarkista aina finish_reason == "length" ja mitoita max_tokens reilusti skeeman odotetun koon yli. Yhdistettynä prompt cachingiin pitkä max_tokens ei nosta kustannuksia merkittävästi, koska laskutus perustuu tosiasiassa generoituihin tokeneihin.
Instructorin validate-repair-retry -silmukka
Kun tarvitset skeeman rajoja, joita constrained decoder ei tue (esim. pattern-regex, kompleksinen union), ratkaisu on validate-repair-retry -silmukka. Instructor-kirjasto tekee tämän automaattisesti: se ottaa Pydantic-mallin, kääntää sen JSON Schemaksi, ajaa kutsun, validoi vasteen, ja jos validaatio epäonnistuu, lähettää virheen takaisin mallille kehotuksena korjata.
import instructor
from anthropic import Anthropic
from pydantic import BaseModel, Field, field_validator
client = instructor.from_anthropic(Anthropic())
class ProductCode(BaseModel):
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")
price_cents: int = Field(gt=0, lt=1_000_000)
@field_validator("sku")
@classmethod
def uppercase(cls, v: str) -> str:
if not v.isupper():
raise ValueError("SKU on annettava isoin kirjaimin")
return v
product = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=512,
response_model=ProductCode,
max_retries=3,
messages=[{"role": "user", "content": "ABC-1234 on 19,90 euroa"}],
)
Kolme uudelleenyritystä on hyvä oletus. Enemmän tuhlaa tokeneja huonosti muotoillulla promptilla; vähemmän ei anna mallille tilaisuutta oppia palautteesta.
Kuinka luotettavaa function calling on vuonna 2026?
Function calling on vuonna 2026 tuotantovalmis silloin, kun (a) käytät natiivia structured outputs -tilaa etkä pyydä JSON:ia system-promptissa, (b) skeemasi on validoitu semanttisesti eikä vain syntaktisesti, ja (c) refusalit ja max_tokens-katkeamiset käsitellään ensiluokkaisina virheinä. Benchmark-luvut kertovat, että GPT-4o osuu 90–95 % baseline function callingissa, Claude Sonnet 4.5 vetää yhtenäistä 92–96 % tulosta pitkissä työkaluketjuissa, ja Gemini 2.5 Pro pyörii 85–90 % tasolla single-shot-kutsuissa. Strict-tilassa kaikki nousevat lähelle 99+ % skeemanoudattamisessa, mutta oikeellisuus (oikeat arvot, ei pelkkä oikea rakenne) pysyy noissa numeroissa.
Käytännön reitti: aloita OpenAI:sta, jos rakennat yksinkertaisia rakenteisia poimintoja; Claudesta, jos työkuormasi on agenttiketju; ja Geministä, jos konteksti on iso tai multimodaalinen. Vendor-lock-inin välttämiseksi kääri kaikki kutsut kirjastolla (Instructor, LiteLLM tai LangChainin with_structured_output), joka normalisoi eron. Lokita silti provider-spesifiset skeemamuunnokset, muuten Geminin skeemapoisto tulee yllätyksenä. Ja aja evals CI:ssä ennen jokaista tuotantopäivitystä; strict-tila ei suojaa mallipäivityksen jälkeen tapahtuvalta laatuvaihtelulta.
Usein kysytyt kysymykset
Mitä eroa on JSON-tilan ja strukturoitujen vasteiden välillä?
JSON-tila takaa, että vaste on jäsennettävissä JSON:ksi, mutta ei sen sisältöä. Strukturoidut vasteet (strict mode) rajoittavat tokenien generoinnin niin, että vaste noudattaa etukäteen määriteltyä JSON Schemaa 99,9+ % osumatarkkuudella. Käytännössä JSON-tila on legacy vuonna 2026, ja strukturoidut vasteet ovat tuotanto-oletus.
Tukeeko Claude strict-tilaa?
Kyllä, mutta ei automaattisesti. strict: true ilman anthropic-beta: structured-outputs-2025-11-13 -otsaketta jätetään huomiotta, ja Anthropicin SDK poistaa lisäksi hiljaisesti minimum-, maximum- ja pattern-rajoitteet skeemasta. Constrained decoding on saatavilla, mutta se pitää tietoisesti kytkeä päälle.
Kannattaako käyttää Pydanticia vai Zodia?
Kieli ratkaisee: Pydantic Pythonissa, Zod TypeScriptissä. Molemmat kääntyvät JSON Schemaksi ja tukevat samat vendor-rajapinnat. Vältä sen sijaan kirjoittamasta raakoja JSON Schema -dictejä käsin, koska kääntäminen tyyppikirjastosta antaa validaation, IDE-tuen ja skeemaversionnin samassa.
Miksi strukturoitu vaste joskus hallusinoi?
Skeema pakottaa rakenteen, ei oikeellisuutta. Jos pakotat mallin täyttämään kentän, jolle ei ole dataa lähdetekstissä, se hallusinoi jonkin uskottavan arvon. Ratkaisu: tee kaikki kentät, jotka voivat puuttua, Optional-kentiksi ja lisää semanttinen validointi koodissa tyyppitarkistuksen päälle.
Mikä LLM on paras function callingiin vuonna 2026?
Riippuu työkuormasta. GPT-4o kelpaa yksinkertaisiin rakenteisiin poimintoihin ja on parhaiten dokumentoitu. Claude Sonnet 4.5 vetää pisimpiä työkaluketjuja luotettavimmin, ja se on agenttien oletusvalinta. Gemini 2.5 Pro voittaa silloin, kun tarvitset 1M+ tokenin kontekstin tai multimodaalisen syötteen. Kaikki tukevat strict-tilaa, joten skeemanoudattaminen ei ole enää valintakriteeri.
Kerroksellinen muisti tekee LLM-agentista johdonmukaisen yli kontekstin rajojen. Käytännön opas Mem0:aan, Zepiin, LangGraph-integraatioon ja tuotannon sudenkuoppiin.
Rakenna tuotantokelpoinen MCP-palvelin TypeScriptillä ja Pythonilla. Käytännön esimerkit Claude Desktop -integroinnista, Streamable HTTP -kuljetuksesta ja OAuth 2.1 -autentikoinnista, plus selkeä turvallisuusmalli.