Sari la conținut
18/30Capitolul 18 din 30

Tool Calling și ieșiri structurate: contractul care nu cedează

24 de apeluri, zero JSON stricat și două date utilizabile. Apoi același endpoint cu o descriere mai bună — și ce nu poate repara o schemă.

Pe această pagină

Dă-i unui model un tool de căutare de zboruri și cere-i să găsească un zbor de la Madrid la Berlin. Iată ce se întoarce:

TEXT
<tool_call>
{"name": "search_flights",
 "arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>

JSON-ul este valid. Numele tool-ului este corect. Fiecare câmp obligatoriu este prezent. Iar apelul este inutil: niciun API de zboruri nu acceptă "Madrid" acolo unde așteaptă un cod de aeroport sau "3rd October 2026" acolo unde așteaptă o dată.

Această ruptură — perfect sintactic, inutilizabil semantic — este subiectul acestui capitol, iar primul lucru de stabilit este că nu este o problemă de JSON. În douăzeci și patru de cereri cu acest tool, modelul a produs 24 de tool calls valide și zero JSON stricat. Nu a eșuat niciodată la partea pe care o depanează toată lumea.

Înainte de mecanică, propoziția care previne cea mai mare confuzie: un tool call este o cerere, nu o acțiune.

Modelul emite un mesaj structurat care spune aș vrea să fie apelat search_flights cu aceste argumente. Apoi se oprește. Codul tău primește acel mesaj, decide dacă îl onorează, apelează ce are de apelat și trimite rezultatul înapoi ca alt mesaj. Modelul nu ți-a atins niciodată baza de date, nu a făcut niciodată o cerere HTTP, nu a avut niciodată credențiale.

Tot ce ține de securitatea agent în Capitolul 30 decurge din această împărțire, la fel ca tot ce ține de designul de agent în Capitolul 23: modelul propune, iar codul tău dispune, iar codul este locul în care trăiește orice garanție.

Așadar, un tool, fără vocabularul din jur, înseamnă două lucruri:

O schemă. O JSON Schema care descrie o funcție: numele ei, ce face și ce argumente primește, cu tipurile și constrângerile lor. Asta intră în prompt și este singurul lucru pe care modelul îl vede vreodată.

Un endpoint. O funcție din codul tău care primește acele argumente și returnează ceva. Modelul nu o vede niciodată, nu știe niciodată în ce limbaj este scrisă și nu poate deosebi o interogare de bază de date de un string hardcodat.

Definițiile tool-urilor intră în prompt, serializate în formatul pe care a fost antrenat modelul. Costă tokens la fiecare apel — un fapt care revine cu o cifră mai târziu în acest capitol.

În loc de proză, răspunsul conține o cerere structurată, iar API-ul raportează un motiv de finalizare care spune asta. Motivul contează: așa știe codul tău să ruleze un tool în loc să îi arate utilizatorului un răspuns.

Acesta este pasul în care nu există model. Validează argumentele față de schemă, decide dacă acest apelant are voie să facă asta și execută.

Rezultatul devine încă un schimb în conversație, într-un rol rezervat pentru el. Modelul îl citește ca pe orice alt context.

Aceasta este bucla din Capitolul 23 și motivul pentru care o singură cerere poate deveni o duzină de dus-întorsuri.

Nimic din toate acestea nu este emergent. După cum a stabilit Capitolul 11, tool calling este un comportament antrenat:1 în timpul post-training, modelul a văzut mii de conversații modelate exact așa. De aceea formatul este specific modelului, de aceea fiabilitatea variază atât de mult între modele de dimensiuni similare și de aceea un model poate apela un tool pe care nu l-a mai văzut niciodată — forma a fost antrenată, tool-ul concret vine din prompt.

Iată tool-ul așa cum îl scriu cei mai mulți oameni prima dată. Observă că nimic din el nu este greșit; este doar subțire:

tools/badFlights.tsTS
{
  name: "search_flights",
  description: "Search for flights.",
  parameters: {
    type: "object",
    properties: {
      from: { type: "string", description: "Airport." },   
      to:   { type: "string", description: "Airport." },   
      date: { type: "string", description: "The date." },  
    },
    required: ["from", "to", "date"],
  },
}

Douăzeci și patru de cereri, șase perechi de orașe încrucișate cu patru moduri de a exprima o dată ("the 3rd of next month", "next Friday", "15 December", "tomorrow"), greedy decoding ca rezultatele să se reproducă:

tool apelatJSON stricatdată în ISOaeroporturi ca IATAtotul corect
schema de mai sus24/2402/244/241/24

Citește primele două coloane înaintea ultimelor trei. Modelul apelează tool-ul corect de fiecare dată și produce JSON bine format de fiecare dată. Eșecul este în întregime în valori, iar valorile sunt inutilizabile: "Madrid" în loc de MAD, "3rd October 2026" în loc de 2026-10-03.

Merită insistat asupra acestui lucru, pentru că determină unde te uiți când se strică ceva. Instinctul este să adaugi un parser JSON cu retry sau să îi ceri modelului mai apăsat JSON valid. Niciuna dintre aceste soluții nu tratează ce s-a întâmplat aici.

Același endpoint. Același cod în spate. Același model, aceleași prompts, aceeași decodare. Singurul lucru care se schimbă este textul din schemă:

tools/goodFlights.tsTS
{
  name: "search_flights",
  description: "Search scheduled flights between two airports on a given day.",
  parameters: {
    type: "object",
    properties: {
      from: {
        type: "string",
        description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",   
        pattern: "^[A-Z]{3}$",
      },
      to: { /* same */ },
      date: {
        type: "string",
        description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",   
        format: "date",
        pattern: "^\\d{4}-\\d{2}-\\d{2}$",
      },
    },
    required: ["from", "to", "date"],
  },
}
FORMAT datăVALOARE datăFORMAT aeroportVALOARE aeroport
schemă subțire2/241/244/244/24
schemă descrisă24/2412/2416/248/24

Formatul datei trece de la 2 din 24 la 24 din 24. Perfect, doar dintr-o schimbare de text, fără să atingi codul și fără logică de retry. Dacă iei un singur obicei operațional din acest capitol, acesta este: când un tool este apelat greșit, soluția este aproape întotdeauna în descriere și este cea mai ieftină soluție din sistem.

Acum citește a doua coloană, care este jumătatea mai importantă.

O schemă constrânge forma. Nu poate furniza cunoaștere.

Link către secțiunea: O schemă constrânge forma. Nu poate furniza cunoaștere.

Data este în format ISO de 24 de ori din 24. Este ziua corectă de 12 ori din 24.

Deci jumătate dintre apeluri poartă acum o dată perfect formatată, dar greșită. Descrierea i-a spus modelului ce formă să producă, iar modelul a produs-o impecabil — dar transformarea lui "next Friday" în 2026-09-11 cere să știi data de astăzi și să faci aritmetică de calendar, iar nicio descriere nu furnizează asta. La fel și pentru aeroporturi: formatul a trecut de la 4 la 16, dar valoarea doar de la 4 la 8, pentru că scrierea lui MAD cere să știi că aeroportul Madridului este MAD.

Această distincție este ideea de rezistență a capitolului:

O schemă este un contract despre formă. Poate face ieșirea modelului parsabilă, tipată și consecventă. Nu o poate face adevărată, iar fiecare mod de eșec care supraviețuiește unei scheme bune este un eșec de cunoaștere, nu un eșec de format.

Cele două au nevoie de soluții diferite, iar confundarea lor irosește săptămâni. Eșecurile de format se repară în descriere sau cu decodare constrânsă, mai jos. Eșecurile de cunoaștere se repară prin punerea cunoașterii în prompt — data curentă în mesajul de sistem, o căutare de aeroport ca al doilea tool pe care modelul îl apelează primul, un enum în schemă când setul este suficient de mic pentru a fi enumerat. Observă ce au toate trei în comun: mută problema din memoria modelului în inputul lui, ceea ce este întregul subiect al Capitolului 24.

Ieșiri structurate și ce este de fapt „decodarea constrânsă”

Link către secțiunea: Ieșiri structurate și ce este de fapt „decodarea constrânsă”

Tot ce este mai sus se bazează încă pe faptul că modelul alege să producă forma corectă. Există o garanție mai puternică disponibilă, iar aceasta este cel mai bun câștig din Capitolul 17.

Amintește-ți cum funcționează generarea: la fiecare pas, modelul produce un logit pentru fiecare token din vocabular, iar samplerul alege unul. Decodarea constrânsă introduce un pas între ele. Dată fiind o gramatică — derivată din JSON Schema — calculează ce tokens ar putea urma legal, setează logits pentru toate celelalte la infinit negativ și lasă samplerul să aleagă din ce rămâne.

Dacă schema spune că următorul lucru trebuie să fie un {, atunci fiecare token care nu este { are probabilitate zero. Nu „improbabil”: zero. Modelul nu poate emite JSON invalid pentru că tokens invalizi au fost eliminați din distribuție înainte de sampling.

Asta sunt, dedesubt, „ieșirile structurate”, „JSON mode” și „guided generation”, iar asta explică cele două proprietăți ale lor. Garanția este totală pentru orice poate exprima gramatica — tipuri, câmpuri obligatorii, enum-uri, imbricare — pentru că este impusă mecanic, nu cerută politicos. Și nu spune nimic despre conținut: o gramatică poate forța ca "date" să fie un string care se potrivește unui tipar de dată și nu poate forța să fie ziua corectă. Este același zid ca în secțiunea precedentă, atins din cealaltă parte.

Două note practice. Nu este gratuită: masca trebuie calculată la fiecare pas, iar gramaticile complexe costă latență măsurabilă. Și schimbă ce face modelul — un model ghidat departe de token-ul preferat poate produce conținut mai slab în timp ce produce structură perfectă, motiv pentru care „cere frumos și validează” rămâne un default rezonabil pentru forme simple, iar decodarea constrânsă își merită costul când forma este complexă sau consumatorul este strict.

Efecte secundare și singura proprietate care contează

Link către secțiunea: Efecte secundare și singura proprietate care contează

Capitolul 14 a măsurat un timeout urmat de un retry care a facturat două generări pentru un singur răspuns. Cu tool-uri, același eșec devine mai rău, pentru că un tool poate face ceva.

Dacă codul tău apelează charge_card, intră în timeout și reîncearcă, ai două taxări. Modelul habar nu are că s-a întâmplat ceva din toate acestea; vede un singur rezultat de tool. Soluția este aceeași ca în orice sistem distribuit și nu este problema modelului: fă operațiunea idempotentă dând apelului o cheie, astfel încât a doua execuție să o recunoască pe prima și să îi returneze rezultatul în loc să facă din nou munca.

Regula de design care urmează merită spusă clar. Separă citirile de scrieri în catalogul tău de tool-uri. O citire poate fi reluată liber, rulată în paralel și cache-uită. O scriere nu poate, și ar trebui să poarte o cheie, o verificare de permisiune și — pentru orice lucru despre care un utilizator ar vrea să știe înainte să se întâmple — un pas de aprobare care pune un om între cerere și acțiune. Acel pas de aprobare nu este o politețe: este unul dintre puținele lucruri care stau între un prompt injection și o consecință reală — și, după cum măsoară Capitolul 30, cel mai slab dintre ele.

Folclorul spune că încărcarea multor tool-uri face modelul să aleagă prost. Merită măsurat, nu repetat, așa că: aceleași douăzeci și patru de cereri, cu tool-ul de zboruri plus un set tot mai mare de altele — inclusiv trei deliberat confundabile (orare de tren, traversări cu feribotul, rute de autobuz).

tool-uri încărcateprompt tokensa ales search_flightsdată în ISO
135324/2424/24
573024/2424/24
101,19321/2421/24
202,11924/2424/24

Selecția nu s-a degradat. Cu douăzeci de tool-uri, trei dintre ele plauzibil confundabile, un model cu jumătate de miliard de parametri l-a ales pe cel corect de douăzeci și patru de ori din douăzeci și patru. Scăderea de la zece înseamnă trei apeluri care au numit un alt tool și nu supraviețuiește trecerii la douăzeci.

Acesta este un rezultat negativ și ar trebui raportat ca atare: în această sarcină, cu aceste tool-uri, „prea multe tool-uri” nu a fost problema. Ce a crescut, monoton și cu un factor de șase, este promptul: de la 353 tokens la 2,119, plătiți la fiecare cerere din conversație, pentru totdeauna, indiferent dacă se folosește sau nu vreun tool.

Așadar, versiunea onestă a folclorului este despre cost și context, nu acuratețe. Douăzeci de tool-uri sunt o taxă permanentă pe fiecare mesaj, iar Capitolul 16 a arătat deja ce face un prefix permanent unei facturi pe parcursul a patruzeci de schimburi. Când oamenii raportează că multe tool-uri afectează calitatea, mecanismul este de obicei că definițiile au scos din context lucrurile care contau — ceea ce este o problemă de Capitol 24 purtând costum de Capitol 18. Tool-urile care sunt cu adevărat aproape duplicate sunt și ele o problemă reală, iar soluția pentru ele nu este mai puține tool-uri, ci descrieri și namespaces mai bune: prefixează-le după sistem (crm.search_customer, billing.search_customer), astfel încât două cataloage unite de la două echipe să nu se ciocnească și modelul să aibă ceva după care să discrimineze.

Trei feluri de tool-uri și cel care deschide partea următoare

Link către secțiunea: Trei feluri de tool-uri și cel care deschide partea următoare

Ajută să sortezi tool-urile după ce fac lumii, pentru că ingineria diferă pentru fiecare.

Tool-urile de date citesc: caută, aduc, interoghează. Pot fi reluate, paralelizate, cache-uite. Eșuează returnând nimic util, iar principalul lor risc este că aduc text neîncredere în context — ceea ce este întreaga suprafață de atac din Capitolul 30.

Tool-urile de acțiune scriu: trimit, creează, taxează, șterg. Nu pot fi reluate fără o cheie, nu pot fi paralelizate în siguranță și sunt motivul pentru care există fluxuri de aprobare.

Tool-urile de orchestrare apelează alte modele. Un tool a cărui implementare este alt agent, cu propriul prompt, propriile tool-uri și propria buclă — iar pentru modelul apelant arată exact ca celelalte două, pentru că o schemă și un endpoint sunt tot ce vede vreodată.

Al treilea tip nu este o curiozitate. Este mecanismul din spatele jumătății „agent-ca-tool” din Capitolul 25 — cealaltă topologie, handoff, cedează conversația și nu o mai primește niciodată înapoi — și funcționează tocmai pentru că interfața din acest capitol este suficient de îngustă încât un întreg agent să încapă în spatele ei.

Ai acum un model care poate cere lucruri și un contract care face cererea parsabilă. Ce nu ai este ceva despre care să întrebe, dincolo de ce încape în prompt.

Cel mai comun tool în producție, de departe, este o căutare într-un corp de text pe care modelul nu l-a văzut niciodată în timpul antrenării: documentația ta, tichetele tale, contractele tale. Sună ca o problemă rezolvată — îi faci embedding, găsești cei mai apropiați vecini, îi lipești în prompt — iar părțile nerezolvate sunt cele care decid dacă răspunsul este de încredere: cum este tăiat textul înainte să primească embedding, ce prag de similaritate este suficient de jos ca să însemne nu știu și cum este atașată o citare la o afirmație, astfel încât un cititor să o poată verifica.

Capitolul 19 este despre retrieval și este capitolul în care un răspuns greșit încetează să fie o curiozitate și începe să fie o răspundere.


Măsurătorile din acest capitol vin din Qwen/Qwen2.5-0.5B-Instruct cu greedy decoding, pe 24 de cereri generate care încrucișează șase perechi de orașe cu patru formulări de dată, folosind propriul chat template al modelului pentru definițiile tool-urilor. Se reproduc exact și sunt un model mic: citește împărțirea format/valoare ca o demonstrație a mecanismului, nu ca un benchmark pentru ce fac modelele actuale. Un model frontieră rezolvă "next Friday" corect mult mai des — și tot nu poate fi făcut să o facă printr-o schemă, ceea ce este partea care se generalizează.

Vocabularul JSON Schema folosit mai sus (type, properties, required, pattern, format, enum) este specificat în draftul JSON Schema pe care îl numește documentația providerului tău; subsetul util este mic și același între provideri, iar diferențele care există — ce keywords sunt impuse prin decodare constrânsă în loc să fie doar transmise modelului — merită citite în ghidul de structured-output al providerului, nu presupuse.

Pentru decodarea constrânsă ca tehnică, bibliotecile de stil guidance și proiectul outlines documentează construcția gramatică-la-mască-de-logit într-un mod care se mapează direct pe samplerul din Capitolul 17. Iar pentru dus-întorsul în sine, cea mai clară specificație nu este un tutorial, ci un protocol: Capitolul 26 îl citește linie cu linie.

  1. Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). Lucrarea care a standardizat rețeta de post-training; forma unui tool call este învățată acolo, din demonstrații, exact ca forma unui răspuns.

Gata să lași LIA să aleagă?

Construiește cu toate modelele AI într-un singur loc — începe gratuit azi.