Шаги потоковой передачи
Шаги потоковой передачи позволяют рабочему процессу выдавать результат постепенно. Каждый выданный фрагмент немедленно записывается в базу данных, поэтому, если шаг завершится с ошибкой посреди потока, уже сохранённые фрагменты останутся и будут доступны при повторном запуске.
Это полезно для потоковых ответов LLM, обработки больших файлов и любых сценариев, где нельзя терять частичный результат.
Интерфейс StreamingStep
Шаг потоковой передачи реализует Queuety\Contracts\StreamingStep:
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(фрагменты предыдущей попытки всё ещё находятся в БД и будут добавлены в начало)
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() в цикле генератора:
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
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 ];
}
}
Используйте его в рабочем процессе так же, как любой другой шаг:
use Queuety\Queuety;
Queuety::workflow( 'ai_chat' )
->then( BuildPromptHandler::class )
->then( StreamLLMResponse::class )
->then( SaveResponseHandler::class )
->dispatch( [ 'user_input' => $input ] );
Полный пример: обработка файлов
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().
$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 доступен для сложных сценариев, например просмотра текущих фрагментов или ручной очистки.