Мазмұнға өту
5 мин оқу

Стримдегі аяқталмаған соңғы оқиға UI-ды неге бұзады?

Ағынды жауаптағы аяқталмаған соңғы оқиға: output бос болғанда алынған мәтінді сақтау, дельталардың жоғалуын, EOF пен incomplete күйін дұрыс өңдеу.

Стримдегі аяқталмаған соңғы оқиға UI-ды неге бұзады?

Ағынды жауапты клиент қосылымды соңына дейін оқыды деп қана сәтті деп санауға болмайды. LLM интерфейсінде бұл ереже пайдаланушы бұрын алған пайдалы мәтіннің сақталатынын немесе парсердің, проксидің не соңғы кадрдың бір қатесінен жоғалатынын анықтайды.

Мұндағы ең қымбат қате сырттай қарағанда қарапайым көрінеді: код мәтінді уақытша буферде ұстайды, соңғы объектіні күтеді, оны алмайды, ерекше жағдайды ұстап, бүкіл жауапты «Бірдеңе дұрыс болмады» деген хабармен ауыстырады. Пайдаланушы бірнеше абзацты көріп, оқи бастаған болуы мүмкін, ал интерфейс сол мәтінді өзі өшіреді. Бұл ұқыптылық емес, алдын алуға болатын дерек жоғалтуы.

Төмендегі қағидалар SSE немесе соған ұқсас бір бағытты ағын үстіндегі мәтіндік UI-ға қатысты. WebSocket үшін де принцип өзгермейді: тасымалдаудың аяқталуы, протоколды талдау және жауаптың мағыналық аяқталуы әртүрлі деңгейлерде болады. Оларды бір finally тармағына біріктіре берсеңіз, қате қайта шыға береді.

Қосылымның жабылуы сәтті жауапты білдірмейді

EOF байттарды оқу аяқталғанын ғана көрсетеді. Ол модель генерацияны аяқтады, шлюз барлық оқиғаны жеткізді немесе клиент соңғы күйді көрді дегенді білдірмейді. Интерфейс үшін бұл үш бөлек нәрсе.

HTML-дегі Server-Sent Events стандарты маңызды бір жағдайды анықтайды: файл бос жолға дейін аяқталмаған оқиғаның ортасында бітсе, клиент жиналған деректерді тастауы керек. Ағынның соңы өздігінен соңғы SSE кадрын жеткізбейді. Бұл прокси data: жолы мен бөлгіш бос жолдың арасында қосылымды үзген кезде ерекше маңызды.

Мынадай ағынды елестетіңіз:

event: response.output_text.delta
data: {"response_id":"r_42","seq":17,"delta":"Алынған мәтін"}

event: response.completed
data: {"response_id":"r_42","status":"completed"}

Бұл жерде клиент жауапты completed күйіне тек екінші оқиғаны талдағаннан кейін ауыстыра алады. Қосылым алғашқы дельтадан кейін жабылса, нәтиже «сәтті» емес, «мәтін алынды, бірақ соңғы нәтиже расталмады» болады. Екінші data: жолының ортасында байланыс үзілсе, браузерлік EventSource жартылай жиналған JSON-ды оқиға ретінде бермеуі керек. Бұл парсердің дұрыс әрекеті, алғашқы дельтаны жоюға себеп емес.

OpenAI Responses API құжаттамасы response.completed пен response.incomplete оқиғаларын ажыратады. Екінші жағдайда жауап объектісінде incomplete күйі және мысалы max_tokens мәні бар incomplete_details.reason болуы мүмкін. Барлық провайдер осы атауларды қолданбайды, бірақ бұл дұрыс модельді көрсетеді: соңғы күй TCP қосылымына емес, жауапқа қатысты.

Ережені ашық жазыңыз: тек completed күйі бар танылған terminal event сәтті аяқталғанын растайды. EOF, AbortError, желілік timeout, JSON парсинг қатесі және жабылған EventSource тек байттардың әрі қарай жоқ екенін көрсетеді.

