خطوات البث
تتيح خطوات البث لسير العمل إنتاج المخرجات تدريجيًا. يُكتب كل جزء مُنتَج في قاعدة البيانات فورًا، لذا إذا فشلت الخطوة أثناء البث، تبقى الأجزاء المحفوظة مسبقًا متاحة عند إعادة المحاولة.
يفيد ذلك في استجابات البث من 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' ];
}
}
واجهة ChunkStore 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 لحالات الاستخدام المتقدمة، مثل فحص الأجزاء قيد المعالجة أو تنظيفها يدويًا.