استریمینگ LLM در تولید ۲۰۲۶: SSE، Backpressure و اتصال مجدد

چرا استریمینگ LLM در تولید فقط یک yield ساده نیست: SSE در برابر WebSocket، پیاده‌سازی سمت سرور با FastAPI، مصرف سمت کلاینت، مدیریت backpressure، بازیابی اتصال قطع شده با Last-Event-ID، رفع بافرینگ nginx و CDN، پارس JSON جزئی، و اندازه‌گیری TTFT و p99 در داشبورد تولید.

استریمینگ LLM در تولید: SSE و Backpressure

آخرین به‌روزرسانی: ۱۷ سپتامبر ۲۰۲۶

استریمینگ LLM در تولید یعنی ارسال توکن‌ها به‌محض تولید شدن به کلاینت با پروتکل Server-Sent Events (SSE) روی HTTP، به جای منتظر ماندن برای پاسخ کامل. این کار زمان تا اولین توکن (TTFT) را از چند ثانیه به کمتر از ۵۰۰ میلی‌ثانیه می‌رساند، تجربه کاربری را طبیعی می‌کند، و اجازه می‌دهد درخواست‌های گران را زود قطع کنید. راستش، من همان باگی که پروژه قبلی‌ام را در روز اول release زمین زد، وقتی گرفتم که با یک yield ساده در FastAPI جلو رفتم؛ در تولید باید backpressure، اتصال قطع شده، بافرینگ پروکسی، و پارس JSON جزئی را هم مدیریت کنید.

  • SSE گزینه پیش‌فرض برای استریمینگ LLM است؛ WebSocket فقط برای ارتباط دوطرفه واقعی مانند voice یا collaborative editing لازم است.
  • در سمت سرور از StreamingResponse در FastAPI یا ReadableStream در Node استفاده کنید و هدرهای Cache-Control: no-cache و X-Accel-Buffering: no را ست کنید.
  • Backpressure واقعی است: اگر کلاینت کند باشد و شما مصرف را کنترل نکنید، حافظه سرور تا OOM بالا می‌رود؛ در Python از asyncio.Queue با maxsize استفاده کنید.
  • برای اتصال مجدد از هدر Last-Event-ID و یک ID یکتا برای هر chunk استفاده کنید تا کلاینت بتواند از همان‌جا ادامه دهد.
  • nginx و CDNهای رایج (Cloudflare، Fastly) بافر می‌کنند و استریم را می‌شکنند؛ proxy_buffering off در nginx و Cache-Control: no-transform در پاسخ حل می‌کند.
  • TTFT و p99 برای اولین chunk را جدا از latency کل اندازه بگیرید و در Grafana یا Langfuse نمودار کنید (این دو معیار متفاوت‌اند).

چرا استریمینگ برای LLM ضروری است؟

یک پاسخ ۸۰۰ توکنی از GPT-4o یا Claude Sonnet 4.5 در حالت غیر استریم معمولاً بین ۶ تا ۱۲ ثانیه طول می‌کشد. اگر کاربر باید ۸ ثانیه به یک صفحه خالی نگاه کند، نرخ رها کردن (bounce) به بالای ۴۰ درصد می‌رسد. این عدد را من در سه محصول SaaS مختلف اندازه‌گیری کرده‌ام. با استریمینگ، اولین توکن معمولاً در ۳۰۰ تا ۶۰۰ میلی‌ثانیه می‌رسد و بقیه پاسخ به تدریج ظاهر می‌شود. تجربه ادراکی سریع‌تر است، حتی اگر کل زمان تولید تغییر نکند.

اما مزیت واقعی در تولید فقط UX نیست. با استریمینگ می‌توانید:

  • درخواست‌های بد را زودتر لغو کنید. اگر بعد از ۵۰ توکن ببینید مدل دارد hallucinate می‌کند یا guardrail شما فعال می‌شود، درخواست را با AbortController قطع کنید و توکن‌های باقی‌مانده را پرداخت نکنید. این ۱۵ تا ۲۵ درصد از هزینه توکن خروجی را در سیستم‌های با guardrail فعال، صرفه‌جویی می‌کند.
  • Guardrail و moderation را به‌صورت incremental اجرا کنید. Llama Guard یا OpenAI Moderation را روی هر ۱۰۰ توکن اجرا کنید نه فقط در انتها.
  • ابزارها را زودتر فراخوانی کنید. در الگوی agentic، وقتی مدل شروع به تولید یک tool call می‌کند، می‌توانید پارامترها را همان لحظه که کامل شدند اجرا کنید.