Мәтінді генерация күйінен бөлек сақтаңыз

Мәтін мен жауап күйін соңғы өңдеуші не растайтын, не тастайтын бір айнымалыда ұстамаңыз. Мәтіннің өз тарихы бар, ал қорытынды күйдің өз белгісіздігі бар.

UI моделінде мына күйлер жеткілікті:

  • connecting: сұрау жасалды, бірде-бір дельта қабылданған жоқ;
  • streaming: кемінде бір жарамды дельта бар, соңғы нәтиже белгісіз;
  • completed: сәтті күйі бар дұрыс terminal event келді;
  • incomplete: terminal event жауаптың аяқталмағанын ашық хабарлады;
  • interrupted: ағын terminal event-сіз аяқталды немесе бұзылды;
  • failed_before_output: қате алғашқы жарамды дельтаға дейін болды.

Бұл клиенттегі артық рәсім емес. incomplete пен interrupted айырмашылығы пайдаланушыға да, инженерге де қажет. Біріншісінде провайдер жауаптың, мысалы лимитке байланысты тоқтағанын хабарлады. Екіншісінде модель тоқтады ма, шлюз құлады ма, желі үзілді ме немесе код соңғы оқиғаны талдай алмады ма, белгісіз.

Күйдің ең қарапайым үлгісі:

type StreamPhase =
  | "connecting"
  | "streaming"
  | "completed"
  | "incomplete"
  | "interrupted"
  | "failed_before_output";

type AnswerState = {
  requestId: string;
  attemptId: string;
  phase: StreamPhase;
  text: string;
  receivedSeq: number;
  terminalReason?: string;
  parserError?: string;
};

text өрісі тек қабылданған мәтіндік дельта келгенде өзгереді. Күй өңдеуші оны нөлге түсіре алмайды. Ол phase мәнін ауыстырып, себепті сақтайды және хабардың жанында қандай әрекеттер көрсетілетінін шешеді.

«Жауап алынбады» деген жалпы сөз екі түрлі жағдайды жасырады. «Толық алынбады» мен «мүлде алынбады» бір нәрсе емес. Бірінші жағдайда пайдаланушыда модель жұмысының бір бөлігі бар. Екіншісінде көрсетуге ештеңе жоқ. Оларды араластырсаңыз, UX пен метрикаларды бұзасыз: бос бас тартулардың үлесі пайдалы мәтіннен кейін үзілген жауаптардан ажыратылмай қалады.

Бос output жеке өңдеуді қажет етеді

Бос жол, мәтіндік блоктың болмауы және мүлде оқиға болмауы бірдей емес. Әр нұсқаны бөлек өңдеңіз, әйтпесе интерфейс жалған қате немесе жалған сәттілік көрсете бастайды.

completed кезіндегі бос output қалыпты болуы мүмкін. Модель тек құрал шақыруын, бас тартуды, құрылымдалған элементті, аудионы, суретті немесе сіздің рендеринг қабатыңыз әлі көрсете алмайтын қызметтік нәтижені қайтаруы ықтимал. Responses API нәтижені типтелген элементтерден құрайды, ал output_text сияқты SDK ыңғайлылығы тек мәтіндік бөліктерді жинақтайды. Сондықтан бос мәтін өрісі жауапта ештеңе болмағанын дәлелдемейді.

Алдымен мазмұнды жіктеңіз, содан кейін не көрсету керегін шешіңіз:

function classifyFinal(response: {
  status: string;
  output?: Array<{ type: string; status?: string }>;
}) {
  const types = new Set((response.output ?? []).map(item => item.type));

  if (response.status === "incomplete") return "incomplete";
  if (response.status !== "completed") return "unexpected_terminal";
  if (types.has("function_call")) return "tool_call";
  if (types.has("refusal")) return "refusal";
  if (types.has("message")) return "message";
  return "empty_completed";
}

