DeepSeek Harness: بروتوكول StreamChunk ومعالجة الأخطاء

آخر تحديث: 2026-08-31

المخرجات المتدفقة هي تجربة تفاعل الوكيل الأساسية — المستخدمون لا يريدون الانتظار 30 ثانية للحصول على رد كامل، يريدون مشاهدة الوكيل يفكر كلمة بكلمة. بروتوكول StreamChunk هو تجريد DSH الموحد للمخرجات المتدفقة، ومعالجة الأخطاء تضمن التدهور السليم عند حدوث استثناءات.

💡 نصيحة: الجزء الصعب في المخرجات المتدفقة ليس "الإرسال" — بل "ماذا تفعل عند حدوث أخطاء." بروتوكول StreamChunk يعامل الأخطاء كنوع من القطع، مما يتيح للمستهلكين معالجة الحالات الطبيعية وغير الطبيعية بشكل موحد.

📋 المتطلبات المسبقة: أكمل 24-llm-adapter.md، تفهم محولات LLM

1. ما ستتعلمه

تسلسل StreamChunk


2. تفاصيل بروتوكول StreamChunk

(1) ▶ مثال 1

TYPESCRIPT
type StreamChunk =
  | TextChunk
  | ToolCallChunk
  | ToolResultChunk
  | ErrorChunk
  | DoneChunk

interface TextChunk {
  type: 'text'
  content: string
}

interface ToolCallChunk {
  type: 'tool_call'
  id: string
  name: string
  arguments: string
  index: number
}

interface ToolResultChunk {
  type: 'tool_result'
  id: string
  toolCallId: string
  result: any
  isError: boolean
}

interface ErrorChunk {
  type: 'error'
  error: Error
  recoverable: boolean
  retryAfter?: number
}

interface DoneChunk {
  type: 'done'
  reason: 'stop' | 'tool_use' | 'length' | 'cancel' | 'error'
  usage?: TokenUsage
}

interface TokenUsage {
  promptTokens: number
  completionTokens: number
  totalTokens: number
}

(2) ▶ مثال 2

100%
graph LR
    START[بدء التدفق] --> TEXT[TextChunk × N]
    TEXT --> TC[ToolCallChunk]
    TC --> TR[ToolResultChunk]
    TR --> TEXT2[TextChunk × N]
    TEXT2 --> DONE[DoneChunk]

تدفق محادثة وكيل نموذجي:

  1. LLM يُخرج نصاً → TextChunk
  2. LLM يقرر استدعاء أداة → ToolCallChunk
  3. تنفيذ الأداة يكتمل → ToolResultChunk
  4. LLM يُكمل الإخراج → TextChunk
  5. التدفق ينتهي → DoneChunk

▶ مثال 3

TYPESCRIPT
const stream = ctx.llm.stream({
  messages: [{ role: 'user', content: 'list project files' }],
  tools: availableTools
})

for await (const chunk of stream) {
  switch (chunk.type) {
    case 'text':
      process.stdout.write(chunk.content)
      break

    case 'tool_call':
      console.log(`\n🔧 Tool call: ${chunk.name}`)
      console.log(`   Arguments: ${chunk.arguments}`)
      break

    case 'tool_result':
      if (chunk.isError) {
        console.log(`   ❌ Tool error: ${chunk.result}`)
      } else {
        console.log(`   ✅ Tool result: ${JSON.stringify(chunk.result)}`)
      }
      break

    case 'error':
      console.error(`\n⚠️ Error: ${chunk.error.message}`)
      if (chunk.recoverable) {
        console.log(`   Will retry in ${chunk.retryAfter}ms`)
      }
      break

    case 'done':
      console.log(`\n✅ Done (${chunk.reason})`)
      if (chunk.usage) {
        console.log(`   Token usage: ${chunk.usage.totalTokens}`)
      }
      break
  }
}

3. أنواع القطع بالتفصيل

(1) TextChunk

أجزاء المحتوى النصي، تُخرج تدريجياً:

TYPESCRIPT
// LLM يُخرج "Hello, world!"
// قد يُنتج عدة TextChunks:
// قطعة 1: { type: 'text', content: 'Hello' }
// قطعة 2: { type: 'text', content: ', ' }
// قطعة 3: { type: 'text', content: 'world' }
// قطعة 4: { type: 'text', content: '!' }

المستهلكون يجب أن يربطوا جميع TextChunks بدلاً من عرضها فرادى.

(2) ToolCallChunk

LLM يطلب استدعاء أداة:

TYPESCRIPT
{
  type: 'tool_call',
  id: 'call_abc123',
  name: 'file_edit',
  arguments: '{"action":"read","path":"src/index.ts"}',
  index: 0
}

ملاحظة: arguments هي سلسلة JSON تحتاج تحليلاً.

(3) ToolResultChunk

النتائج بعد تنفيذ الأداة:

TYPESCRIPT
{
  type: 'tool_result',
  id: 'result_xyz789',
  toolCallId: 'call_abc123',
  result: { content: 'export const name = ...' },
  isError: false
}

isError: true يشير لفشل تنفيذ الأداة؛ النتيجة تحتوي معلومات الخطأ.

(4) استدعاءات أدوات متعددة

محادثة واحدة قد تستدعي أدوات متعددة:

TEXT 📖 للعرض فقط
TextChunk: "Let me check two files"
ToolCallChunk: { name: "file_edit", id: "call_1", index: 0 }
ToolCallChunk: { name: "file_edit", id: "call_2", index: 1 }
ToolResultChunk: { toolCallId: "call_1", result: ... }
ToolResultChunk: { toolCallId: "call_2", result: ... }
TextChunk: "The contents of both files above..."
DoneChunk: { reason: "stop" }

4. آلية الاسترداد من الأخطاء

(1) تصنيف الأخطاء

TYPESCRIPT
// خطأ غير قابل للاسترداد (مثلاً مفتاح API غير صالح)
{
  type: 'error',
  error: new Error('Invalid API key'),
  recoverable: false
}

// خطأ قابل للاسترداد (مثلاً حد المعدل)
{
  type: 'error',
  error: new Error('Rate limit exceeded'),
  recoverable: true,
  retryAfter: 5000
}

// خطأ قابل للاسترداد (مثلاً تقلب الشبكة)
{
  type: 'error',
  error: new Error('Connection timeout'),
  recoverable: true,
  retryAfter: 2000
}

(2) تدفق الاسترداد التلقائي

100%
graph TD
    ERR[ErrorChunk] --> CHECK{recoverable؟}
    CHECK -->|لا| FAIL[إنهاء التدفق + DoneChunk<br/>reason: error]
    CHECK -->|نعم| WAIT[انتظار retryAfter]
    WAIT --> RETRY[إعادة محاولة الطلب]
    RETRY --> SUCCESS{نجاح؟}
    SUCCESS -->|نعم| CONTINUE[استمرار التدفق]
    SUCCESS -->|لا| ERR2[ErrorChunk آخر]
    ERR2 --> CHECK2{محاولات مستنفدة؟}
    CHECK2 -->|لا| WAIT
    CHECK2 -->|نعم| FAIL

(3) الاسترداد اليدوي

TYPESCRIPT
for await (const chunk of stream) {
  if (chunk.type === 'error') {
    if (chunk.recoverable) {
      ctx.logger.warn(`Stream error, recoverable: ${chunk.error.message}`)
      // الإطار يُعيد المحاولة تلقائياً
    } else {
      ctx.logger.error(`Stream error, non-recoverable: ${chunk.error.message}`)
      break
    }
  }
}

(4) معالجة أخطاء الأدوات

عند فشل تنفيذ أداة، الإطار يُعيد الخطأ كـ ToolResultChunk لـ LLM:

TYPESCRIPT
{
  type: 'tool_result',
  id: 'result_1',
  toolCallId: 'call_1',
  result: { error: true, message: 'Permission denied: /etc/passwd' },
  isError: true
}

LLM يمكنه بعدها اختيار:


5. المقاطعة والإلغاء

(1) إلغاء المستخدم

عندما ينقر المستخدم زر "إيقاف" في واجهة الويب، يُلغى التدفق:

TYPESCRIPT
const controller = new AbortController()

const stream = ctx.llm.stream(request, { signal: controller.signal })

// المستخدم يُلغي
controller.abort()

// التدفق يُنتج DoneChunk
// { type: 'done', reason: 'cancel' }

(2) إلغاء المهلة

TYPESCRIPT
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 60000)

try {
  for await (const chunk of ctx.llm.stream(request, { signal: controller.signal })) {
    // معالجة القطعة
  }
} finally {
  clearTimeout(timer)
}

(3) إلغاء شرطي

TYPESCRIPT
let tokenCount = 0

for await (const chunk of stream) {
  if (chunk.type === 'text') {
    tokenCount += chunk.content.length
    if (tokenCount > 10000) {
      controller.abort()
      break
    }
  }
}

(4) تنظيف الإلغاء

بعد الإلغاء، الإطار يضمن:


6. استراتيجيات إعادة المحاولة

(1) استراتيجيات إعادة المحاولة المدمجة

نوع الخطأ المحاولات استراتيجية التراجع
حد المعدل (429) 3 تراجع أسي
مهلة الشبكة 2 فاصل ثابت
خطأ الخادم (5xx) 2 تراجع أسي
خطأ المصادقة (401) 0 بدون إعادة محاولة
خطأ الطلب (400) 0 بدون إعادة محاولة

(2) إعداد إعادة المحاولة

TYPESCRIPT
export const Config = Schema.object({
  maxRetries: Schema.number().default(3).description('Maximum retry attempts'),
  retryDelay: Schema.number().default(1000).description('Initial retry delay (ms)'),
  retryMultiplier: Schema.number().default(2).description('Delay multiplier for exponential backoff')
})