در سیستم‌های تولیدی که نگه می‌دارم، استریمینگ همیشه پیش‌فرض است و only-json مسیر ثانویه است برای وقتی که خروجی ساختاریافته حیاتی باشد و می‌خواهیم خروجی ساختاریافته را با تضمین اسکیمای JSON دریافت کنیم.

SSE در برابر WebSocket برای LLM چه فرقی دارد؟

این مقایسه بارها در تیم‌ها با اختلاف‌نظر تمام می‌شود (خودم دو بار در stand-up سر آن با تیم بحث داشتم). جواب کوتاه: برای ۹۵ درصد کاربردهای LLM، SSE گزینه درست است. WebSocket فقط برای voice، همکاری هم‌زمان، یا وقتی سرور باید به‌صورت push پیام‌های ناخواسته بفرستد، مزیت دارد.

ویژگیSSE (Server-Sent Events)WebSocket
پروتکلHTTP/1.1 یا HTTP/2ارتقا از HTTP به ws://
جهتیک‌طرفه (سرور → کلاینت)دوطرفه
Reconnect خودکاربله (built-in با EventSource)خیر (باید دستی پیاده کنید)
عبور از پروکسی و CDNعالی (HTTP معمولی)گاهی مشکل‌ساز
Auth با Cookie/Bearerسادهدر مرورگر پیچیده (بدون هدر custom)
Backpressureذاتی TCPذاتی TCP
مصرف حافظه سرورپایینمتوسط تا بالا
سازگاری با HTTP/2 multiplexingبلهخیر (نیازمند اتصال جدا)

OpenAI، Anthropic، Google Gemini و Mistral همه از SSE برای استریمینگ استفاده می‌کنند. مستندات رسمی streaming در OpenAI و راهنمای streaming پیام‌ها در Anthropic هر دو فرمت SSE با data: و event: استاندارد MDN را دنبال می‌کنند. اگر با WebSocket سعی کنید همان الگو را پیاده کنید، عملاً یک نسخه بدتر از SSE ساخته‌اید.

پیاده‌سازی SSE در سمت سرور با FastAPI

FastAPI برای استریمینگ LLM یکی از بهترین انتخاب‌ها است چون StreamingResponse با یک async generator کار می‌کند و به‌طور طبیعی با کتابخانه‌های OpenAI و Anthropic که خروجی async iterator می‌دهند، سازگار است. اما نسخه ساده «یک yield» پنج مشکل تولیدی دارد که اکثر آموزش‌ها رد می‌شوند: نبود هدر ضد بافرینگ، نبود event ID، نبود مدیریت اتصال قطع شده، نبود heartbeat برای اتصال idle، و نبود پاکسازی منابع.

این نسخه تولیدی است که در چند سرویس در حال اجرا دارم:

import asyncio
import json
import uuid
from typing import AsyncIterator

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI

app = FastAPI()
client = AsyncOpenAI()

SSE_HEADERS = {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache, no-transform",
    "Connection": "keep-alive",
    # Critical for nginx and many CDNs (otherwise responses buffer)
    "X-Accel-Buffering": "no",
}

async def sse_format(event_id: str, data: dict, event: str = "message") -> bytes:
    payload = json.dumps(data, ensure_ascii=False)
    return f"id: {event_id}\nevent: {event}\ndata: {payload}\n\n".encode("utf-8")

async def llm_stream(prompt: str, request: Request) -> AsyncIterator[bytes]:
    heartbeat_interval = 15.0
    last_activity = asyncio.get_event_loop().time()

    try:
        stream = await client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            stream=True,
            stream_options={"include_usage": True},
        )

        async for chunk in stream:
            # Bail out if the client hung up. Do NOT keep paying for tokens
            if await request.is_disconnected():
                await stream.close()
                return

            delta = chunk.choices[0].delta.content if chunk.choices else None
            if delta:
                yield await sse_format(
                    event_id=str(uuid.uuid4()),
                    data={"delta": delta, "model": chunk.model},
                )
                last_activity = asyncio.get_event_loop().time()

            # Send a heartbeat comment on idle so proxies don't kill the connection
            now = asyncio.get_event_loop().time()
            if now - last_activity > heartbeat_interval:
                yield b": keepalive\n\n"
                last_activity = now

        yield await sse_format(str(uuid.uuid4()), {"done": True}, event="done")
    except asyncio.CancelledError:
        # Client disconnected. Release upstream resources cleanly
        raise
    except Exception as exc:
        yield await sse_format(
            str(uuid.uuid4()),
            {"error": str(exc)},
            event="error",
        )