empty_completed күйін output элементтерінің түрлерін тексермей, «Модель ештеңе жауап бермеді» деген мәтінмен алмастырмаңыз. Мұндай мәтін өнім шынымен мәтін күткенде және бұл шарт шақырушы кодпен келісілгенде ғана орынды. Ішкі оркестратор үшін tool call-дан кейінгі бос мәтін құрал орындаушысы жұмысын жалғастыруы керек екенін білдіруі мүмкін, чат рендері емес.

Егер тек мәтіндік сценарийді қабылдасаңыз, ережені адал жазыңыз: «Жауап мәтіндік мазмұнсыз аяқталды». Пайдаланушыға сұрауды қайталауды ұсыныңыз, бірақ мұны желілік қате деп көрсетпеңіз. Желінің бұзылуы мен дұрыс бос жауап әртүрлі тексеруді талап етеді.

Өткізіліп кеткен дельтаны жолдарды біріктірумен түзетуге болмайды

Мәтінде бір сөйлем жетіспесе, команда жиі if (!text.endsWith(delta)) text += delta сияқты дедупликация қосады. Бұл қайта қосылғаннан кейінгі көрінетін қайталауларды тез жояды. Бірақ заңды қайталауларды да үнсіз өшіреді, бірдей аяқталатын жолдарды бұзады және дельтаның жоғалғанын анықтамайды.

Дұрыс дедупликация мәтінге емес, оқиғаның бірегейлігіне сүйенеді. Протокол sequence number берсе, соны қолданыңыз. Оқиға идентификаторы болса, оны сақтаңыз. Екеуі де жоқ болса, провайдерден оқиғаны алған шекарада шлюз монотонды нөмір қосуы керек.

Күтілетін мінез-құлқы бар өңдеуші:

type DeltaEvent = {
  type: "text.delta";
  response_id: string;
  seq: number;
  delta: string;
};

function acceptDelta(state: AnswerState, event: DeltaEvent): AnswerState {
  if (event.response_id !== state.requestId) return state;
  if (event.seq <= state.receivedSeq) return state;

  if (event.seq > state.receivedSeq + 1) {
    return {
      ...state,
      phase: "interrupted",
      terminalReason: `gap_before_seq_${event.seq}`
    };
  }

  return {
    ...state,
    phase: "streaming",
    receivedSeq: event.seq,
    text: state.text + event.delta
  };
}

Мұндай код жоғалған сөздерді болжауға тырыспайды. Ол үзілісті белгілеп, бұрын қабылданған мәтінді сақтайды. Gap табылғаннан кейін осы жауапқа жаңа дельталарды қолдануды тоқтатуға, серверден расталған жауап суретін сұрауға немесе пайдаланушыға ішінара нәтиже көрсетуге болады. Таңдау келісімге байланысты, бірақ үзілісті жасыруға болмайды.

Кейде провайдер дельталарды нөмірсіз жібереді, ал браузер өзі қайта қосылады. Нативті EventSource қайта қосыла алады, ал стандарт үзілістен кейін жалғастыру үшін Last-Event-ID механизмін қарастырады. Бірақ бұл сервер оқиғаларға id беріп, реттілікті қалпына келтіре алғанда ғана көмектеседі. Автоматты қайта қосылуды дерек жоғалмайтынының кепілі деп қабылдамаңыз.

Браузер мен LLM API арасында прокси болса, екі жолдың бірін таңдаңыз. Прокси upstream ағынын terminal event-ке дейін өзі оқып, клиентке нөмірленген реттілік берсін. Немесе прокси әрекетке бірегей ID беріп, клиент қайта қосылғанда осы ID бойынша жиналған жауап суретін сұрасын. Екінші жолды жөндеу оңайырақ, себебі браузер қайталанып жеткізілген бөліктерден тарихты қайта жинауға міндетті емес.

Парсер қатесі қабылданған оқиғаларды өшірмеуі керек

Endpoint-ті кодты өзгертпей ауыстырыңыз
base_url өзгерту бұрынғы шақыруларды AI Router-дың OpenAI-үйлесімді endpoint-іне ауыстырады.

