Asosiy kontentga o‘tish
Innovialabs

Veb-ilovada LLM javoblarini oqim bilan chiqarish

Amaliyot4 daq

Qisqacha

Model API’dan oqim so‘rang, har bir bo‘lakni brauzerga Server-Sent Events sifatida uzating, uni fetch va reader orqali o‘qing, matnni kelishi bilan interfeysga qo‘shing va proksi javobni buferlamasligiga ishonch hosil qiling.

Qisqa javob

Oqimli chiqarish — foydalanuvchi javobni to‘liq kutmasdan, so‘zma-so‘z ko‘rishi. Zanjir uch bo‘g‘indan iborat:

  1. Backend → model API: provayder SDK’sida streaming rejimini yoqasiz va bo‘laklarni generatsiya bo‘lishi bilan olasiz.
  2. Backend → brauzer: bo‘laklarni oqimli HTTP javobda, odatda Server-Sent Events (SSE) formatida uzatasiz.
  3. Brauzer → interfeys: oqimni o‘qiysiz va matnni ekrandagi xabarga qo‘shib borasiz.

Model API’ni to‘g‘ridan-to‘g‘ri brauzerdan chaqirmang: kalit ochilib qoladi. Backend doim o‘rtada turadi.

SSE yoki toza fetch oqimi

VariantAfzalliklariKamchiliklari
EventSource (nativ SSE)Oddiy API, avtomatik qayta ulanishFaqat GET, so‘rov tanasi va o‘z sarlavhalari yo‘q
fetch + SSE formatiJSON bilan POST, avtorizatsiya sarlavhalari, AbortControllerOqimni o‘zingiz tahlil qilasiz
fetch + oddiy matnEng sodda formatMetama’lumot va xatolarni uzatish qiyin
WebSocketIkki tomonlama aloqaBir tomonlama oqim uchun ortiqcha infratuzilma

Chat uchun eng amaliysi — POST so‘rovli fetch va SSE formatidagi javob: xabarlar tarixini tanada yuborish mumkin, hodisalar tuzilmasi esa tushunarli qoladi.

Backend: oqimni uzatish

Misolda Fetch API’ning standart Response va ReadableStreami ishlatilgan — ular zamonaviy Node.js muhitlari va route handler’larda ishlaydi. llm.stream — SDK’ngizning shartli oqimli chaqiruvi.

export async function POST(req: Request) {
  const { messages } = await req.json();
  const encoder = new TextEncoder();
  const send = (obj: unknown) => encoder.encode(`data: ${JSON.stringify(obj)}\n\n`);

  const body = new ReadableStream({
    async start(controller) {
      try {
        for await (const text of llm.stream(messages, { signal: req.signal })) {
          controller.enqueue(send({ text }));
        }
        controller.enqueue(send({ done: true }));
      } catch {
        controller.enqueue(send({ error: "generation_failed" }));
      } finally {
        controller.close();
      }
    },
  });

  return new Response(body, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      "X-Accel-Buffering": "no",
    },
  });
}

Asosiysi: foydalanuvchi chiqib ketganda generatsiya to‘xtashi uchun so‘rovning bekor qilish signalini model chaqiruviga uzating. Xatolarni oddiy hodisa sifatida yuboring — oqim o‘rtasida nosozlik bo‘lganda HTTP status allaqachon 200 bo‘ladi.

Frontend: o‘qish va bekor qilish

const controller = new AbortController();

async function ask(messages: Message[], onText: (t: string) => void) {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages }),
    signal: controller.signal,
  });
  if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);

  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const events = buffer.split("\n\n");
    buffer = events.pop() ?? "";
    for (const event of events) {
      if (!event.startsWith("data: ")) continue;
      const msg = JSON.parse(event.slice(6));
      if (msg.error) throw new Error(msg.error);
      if (msg.text) onText(msg.text);
    }
  }
}
// «To‘xtatish» tugmasi: controller.abort();

Tarmoq bo‘lagi bitta hodisaga teng emas: unda hodisaning yarmi yoki bir nechtasi bo‘lishi mumkin. Shuning uchun kod bufer saqlaydi va faqat bo‘sh qator bilan ajratilgan to‘liq hodisalarni tahlil qiladi.

Markdown’ni darhol chizish

  • Har bir tokenni alohida emas, to‘plangan butun matnni qayta chizing: belgilash kontekstga bog‘liq (ro‘yxatlar, kod bloklari).
  • Yangilanish chastotasini cheklang, masalan animatsiya kadri boshiga bir marta, shunda interfeys har bir mayda bo‘lakda qayta chizilmaydi.
  • Oqim o‘rtasida yopilmagan kod bloki yoki jadval — normal holat: yaxshi markdown renderer buni hal qiladi, yakuniy chizish esa hammasini tekislaydi.
  • HTML’ni tozalang. Model chiqishi — ishonchsiz ma’lumot, xom HTML’ni sahifaga qo‘ymang.
  • Avtomatik aylantirishni faqat foydalanuvchi chat pastida turganda saqlang.

Xatolar va proksidagi buferlash

Eng ko‘p uchraydigan shikoyat: «lokalda streaming ishlaydi, prod’da esa javob birdaniga chiqadi». Sabab deyarli doim buferlashda:

  • Nginx sukut bo‘yicha proksilangan javoblarni buferlaydi. Kerakli marshrut uchun proxy_buffering off; bilan o‘chiring yoki X-Accel-Buffering: no sarlavhasini yuboring.
  • Proksi yoki CDN’dagi siqish (gzip/brotli) yuborishdan oldin ma’lumotni to‘plashi mumkin. text/event-streamni istisno qiling.
  • CDN va balanslagichlar bo‘sh turish timeouti bo‘yicha ulanishni uzishi mumkin. Uzoq pauzalarda : ping kabi izoh qatorini yuborib turing.
  • Serverless platformalar javob davomiyligini cheklashi mumkin — platformangizda streaming qo‘llab-quvvatlanishini tekshiring.

Shuningdek quyidagilarni ishlang: «To‘xtatish» bosilishi (qisman javobni saqlang), tarmoq uzilishi (qayta urinish tugmasini ko‘rsating) va provayder limit xatolari (bo‘sh pufak o‘rniga tushunarli xabar).

Ko‘p beriladigan savollar

Yo‘q, umumiy generatsiya vaqti o‘zgarmaydi. Seziladigan tezlik yaxshilanadi: birinchi so‘zlar deyarli darhol chiqadi va foydalanuvchi yuklanish belgisiga tikilib o‘tirmaydi.

Yana o‘qing

G‘oya yoki vazifangiz bormi? Bizga aytib bering

Ish kuni davomida javob beramiz va yechim taklif qilamiz. Konsultatsiya bepul.