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.

··9 perc olvasás

  • Nuxt
  • AI SDK
  • Streaming
  • Structured output
  • Tool approval
  • Nitro
Egy sorozat a böngészőtől a Nitro útvonalon át a modellszolgáltatóig és egy eszközig, mellette a chat panel, az üzenetlista és a part renderer komponensfája

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.

Egy streamelt forduló egy Nuxt-alkalmazásbanNégy résztvevő időrendben: a böngésző, amely a useChat hookot futtatja, a Nitro útvonal, a modellszolgáltató és a saját eszközkódod. A böngésző átküldi az üzenettömböt a Nitro útvonalnak. Az útvonal meghívja a streamTextet, a szolgáltató visszastreameli a szövegdelta-kat, az útvonal továbbít egy UI üzenetstreamet a böngészőnek, majd meghívja az eszközt a modell bemenetével, az eszköz visszaadja az eredményét az útvonalnak, és az útvonal megtesz egy utolsó kört, hogy a modell használhassa azt. A modellhívás és az eszközhívás akcentusszínnel van rajzolva.böngészőuseChat()Nitro útvonalchat.tsszolgáltatómodell-APIeszköza te kódodsendMessage(text)streamText()szövegdeltaUI üzenetstreamexecute(input)eszközeredményutolsó kör
Egy streamelt forduló: a böngésző soha nem látja az API-kulcsot, az útvonal birtokolja a modellhívást, és az eszközeredmény nem a kérés végét jelenti, hanem egy új kört.

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.

A streamelt chat kliensoldala NuxtbanEgy komponensfa. Felül a ChatPanel.vue három dolgot tartalmaz: a ChatMessageList-et, amely végigiterál az üzeneteken, és partonként egy MessagePart komponenst renderel a part típusára futó switch-csel; az AI SDK Vue csomag useChat hookját, amely a reaktív üzeneteket tartja, és a szerverre postol; és a ChatInput komponenst, amely meghívja a hookon a sendMessage-et. A useChat hook a szerveri chat.ts Nitro útvonalra postol.ChatPanel.vueaz oldalChatMessageListv-for messageuseChat()@ai-sdk/vueChatInputsendMessageMessagePart.vueswitch a típusrachat.tsNitro útvonalrendereliküldPOST
A kliens három komponensből és egy hookból áll. A renderelést teljesen a parts tömb hajtja, így egy új parttípus egy új ág egyetlen komponensben, nem pedig a napló átírása.

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 vanKimenettípusAmit kikényszerít
Sima szövegOutput.text()Semmi; egy stringet kapsz
Egy objektumOutput.object({ schema })Sémával validált objektum
Fix számú sorOutput.array({ element, minItems, maxItems })Elemséma és határok
Címke egy fix halmazbólOutput.choice({ options })Az opciók egyike kell legyen
Szabad JSONOutput.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 és maxLength nem 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: false kell, a tömbök minItems értéke csak 0 vagy 1 lehet, az enumokban csak string, szám, boolean vagy null lehet, a külső $ref pedig 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

  1. Tartsd a kulcsot a szerveren. Egy Nitro útvonal a server/api alatt, a kulcs a runtimeConfig-ben, csak a környezetből olvasva.
  2. Streamelj és renderelj partokat, ne stringeket. Stabil kulcs partonként, és látható állapot az eszköz partokhoz.
  3. 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.
  4. 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.
  5. Validáld a saját szabályaiddal, amit a modell visszaad, mert a séma nem tud mindent kifejezni.
  6. Kérj jóváhagyást a következményes eszközökhöz a toolApproval opcióval, és signáld a jóváhagyásokat az experimental_toolApprovalSecret értékkel, hogy egy jóváhagyást gyártó kliens ne ugorja át az embert.
  7. A stream hibáit onError-ral kezeld, és különítsd el a meghiúsult objektumot a hiányzótól.
  8. Í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.
  9. 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

  1. AI SDK docs: Vue.js (Nuxt) quickstart
  2. AI SDK docs: Generating structured data
  3. AI SDK docs: Tool approvals
  4. Claude API docs: Structured outputs
  5. AI SDK docs: Telemetry
  6. EU AI Act, Article 50: Transparency obligations
  7. Faegre Drinker: Commission confirms the Transparency Code of Practice (2026. július)

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.

Pont erre van szükséged?

Írj a projektedről vagy a pozícióról – szívesen hallok felőled.