JSON парсинг қатесі модельдің жарамсыз JSON жібергенінен ғана болмайды. Оған SSE кадрының қате шекарасы, reverse proxy буферлеуі, бірнеше data: жолының араласуы, gzip ағыны, ReadableStream тоқтатылуы немесе желінің кез келген chunk-ына JSON.parse шақыратын клиент себеп болуы мүмкін.

Желілік chunk-ты ешқашан оқиға деп есептемеңіз. HTTP бір UTF-8 таңбасын бірнеше бөлікке бөліп, бір chunk ішінде бірнеше SSE хабарын жіберуі немесе кадрдың соңғы жартысын буферде қалдыруы мүмкін. Байтарды TextDecoder арқылы stream: true режимінде біртіндеп талдаңыз, кадрларды бос жолмен бөліңіз, содан кейін ғана SSE өрістерін өңдеңіз.

Өз fetch клиентіңіз үшін ықшам мысал:

const decoder = new TextDecoder();
let buffer = "";

for await (const chunk of response.body!) {
  buffer += decoder.decode(chunk, { stream: true });

  let boundary: number;
  while ((boundary = buffer.indexOf("\n\n")) !== -1) {
    const frame = buffer.slice(0, boundary);
    buffer = buffer.slice(boundary + 2);

    const data = frame
      .split(/\r?\n/)
      .filter(line => line.startsWith("data:"))
      .map(line => line.slice(5).trimStart())
      .join("\n");

    if (!data || data === "[DONE]") continue;

    try {
      acceptProtocolEvent(JSON.parse(data));
    } catch (error) {
      markParserFailure({ frame, error: String(error) });
      preserveVisibleText();
      stopCurrentAttempt();
      break;
    }
  }
}

Бұл толық SSE парсерін алмастырмайды. Мысал жауапкершілік шекарасын көрсетеді: алдымен кадр, содан кейін JSON. Production кодында \r\n, түсіндірмелерді, бірнеше data: жолын, event және id өрістерін, буфер өлшемінің шегін және сұрауды тоқтатуды ескеріңіз.

catch ішіндегі маңызды әрекет мынау: қате жазылады, әрекет interrupted күйіне ауысады, ал state.text сол күйінде қалады. Ол жерде жалпы resetConversationMessage() шақырмаңыз. Жалпы тазалау ыңғайлы көрінеді, бірақ пайдаланушы көріп үлгерген жауап бөлігін жоғалтуы мүмкін.

Incomplete күйі UI қатесіне тең емес

incomplete провайдер ережелері бойынша генерация толық жауапқа айналмағанын білдіреді. Себеп output лимиті, жою, саясат, ішкі тоқтау немесе басқа нақты көрсетілген жағдай болуы мүмкін. Интерфейс бұл күйді «дайын» деп жасыра алмайды, бірақ алынған дельталар болмағандай да әрекет етпеуі керек.

Кәдімгі чат мәтіні үшін мына саясатты қолданыңыз:

  1. Қабылданған дельталардың бәрін орнында қалдырыңыз.
  2. Генерация индикаторын «жауап үзілді» немесе қауіпсіз әрі түсінікті нақты себепке ауыстырыңыз.
  3. Сервер жанама әрекеттерді қайталамай жаңа әрекет жасай алса ғана «Жалғастыру» әрекетін көрсетіңіз.
  4. Жаңа генерацияға рұқсат болса, «Қайталау» әрекетін көрсетіңіз, бірақ оны бұрынғы мәтінмен автоматты түрде біріктірмеңіз.
  5. Бастапқы әрекетті нақты күйімен тарихта сақтаңыз.

Мәтін бар кезде «үзілді» сөзі «қате» сөзінен дұрысырақ. Ол жауаптың дұрыс немесе толық екеніне уәде бермейді. Бірақ мәтіннің 99 пайызы алынған әр үзіліске үлкен қызыл баннер шығарудың қажеті жоқ. Чатта хабардың астындағы шағын күй жолы жеткілікті.

