流式步骤
流式步骤可让工作流逐步生成输出。每个生成的块都会立即写入数据库,因此如果步骤在流式处理中途失败,已持久化的块仍会保留,并可在重试时使用。
这适用于 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 ) |
生成字符串块的生成器。每次生成都会立即写入数据库。 |
on_complete( $chunks, $state ) |
在流结束后调用。接收所有块(之前的和新的),并返回要合并到工作流状态的数据。 |
config() |
可选配置。支持 max_attempts 和 backoff 键;backoff 接收策略名称(exponential、linear、fixed),因为延迟列表目前会被丢弃(#16)。 |
块的持久化方式
每当生成器生成一个值,Queuety 就会调用 ChunkStore::append_chunk() 将其写入 queuety_chunks 表。不会进行批处理或缓冲。每次生成都会执行一次 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 类负责管理块的持久化。通过 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 可用于检查处理中块或手动清理等高级场景。