Model, který se ptá, si nevymýšlí
Zapojit jazykový model do aplikace je dneska otázka pár řádků kódu. Pošleš text, dostaneš text. Dokud po modelu chceš překlad nebo shrnutí, funguje to výborně. Problém přijde ve chvíli, kdy má odpovědět na něco, co ví jenom tvoje aplikace.
Řekněme, že programuješ zákaznickou podporu pro e-shop. Chceš, aby model odpovídal na dotazy typu „kde mám balík“ nebo „kdy mi dorazí objednávka“. Model umí česky, umí být zdvořilý a umí odpověď hezky naformulovat. Jenom netuší, jaké objednávky máš v databázi, protože do ní vidíš ty, ne on.
První nápad bývá přiložit mu potřebná data rovnou k dotazu. Jenže bys musel dopředu vědět, na co se zákazník zeptá. Poslat mu celou databázi nejde, a i kdyby to šlo, platí se za každé slovo, které mu pošleš.
A když mu data nedáš, může se stát něco horšího, než že odmítne odpovědět. Model se totiž nemusí přiznat, že neví. Občas si odpověď vymyslí, a vymyšlená zní úplně stejně přesvědčivě jako pravdivá. Zákazník se dozví číslo zásilky, které nikdy neexistovalo.
Existuje ale ještě jedna cesta: prohodit role. Místo abys hádal, co bude model potřebovat, popíšeš mu, co tvoje aplikace umí zjistit. Model se pak sám ozve, že by potřeboval vyhledat objednávku číslo 2026–001234, ty mu výsledek dodáš a on z něj složí odpověď pro zákazníka. Technika se jmenuje tool calling, občas taky function calling, a v PHP ji za tebe obslouží knihovna AI Access, která právě vyšla ve verzi 1.0.
Jedna věc je na tom důležitější než všechno ostatní: model tvůj kód nespouští. Jen řekne, co by rád zavolal a s jakými argumenty. Jestli se to opravdu stane, rozhoduje tvoje aplikace. Zní to jako detail, ale je v tom celý rozdíl mezi modelem, který se ptá, a modelem, který poroučí.
Tool calling je technika, která umožňuje LLM modelům (jako ChatGPT nebo Claude) vyžádat si od tvé PHP aplikace spuštění konkrétní funkce, a získat tak aktuální data nebo provést akci. Structured output naopak zaručuje, že model vrátí odpověď v přesně definovaném, validním JSON formátu. V tomto článku si ukážeme, jak obojí elegantně implementovat pomocí nové PHP knihovny AI Access.
Jak nastavit Tool calling v PHP
Nástroj popíšeš objektem Tool. Dáš mu jméno, popis toho,
k čemu je, schéma argumentů a funkci, která ho vykoná. Jméno modelu
v ukázce nikde nehledej, nese si ho klient, kterému jsi ho zadal při
vytvoření; předat ho můžeš i přímo do createChat():
use AIAccess\Chat\Tool;
$chat = $client->createChat();
$chat->addTool(new Tool(
name: 'findOrder',
description: 'Najde objednávku podle čísla a vrátí její stav a datum odeslání.',
parameters: [
'type' => 'object',
'properties' => [
'orderNumber' => ['type' => 'string', 'description' => 'Číslo objednávky ve tvaru 2026-001234'],
],
'required' => ['orderNumber'],
],
handler: fn(array $args) => $this->orders->findByNumber($args['orderNumber'])->toArray(),
));
echo $chat->sendMessage('Kde mám objednávku 2026-001234?')->getText();
To je celé. Jediné sendMessage() obstará celou výměnu:
model si řekne o nástroj, knihovna zavolá tvoji obsluhu, pošle výsledek
zpátky a počká, až model odpoví. Obsluha vrací pole nebo řetězec,
protože přesně to model dostane k přečtení.
Stojí za to zastavit se u popisu, protože se snadno odbude jako formalita. Přitom je to prompt jako každý jiný. Model se podle něj rozhoduje, jestli nástroj vůbec zavolat, a nemá k tomu žádné jiné vodítko. „Najde objednávku podle čísla a vrátí její stav a datum odeslání“ je dobrý popis. „Objednávky“ je špatný. Totéž platí pro popisy jednotlivých argumentů: když modelu řekneš, jak vypadá číslo objednávky, přestane ti tam posílat jméno zákazníka.
Smyčka konverzace a limit API volání
Jedno kolo znamená jednu otázku modelu a jednu tvou odpověď. Zajímavé je, že jich za sebou proběhne klidně několik, aniž bys s tím musel cokoli dělat.
Přidej k předchozímu nástroji ještě dva, findCustomer pro
vyhledání zákazníka podle e-mailu a listOrders pro výpis jeho
objednávek. Na otázku
jaké objednávky má zákazník jana@example.com model nejdřív
zavolá findCustomer, počká na výsledek, z něj si vezme
identifikátor a teprve pak sáhne po listOrders. Nikde jsi ten
postup nepopsal, model si ho složil sám z popisů, které jsi mu dal.
Celé té sérii se říká smyčka. Má strop, protože jinak by ti model mohl kroužit dokola na tvůj účet:
$chat->setToolLoop(maxRounds: 3);
Výchozích osm kol na běžnou práci bohatě stačí; když se konverzace
ani do té doby neustálí, dostaneš TooManyRoundsException.
A jestli tě zajímá, co celá výměna stála, ptej se metodou
getTotalUsage(), protože getUsage() na odpovědi
mluví jen o posledním kole.
Bezpečnost a autorizace při function callingu
Zatím to vypadá, že se model může dostat ke všemu, co mu nabídneš. U hledání objednávky je to v pořádku. U jejího storna už ne, tam chceš rozhodnout sám.
Automatická smyčka se rozjede jen tehdy, když má každý volaný nástroj obsluhu. Jakmile jedna chybí, knihovna se zastaví a vrátí řízení tobě. Není to opomenutí, které bys musel ošetřovat, ale způsob, jak říct „tohle si ohlídám sám“:
$chat->addTool(new Tool(
name: 'cancelOrder',
description: 'Zruší objednávku podle čísla.',
parameters: [
'type' => 'object',
'properties' => ['orderNumber' => ['type' => 'string']],
'required' => ['orderNumber'],
],
// obsluha tu schválně není
));
$response = $chat->sendMessage('Zruš mi prosím objednávku 2026-001234.');
foreach ($response->getToolCalls() as $call) {
$number = $call->arguments['orderNumber'];
if ($this->user->isAllowed('order', 'cancel')) {
$this->orders->cancel($number);
$chat->addToolResult($call, "Objednávka $number byla zrušena.");
} else {
$chat->addToolResult($call, 'Na zrušení objednávky nemáš oprávnění.', isError: true);
}
}
echo $chat->sendMessage()->getText();
Model tedy o storno požádá, ale provede ho tvůj kód až po kontrole.
Všimni si druhé větve: odmítnutí je taky platný výsledek. Pošleš
ho zpátky s příznakem isError a model s ním umí pracovat,
takže zákazníkovi vysvětlí, proč to nešlo, místo aby čekal na něco, co
nikdy nepřijde.
Stejná cesta se hodí i tam, kde chceš volání jen logovat nebo omezit.
A když potřebuješ opak, tedy přimět model sáhnout po konkrétním
nástroji, řekneš si o to metodou setToolChoice().
Řešení halucinací a chyb modelu
Občas se stane, že model požádá o nástroj, který neexistuje, nebo pošle argumenty, které neodpovídají schématu. Nestává se to často, ale ani vzácně, a hlavně to není chyba tvé aplikace, takže to knihovna neřeší výjimkou.
Knihovna takový omyl pošle zpátky modelu jako chybový výsledek a nechá ho, ať se opraví. Modely v tom bývají překvapivě dobré: přečtou si, co bylo špatně, a zavolají nástroj znovu pořádně. Kdyby místo toho vyletěla výjimka, přišel bys o celou odpověď kvůli chybě, kterou si model umí spravit sám.
Jiná věc je selhání tvojí vlastní obsluhy. Když spadne databáze, výjimka propadne k tobě, což je správně, protože rozbitou databázi nemá řešit model. Pokud chceš, aby se o problému dozvěděl a zkusil jinou cestu, řekneš si o to:
$chat->setToolLoop(catchErrors: true);
I pak ale platí jedna výjimka z výjimek: chyby typu Error,
tedy překlepy ve tvém kódu, propadnou vždycky. Kdyby se posílaly modelu
jako výsledek nástroje, tvoje chyba by se schovala uvnitř konverzace a ty by
ses o ní nikdy nedozvěděl.
Pod kapotou se toho děje víc, než by mělo smysl tady vypisovat. Gemini třeba volání nástroje vůbec neohlásí mezi důvody ukončení odpovědi a přemýšlející modely chtějí své úvahy dostat v dalším kole zpátky nezměněné, jinak požadavek odmítnou. Knihovna to řeší za tebe, takže na to narazíš jedině ve chvíli, kdy nakoukneš do syrových odpovědí.
Structured output v PHP: Vynucení přesného JSON schématu
Zatím se ptal model. Teď obraťme situaci: chceš z e-mailu od zákazníka vytáhnout data do databáze. Žádná otázka, žádná akce, jen hodnoty.
Nabízí se o ten JSON prostě požádat v promptu. Devětkrát z deseti to projde. Podesáté dostaneš tohle:
Jistě! Zde jsou požadovaná data:
```json
{"category": "reklamace", "orderNumber": "2026-001234"}
```
Odpověď je věcně správná, jenže json_decode() na ní
selže, protože kolem je věta a markdownový blok. Jindy model pojmenuje
klíč jinak, než jsi chtěl, nebo vrátí číslo jako text. V testech to
neodhalíš, protože většinou to prostě funguje. Odhalíš to v provozu,
obvykle na datech, která nikdo nečekal.
Řešení není prosit lépe, ale tvar odpovědi předepsat JSON schématem:
$chat->setResponseSchema([
'type' => 'object',
'properties' => [
'category' => ['type' => 'string', 'enum' => ['reklamace', 'dotaz', 'spam']],
'orderNumber' => ['type' => ['string', 'null'], 'description' => 'Číslo objednávky, pokud je v textu uvedeno'],
'urgency' => ['type' => 'integer', 'description' => 'Naléhavost od 1 do 5'],
],
'required' => ['category', 'orderNumber', 'urgency'],
'additionalProperties' => false,
]);
$data = $chat->sendMessage($email)->getJson();
Metoda getJson() vrátí rovnou dekódovaná data. Žádné
odlupování markdownu, žádné json_decode() navíc. A hlavně:
rozdíl proti promptu není v tom, že by model instrukci najednou respektoval
líp. Tvar odpovědi vynucuje poskytovatel, ne dobrá vůle modelu.
Tři věci se u schémat vyplatí vědět. Popisuj jednotlivá pole, model
je čte a řídí se jimi; z popisu „částka bez měny a bez mezer“
dostaneš 1500, zatímco z holé „částky“ klidně
"1 500 Kč". Na uzavřené seznamy používej enum,
jako je to u kategorie výš, model si pak nemůže vymyslet čtvrtou možnost.
A počítej se striktním režimem: OpenAI a Grok vyžadují
additionalProperties: false a všechny klíče v
required, takže volitelnou hodnotu tam nech a povol jí místo
toho null, přesně jako u čísla objednávky.
Jednu výjimku je dobré znát předem: DeepSeek schéma neumí. Jeho API si
o JSON říct dovede, ale jeho tvar předepsat neumožňuje, takže
setResponseSchema() tam sice najdeš, jenže vždycky skončí
výjimkou, která tě odkáže na prostý JSON režim. Ostatní čtyři
poskytovatelé i generický klient pro
dialekt OpenAI schéma zvládají.
Jaký je rozdíl mezi Structured output a Tool callingem?
Obojí vede k tomu, že model vrátí JSON podle schématu, a proto se to snadno plete. Rozdíl je v tom, kdo komu co podává.
Structured output je tvar odpovědi. Model skončí a ty dostaneš data. Použiješ ho, když chceš od modelu výsledek: roztřídit e-mail do kategorie, vytáhnout hodnoty z textu, rozebrat adresu na části.
Volání nástroje je otázka mířená na tebe. Model se zastaví a čeká, až mu něco zjistíš, a pak pokračuje. Použiješ ho, když model potřebuje informaci nebo akci, kterou vlastní tvoje aplikace.
Zkráceně: structured output je odpověď, nástroj je otázka. A nic ti nebrání použít obojí najednou, tedy nechat model nejdřív zjistit stav objednávky a pak ti vrátit hotový záznam pro databázi.
AI Access je PHP
knihovna, která sjednocuje OpenAI, Claude, Gemini, DeepSeek a Grok do jednoho
rozhraní, a to bez jediné závislosti. Kromě volání nástrojů a
strukturovaného výstupu umí streamování, obrázky a dokumenty na
vstupu, embeddingy pro
vyhledávání podle významu i dávkové zpracování za
poloviční cenu. Instaluje se přes
composer require ai-access/ai-access, chce PHP 8.3 a v distribuci
nese dvacítku spustitelných příkladů. O tom, jak vznikla, jsem
psal na svém blogu.
Chcete-li odeslat komentář, přihlaste se