Құрылымдалған нәтижелер үшін талап қатаңырақ. Схемаға сай JSON күтілсе, синтаксистік тұрғыдан кездейсоқ талданған толық емес объектіні тұтынушыға бермеңіз. Міндетті өріс жетіспеуі, массив аяқталмауы немесе жол үзілуі мүмкін. SQL, бағдарламалық код, құрал аргументтері, заңдық нысандар және медициналық нұсқаулар үшін «толық terminal success және мазмұнды тексеру» қағидасын қолданыңыз. Ішінара мәтінді журналда сақтауға немесе қарапайым нобай ретінде көрсетуге болады, бірақ оны іске қоспаңыз.

Incomplete-ті content filtering немесе бас тартумен шатастырмаңыз. Бас тарту түсіндірмесі толық жеткізілген жауап болуы мүмкін. Incomplete ішінде пайдалы бейтарап мәтін болуы ықтимал. Өнім аналитикасында бұл оқиғалар бөлек санаттарға жатады.

Соңғы оқиғаны дельта сияқты мұқият тексеріңіз

Қазіргі клиентті сақтаңыз
AI Router OpenAI-үйлесімді сұрауларды қабылдайды, сондықтан SDK, код пен промпттарды өзгеріссіз қалдыруға болады.

Клиенттер әр дельтаны тексеріп, соңғы оқиғаға тек type өрісі бойынша сене береді. Бұл жеткіліксіз. Terminal event белсенді жауапқа қатысты, рұқсат етілген ретпен келген және бұрын қабылданған деректерге қайшы болмауы керек.

type TerminalEvent = {
  type: "response.completed" | "response.incomplete" | "response.failed";
  response: {
    id: string;
    status: "completed" | "incomplete" | "failed";
    incomplete_details?: { reason?: string };
  };
  seq?: number;
};

function acceptTerminal(state: AnswerState, event: TerminalEvent): AnswerState {
  if (event.response.id !== state.requestId) return state;
  if (state.phase === "completed" || state.phase === "incomplete") return state;
  if (event.seq !== undefined && event.seq < state.receivedSeq) return state;

  if (event.response.status === "completed") {
    return { ...state, phase: "completed" };
  }

  if (event.response.status === "incomplete") {
    return {
      ...state,
      phase: "incomplete",
      terminalReason: event.response.incomplete_details?.reason ?? "unknown"
    };
  }

  return {
    ...state,
    phase: state.text ? "interrupted" : "failed_before_output",
    terminalReason: "provider_failed"
  };
}

Ескі әрекеттен келген кеш completed жаңа әрекетті жауып тастамауы керек. Бұл жоюдан, модель ауысуынан немесе хабарды қайта жіберуден кейін болады. requestId нақты генерацияға тиесілі, ал attemptId бір пайдаланушы әрекетінің қайта іске қосылғанын ажыратуы керек.

Кері қате де кездеседі: фронтенд terminal event алады, бірақ сервер агрегаторы кейін оны timeout мәнімен ауыстырады. Terminal state қайтымсыз болуы керек. completed және incomplete күйлерінен кейін кейін келген сокет жабылуы оқиғасына байланысты жазбаны interrupted күйіне ауыстырмаңыз. Қалыпты аяқталғаннан кейін сокет жабылады, бұл жаңа бизнес қатесі емес.

Қайталау бір жауаптың орнына екі жауап тудыруы мүмкін

Ағын үзілгенде автоматты retry қауіпсіз көрінеді. Ол тек қайталау құрал шақыруын, CRM жазбасын, хат жіберуді немесе сыртқы жүйе күйін өзгертуді бастамайтын таза генерацияда қауіпсіз.

Егер жауап құралды шақыруы мүмкін болса, әрекеттің қай кезеңде тоқтағанын анықтаңыз:

  • құрал сұралмады;
  • модель құралды сұрады, бірақ орындаушы жұмысын бастамады;
  • орындаушы аяқтады, бірақ нәтиже модельге жетпеді;
  • модель нәтижені алып, пайдаланушыға мәтін жаза бастады.