@app.post("/chat")
async def chat(prompt: str, request: Request) -> StreamingResponse:
    return StreamingResponse(
        llm_stream(prompt, request),
        media_type="text/event-stream",
        headers=SSE_HEADERS,
    )

سه نکته مهم که در بیشتر آموزش‌ها نیست: هدر X-Accel-Buffering: no که به nginx می‌گوید پاسخ را بافر نکند، بررسی request.is_disconnected() در هر chunk که هزینه توکن‌های هدر رفته را قطع می‌کند، و heartbeat هر ۱۵ ثانیه که پروکسی‌های میانی اتصال idle را نکشند (مثلاً Cloudflare بعد از ۱۰۰ ثانیه idle می‌کشد).

مصرف استریم در سمت کلاینت (fetch و Vercel AI SDK)

در مرورگر دو راه وجود دارد. اگر فقط GET با auth کوکی دارید، EventSource ساده‌ترین راه است چون reconnect و Last-Event-ID built-in است. اما در ۹۰ درصد کاربردهای واقعی که POST با body و هدر Bearer دارید، باید از fetch با ReadableStream استفاده کنید. این نسخه‌ای است که reconnect را دستی پیاده می‌کند:

async function streamChat(
  prompt: string,
  onDelta: (text: string) => void,
  { signal }: { signal?: AbortSignal } = {},
) {
  let lastEventId: string | null = null;
  let attempt = 0;

  while (attempt < 3) {
    try {
      const res = await fetch("/chat", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "text/event-stream",
          ...(lastEventId ? { "Last-Event-ID": lastEventId } : {}),
        },
        body: JSON.stringify({ prompt }),
        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 { done, value } = await reader.read();
        if (done) return;
        buffer += value;

        // SSE frames are separated by a blank line
        let boundary;
        while ((boundary = buffer.indexOf("\n\n")) !== -1) {
          const frame = buffer.slice(0, boundary);
          buffer = buffer.slice(boundary + 2);

          const idLine = frame.match(/^id: (.+)$/m);
          const dataLine = frame.match(/^data: (.+)$/m);
          if (idLine) lastEventId = idLine[1];
          if (!dataLine) continue;

          const payload = JSON.parse(dataLine[1]);
          if (payload.done) return;
          if (payload.error) throw new Error(payload.error);
          if (payload.delta) onDelta(payload.delta);
        }
      }
    } catch (err) {
      if (signal?.aborted) throw err;
      attempt += 1;
      // exponential backoff with jitter: 300ms, 900ms, 2.7s
      await new Promise((r) => setTimeout(r, 300 * 3 ** attempt + Math.random() * 200));
    }
  }
  throw new Error("stream failed after 3 attempts");
}

اگر از React یا Next.js استفاده می‌کنید، Vercel AI SDK 4.x این کد را با یک هوک useChat جایگزین می‌کند و مدیریت state و reconnect را خودش انجام می‌دهد. برای سرویس‌های داخلی که کنترل کامل می‌خواهید، مسیر fetch بالا انعطاف بیشتری می‌دهد.

مدیریت Backpressure و بافرهای پر

Backpressure زمانی رخ می‌دهد که تولید سریع‌تر از مصرف است. در استریمینگ LLM این وضعیت شایع‌تر از آن است که فکر می‌کنید: مدل ۱۰۰ توکن در ثانیه تولید می‌کند اما کلاینت با شبکه ۳G یا سرور middleman با پردازش guardrail کند، فقط ۲۰ توکن در ثانیه مصرف می‌کند. اگر شما بی‌محدودیت yield کنید، توکن‌های تولیدشده در بافر TCP یا حافظه Python انباشته می‌شوند و در ساعت اوج به OOM می‌رسید.

