Blog/Webfejlesztés
LLM funkciók a Nuxtban: streamelés, strukturált kimenet, eszközjóváhagyás
Nuxt AI végigtől végig: a kulcsot tartó szerverútvonal, message partok, szigorú strukturált kimenet, jóváhagyást igénylő eszközök, hibák és a 50. cikkely.
Balázs Csorba··9 perc olvasás
- Nuxt
- AI SDK
- Streaming
- Structured output
- Tool approval
- Nitro

A lényeg röviden
- A Nuxt AI funkciók egy Nitro szerverútvonalhoz tartoznak: a modell API-kulcsa a runtimeConfig-ben marad, a providernél lévő hívás a szerveren történik, és az @ai-sdk/vue csak az eredményt rendereli.
- Egy streamelt válasz egy rendezett message parts tömb, nem hosszabbodó string, és egy eszközhívás létrehoz egy tool- előtagú partot, amely önmagában nem tartalmaz szöveget.
- A strukturált kimenetet az output opcióval állítjuk be, és a szolgáltatók a JSON Schema csak egy részhalmazát támogatják: nincs minimum, maximum, minLength vagy maxLength, és nincsenek rekurzív sémák.
- Az AI SDK 7-ben az eszközjóváhagyás a híváson lévő toolApproval szabály, és a jóváhagyásokat az experimental_toolApprovalSecret értékkel kell aláírni, mert a kliens birtokolja az üzenetelőzményt.
- Az EU AI Act 50. cikkelye 2026. augusztus 2. óta hatályos: a chatbotnak legkésőbb az első interakciónál szólnia kell, hogy MI-vel beszélnek az emberek.
A Nuxt AI funkciók egy Nitro szerverútvonal és egy kis Vue kliens: az API-kulcs a runtimeConfig-ben él, és soha nem jut el a böngészőig, a providernél lévő hívás a szerveren történik, az oldal pedig a beérkező message partokat streameli és rendereli. Ennyi az egész architektúra. A nehéz részek a körülötte lévők: sémához kötött strukturált kimenet, olyan eszközök, amelyek ember nélkül nem futhatnak, hibák, amelyeket ki tudsz magyarázni a felhasználónak, és az a közlés, amelyet az EU felé néző chatbot az AI Act 50. cikkelye alapján ad.
Ez a cikk ezt az architektúrát állítja össze az AI SDK-val, annak 2026. szeptemberi állapotában, 7. verzióval, és minden kódrészlet meg van jelölve, melyik verzióval ellenőrizték. A helper nevek főverziók között vándorolnak, tehát rögzítsd a főverziót a package.json-ban, és a rögzített verzió dokumentációját olvasd.
Hol legyen a modellhívás?
A szerveren, mindig. Ha a böngésző közvetlenül hív egy modell-API-t, a kulcsot átadja annak, aki megnyitja a fejlesztői eszközöket, és sehol nincs olyan pont, ahol rate limitet kényszeríthetnél ki, személyes adatokat maszkolhatnál vagy naplózhatnád, mi történt. Nuxtban ez egy fájl a server/api alatt, amit a Nitro HTTP-útvonalvá alakít, meg a @ai-sdk/vue a kliensben.
// nuxt.config.ts – runtime config is server-only
export default defineNuxtConfig({
runtimeConfig: { aiGatewayApiKey: '' }, // filled from NUXT_AI_GATEWAY_API_KEY
})// server/api/chat.ts – AI SDK 7
import { streamText, convertToModelMessages, toUIMessageStream,
createUIMessageStreamResponse, createGateway } from 'ai'
import type { UIMessage } from 'ai'
export default defineEventHandler(async (event) => {
const { messages }: { messages: UIMessage[] } = await readBody(event)
const gateway = createGateway({ apiKey: useRuntimeConfig().aiGatewayApiKey })
const result = streamText({
model: gateway('anthropic/claude-sonnet-5'),
messages: await convertToModelMessages(messages),
})
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
})
}) Itt három típus dolgozik. A UIMessage az, amit a kliens küld: a teljes beszélgetés UI-metaadatokkal, például időbélyegekkel. A convertToModelMessages() levágja ezeket a metaadatokat arra a ModelMessage[]-re, amelyet a modell vár. A toUIMessageStream() pedig a modell nyers válaszstreamjét alakítja UI-stream-protokollá, ahogyan azt a Nuxt quickstart dokumentálja. Ha a Vercel AI Gateway helyett közvetlenül szolgáltatót használsz, a forma ugyanaz, csak a model sor változik.
Hogyan működnek a streamelt message partok?
Egy streamelt válasz nem egyre hosszabbodó string. Minden üzeneten egy rendezett parts tömb van, és a kliens beérkezés közben fűzi hozzá őket. Egy part lehet szöveg, reasoning trace, fájl vagy eszközhívás, az eszközhívás part neve pedig tool- meg az a kulcs, amely alatt definiáltad az eszközt. Ezért renderelsz egy v-for-ral és egy part.type-on futó switch-csel, nem a message.content interpolálásával.
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
const { messages, sendMessage } = useChat() // posts to /api/chat
const input = ref('')
const submit = () => { sendMessage({ text: input.value }); input.value = '' }
</script>
<template>
<div v-for="(message, index) in messages" :key="message.id ? message.id : index">
<template v-for="(part, i) in message.parts" :key="`${message.id}-${part.type}-${i}`">
<p v-if="part.type === 'text'">{{ part.text }}</p>
<ToolCallCard v-else-if="part.type === 'tool-get_order'" :part="part" />
</template>
</div>
</template> Két következmény. Először: tartsd stabilan a kulcsot partonként, különben a Vue a stream közepén a rossz DOM csomópontot hasznosítja újra, és a szöveg villog. Másodszor: egy eszközhívás partot ad, de szöveget nem, tehát egy naiv chatnapló üres fordulót mutat: a modell egy kört fejezett be, nem a beszélgetést. Erre való a stopWhen. Az alapértelmezés isStepCount(1), ami az első kör után megáll, akkor is, ha eszközeredmények várnak; ha ezt feljebb állítod, a modell látja a saját eszközkimenetét, és megválaszolja az eredeti kérdést.
// server/api/chat.ts – AI SDK 7
const result = streamText({
model: gateway('anthropic/claude-sonnet-5'),
messages: await convertToModelMessages(messages),
stopWhen: isStepCount(5), // default: isStepCount(1)
tools: {
get_order: tool({
description: 'Look up one order by its order number.',
inputSchema: z.object({ orderNumber: z.string().describe('Order number, e.g. 4711') }),
execute: async ({ orderNumber }) => db.findOrder(orderNumber),
}),
},
}) Korlátozd ezt a számot. A körlimit az egyetlen, ami egy összezavarodott modell és egy eszközhurok között áll, és ugyanaz a leállási feltétel, amit bárhol máshol is leírnál. A böngészőnek azt is mutatnia kell, hogy fut egy eszköz: egy spinner a tool- parton megmondja a felhasználónak, miért tart négy másodpercig a válasz, és ez a felhasználó által érzékelt minőség nagy része.
Hogyan kapok strukturált kimenetet, és mik a korlátai?
A strukturált kimenet a generateText és a streamText hívás tulajdonsága, amit az output opcióval állítunk be, és ugyanaz a séma irányítja a modellt és validálja az eredményt. Válaszd a lehető legszűkebb formát, ami a feladathoz illik.
| Amire szükséged van | Kimenettípus | Amit kikényszerít |
|---|---|---|
| Sima szöveg | Output.text() | Semmi; egy stringet kapsz |
| Egy objektum | Output.object({ schema }) | Sémával validált objektum |
| Fix számú sor | Output.array({ element, minItems, maxItems }) | Elemséma és határok |
| Címke egy fix halmazból | Output.choice({ options }) | Az opciók egyike kell legyen |
| Szabad JSON | Output.json() | Csak érvényes JSON, forma nélkül |
// server/api/triage.ts – AI SDK 7
const { output } = await generateText({
model: gateway('anthropic/claude-sonnet-5'),
output: Output.object({
name: 'Triage',
description: 'A routing decision for one support ticket.',
schema: z.object({
severity: z.number().describe('1 to 4, 4 is a total outage'),
team: z.enum(['billing', 'shipping', 'platform']),
summary: z.string().describe('One sentence, no customer name'),
}),
}),
prompt: ticket.body,
})Most az a rész, amelybe belenyúl. A strukturált kimenetek korlátozott dekódolással működnek, ami azt jelenti, hogy a szolgáltató a JSON Schema egy részhalmazát támogatja, nem az egészét. A Claude strukturált kimenetekről szóló dokumentációja pontosan felsorolja ezt a részhalmazt, és három bejegyzés dönti el, hogy a terved átlépi-e a határt:
- Nincsenek számszerű vagy szöveges határok. A
minimum,maximum,multipleOf,minLengthésmaxLengthnem támogatott. A tartományokat a saját kódodban validáld, ahol egy használhatos hibaüzenetet is tudsz adni. - Nincsenek rekurzív sémák. Egy kommentfa vagy egy összekapcsolt munkatétel laposítani kell szülőazonosítókkal rendelkező csomópontok listájává, vagy vissza kell adni szövegként.
- Egyébként szigorú szabályok. Az objektumokhoz
additionalProperties: falsekell, a tömbökminItemsértéke csak 0 vagy 1 lehet, az enumokban csak string, szám, boolean vagy null lehet, a külső$refpedig nem használható.
Ha nem támogatott funkciót használsz, már a kérésnél 400-at kapsz részletekkel, nem pedig egy halk generálási hibát. A dróton a szerződés output_config.format, type: "json_schema" értékkel; a régebbi output_format paraméter már elavult. És ne felejtsd el a könyvelést: a strukturált kimenet előállítása maga is egy kör, tehát a stopWhen-nek az eszközhívásokra és a kimenetre együtt kell helyet adnia.
Hogyan kerül ember a hurkon belül?
Az AI SDK 7-ben a jóváhagyás a hívásra vonatkozó szabály, nem az eszköz tulajdonsága. A toolApproval az eszközneveket egy állapothoz rendeli, és a eszközjóváhagyások dokumentációja négyet definiál: nincs jóváhagyási metaadat, és normálisan fut; automatikus jóváhagyás rögzítése; automatikus elutasítás rögzítése indoklással; és a user-approval, amely kérést bocsát ki, és válaszra vár. Mivel ez egyszerre függvény és térkép, a döntés függhet a feldolgozott bemenettől és a hívó szerepétől.
// AI SDK 7
const result = await generateText({
model: gateway('anthropic/claude-sonnet-5'),
messages,
tools: { issue_refund: tool({ inputSchema: refundSchema, execute: runRefund }) },
toolApproval: {
issue_refund: async ({ amountCents }, { runtimeContext }) => {
if (runtimeContext.role !== 'support-lead') {
return { type: 'denied', reason: 'Only a support lead can refund' }
}
return amountCents > 5000 ? 'user-approval' : undefined
},
},
}) A kliensoldal egy parttípus és egy hívás. A jóváhagyási kérelmek state: 'approval-requested' állapú eszköz partként jelennek meg, és az addToolApprovalResponse() függvénnyel válaszolsz rájuk, opcionálisan sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses értékkel, hogy az SDK a válasz után újraküldje a kérést. Ha egy eszközt elutasítanak, szólj a modellnek, hogy ne próbálja újra, különben jóváhagyási hurkot építettél.
Ha egy eszköz a te saját oldaladon végzett művelet, nem a felhasználóén, a modellt is benyomhatod a böngészőbe, és átadhatod az oldalnak. Ez a WebMCP út, és jól illeszkedik: a Nitro útvonal tartja a kulcsot és a jóváhagyásokat, az oldal eszköze a felületet.
Hogyan viselkednek a hibák, az időlimitek és a tartalékok?
Az első meglepetés: egy streamben a hibák nem dobódnak. A streamText azonnal streamelni kezd, és a félúton bekövetkező hiba a stream részévé válik, nem kivétellé, így a kapcsolat nem szakad meg, és a felhasználónak megvan az első bekezdése. Ez csak akkor segít, ha kezeled is, vagyis egy onError callback a szerveren és egy hibapart a felületen.
A második: a hiányzó kimenet nem ugyanaz, mint az elromlott kimenet. A nem streamelt hívások a séma illesztésének kudarcát NoObjectGeneratedError formájában jelzik, ami megőrzi a legenerált text-et, a válaszmetaadatokat és a tokenfelhasználást, így naplózhatod, mi ment félre, találgatás nélkül. Ha az utolsó kör nem leállási oknál, hanem eszközhívásokon ér véget, akkor az output olvasása NoOutputGeneratedError-t dob. Mindkettő szokásos vezérlési folyamat; kezeld őket külön, mert csak az egyik hiba.
Minden másról az indulás előtt döntsd el, mit lát a felhasználó. Az idő limithez kell egy leírt felső korlát, mert a proxy alapértelmezései nem termékdöntések. A kedvezőbb modellre váltó tartalékhoz küszöb kell, nem megérzés, a költség- és késleltetési kereteket pedig érdemes funkciónként mérni, nem feltételezni. Az elutasított kérés nem olyan hiba, amit el kell dugni: mondd meg, mit utasítottak el. Az offline út pedig többet ér, mint elsőre hangzik, mert az a funkció, amelyik csak hálózattal működik, az a funkció, amelyet nem tudsz bemutatni.
<!-- Your own error surface, not SDK output -->
<p role="alert">The assistant is unavailable, so your message was not sent.</p> Ha az emberek mellett az ügynököket is kiszolgálod, ezek egyike sem opcionális. Saját llms.txt- és Markdown-ábrázolásom lefedi azt az esetet, amikor egyáltalán nincs böngésző-session, és erről szól az llms.txt verszum Accept: text/markdown cikk.
Mit kér a 50. cikkely egy chatbottól?
2026. augusztus 2. óta hatályos az EU AI Act 50. cikkelyének átláthatósági kötelezettsége, és a csapatokat az egyszerűbbik fogja meg: ha egy rendszer közvetlenül emberrel lép interakcióba, tájékoztatni kell arról, hogy egy MI-rendszerrel interakcióba lép. Az 50. cikkely szövege a közlést legkésőbb az első interakció időpontjára teszi, ami kizár egy olyan sort a láblécben, amit senki nem olvas. Tedd oda, ahol a beszélgetés kezdődik, és tedd oda, mielőtt megérkezik az első token. A Bizottság végleges iránymutatásai az átláthatósági kötelezettségekről 2026. júliusban jelentek meg; a fejlesztők teljes ellenőrzőlistája a 50. cikkely fejlesztői ellenőrzőlistájában van. Ez nem jogi tanács.
A telemetria, amelyet az első naptól akarsz, kicsi, és nagyrészt a bizalomról szól. Kérésenként: a modell és a verzió, a tokenek be és ki, az első token ideje és a teljes időtartam. Eszközönként: melyik eszköz, a jóváhagyás állapota, ki hagyta jóvá és mikor. Hibánként: melyik hibaosztály, és hogy tűz-e tartalék. Plusz egy szám, amelyet csak a termékfelelős néz: a session indulásakor érvényes közlési állapot. Az AI SDK szállít egy telemetriamodult, de a jóváhagyási naplót te írod, és éppen azt fogja kérni egy auditor vagy egy dühös ügyfél.
Két szabály tartja ezt őszintén. Naplózd a kérés azonosítóját, a modellt és az eredményt, ne a prompt szövegét: a promptok a leggyorsabb út a személyes adatokig egy olyan naplótárban, amelyet senki nem nézett át. És döntsd el a szállítás előtt, milyen adatok hagyják el a gépet, mert ezt utólag felépíteni egyenlő egy újraírással. Ha egy csapattal végigmész ezen, akkor a Vue és Nuxt oldal nagyrészt SSR, streamelés, és a titkok runtimeConfig-ben tartása.
Nuxt AI ellenőrzőlista
- Tartsd a kulcsot a szerveren. Egy Nitro útvonal a
server/apialatt, a kulcs aruntimeConfig-ben, csak a környezetből olvasva. - Streamelj és renderelj partokat, ne stringeket. Stabil kulcs partonként, és látható állapot az eszköz partokhoz.
- Korlátozd a köröket.
stopWhen: isStepCount(n)egy általad választott számmal, plusz egy eszköz, amely elutasítja a nonszensz bemenetet. - Válaszd a legszűkebb kimenettípust, ami illik, és a támogatott séma-részhalmazon belül tervezz: nincs határérték, nincs rekurzió, zárt objektumok.
- Validáld a saját szabályaiddal, amit a modell visszaad, mert a séma nem tud mindent kifejezni.
- Kérj jóváhagyást a következményes eszközökhöz a
toolApprovalopcióval, és signáld a jóváhagyásokat azexperimental_toolApprovalSecretértékkel, hogy egy jóváhagyást gyártó kliens ne ugorja át az embert. - A stream hibáit
onError-ral kezeld, és különítsd el a meghiúsult objektumot a hiányzótól. - Írd le az idő limitet és a tartalék küszöbét indulás előtt, és mutasd a felhasználónak, melyik lépett életbe.
- Közlés az MI-ről az első interakciónál, és naplózd a közlési állapotot a kéréssel együtt.
Források
Gyakori kérdések
Hogyan tartsam ki a böngészőből az LLM API-kulcsot egy Nuxt alkalmazásban?
Tedd a kulcsot a runtimeConfig-be a nuxt.config.ts fájlban, hagyd üresen az ottani értéket, és töltsd be egy környezeti változóból, például NUXT_AI_GATEWAY_API_KEY. Ezután hívd meg a modellt a server/api alatti fájlból, amelyet a Nitro HTTP-útvonalként tesz elérhetővé. A klienskomponens ezzel az útvonalal beszél, nem a szolgáltatóval, így a kulcs, a rate limitek és a maszkolás is a szerveren marad.
Mi az a UI message part az AI SDK-ban?
Egy streamelt chatben minden üzenet egy rendezett parts tömb, és a kliens beérkezés közben fűzi hozzá őket. Egy part lehet szöveg, reasoning trace vagy eszközhívás, az eszközpartok neve pedig a tool- előtagból és abból a kulcsból áll, amely alatt definiáltad az eszközt. Ezért renderelsz egy hurokkal a message.parts elemein és egy switch-csel a part.type-on, nem egyetlen content string interpolálásával.
Miért mutat üres fordulót a chatbotom eszközhívás után?
Mert az eszközhívás előállítása egy kört fejez be, nem a beszélgetést. Az AI SDK-ban az alapértelmezett leállási feltétel isStepCount(1), vagyis a generálás az első kör után leáll, noha az eszközeredményeket még vissza kell küldeni a modellnek. Emeld meg a stopWhen értékét, például isStepCount(5)-re, hogy a modell lássa a saját eszközkimenetét, és megválaszolja az eredeti kérdést.
Mely JSON Schema kulcsszavakat nem támogat a strukturált kimenet?
A szolgáltatók korlátozott dekódolással a JSON Schema egy részhalmazát kényszerítik ki. A Claude API-n ez kizárja a rekurzív sémákat, a számszerű megkötéseket (minimum, maximum, multipleOf), a szöveges megkötéseket (minLength, maxLength), az enumokon belüli összetett típusokat, a külső $ref hivatkozásokat, valamint a minItems 0 vagy 1 értékénél szigorúbb tömbmegkötéseket. Az objektumoknak additionalProperties értékét false-ra kell állítaniuk. Nem támogatott kulcsszó esetén már a kérésnél 400-as hiba jön részletekkel.
Kell-e az EU felhasználóit értesíteni, hogy egy weboldalas chatbot AI-t használ?
Igen. Az EU AI Act 50. cikkelye 2026. augusztus 2. óta hatályos, és megköveteli, hogy az embereket értesítsük, amikor egy rendszer közvetlenül interakcióba lép velük. Az 50. cikkely szövege a közlést legkésőbb az első interakcióra teszi, ezért a chatbot ott mondja el, hol kezdődik a beszélgetés, nem pedig a láblécben. Ez egy összefoglaló fejlesztőknek, nem jogi tanács.