Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Новини 07 жовтня 2026

Стрімінг відповідей ШІ з Laravel до React: досвід команди There There

Фрік Ван дер Ертен (Freek Van der Herten) продовжує серію дописів про агента та інструменти, які команда Spatie побудувала на основі Laravel AI SDK для There There - гелпдеск-системи, що наразі розробляється. Попередній допис завершився невеликою інтригою: агент передається стрімінговому сервісу, який перетворює потік відповіді SDK на події, зрозумілі фронтенду. Саме цьому присвячено новий матеріал.

Підхід There There

Після двох десятиліть власної підтримки клієнтів команда хотіла створити гелпдеск, де ШІ робить агентів підтримки швидшими, а не намагається їх замінити. Людина читає, думає й спрямовує, а модель на її очах у реальному часі готує чернетки та шукає потрібну інформацію. There There зараз перебуває в закритій бета-версії, а заявку на ранній доступ можна подати на there-there.app.

Стрімінг із Laravel до React

Найочевидніший спосіб передати відповідь LLM у браузер - дочекатися її завершення й відрендерити все одразу. Це просто, але агент здається млявим. Натомість команда хоче, щоб відповідь з'являлася слово за словом, а міркування агента (наприклад, який інструмент він викликає) озвучувалися по ходу.

Laravel AI SDK надає кожен крок розмови як типізовану подію. Сервіс ітерує цей потік і надсилає фронтенду newline-delimited JSON (NDJSON) - один рядок на подію.

Ось ядро AgentStreamingService:

public function stream(Agent&HasTools $agent, string $message, AgentChat $chat): StreamedResponse
{
    return response()->stream(function () use ($agent, $message, $chat): Generator {
        yield from $this->streamAgent($agent, $message, $chat);
    }, 200, [
        'Content-Type' => 'application/x-ndjson',
        'Cache-Control' => 'no-cache',
    ]);
}