Екінші және үшінші жағдайларда идемпотентті кілтсіз сол сұрауды қайталауға болмайды. Әйтпесе «шот жаса» екі шотқа айналуы мүмкін. Streaming архитектурасында бұл жиі болады: UI үзілісті көріп retry жібереді, ал серверлік тапсырма әлі жұмыс істеп, бастапқы операцияны аяқтайды.

Мәтінді жалғастыру үшін модельден «жауапты қайта жазуды» сұрамаңыз және екі генерацияны шекарасын белгілемей біріктірмеңіз. Оған пайдаланушы көріп тұрған расталған үзіндіні беріп, «соңғы сөйлемнен кейін жалғастыр, бұрын шығарылған мәтінді қайталама» деген тар нұсқау жасаңыз. Сервер жаңа бөліктің сол логикалық тізбекке жататынын растағанша оны бөлек блок ретінде көрсетіңіз.

Бір OpenAI-үйлесімді endpoint қолдансаңыз, AI Router қолданыстағы SDK мен ағын өңдеушісін шақыруды қайта жазбай сақтауға мүмкіндік береді. Бірақ completed, incomplete және transport failure мағынасын base_url ауысуы емес, клиенттік келісім анықтауы керек.

Нашар соңғы күйдегі клиент әрекетінің чеклисті

Жүктемені кілттер бойынша шектеңіз
AI Router rate limit-тері интерфейс логикасында емес, кілт деңгейінде қолданылады.

Бұл чеклистіні рендеринг кодының жанына қойып, тестке айналдырыңыз. Ол қатенің сыртқы түрін емес, күйдің сақталуын тексереді.

Мәтін бар кезде

  • Жарамды дельта келгенде оны хабардың тұрақты күйіне бірден қосыңыз.
  • Terminal event-сіз EOF болса, мәтінді қалдырып, interrupted күйін қойыңыз.
  • response.incomplete кезінде мәтінді қалдырып, себебін жазыңыз.
  • Қабылданған дельтадан кейін парсер қатесі болса, әрекетті тоқтатыңыз, бірақ мәтінді тазаламаңыз.
  • Sequence number арасы үзілсе, оны тіркеп, жауапты аяқталған деп көрсетпеңіз.

Мәтін жоқ кезде

  • Мәтіндік элементтері жоқ completed кезінде output түрлерін тексеріңіз, нәтижені желі қатесіне алмастырмаңыз.
  • Алғашқы дельтаға дейінгі incomplete күйінде генерация мәтінсіз үзілгенін көрсетіңіз.
  • Алғашқы дельтаға дейінгі parser error кезінде техникалық қате мен қайталау батырмасын көрсетіңіз.
  • Пайдаланушы тоқтатса, протокол берсе cancelled күйін сақтаңыз және мұны модель қатесі деп атамаңыз.
  • Белгісіз terminal event кезінде типтің бастапқы атауын диагностикада сақтап, жауапты completed деп белгілемеңіз.

UI осы күйлер арасында ауысқанда хабарды толық алмастырмайтынын тексеріңіз. React және ұқсас жүйелерде бұл хабардың тұрақты идентификаторын сақтап, жеке өрістерді жаңартуды білдіреді. interrupted кезінде жаңа error bubble жасасаңыз, пайдаланушы екі қайшы объект көреді: күйі жоқ мәтін және мәтіні жоқ қате.

Ағынды жол емес, оқиғалар журналы ретінде тестілеңіз

«Hello», содан кейін completed жіберетін тест өте аз нәрсені тексереді. Ол шынайы желіде интерфейстің деректерді жоғалтуына себеп болатын қателерді ұстамайды.

Оқиғалар реттілігінен фикстуралар жасаңыз. Әр фикстура соңғы мәтінді, фазаны, себепті және қолжетімді әрекеттерді тексеруі керек. Кемінде мына сценарийлер пайдалы:

