Перейти к содержимому

Шаги потоковой передачи

Шаги потоковой передачи позволяют рабочему процессу выдавать результат постепенно. Каждый выданный фрагмент немедленно записывается в базу данных, поэтому, если шаг завершится с ошибкой посреди потока, уже сохранённые фрагменты останутся и будут доступны при повторном запуске.

Это полезно для потоковых ответов LLM, обработки больших файлов и любых сценариев, где нельзя терять частичный результат.

Интерфейс StreamingStep

Шаг потоковой передачи реализует Queuety\Contracts\StreamingStep:

PHP
namespace Queuety\Contracts;

interface StreamingStep {
    public function stream( array $state, array $existing_chunks = [] ): \Generator;
    public function on_complete( array $chunks, array $state ): array;
    public function config(): array;
}
Метод Описание
stream( $state, $existing_chunks ) Генератор, выдающий строковые фрагменты. Каждый вызов yield немедленно вызывает запись в БД.
on_complete( $chunks, $state ) Вызывается после завершения потока. Получает все фрагменты (предыдущие и новые) и возвращает данные для объединения с состоянием рабочего процесса.
config() Необязательная конфигурация. Поддерживает ключи max_attempts и backoff; backoff принимает название стратегии (exponential, linear, fixed), поскольку список задержек сейчас отбрасывается (#16).

Как сохраняются фрагменты

Каждый раз, когда генератор выдаёт значение, Queuety вызывает ChunkStore::append_chunk(), чтобы записать его в таблицу queuety_chunks. Пакетной записи и буферизации нет. Один вызов yield — одна команда INSERT.

В строке фрагмента хранятся job_id, workflow_id, step_index, chunk_index (начиная с нуля) и строка content.

Возможность возобновления

При повторном запуске шага потоковой передачи после сбоя исполнитель загружает все ранее сохранённые фрагменты и передаёт их как параметр $existing_chunks. Ваша реализация определяет, как продолжить работу:

  • Пропускать уже обработанные элементы — проверять количество имеющихся фрагментов
  • Переходить к нужной позиции в страницах API — использовать количество фрагментов как смещение страницы
  • Начинать заново — полностью игнорировать $existing_chunks (фрагменты предыдущей попытки всё ещё находятся в БД и будут добавлены в начало)
PHP
public function stream( array $state, array $existing_chunks = [] ): \Generator {
    $offset = count( $existing_chunks );

    $pages = $this->client->paginate( $state['query'], offset: $offset );

    foreach ( $pages as $page ) {
        yield json_encode( $page );
    }
}

Сигналы активности во время потоковой передачи

Длительные потоки должны отправлять сигналы активности, чтобы обнаружитель устаревших задач не перехватил задачу. Вызывайте Heartbeat::beat() в цикле генератора:

PHP
use Queuety\Heartbeat;

public function stream( array $state, array $existing_chunks = [] ): \Generator {
    foreach ( $this->client->stream( $state['prompt'] ) as $i => $chunk ) {
        Heartbeat::beat( [ 'chunks' => $i + 1 ] );
        yield $chunk;
    }
}

Подробнее см. в разделе Сигналы активности об интерфейсе Heartbeat API.

Очистка фрагментов

Когда поток успешно завершается и возвращает on_complete(), исполнитель вызывает ChunkStore::clear_chunks(), чтобы удалить все фрагменты задачи. Объединённый результат из on_complete() становится надёжно сохранённой записью. По замыслу фрагменты являются временными.

Если шаг завершается с ошибкой и запускается повторно, фрагменты не удаляются. При следующей попытке они передаются через $existing_chunks.

Полный пример: потоковая передача LLM

PHP
use Queuety\Contracts\StreamingStep;
use Queuety\Heartbeat;

class StreamLLMResponse implements StreamingStep {

    public function stream( array $state, array $existing_chunks = [] ): \Generator {
        // If we have existing chunks from a previous attempt, we can skip
        // re-requesting. The on_complete method will concatenate everything.
        if ( ! empty( $existing_chunks ) ) {
            return;
        }

        $response = OpenAI::chat()->createStreamed( [
            'model'    => 'gpt-4',
            'messages' => $state['messages'],
        ] );

        foreach ( $response as $i => $chunk ) {
            $text = $chunk->choices[0]->delta->content ?? '';
            if ( '' !== $text ) {
                Heartbeat::beat( [ 'tokens' => $i ] );
                yield $text;
            }
        }
    }

    public function on_complete( array $chunks, array $state ): array {
        return [
            'llm_response' => implode( '', $chunks ),
        ];
    }

    public function config(): array {
        return [ 'max_attempts' => 5 ];
    }
}

Используйте его в рабочем процессе так же, как любой другой шаг:

PHP
use Queuety\Queuety;

Queuety::workflow( 'ai_chat' )
    ->then( BuildPromptHandler::class )
    ->then( StreamLLMResponse::class )
    ->then( SaveResponseHandler::class )
    ->dispatch( [ 'user_input' => $input ] );

Полный пример: обработка файлов

PHP
use Queuety\Contracts\StreamingStep;
use Queuety\Heartbeat;

class ProcessCSVRows implements StreamingStep {

    public function stream( array $state, array $existing_chunks = [] ): \Generator {
        $file   = fopen( $state['csv_path'], 'r' );
        $offset = count( $existing_chunks );
        $row    = 0;

        // Skip the header.
        fgetcsv( $file );

        while ( $line = fgetcsv( $file ) ) {
            $row++;

            // Skip rows already processed in a previous attempt.
            if ( $row <= $offset ) {
                continue;
            }

            $result = $this->process_row( $line );
            Heartbeat::beat( [ 'row' => $row ] );
            yield json_encode( $result );
        }

        fclose( $file );
    }

    public function on_complete( array $chunks, array $state ): array {
        $results = array_map( 'json_decode', $chunks );
        return [
            'processed_count' => count( $results ),
            'results'         => $results,
        ];
    }

    public function config(): array {
        return [];
    }

    private function process_row( array $line ): array {
        // Your row processing logic here.
        return [ 'id' => $line[0], 'status' => 'ok' ];
    }
}

API ChunkStore

Класс ChunkStore управляет сохранением фрагментов. Получить к нему доступ можно через Queuety::chunk_store().

PHP
$store = Queuety::chunk_store();
Метод Возвращает Описание
get_chunks( int $job_id ) string[] Получает все фрагменты задачи, отсортированные по индексу фрагмента
append_chunk( int $job_id, int $chunk_index, string $content, ?int $workflow_id, ?int $step_index ) void Добавляет один фрагмент
clear_chunks( int $job_id ) void Удаляет все фрагменты задачи
chunk_count( int $job_id ) int Считает количество фрагментов задачи
get_accumulated( int $job_id ) string Объединяет все фрагменты в одну строку

Обычно вызывать ChunkStore напрямую не нужно. Исполнитель автоматически обрабатывает сохранение и очистку при выполнении StreamingStep. API доступен для сложных сценариев, например просмотра текущих фрагментов или ручной очистки.