Veb-ilovada LLM javoblarini oqim bilan chiqarish
Amaliyot4 daqInnovialabs tahririyati
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:
- Backend → model API: provayder SDK’sida streaming rejimini yoqasiz va bo‘laklarni generatsiya bo‘lishi bilan olasiz.
- Backend → brauzer: bo‘laklarni oqimli HTTP javobda, odatda Server-Sent Events (SSE) formatida uzatasiz.
- 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
| Variant | Afzalliklari | Kamchiliklari |
|---|---|---|
EventSource (nativ SSE) | Oddiy API, avtomatik qayta ulanish | Faqat GET, so‘rov tanasi va o‘z sarlavhalari yo‘q |
fetch + SSE formati | JSON bilan POST, avtorizatsiya sarlavhalari, AbortController | Oqimni o‘zingiz tahlil qilasiz |
fetch + oddiy matn | Eng sodda format | Metama’lumot va xatolarni uzatish qiyin |
| WebSocket | Ikki tomonlama aloqa | Bir 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 yokiX-Accel-Buffering: nosarlavhasini 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
: pingkabi 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).