private function streamAgent(Agent&HasTools $agent, string $message, AgentChat $chat): Generator
{
    set_time_limit(120);

    $fullContent = '';
    $toolCalls = [];

    foreach ($agent->stream($message) as $event) {
        if ($event instanceof ToolCall) {
            yield json_encode(['type' => 'tool_call', 'tool_name' => $event->toolCall->name."\n";
        }

        if ($event instanceof TextDelta) {
            $fullContent .= $event->delta;
            yield json_encode(['type' => 'delta', 'content' => $event->delta]))."\n";
        }

        if ($event instanceof ToolResult) {
            $toolCalls[] = [
                'name' => $event->toolResult->name,
                'arguments' => $event->toolResult->arguments,
                'result' => $event->toolResult->result,
            ];
        }
    }

    $chat->messages()->create([
        'role' => MessageRole::Assistant,
        'content' => $fullContent,
        'tool_calls' => $toolCalls ?: null,
    ]);

    yield json_encode([
        'type' => 'done',
        'html' => $this->parser->buildFinalHtml($fullContent),
    ])."\n";
}

На що варто звернути увагу:

  • response()->stream() приймає генератор і негайно надсилає клієнту кожен yield - у цьому весь фокус із серверним пушем прогресу.
  • Події типізовані (delta, tool_call, done), тож фронтенд відрізняє фрагмент тексту від виклику інструмента.
  • Готове повідомлення зберігається в базі даних в тому ж проході. Сам потік є джерелом істини, а збережена копія використовується для відновлення чату, коли користувач відкриє його наступного разу.

Споживання потоку на фронтенді

На клієнті потік читається звичайним fetch. Жодних бібліотек і жодного SSE-фреймінгу. Ось хук, скорочений до суттєвого:

const response = await fetch(sendUrl, {
    method: 'POST',
    headers: { ...csrfHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify(buildBody(text, html)),
    signal: controller.signal,
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() ?? '';

    for (const line of lines) {
        processLine(line);
    }
}

if (buffer.trim()) {
    processLine(buffer);
}

Єдине, що треба зробити правильно, - це буферизація. Один виклик reader.read() може повернути частковий рядок, цілий рядок або одразу кілька. Тому текст розбивається за \n, останній (можливо, неповний) фрагмент лишається в buffer, а наприкінці його також обробляють. Кожен завершений рядок - це одна подія.

Обробка подій у стані

Диспетчеризація кожної події у стан - це короткий switch:

if (event.type === 'tool_call') {
    setMessages((prev) => updateLastStreaming(prev, {
        toolStatus: formatToolName(event.tool_name),
    }));
} else if (event.type === 'delta') {
    setMessages((prev) => updateLastStreaming(prev, (last) => ({
        content: last.content + event.content,
        toolStatus: null,
    })));
} else if (event.type === 'done') {
    setMessages((prev) => updateLastStreaming(prev, {
        html: event.html,
        isStreaming: false,
    }));
}

Коли надходить tool_call, над відповіддю, що ще стрімиться, з'являється невелика позначка на кшталт «Looking up tickets». Коли починають надходити delta, позначку прибирають і дописують текст до вмісту повідомлення. Із подією done підставляється відрендерений на сервері HTML, а прапорець стрімінгу вимикається. Загалом виглядає так, ніби агент друкує просто перед вами.

Підсумок

Існують і «важчі» способи: server-sent events, websockets, повноцінна стрімінгова бібліотека з обох боків. Для чат-інтерфейсу нічого з цього не потрібно. Генератор на сервері, NDJSON у каналі та невеликий буферний цикл на клієнті - це приблизно сотня рядків від початку до кінця, і працює це саме так, як має поводитися справжній стрімінговий LLM-чат.

Докладніше про події, які генерує SDK, можна дізнатися в репозиторії Laravel AI SDK. А якщо хочете спробувати There There, команда зараз у закритій бета-версії - подати заявку на ранній доступ можна на there-there.app.

8

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Laravel AI SDK на практиці
Новини 09 жовтня 2026

Laravel AI SDK на практиці: агент, що стоїть за There There

Фрік Ван дер Гертен розповідає, як у Spatie побудували AI-шар хелпдеска There There на Laravel AI SDK: клас агента, інструменти з типізованою JSON-схемою та делегування роботи звичайним domain-діям.

9
CSRF і завантаження
Новини 09 жовтня 2026

Порада з безпеки: як завантаження файлів може обійти захист від CSRF

Стівен Ріс-Картер пояснює, чому файли користувачів варто віддавати з окремого домену: HTML і JS у межах основного застосунку дають доступ до cookie та CSRF-токенів.

18

Вакансії за темою

Kredyt Kapital
13 днів тому

Middle Full-Stack Developer (PHP / Laravel / Vue / React)

Middle Full-Stack Developer для розробки внутрішніх IT-продуктів. Основний стек: PHP 8.3+, Laravel, PostgreSQL, REST API, Vue.js/React. Розробник займатиметься backend (бізнес-логіка, Eloquent, Jobs/Queues) і frontend (компоненти, інтерфейси), інтеграціями та оптимізацією. Вимоги: 2-3+ років комерційного досвіду з PHP, 2+ років практики з Laravel, впевнене знання PostgreSQL, Git, JavaScript ES6+.

DL SOLUTION, ТОВ
30 днів тому

Full Stack Developer (PHP, React, Middle, Middle+)

Full Stack розробник для підтримки та розвитку аналітичного продукту перевірки контрагентів. Робота зі складною бізнес-логікою, базами даних та інтеграцією AI-рішень. Стек: PHP 8.x (Laravel/Symfony), React, MySQL, REST API. Вимоги: 3+ років комерційного досвіду, глибоке розуміння SQL, Git, Docker, CI/CD, Linux, OWASP.

Hiring Elixir Нова
2 дні тому

Fullstack AI Evangelist in UKR/RUS speaking team

Фулстек-розробник з акцентом на AI-асистовану розробку: створюватиме та підтримуватиме сервіси на PHP/WordPress, Python і ReactJS, будуватиме й налагоджуватиме внутрішні та зовнішні API-інтеграції, перетворюватиме макети Figma на адаптивні інтерфейси. Потрібен досвід комерційної розробки від 2 років, регулярна робота з AI-асистентами для коду, знання RESTful-сервісів, MySQL і Redis, англійська B2. Гібридний формат: 3 дні на тиждень в офісі у Варшаві.

Пакети за темою

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,224 v2.5.0 13 26

Laravel Permission

spatie/laravel-permission

Управління дозволами та правами доступу для Laravel 12 та новіших версій.

12,967 8.3.0 13 29