(3) منطق إعادة محاولة مخصص

TYPESCRIPT
async function streamWithRetry(
  ctx: Context,
  request: LLMRequest,
  maxRetries = 3
): Promise<void> {
  let attempt = 0
  
  while (attempt <= maxRetries) {
    try {
      for await (const chunk of ctx.llm.stream(request)) {
        if (chunk.type === 'error' && chunk.recoverable) {
          attempt++
          const delay = Math.min(1000 * Math.pow(2, attempt), 30000)
          ctx.logger.warn(`retry ${attempt}/${maxRetries} in ${delay}ms`)
          await sleep(delay)
          break  // إعادة الدخول لحلقة while
        }
        // معالجة القطع الطبيعية
      }
      return  // نجاح
    } catch (error) {
      attempt++
      if (attempt > maxRetries) throw error
    }
  }
}

7. التسجيل والقابلية للملاحظة

(1) تسجيل التدفق

TYPESCRIPT
ctx.on('llm/stream/start', (request) => {
  ctx.logger.info(`stream started: model=${request.model}`)
})

ctx.on('llm/stream/chunk', (chunk) => {
  ctx.logger.debug(`chunk: type=${chunk.type}`)
})

ctx.on('llm/stream/end', (done) => {
  ctx.logger.info(`stream ended: reason=${done.reason}, tokens=${done.usage?.totalTokens}`)
})

(2) مقاييس الأداء

TYPESCRIPT
interface StreamMetrics {
  ttfb: number              // Time to First Byte
  totalDuration: number     // المدة الإجمالية
  chunkCount: number        // عدّاد القطع
  toolCallCount: number     // عدّاد استدعاءات الأدوات
  retryCount: number        // عدّاد إعادة المحاولة
  tokenUsage: TokenUsage    // استخدام الرموز
}

(3) التسجيل المهيكل

TYPESCRIPT
ctx.logger.info('llm_request', {
  model: request.model,
  messageCount: request.messages.length,
  toolCount: request.tools?.length || 0,
  stream: true
})

ctx.logger.info('llm_response', {
  model: response.model,
  duration: Date.now() - startTime,
  tokens: response.usage?.totalTokens
})

❓ أسئلة شائعة

س بعد إلغاء التدفق، هل تُتراجع الأدوات المُستدعاة بالفعل؟
ج لا. تنفيذ الأداة يسري فوراً (مثلاً الملفات المُنشأة بالفعل). الإلغاء يوقف فقط مخرجات LLM اللاحقة.
س هل ينتهي التدفق دائماً بعد ErrorChunk؟
ج ليس بالضرورة. إذا كان الخطأ قابل للاسترداد، الإطار يُعيد المحاولة والتدفق قد يستمر. فقط الأخطاء غير القابلة للاسترداد أو المحاولات المستنفدة تُنتج DoneChunk لإنهاء التدفق.
س كيف أحسب TTFB للمخرجات المتدفقة؟
ج سجّل وقت وصول أول قطعة: typescript const start = Date.now() for await (const chunk of stream) { if (chunk.type === 'text') { const ttfb = Date.now() - start ctx.logger.info(`TTFB: ${ttfb}ms`) break } }
س ما فائدة index في ToolCallChunks المتعددة؟
ج index يُحدد ترتيب استدعاءات الأدوات المتوازية. استجابة LLM واحدة قد تحتوي عدة tool_calls؛ index يزداد من 0.
س هل يمكن دعم المخرجات المتدفقة والكاملة في نفس الوقت؟
ج نعم. المحولات تنفذ كلتا الدالتين complete() و stream(). المستدعون يختارون حسب الحاجة.
س كيف أُحاكي المخرجات المتدفقة للاختبار؟
ج typescript async function* mockStream(): AsyncIterable<StreamChunk> { yield { type: 'text', content: 'Hello' } yield { type: 'text', content: ', world!' } yield { type: 'done', reason: 'stop' } }

📖 ملخص


📝 تمارين

1. ⭐ أساسي: اكتب مستهلكاً يتكرر على تدفق StreamChunk، يعدّ قطع text/tool_call/tool_result/error/done منفصلة. اختبر بتدفق محاكاة.

2. ⭐⭐ متوسط: نفّذ مستهلك تدفق بمهلة — إذا لم تصل قطعة خلال 30 ثانية، ألغِ التدفق تلقائياً. استخدم AbortController للإلغاء.

3. ⭐⭐⭐ تحدٍ: نفّذ غلاف إعادة محاولة مخصص حول ctx.llm.stream() يُعيد المحاولة تلقائياً عند أخطاء قابلة للاسترداد (تراجع أسي، 3 محاولات كحد أقصى). سجّل تأخير ونتيجة كل محاولة. اختبر بمحول LLM محاكاة يتناوب بين الفشل والنجاح.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%