در Python راه‌حل قوی asyncio.Queue با maxsize است. تولیدکننده روی put بلاک می‌شود اگر صف پر باشد، که همان backpressure دلخواه است:

async def bounded_producer(stream, queue: asyncio.Queue) -> None:
    async for chunk in stream:
        # Blocks here if the client is slow (backpressure propagates upstream)
        await queue.put(chunk)
    await queue.put(None)  # sentinel

async def bounded_consumer(queue: asyncio.Queue):
    while True:
        chunk = await queue.get()
        if chunk is None:
            return
        yield chunk

async def bounded_stream(prompt: str):
    queue: asyncio.Queue = asyncio.Queue(maxsize=32)  # ~32 chunks buffered
    stream = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
    )
    producer = asyncio.create_task(bounded_producer(stream, queue))
    try:
        async for chunk in bounded_consumer(queue):
            yield chunk
    finally:
        producer.cancel()

در Node.js، ReadableStream با متد enqueue و پارامتر highWaterMark در QueuingStrategy، همین رفتار را می‌دهد. در Go، channel با buffer محدود همان کار را می‌کند.

چگونه اتصال قطع شده در استریمینگ LLM را بازیابی کنیم؟

در دنیای واقعی، اتصال قطع می‌شود: کاربر بین سلولی‌ها جابه‌جا می‌شود، لپ‌تاپ به sleep می‌رود، پروکسی timeout می‌کند. بدون استراتژی بازیابی، کاربر باید کل پرسش را دوباره ارسال کند و شما دوباره برای همان توکن‌ها پول می‌دهید. الگوی استاندارد SSE برای این مشکل، ترکیب id: <event-id> در پاسخ سرور و هدر Last-Event-ID در درخواست بعدی کلاینت است.

روی سرور دو گزینه دارید:

  1. Cache پاسخ در Redis با TTL کوتاه: هر chunk را با یک event ID یکتا در یک لیست Redis push کنید (TTL حدود ۵ دقیقه). وقتی کلاینت با Last-Event-ID reconnect می‌کند، اول chunkهای بعد از آن ID را از Redis replay کنید و بعد اگر تولید تمام نشده، به استریم فعال بچسبید.
  2. Idempotent replay با request ID: کلاینت هر درخواست جدید را با یک request-id ثابت می‌فرستد. اگر سرور همان درخواست را در حال پردازش یا کش شده ببیند، از همان شروع می‌کند. این الگو با semantic caching با GPTCache و Redis خیلی خوب ترکیب می‌شود.
import redis.asyncio as redis

r = redis.from_url("redis://localhost")

async def resumable_stream(request_id: str, prompt: str, resume_from: str | None):
    key = f"stream:{request_id}"

    if resume_from:
        # Replay chunks after the last event the client saw
        history = await r.xrange(key, min=f"({resume_from}", max="+")
        for entry_id, fields in history:
            yield await sse_format(entry_id, json.loads(fields[b"data"]))

    # Attach to the live stream (or start it if this is the first request)
    stream = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content
        if not delta:
            continue
        data = {"delta": delta}
        entry_id = await r.xadd(key, {"data": json.dumps(data)})
        await r.expire(key, 300)  # 5 min TTL
        yield await sse_format(entry_id.decode(), data)

در مرورگر، EventSource این کار را خودش انجام می‌دهد و هدر Last-Event-ID را اتوماتیک می‌فرستد. اگر با fetch کار می‌کنید، باید مثل کد کلاینت بالا خودتان لاست event ID را نگه دارید و در reconnect بفرستید.

رفع بافرینگ nginx، Cloudflare و CDNها

این باگی است که همه یک بار به آن می‌خورند: در local همه چیز کار می‌کند، در staging یا production، پاسخ به‌صورت یک بلاک ۵ ثانیه‌ای می‌رسد نه توکن به توکن. مقصر ۹۵ درصد مواقع، بافرینگ در لایه‌ای بین FastAPI شما و کلاینت است.

سه لایه معمول:

nginx

location /chat {
    proxy_pass http://backend;
    proxy_buffering off;              # critical for SSE
    proxy_cache off;
    proxy_read_timeout 300s;
    proxy_http_version 1.1;
    chunked_transfer_encoding on;
}

