Övergången från ”LLM-chattbot” till ”LLM-drivet system som utför arbete” sker i lagret för strukturerade utdata och funktionsanrop. Här slutar LLM:en att bara producera prosa och börjar producera data, välja åtgärder och integrera med resten av infrastrukturen.
I produktion betyder ”strukturerade utdata” inte att ”JSON-läget fungerade en gång i mitt test”. Det betyder en robust pipeline som hanterar modellvariation, felaktiga utdata, partiella fel, schemautveckling och den stökiga verkligheten att LLM:er inte alltid följer instruktioner.
Artikeln beskriver de mönster som faktiskt håller i produktion. Vi förutsätter att du kan grunderna – OpenAI:s tool_choice, Anthropics verktygsanvändning och JSON-schemabegränsningar – och går djupare i vad som gör systemen tillförlitliga.
De två lägena
Två närbesläktade men skilda förmågor:
Strukturerade utdata: LLM:en producerar utdata som följer ett schema, vanligen JSON. Används när svaret behövs i ett format som kan bearbetas programmatiskt.
Funktions-/verktygsanrop: LLM:en får en uppsättning funktioner, väljer vilken som eventuellt ska anropas och producerar parametrarna. Värdsystemet kör funktionen och returnerar resultatet. LLM:en kan anropa fler funktioner eller ge ett slutsvar.
Modell-API:er exponerar dem vanligen via:
- En parameter
response_formateller motsvarande som tar ett JSON-schema för vanliga strukturerade utdata – OpenAI Structured Outputs, Google GeminisresponseSchemaoch så vidare. - En matris
toolssom beskriver funktionerna och ett verktygsanropssvar när en funktion används. Anthropics API för verktygsanvändning är även sättet att begränsa utdata till ett schema: definiera ett enda verktyg med önskat schema och tvinga modellen att anropa det.
Båda fungerar och hänger ihop. Ett ”funktionsanrop” är i praktiken strukturerade utdata där schemat är funktionssignaturen.
Mönster 1: strikta, explicita scheman
Den största enskilda tillförlitlighetsvinsten finns i schemana.
Ett löst schema:
{
"type": "object",
"properties": {
"category": { "type": "string" },
"priority": { "type": "string" }
}
}
Ett strikt schema:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "feature_request", "complaint"],
"description": "Ärendets kategori. Använd 'technical' för produktfel och 'account' för problem med inloggning eller lösenord."
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Använd 'urgent' endast för avbrott eller verksamhetskritisk påverkan. 'High' för blockerande problem hos viktiga kunder. 'Medium' för normal påverkan. 'Low' för önskemål."
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
Den strikta versionen:
- Begränsar värdena till kända enumvärden och hindrar fri drift.
- Har beskrivningar som fungerar som inbäddade prompter och används av modellen.
- Kräver fält så att svaren inte blir ofullständiga.
- Förbjuder extra fält och därmed slumpmässigt hallucinerade nycklar.
I produktion bör varje schemafält ha en beskrivning, varje enum vara explicit och varje obligatoriskt fält markeras. Detta är ”schema som prompt”: schemat utför promptutformning.
Mönster 2: begränsad generering
De stora leverantörerna stöder nu begränsad generering: modellen begränsas vid avkodningen till att endast producera giltiga utdata.
- OpenAI:
response_format: { type: "json_schema", json_schema: { ..., strict: true } } - Anthropic: verktyg med strikta scheman.
- Öppen källkod:
outlines,lm-format-enforcer,jsonformer, vLLM:s grammatikbaserade avkodning.
Använd detta. Alltid. Det eliminerar en hel felklass: felaktig JSON, hallucinerade fält och saknade obligatoriska fält. Prestandakostnaden är försumbar.
När begränsad generering saknas, för vissa öppna modeller eller konfigurationer, är validering och omförsök reservlösningen (se mönster 4).
Mönster 3: schemaversionering
Scheman utvecklas. Du lägger till och fasar ut fält samt ändrar enumvärden.
En schemaändring är en kodändring. Den ska vara:
- Versionshanterad. Märk varje schema med versionsnummer.
- Testad. Kör utvärderingssviten mot det nya schemat före driftsättning.
- Kommunicerad. Nedströmskonsumenter känner till ändringen.
- Bakåtkompatibel när det går. Lägg till nya valfria fält, ta inte bort obligatoriska.
Ett välfungerande mönster är att lagra scheman som TypeScript-typer eller Pydantic-modeller, versionshantera dem och generera JSON Schema från dem. Typerna används både av modell-API:et och applikationskoden.
class TicketClassificationV2(BaseModel):
category: Literal["billing", "technical", "account", "feature_request", "complaint"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float = Field(ge=0, le=1, description="Tilltro till klassificeringen, 0–1")
needs_human_review: bool = Field(description="Sant om något fält har låg tilltro eller en ovanlig signal")
reasoning: str = Field(description="Kort motivering till klassificeringen, särskilt i svårbedömda fall")
En Pydantic-modell definierar schemat, validerar utdata och fungerar som typ i Python-koden. En enda sanningskälla.
Mönster 4: validering och omförsök
Validera utdata före användning även med begränsad generering:
from pydantic import ValidationError
def call_with_validation(prompt, schema, max_retries=2):
for attempt in range(max_retries + 1):
response = llm_call(prompt, response_format=schema)
try:
parsed = schema.model_validate_json(response.content)
return parsed
except ValidationError as e:
if attempt < max_retries:
prompt = build_retry_prompt(prompt, response.content, e)
continue
raise
Omförsöksprompten ska innehålla de ursprungliga instruktionerna, modellens föregående utdata och en specifik felbeskrivning:
Ditt föregående svar hade ett valideringsfel:
{error message}
Dina föregående utdata:
{previous output}
Rätta felet och producera ett giltigt svar.
Omförsök fungerar förvånansvärt väl. Ett enda försök reparerar vanligen ett modellfel.
Begränsningar: försök inte för alltid, högst 2–3 gånger. Försök inte igen vid andra fel än valideringsfel, exempelvis hastighetsgränser eller innehållsfilter. Logga försöken. En ökande andel signalerar modelldrift eller promptproblem.
Mönster 5: reflektion över resultat
Låt modellen reflektera över verktygsresultatet före användning vid funktionsanrop med höga insatser.
En enkel loop:
1. Anropa LLM:en med tillgängliga verktyg.
2. Modellen väljer att anropa verktyg X.
3. Kör X.
4. Skicka resultatet till modellen.
5. Modellen producerar slutsvaret.
En reflektionsloop:
1. Anropa LLM:en med tillgängliga verktyg.
2. Modellen väljer att anropa verktyg X.
3. Kör X.
4. Skicka resultatet till modellen.
5. Modellen bedömer: motsvarar resultatet förväntningarna? Bör jag agera på det?
6. Om ja ger modellen slutsvaret. Om nej anropar den ett annat verktyg eller ber om ett förtydligande.
Det fångar bland annat:
- Verktyget returnerade 0 resultat när data förväntades – modellen känner igen tomfallet.
- Verktyget returnerade ett fel – modellen hanterar det uttryckligen i stället för att ignorera det.
- Verktyget returnerade oväntade data – modellen märker det och anpassar sig.
Be modellen utvärdera verktygsresultat explicit, exempelvis med ett strukturerat mönster ”utvärdera, sedan agera”.
Det kostar latens och token. För åtgärder med höga insatser – skicka e-post, hantera betalning, ändra poster – är det värt det. Hoppa över det vid informationshämtning med låg risk.
Mönster 6: idempotens
LLM:er anropar ibland samma verktyg två gånger eller upprepar anrop som redan lyckats. Utan idempotens uppstår dubletter: två återbetalningar, två e-postmeddelanden eller två poster.
Mönster för idempotens:
Idempotensnycklar. Varje verktygsanrop får en unik nyckel som genereras på klientsidan och ingår i anropet. Nedströms-API:et eller verktygsomslaget identifierar dubletter och returnerar befintligt resultat.
Hämta-eller-skapa-semantik. Verktyg som skapar poster söker först. ”Skapa kund med e-postadress X” kontrollerar om kunden finns och returnerar i så fall den befintliga posten.
Åtgärdsloggar. Verktyg loggar varje åtgärd. Omslaget kontrollerar loggen före körning och returnerar cachelagrat resultat om åtgärden redan är utförd.
Konservativ verktygsdesign. Verktyg med betydande konsekvenser kräver uttrycklig bekräftelse eller mänskligt godkännande. LLM:en kan inte utlösa dem av misstag i en tät loop.
Alla verktyg med sidoeffekter ska utformas för idempotens. Att utelämna detta är en viktig källa till produktionsfel.
Mönster 7: observerbarhet för verktygsanrop
Du måste veta vad som händer. Logga för varje anrop:
- Tidsstämpel.
- Verktygsnamn och argument.
- Resultat eller fel.
- Varaktighet.
- Tillhörande användare/session.
- Anropskedjan i turen – ingick anropet i en längre kedja?
Bygg instrumentpaneler för dessa data. Vanliga vyer:
- Antal verktygsanrop per verktyg.
- Felfrekvens per verktyg.
- Genomsnittlig varaktighet per verktyg.
- Mönster i verktygssekvenser – vilka verktyg brukar anropas tillsammans?
- Hallucinerade verktygsanrop där LLM:en försökte anropa ett verktyg som inte finns.
De visar var systemet brister och var det kostar pengar.
Mönster 8: hallucinerade argument
LLM:er hittar ibland på parametervärden. De anropar search_customers(email="...") med en adress som inte motsvarar frågan eller book_meeting(date="...") med ett datum som aldrig nämnts.
Motåtgärder:
Strikt schema med beskrivningar. ”user_id måste ha nämnts tidigare i konversationen. Hitta inte på ID:n.”
Validering i verktygsomslaget. Om värdet är orimligt, exempelvis ett okänt användar-ID eller ett passerat datum, returnerar verktyget ett strukturerat fel så att modellen omprövar.
Reflektion. ”Bekräfta före anropet att värdena har stöd i konversationen.”
Begränsade verktygsbeskrivningar. Verktyg för specifika entiteter exponerar bara entitets-ID:n som redan hämtats. Exponera inte rå sökning.
Revisionsloggar. Fånga mönster av hallucinerade argument och justera prompter eller scheman.
Mönster 9: kontrollerad degradering
Verktyg och API:er fallerar, och hastighetsgränser nås. Rätt svar är sällan att säga att ingenting fungerar.
Mönster:
Cachelagrade eller inaktuella data. Returnera cachedata med uppgift om att de är inaktuella när livekällan är otillgänglig.
Partiellt slutförande. Om tre av fem deluppgifter lyckas, redovisa vad som utfördes och inte.
Reservvägar. Om huvudverktyget fallerar känner modellen till ett alternativ. Om search_documents fallerar kan den exempelvis använda search_web med lämpliga förbehåll.
Användarsynliga feltillstånd. Om verktyget verkligen inte kan slutföra uppgiften ger modellen ett tydligt felmeddelande, inte ett hallucinerat lyckat resultat.
Modellen behöver känna till de här mönstren. Dokumentera mönstren i systemprompten:
Om ett verktyg returnerar ett fel:
- Prova det alternativa verktyget om ett sådant finns.
- Redovisa partiella resultat tydligt om användaren redan har lämnat information.
- Påstå aldrig att åtgärden lyckades när verktyget returnerade ett fel.
Mönster 10: strömmande strukturerade utdata
Att strömma partiella strukturerade utdata ger bra UX: användaren ser resultatet ta form i realtid.
Implementering:
- De flesta moderna modell-API:er strömmar JSON token för token.
- Tolka partiell JSON inkrementellt med exempelvis
partial-json-parsereller en liten egen strömtolk. - Uppdatera gränssnittet när fälten anländer.
Det fungerar särskilt bra för utdata med flera avsnitt, såsom en lång produktbeskrivning, en analys med flera insikter eller en kodgranskning med flera fynd – de upplevs som betydligt mer responsiva när de strömmas.
Förbehåll: fatta inga beslut utifrån partiella utdata. Strömma för visning, men invänta slutförandet innan det strukturerade resultatet används.
Mönster 11: funktionsanrop kontra explicita beslutsanrop
Inbyggda funktionsanrop är praktiska: modellen ”bestämmer” när ett verktyg ska användas. I vissa arbetsflöden är ett explicit beslutsanrop mer tillförlitligt.
Exempel: ett supportflöde där modellen väljer mellan flera åtgärder.
Inbyggt funktionsanrop: Ge modellen fem verktyg (refund, send_article, escalate_to_human, ask_clarifying_question, close_ticket) och låt den välja.
Explicit beslutsanrop: Anropa först modellen med ett enda verktyg, decide_action, med en parameter för åtgärdsval. Anropa sedan modellen igen med enbart relevant verktyg.
Det explicita sättet är långsammare och utförligare men mer tillförlitligt. Modellen är mer fokuserad i varje steg och värdsystemet har större kontroll.
Det explicita sättet vinner ofta i arbetsflöden med höga insatser. Inbyggda funktionsanrop räcker för utforskande eller enkla flöden.
Mönster 12: formatering av verktygsresultat
Hur resultatet returneras spelar roll. Modellen läser det.
Dåligt:
{"id": "cus_123", "n": "Johan", "p": "12345"}
Bättre:
{
"customer_id": "cus_123",
"name": "Johan Andersson",
"phone": "+46-8-555-0123",
"tier": "premium",
"open_tickets": 0
}
Bäst (i vissa fall):
Kund hittad:
- ID: cus_123
- Namn: Johan Andersson
- Nivå: Premium
- Telefon: +46-8-555-0123
- Öppna ärenden: 0
Kunden tillhör premiumnivån och har inga öppna ärenden.
Den ”bästa” formen är människoläsbar, ger kontext och är enklare för modellen i efterföljande generering. Den ”bättre” är mer strukturerad och maskinläsbar. Testa vilken modellen hanterar bäst för nedströmsuppgiften.
För vissa verktyg fungerar både strukturerad text och berättande text bra: ”Här är resultatet: [berättande]. Rådata: [JSON].”
Mönster 13: schemamedvetna omförsök
Vissa valideringsfel går inte att reparera eftersom modellen i grunden missförstått uppgiften. Andra är enkla att rätta. Klassificera felet och agera därefter.
def handle_validation_error(error):
if "missing required field" in str(error):
return retry_with_message("Du utelämnade det obligatoriska fältet X. Inkludera det.")
elif "value not in enum" in str(error):
return retry_with_message("Värdet X ingår inte i den tillåtna mängden. Välj bland: ...")
elif "type mismatch" in str(error):
return retry_with_message("Fältet X måste vara ett tal, inte en sträng.")
else:
# Okänt fel – ett generellt omförsök
return retry_with_message("Ditt svar innehöll ett fel. Försök igen.")
Anpassade omförsök lyckas oftare än generella.
Mönster 14: komponerbarhet
Verktyg ska kunna kombineras. Små, fokuserade verktyg som gör en sak kan sättas ihop av modellen till komplexa arbetsflöden.
Ett monolitiskt verktyg process_customer_request(query) som gör allt är en svart låda. Modellen kan varken observera eller styra den interna logiken.
Fokuserade verktyg – search_customer(email), get_recent_orders(customer_id), check_subscription_status(customer_id), escalate_to_human(reason) – kan kombineras till rätt flöde för varje situation.
Välj rätt granularitet. Varje verktyg gör en sak. Verktygen sätts samman till arbetsflöden.
Mönster 15: schema för ”jag vet inte”
Modellera osäkerhet explicit i schemat:
class CustomerInfo(BaseModel):
name: str
name_confidence: Literal["high", "medium", "low"]
needs_clarification: bool
clarification_question: Optional[str] = None
Modellen kan returnera ”low confidence” och en förtydligande fråga i stället för att fabulera. Det är betydligt bättre än en modell som alltid fyller i fälten tvärsäkert, ibland med hallucinerade data.
Genomarbetat exempel: fakturahantering
Så här ser ett produktionsmässigt fakturahanteringssystem ut.
Indata: PDF-faktura bifogad till e-post. Mål: Extrahera strukturerade data och dirigera dem till ekonomisystemet.
Schema:
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
total: float
confidence: Literal["high", "medium", "low"]
class Invoice(BaseModel):
vendor_name: str
vendor_id: Optional[str] = None # null om leverantören saknas i våra register
invoice_number: str
invoice_date: date
due_date: Optional[date] = None
line_items: List[LineItem]
subtotal: float
tax: float
total: float
currency: str # ISO 4217
confidence: Literal["high", "medium", "low"]
needs_review: bool
review_reasons: List[str] # Specifika skäl till att granskning behövs
Arbetsflöde:
- OCR-steg: En bildmodell extraherar text ur PDF-filen.
- Extraktionssteg: LLM-anrop med schemat ovan och begränsad generering.
- Valideringssteg: Pydantic validerar. Valideringsfel utlöser ett omförsök med felåterkoppling.
- Avstämningssteg: Ett verktyg söker leverantören i registren. Matchar
vendor_name, lägg tillvendor_id. Annars sättsneeds_review=true. - Matematikkontroll: Verifiera
sum(line_items.total) ≈ subtotalochsubtotal + tax ≈ total. Sätt annarsneeds_review=true. - Tilltroskontroll: Sätt
needs_review=trueomconfidenceär low eller någon rad har låg tilltro. - Dirigering: Skicka till mänsklig granskningskö om
needs_review=true, annars till ekonomisystemet. - Loggning: Logga varje stegs indata, utdata, varaktighet och fel.
Hanterade fellägen:
- Felaktig JSON: begränsad generering förebygger, omförsök hanterar specialfall.
- Hallucinerade fält: schemat är strikt.
- Räknefel: valideras.
- Okända leverantörer: flaggas.
- Låg tilltro: flaggas.
- Verktygsfel: hanteras explicit.
Produktionsprestanda: Cirka 95 procent av fakturorna går rakt igenom och 5 procent flaggas. Felfrekvensen bland de automatiskt hanterade är <0,5 procent, väl inom acceptabel nivå. I granskningskön bekräftas cirka 80 procent som korrekta och 20 procent behöver rättas.
Detta är produktionsmässiga strukturerade utdata. Inte bara ”JSON-läget fungerade en gång”, utan en pipeline för verkliga fellägen.
Vanliga misstag
Några mönster vi ser gång på gång:
Misstag 1: Ingen validering. Pydantic, zod eller något annat – validera bara. Lita inte på modellen.
Misstag 2: Vaga beskrivningar. ”category: string” hjälper inte modellen. ”category: ett av billing, technical, account_access, där billing omfattar …” gör det.
Misstag 3: För många verktyg. Med 30 verktyg väljer modellen fel. Begränsa varje anrop till färre än 10 relevanta verktyg.
Misstag 4: Inget omförsök vid valideringsfel. Ett felaktigt resultat slår ut hela flödet. Försök en gång till med återkoppling.
Misstag 5: Ingen observerbarhet. Utan spår går det inte att diagnostisera produktionsfel.
Misstag 6: Ingen idempotens för verktyg med sidoeffekter. Dubbla återbetalningar och e-postmeddelanden är ett förutsägbart fel.
Misstag 7: Lita på LLM-valda argument utan validering. Hallucinerade användar-ID:n och datum. Validera argumenten före körning.
Misstag 8: Ingen schemaversionering. Schemaändringar bryter nedströmskonsumenter. Versionshantera.
Från demo till produktionssystem
Strukturerade utdata och funktionsanrop är bron från ”en LLM som talar” till ”en LLM som utför arbete”. Rätt utfört möjliggör de produktions-AI. Fel utfört går de sönder på intressanta och dyra sätt.
De viktiga mönstren är strikta scheman, begränsad generering, validering med omförsök, reflektion över verktygsresultat, idempotens, kontrollerad degradering, schemamedveten felhantering och heltäckande observerbarhet.
Varje sådant mönster skiljer en demo från ett produktionssystem. Bygg in dem från början.