const cases = [
  {
    name: "қалыпты аяқталу",
    events: [delta(1, "Бірінші абзац."), done(2)],
    expect: { text: "Бірінші абзац.", phase: "completed" }
  },
  {
    name: "мәтіннен кейінгі үзіліс",
    events: [delta(1, "Бірінші абзац."), eof()],
    expect: { text: "Бірінші абзац.", phase: "interrupted" }
  },
  {
    name: "мәтіннен кейінгі incomplete",
    events: [delta(1, "Бірінші абзац."), incomplete(2, "max_tokens")],
    expect: { text: "Бірінші абзац.", phase: "incomplete" }
  },
  {
    name: "реттіліктің үзілуі",
    events: [delta(1, "Бірінші бөлік "), delta(3, "үшінші бөлік")],
    expect: { text: "Бірінші бөлік ", phase: "interrupted" }
  },
  {
    name: "аяқталмаған SSE кадры",
    events: [raw("data: {\\\"type\\\":\\\"text.delta\\\""), eof()],
    expect: { text: "", phase: "failed_before_output" }
  }
];

Соңғы фикстура жеке SSE парсерін тексеру үшін аса пайдалы. Стандарт бойынша мұндай кадр оқиғаларды өңдеушіге жетпеуі керек. Егер тест жартылай JSON алса, сіз SSE емес, демоға ыңғайлы үзіндіні талдап жатырсыз.

Белсенді генерация кезінде жоюды бөлек тестілеңіз. Пайдаланушы тоқтатқаннан кейін ескі әрекеттің кеш келген дельталары мен terminal event-і жаңа хабарды өзгертпеуі керек. Мұны таймерлермен емес, оқиғаларды әдейі қате ретпен жеткізумен тексеріңіз.

Бақылау жүйесіне response ID, attempt ID, terminal event түрі, соңғы дельта нөмірі, incomplete себебі, таңба саны, алғашқы дельтаға дейінгі уақыт және соңғы дельтадан ағын соңына дейінгі уақытты жазыңыз. Толық prompt пен output-ты әдепкіде техникалық журналдарға жібермеңіз. Банк, healthcare және басқа реттелетін сценарийлерде бүркемесіз журнал деректерді сақтау ережелерін бұзуы мүмкін.

Тағы бір метрика енгізіңіз: мәтіні бос емес interrupted үлесі. Ол пайдаланушыға келген зиянды HTTP қателерінің жалпы пайызынан жақсы көрсетеді. Екінші пайдалы өлшем, мәтін күтілген кездегі бос completed үлесі, output түрлерінің маршрутизациясы немесе провайдердің жаңа жауап пішімі бұзылғанын тез байқатады.

Ағынды UI әр жауаптың міндетті түрде аяқталатынына уәде бермеуі мүмкін. Бірақ соңғы растау жетпеді деп расталған деректерді тастамауы керек. Дельталарды бөлек сақтаңыз, сәттілік үшін нақты terminal status талап етіңіз және аяқталмағандықты хабар күйі ретінде қарастырыңыз. Сонда бір нашар кадр бірнеше пайдалы абзацты бос орынға айналдырмайды.

Жиі қойылатын сұрақтар

Стрим HTTP қатесінсіз жабылса, мәтінді тазалау керек пе?

Өйткені жабылған HTTP қосылымы тек тасымалдау деңгейі туралы хабарлайды. Ол провайдердің соңғы домендік күйді жібергенін немесе клиенттің оны алып, талдағанын дәлелдемейді. Егер UI оқу аяқталған сайын буферді тазаласа, дұрыс алынған дельталарды өзі жояды.

Аяқталмаған жауаптағы мәтінді пайдаланушыға көрсетуге бола ма?

Иә, егер дельта тексеруден өтіп, ағымдағы сұрауға байланыстырылса. Оны қарапайым нәтиже ретінде сақтап, түсінікті күйді көрсетіңіз және пайдаланушыға жалғастыруға, қайта жіберуге немесе мәтінді көшіруге мүмкіндік беріңіз. Ақшаны аудару сияқты толық орындалмаған қауіпті командаларды ішінара көрсетуге болмайды.