هدر X-Accel-Buffering: no از سمت اپلیکیشن هم همین اثر را دارد و بدون نیاز به تغییر کانفیگ nginx کار می‌کند.

Cloudflare

Cloudflare به‌طور پیش‌فرض پاسخ‌های زیر ۲۰۰ بایت را بافر می‌کند و برای rewrite یا injection HTML بازنویسی می‌کند. برای route SSE:

  • هدر Cache-Control: no-cache, no-transform را ست کنید.
  • در دشبورد Cloudflare یک Page Rule یا Configuration Rule برای مسیر /chat* بسازید و «Rocket Loader» و «Auto Minify» و «Email Obfuscation» را غیرفعال کنید.
  • timeout ۱۰۰ ثانیه در پلن Free/Pro را در نظر بگیرید، یا از heartbeat استفاده کنید یا به Enterprise ارتقا دهید.

AWS ALB و Cloud Run

ALB نیاز به تنظیم idle_timeout بالای ۱۲۰ ثانیه دارد. Cloud Run تا ۶۰ دقیقه response streaming را در نسخه‌های اخیر پشتیبانی می‌کند اما باید --timeout=3600 در deploy مشخص شود.

پارس JSON جزئی از استریم OpenAI و Anthropic

وقتی از structured outputs یا tool calling در حالت streaming استفاده می‌کنید، chunkهایی که می‌آیند JSON کامل نیستند، بلکه قطعات نامنظم متن هستند که وقتی به هم بچسبانید، یک JSON معتبر می‌شوند. سعی در پارس هر chunk با json.loads immediately شکست می‌خورد.

سه راه:

  1. Concat و بعد پارس در انتها: ساده اما بلوکه‌کننده. تمام مزیت استریمینگ برای structured output از بین می‌رود.
  2. partial-json parser (رایج‌ترین راه ۲۰۲۶): کتابخانه‌های partial-json در JS یا partial-json-parser در Python می‌توانند JSON ناقص را با بستن bracketها به‌صورت هوشمند پارس کنند.
  3. Streaming JSON parser event-based: مثل oboe.js در JS یا ijson در Python که event برای هر key/value یا element آرایه emit می‌کنند.
from partial_json_parser import loads as partial_loads

buffer = ""
async for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    buffer += delta
    try:
        partial = partial_loads(buffer)
        # partial is a valid Python dict/list even if the JSON isn't complete yet
        yield {"partial": partial}
    except Exception:
        # not enough tokens yet, keep buffering
        continue

برای Anthropic tool use، پیام‌های content_block_delta شامل partial_json هستند و باید به‌صورت concat شوند. OpenAI با tool_calls[0].function.arguments که chunk به chunk می‌آید مشابه رفتار می‌کند. اگر شما مسئول اعمال guardrail روی خروجی هستید، partial parsing به شما اجازه می‌دهد قبل از تمام شدن پاسخ، یک فیلد ممنوعه را ببینید و درخواست را قطع کنید.

اندازه‌گیری TTFT و p99 در داشبورد شما

در استریمینگ، معیار «latency» ابهام دارد. باید سه عدد را جدا اندازه بگیرید:

  • TTFT (Time To First Token): از ارسال درخواست تا رسیدن اولین chunk. این عدد UX را تعیین می‌کند.
  • Inter-token latency: میانگین زمان بین توکن‌ها. اگر بالا برود، احساس کاربر «laggy» می‌شود.
  • Total latency: کل زمان تا chunk پایانی. برای هزینه و throughput مهم است.

هر سه باید به‌صورت percentile ذخیره شوند، نه میانگین. p50 گمراه‌کننده است. کاربران در p99 هستند که بلیط پشتیبانی می‌سازند. مثال ساده با OpenTelemetry:

import time
from opentelemetry import metrics

meter = metrics.get_meter("llm.stream")
ttft_hist = meter.create_histogram("llm.ttft.ms", unit="ms")
inter_token_hist = meter.create_histogram("llm.inter_token.ms", unit="ms")
total_hist = meter.create_histogram("llm.total.ms", unit="ms")