LLM ағынындағы бос output нені білдіреді?

Бос output өздігінен қатені білдірмейді. Модель құрал шақыруы, бас тарту жауабы, қызметтік оқиға жіберуі немесе алғашқы мәтіндік дельтаны шығарып үлгермеуі мүмкін. Мәтіннің жоқтығын чатқа шартсыз шығаратын бос жол ретінде емес, жеке күй ретінде өңдеңіз.

Стримде дельтаның өткізіліп кеткенін қалай білуге болады?

Өткізіліп кеткен дельта толық емес мәтін ретінде көрінуі мүмкін, бірақ себеп қайта қосылу, қате дедупликация, декодер қатесі немесе проксидің SSE кадрын бөлуі болуы ықтимал. Sequence number, жауап идентификаторы және қабылданған оқиғалар журналы қажет. Себепті мәтіннің сыртқы түріне қарап анықтау мүмкін емес.

EOF SSE стримінің сәтті аяқталғанын білдіре ме?

Жоқ. SSE оқиғаларды орау ережелерін анықтайды, бірақ нақты LLM протоколында қай оқиға сәтті аяқталуды білдіретінін белгілемейді. Клиент провайдер келісімінде қарастырылса, айқын terminal event немесе completed күйі бар соңғы объектіні күтуі керек.

Аяқталмаған стримді автоматты түрде қайталауға бола ма?

Сұрауды тек әрекет идемпотентті болғанда және жаңа әрекетті ескі әрекетпен байланыстырудың түсінікті жолы барда қайталаңыз. Мәтін генерациясында «осы жерден жалғастыруды» ұсынған немесе бұрын алынған үзіндіні контекстке қосып жаңа жауап бастаған дұрыс. Автоматты қайталау дубликаттарға, құралдың екі рет орындалуына және артық шығынға әкелуі мүмкін.

Интерфейс incomplete status күйін қалай көрсетуі керек?

Кәдімгі мәтін үшін сақталған үзіндінің жанында «жауап үзілді» деген белгі жеткілікті. JSON, SQL, код, құрал аргументтері және орындалатын басқа деректер үшін ішінара нәтижені дайын деп санауға болмайды. Оны диагностика үшін сақтаңыз, бірақ бөлек тексерусіз әрі қарай жібермеңіз.

LLM стримінің оқиғаларын дедупликациялау керек пе?

Иә, бірақ уақыт шектеуі, сұрау идентификаторы және sequence number бойынша ғана. Қайта жеткізу қалыпты жағдай, ал бірдей жолдарды көзсіз біріктіруге болмайды. Күй тек белсенді жауаптың оқиғаларын қабылдап, жойылғаннан кейін немесе бұрынғы әрекеттен келген оқиғаларды елемеуі керек.

Аяқталмаған стримдер үшін қандай метрикалар мен журналдар қажет?

Әдетте response ID, attempt ID, sequence number, оқиға түрі, алғашқы және соңғы дельта уақыты, сақталған мәтін ұзындығы, соңғы күй және incomplete себебі жеткілікті. Пайдаланушының толық prompt-ы мен жауабын бүркемелеу ережесінсіз кәдімгі журналға жазбаңыз. Олар тез арада персоналдық деректер қоймасына айналады.

Ағынды жауаптар клиентіне қандай тесттер қажет?

Мына бес жағдайды тексеріңіз: қалыпты completed, completed кезіндегі бос output, terminal event-і жоқ дельталар, бірнеше дельтадан кейінгі terminal incomplete және SSE кадрының ортасында үзілген байланыс. Соңғы жағдай өте маңызды, себебі SSE стандарты бойынша браузер аяқталмаған кадрды оқиға ретінде жеткізбеуі керек. Браузер жолын да, прокси бар болса серверлік жолды да тестілеңіз.