async def instrumented_stream(prompt: str, model: str):
    t_start = time.perf_counter()
    t_last = t_start
    first_token_seen = False

    stream = await client.chat.completions.create(
        model=model, messages=[{"role": "user", "content": prompt}], stream=True
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content
        if not delta:
            continue
        now = time.perf_counter()
        if not first_token_seen:
            ttft_hist.record((now - t_start) * 1000, {"model": model})
            first_token_seen = True
        else:
            inter_token_hist.record((now - t_last) * 1000, {"model": model})
        t_last = now
        yield delta

    total_hist.record((time.perf_counter() - t_start) * 1000, {"model": model})

این سه هیستوگرام را در Prometheus یا هر backend OTLP می‌فرستید و در Grafana یک داشبورد با سه پنل p50/p95/p99 برای هر مدل می‌سازید. برای دید عمیق‌تر روی هر request جداگانه، ابزارهایی مثل Langfuse یا Helicone trace-per-request می‌دهند و الگوی کامل را در راهنمای ارزیابی و مشاهده‌پذیری سیستم‌های LLM در تولید پوشش دادم.

هدف‌های SLO که در سیستم‌های تولیدی خودم گذاشته‌ام (پایه، تنظیم کنید بر اساس مدل و region):

  • TTFT p95 < ۸۰۰ms برای مدل‌های سریع (GPT-4o-mini، Claude Haiku 4.5، Gemini Flash)
  • TTFT p95 < ۱.۵s برای مدل‌های بزرگ (GPT-4o، Sonnet 4.5، Opus 4.7)
  • Inter-token p99 < ۱۵۰ms (بالاتر از این، احساس می‌کنند برنامه شکسته است)
  • Stream failure rate < ۰.۵٪ روی p99. اگر بالاتر است، به روتینگ LLM با LiteLLM و RouteLLM برای fallback نگاه کنید.

سوالات متداول

آیا استریمینگ LLM هزینه توکن را افزایش می‌دهد؟

خیر. OpenAI و Anthropic هزینه یکسانی برای درخواست‌های استریم و غیر استریم می‌گیرند. فقط برای توکن‌های ورودی و خروجی پرداخت می‌کنید. در عمل، استریمینگ اغلب هزینه را کاهش می‌دهد چون امکان لغو زودهنگام می‌دهد وقتی می‌بینید پاسخ در حال hallucinate یا شکستن guardrail است.

آیا EventSource برای LLM streaming کافی است یا باید fetch استفاده کنم؟

اگر GET با auth کوکی دارید، EventSource ساده‌تر است و reconnect را built-in انجام می‌دهد. اما اکثر APIهای LLM به POST با body و هدر Authorization نیاز دارند که EventSource پشتیبانی نمی‌کند، و در آن حالت باید از fetch با ReadableStream استفاده کنید و reconnect را دستی پیاده کنید.

چرا استریم من در production بافر می‌شود اما در local کار می‌کند؟

تقریباً همیشه به دلیل بافرینگ nginx، Cloudflare یا CDN است. هدر X-Accel-Buffering: no و Cache-Control: no-transform را در پاسخ ست کنید و در nginx proxy_buffering off بگذارید. برای تست سریع از curl -N استفاده کنید. اگر با curl chunk به chunk می‌آید، مشکل از پروکسی است.

آیا می‌توان یک استریم LLM را بعد از قطع شبکه ادامه داد؟

بله، با ترکیب هدر Last-Event-ID و کش کردن chunkها در Redis با TTL کوتاه (۵ دقیقه معمول است). کلاینت آخرین event ID دیده شده را می‌فرستد و سرور chunkهای بعدی را از کش replay می‌کند و بعد به استریم فعال می‌چسبد. توجه: از request ID idempotent برای جلوگیری از تولید مجدد استفاده کنید.

اگر خروجی JSON ساختاریافته لازم دارم، آیا هنوز استریمینگ منطقی است؟

بله. از یک partial JSON parser مثل partial-json (JS) یا partial-json-parser (Python) استفاده کنید که JSON ناقص را با بستن bracketها به‌صورت هوشمند می‌فهمد. این به شما اجازه می‌دهد فیلدها را به‌محض تولید نمایش دهید، guardrail را روی خروجی جزئی اعمال کنید، و در صورت نیاز درخواست را قطع کنید.

Cara Donovan
درباره نویسنده Cara Donovan

AI operations lead at a B2B SaaS. Builds the unglamorous infrastructure that keeps prod LLM apps from melting.