خطأ تدفق API LLM: الأسباب والحلول

خطأ تدفق API LLM: الأسباب والحلول

رسالة خطأ error: llm api error: an error occurred during streaming تأتي دائماً تقريباً من أحد ستة مصادر: فشل مزود في منتصف التدفق يُرسل كحدث SSE بعد أن أعاد الاتصال بالفعل 200، أو انتهاء مهلة قراءة من جانب العميل أو البوابة، أو حد معدل (429) يحدث في منتصف التوليد، أو جزء غير متوقع أو تالف يكسر محلل SSE الخاص بك، أو انقطاع في الشبكة، أو مشكلة مصادقة/حصة تظهر فقط بعد فتح التدفق. افحصها بهذا الترتيب — معظم حالات فشل التدفق تقع في الثلاثة الأولى.

طابق ما تراه مع سبب قبل أن تقرأ أكثر:

ما تلاحظه اذهب إلى
يبدأ التدفق، تصل بعض الرموز، ثم ينقطع دون تغيير في حالة HTTP السبب 1
يتوقف التدفق لمدة 10 ثوانٍ+ دون رموز جديدة، ثم يرفع عميلك انتهاء مهلة السبب 2
يحدث الخطأ غالباً أثناء ارتفاع حركة المرور أو بعد دفعة من الطلبات السبب 3
يذكر الاستثناء KeyError، JSONDecodeError، أو فشل تحليل، وليس إنهاء شبكة السبب 4
الخطأ عام (APIConnectionError، ECONNRESET) ويحدث بشكل غير متناسق، حتى على الشبكات المستقرة السبب 5
يحدث الخطأ عند أول طلب بعد تدوير مفتاح، أو الوصول إلى حد إنفاق، أو تغيير البيئة السبب 6

قائمة الفحص التشخيصية الكاملة أدناه تكرر هذا مع السبب المحتمل موضحاً في كل صف.

النقاط الرئيسية

  • يمكن أن يصل خطأ التدفق كحدث error من نوع SSE بعد أن يكون حالة HTTP قد عادت بالفعل 200، لذا فإن منطق إعادة المحاولة القائم على حالة الرمز فقط يفوته.
  • واجهة برمجة تطبيقات الرسائل من Anthropic تبلغ عن الأحمال الزائدة في منتصف التدفق كـ event: error مع "type": "overloaded_error" داخل جسم التدفق، وليس كحالة HTTP 529 جديدة.
  • يوثق OpenRouter نفس النمط عبر المزودين: بمجرد شحن الرمز الأول، يصل الفشل كـ chat.completion.chunk مع حقل error على المستوى الأعلى و finish_reason: "error".
  • المكتبات العميلة التي تفترض أن كل جزء مُدفق ناجح سوف تتعطل مع خطأ مضلل (مثل APIConnectionError بدلاً من السبب الحقيقي) عندما يرسل مزود حمولة خطأ منظمة في منتصف التدفق.
  • معظم الإصلاحات هي نفسها بغض النظر عن المزود: تحليل جسم التدفق بحثاً عن أحداث الخطأ، تعيين مهلة توقف على مستوى الرمز منفصلة عن مهلة الاتصال، واستخدام التراجع الأسي المرتكز إلى نوع الخطأ، وليس فقط حالة HTTP.

قائمة الفحص التشخيصية: ابحث عن السبب الجذري أولاً

قم بتشغيل هذا قبل تغيير أي كود. يربط كل صف عرضاً يمكنك ملاحظته بالقسم الذي يصلحه.

ما تلاحظه السبب املحتمل اذهب إلى
يبدأ التدفق، تصصل بعض الرمز، ثم ينقطع دون تغيير في حاةة HTTP خطأ مزود في منتصف التدفق (تحميل زائد، مرشح محتوى، تعطل المزود) السبب 1
يتوقف التدفق لمدة 10 ثوانٍ+ دون رمز جديدة، ثم يرفع عملك انتهاء مهلة انتهاء مهلة القراءة أو توقف التدفق الخامل السسبب 2
يحدةث الخطأ غالباً أثناء ارتفاع حرمة المرور أو بعد دفعة من الطلبات حد معدل (429) في منتصف التدفق السسبب 3
يذكر الاستثناء KeyError، JSONDecodeError، أو فشل تحليل، ولي س إنهاء شبكة جزؤ تالف أو غير متوقع السسبب 4
الخطأ عام (APIConnectionError، ECONNRESET) ويحدث بشكل غير متناسق، حتى على الشبكات المستقرة انقطاع اتصال أو خطأ مزود مقنع السبب 5 و السبب 4
يحددث الخطأ عند أول طلب بعد تدوير مفتاح، أو الوصول إلى حد إنفاق، أو تغيير البيئة فشل مصادقة أو حصة السبب 6

إذا أظهرت سجلاتك فقط اسم استثناء عام ولا توجد رسالة من المزود، فهذا بحد ذاته عرض — انظر السبب 4 و السبب 5 لمعرفة لماذا تخفي المغلفات العامة السسبب الحقيقي.

السبب 1: خطأ مزود في منتصف التدفق (تحميل زائد، مرشح محتوى، تعطل المزود)

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

توثق واجةة برمجة تطبيقات الرسال من Anthropic هذ ا مباشرة: قد ترسل API “أحياناً [أخطاءً] في تدفق الأحداث”، وتعطي هذ المثال بالضبط لحالة تحميل زائد تصل في منتصف التدفق:

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}

هذة فجوة حقيقية في الكثيير م منطق إعادة المحاولة. إذا كان ردمزك يفحص فقط response.status_code، فق قد رأى بالفعل 200 قبل حدوث الفشل، لذل ل تبدأ إعادة المحاولة أبداً. يصف تقرير حادثة من طرف ثالث هذا النمط ال محدد بوضوح: “منطق إعادة المحاولة الذ يعتم د عى رمز حاة HTTP لا يبدأ أبداً لأن الحالة كانت قد 200. النتيجة هي استجابة مقطوعة بصمت” — ويوصي بتحليل جسم التدفق بحثاً عن أحداث خطأ بدلاً من الثقة ف رمز الحالة فقط.

يوصف OpnRouter نفس المشكلة الهيكلية ع مزودي التوجيه، ليس مجرد بائع راحد. بمجرد كتابة الرمز الأول، “تم إرسال حالة 200 OK والرؤوس بالفعل — لا يمكن تغييرها”، لذا فإن فشل مزود “يجب أن يصل في الحزمة كحدث SSE”. أسبابها الموثقة لهذا النوع من الخطأ:

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

حملة خطأ منتصف التدفق من OpnRouter تحمل الخطأ داحل جزء يبدو عادياً، مع finish_reason يخبرك بأن التدفق انتهى بشكل غير طبيعي:

{"id":"gen-abc123","object":"chat.completion.chunk","created":1234567890,"model":"openai/gpt-4o","provider":"OpenAI","error":{"code":429,"message":"Rate limit exceeded","metadata":{"error_type":"rate_limit_exceeded"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

كيفة تأكيد أن هذا هو سسبك: سجل تدفق SSE الخام (ليس فقط النص المحلل) لطلب فاشل. إذا رأيت حدث error أو جزء بحقل error على المستوى الأعلى في أي مكان في التدفق قبل الحدث النهائي، فهذا هو.

الإصلاح: عالج حمولات finish_reason: "error" و event: error كحالات فشل قابلة لإعادة المحاولة، ليس كم شات ناجحة بمحتوى فارغ. أعد المحاولة بالتراجع على نوع الخطأ المحدد داخل الحمولة، وليس على حالة HTTP، نظراً لأن حالة HTTP ستكون قد قرأت 200 بالفعل.

السبب 2: انتهاء مهلة القراءة أو توقف التدفق الخامل

يختلف التوقف عن الخطأ الصلب: يبقى الاتصال مفتوحاً، و لكن لا تصصل رموز جدية لفترة طويلة. معظم عملاء HTTP يخلطون ب ين مفه ومين للمهلة — مهلة إجمالية للطلب، ومهلة لكل قراءة (خاملة). يمكن للتدفق الطويل أن يتجاوز بشروعية مهلة إجمالية قصريرة عى استجابة كبيرة، ب ينما يمكن للتدفق العالق حقاً أن يبقى داخل نافذة مهلة إجمالية إذا لم يكن هناك فحص قراءة خاملة.

يضبط SDK بايثون من OpnAI مهلة إجمالية افتراضية للطلب قدرها 10 دقائق: “بشكل افتراضي، تنتهي مهلة الطلبات بعد 10 دقائق. يمكنك تكوين ذلك باستخدام خيار timeout، الذي يقبل رقماً عائماً أو كائن hatpx.Tiimeout… عند انتهاء المهلة، يتم إلقاء APITimeoutError.” كما يعيد محاولة بعش حالات الفشل تلقائياً: “أخطاء الاتصال (على سبيل المثال، بسبب مشكلة اتصال شبكة)، 408 مهلة الطلب، 409 تعارض، 429 حد معدل، و >=500 أخطاء داخلية يتم إعادة محاولتها بشكل افتراضي”، مرتين، قابلة للتكوين عبر max_retries.

أكده: قس الفارق الزمني بين آخر رمز تم اسقباله والخطأ. مدة ثابتة تتوافق مع قيمة المهلة المكوّنة تشير إلى مهلة، ل إلى فشل من جانب المزود.

الإصلح: ضع مهلتين، ليس واحدة — مهلة اتصال/إجمالية لدورة حياة الطلب، ومهلة قراءة خاملة منفصلة تبدأ إذا لم يصل جء خلال N ثانية. يسمح لك ذل بالتمييز ب ين “النموذج بطيء” و “التدفق مات”. 15-30 ثانية هي مهلة قراءة خاملة معقولة لاستكمالات الدردشة؛ ارفعها إذا كان لنموذجك مراحل “تفكير” صامتة طويلة قبل الرمز الأول.

سبب 3: حد المعدل (429) في منتصف التدفق

عادةً ما ترفض حدود المعدل طلباً قبل أن يبدأ. و لكن بعض البوابات ت طبق الحدود لكل رمز أو لكل نافذة، لذل يمك للطلب أن يبدأ التدفق ويتقطع مجرد أن يعبر ميزانية معينة في منتصف التوليد — ننفس شكل الخطأ ف الحزمة كما في السبب 1، ولكن مع رمز 429 يصل داخل التدفق بدلاً من كونة حالة الاستجابة الأولية.

أكده: تحقق مم إذا كانت حالات الفشل تتجمع أثناء دفعات حركة المرور أو عند حد ثابت من الطلبات ف الدقيقة. رأس Retry-After، أو حقل مكافىء في حمولة الخطأ، يؤكد ذلك.

الإصلاح: احترم Ratry-After عند وجوده، قلل التوازي قبل إعادة المحاولة، وتراجع أسي. حالات الوصول المتكررة هي إشارة لخفض عدد التدفقات المتوازية أو ترقية مستوى الحساب، ليس لإعادة المحاولة بشكل أكثر عدوانية.

سبب 4: جزء تالف أو غير متوقع يكسر المحلل

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

حلة موثقة: توقع معالج ollama_chat من LiteLLM أن يحتوي كل جزء على حقل message. عندما أعاد Ollama بدلاً من ذلك خطأ منظم مثل {"error": "error parsing tool call: ..."}، قام المعالج ببحث غير محمي chunk["message"]، مما أثار KeyError: 'message'. التقط معالج استثناء أوسع ذلك وأعاد طرحه كـ APIConnectionError — خطأ يبدو شبكياً لما كان في الواقع فشل تحليل JSON داخل مخرجات استدعاء أداة النموذج. تقرير الخطأ صريح بشأن التكلفة: المهندسون الذين يحققون فيه “سجلوا reason=timeout / LLM request timed out لما كان في الواقع JSON لاستدعاء أداة غير صالحة”، مما كلف “يومين من التشخيص الخاطئ” مطاردة أسباب شبكة وبنية تحتية لم تكن المشكلة.

النمط العام: يرسل مزود خطأ بشكل لا يتوقعه كود التحليل الخاص بك، يتم إطلاق استثناء منخفض المستوى (KeyError، TypeError، خطأ فك تشفير JSON)، وتقوم كتلة except شاملة بلفه في خطأ اتصال أو تدفق عام يمحو السبب الحقيقي.

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

الإصلح: تحقق من وجود مفتاح error قبل الوصول إلى حقول متوقعة مثل message أو delta، وتفرع عليه بشكل صريح. احفظ رسالة الخطأ من المزود الاصلية ورمر حالتها عند إعادة الطرح، بدلاً من طي كل فشل في نوع استثناء عام واحد.

سبب 5: انقطاع اتصال الشبكة

أحياناً يك ون السبب حقاً هو الشبكة: وكيل أو مز وان تحميل يغلق اتصالاً طويل الأمد بعد فترة خمول، أو تتغير شبكة العميل ف منتصف الطلب. هذا يشبه السبب 2 و لكن الإصلح يختلف — الاتصال نفهه ذهب، وليس فقط خاملاً داخل نافذة المهلة الخصة بك.

أكده: ابحث عن أخطاء من نمط إعادة تعيين الاتصال (ECONNRESET, Broken pipe, Connection reset by peer) بدلاً من استثناء مهلة على مستوى التطبيق، وتحقق مما إذا كانت حالات الفشل ترتبط بمهلة اتصال خامل معروفة لواسيط معينة (العديد من موازنات التحمل تعين افتراضياً 60 ثانية من عدم النشاط).

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

سبب 6: فشل المصادقة أو الحصة يظهر في منتصف التدفق

بعض البوابات تتحقق من المصادقة والفواتير بشكل كسول — يتم فتح الاتصال، ثم يحدث فحص المصادقة أو الرصيد أثناء توليد الجزء الأول، ويتم الإبلاغ عن فشل هناك كخطأ في التدفق بدلاً من 401/402 نظيف في وقت الطلب.

أكده: تحقق مما إذا كان الخطأ يحدث في كل طلب من مفتاح معين، بدلاً من بشكل متقطع، وما إذا كان قد بدأ مباشرة بعد تدوير مفتاح، أو حدث فاتورة، أو تغيير ب يئة (مفاتح تطوير ضد عنوان URL أساسي للإنتاج، أو العكس).

الإصلح: تحقق من صيغة المفتاح (Authorization: Bearer <key>، وليس المفتاح الخام)، تأكد أن المفتاح نشط، وتحقق من رصيد الحساب بشكل منفصل عن تصحيح أخطاء الطلب. كل طلب يفشل بشكل متطابق بغض النظر عن المطالبة أو النموذج يشير هنا وليس إلى الأسباب الخمسة السابقة.

نمط إعادة المحاولة والتراجع الذي يعالج جميع الأسباب الستة

استراتيجية إعادة محاولة واحدة يمكن أن تغطي الأسباب 1 إلى 5 إذا كانت تفحص جسم التدفق، وليس فقط حالة HTTP:

import time
import openai

def stream_with_recovery(client, **kwargs):
    max_attempts = 3
    for attempt in range(max_attempts):
        try:
            collected = ""
            stream = client.chat.completions.reate(stream=True, **kwags)
            for chunk in stream:
                choice = chunk.choices[0] if chunk.choices else None
                if choice and getattr(choice, "finish_reason", None) == "error":
                    raise RuntimeError(f"mid-stream error: {chunk}")
                if choice and choice.delta.content:
                    collected += choice.delta.content
            return collected
        except (openai.APITimeoutError, openai.APIConnectionError, openai.RateLimitError) as e:
            if attempt == max_attempts - 1:
                raise
            time.sleep(2 ** attempt)
    return collected

يستخدم هذا المثال شكل عميل متوافق مع OpenAI والذي يعمل أيضاً مع نقطة نهاية استكمال الدردشة المتوافقة مع OpenAI من Novita AI عن طريق تعيين base_url="https://api.novita.ai/openai". ثلاث أشياء مهمة بالإضافة إلى الكود أعلاه:

  • تحقق من محتويات الجزء بحثاً عن خطأ داخل الحزمة قبل افتراض اكتمال طبيعي، حسب السبب 1.
  • استخدم التراجع الأسي (2 ** attempt، محدود) بدلاً من إعادة المحاولة الفورية، خاصة لحدود المعدل وأخطاء التحميل الزائد.
  • سجل رسالة الخطأ الخام من المزود قبل لفها في نوع الاستثناء الخاص بك، حتى لا يكرر التصحيح المستقبلي التشخيص الخاطئ لمدة يومين في السبب 4.

إذا كان مزود أو نموذج معين يفشل أكثر من غيره، فإن توجيه فئة الطلب تلك إلى نموذج مختلف هو تخفيف يستحق أن يكون في مكانه قبل أن تحتاجه — راجع تشغيل خدمة LLM متعددة المزودين بهدف زمن تشغيل محدد لمعرفة كيفية تعريف سياسة الاحتياط هذه بدلاً من الارتجال أثناء حادثة.

الاستنتاج

تشخيص هذا الخطأ هو عملية استبعاد، وليس تخميناً: استخدم قائمة الفحص في الأعلى لمطابقة عرضك مع أحد الأسباب الستة، ثم أكده بالإشارة المحددة التي يذكرها كل قسم — حدث error داخل الحزمة، مدة توقف تطابق مهلة، 429 مرتبط بالازدحام، حمولة خام اختنق بها المحلل، سلسلة إعادة تعيين اتصال، أو فشل يتكرر في كل طلب من مفتاح واحد. الأسباب 1 إلى 3 هي الأكثر شيوعاً، والأسباب 1 و3 و5 تشترك في نفس الإصلاح الأساسي: توقف عن الثقة في رمز حالة HTTP بمجرد بدء التدفق، وبدلاً من ذلك أعد المحاولة بالتراجع بناءً على ما يبلغه التدفق نفسه.

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

الأسئلة الشائعة

لماذا يعمل طلب LLM API الخاص بي أحياناً ويفشل أحياناً أخرى مع خطأ في التدفق؟

حالات الفشل المتقطعة تشير إلى سبب في منتصف التدفق بدلاً من مشكلة تكوين — أخطاء التكوين مثل مفتاح سيئ أو نقطة نهاية خاطئة تفشل في كل مرة. تحقق من تحميل المزود الزائد وحدود المعدل أولاً، لأن كلاهما يعتمد على حركة المرور.

هل خطأ التدفق هو نفسه انتهاء المهلة؟

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

لماذا تقول رسالة الخطأ الخاصة بي “خطأ في الاتصال” عندما كانت المشكلة الحقيقية شيئاً آخر؟

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

هل يمكنني استئناف تدفق بعد خطأ في منتصف التدفق بدلاً من البدء من جديد؟

معظم واجهات برمجة تطبيقات التدفق المتوافقة مع OpenAI وبأسلوب Anthropic لا تدعم استئناف تدفق جزئي عند نقطة الفشل. أعد محاولة الطلب بالكامل، وتجاهل المخرج الجزئي بدلاً من إلاحقة به، لتجنب المحتوى المكرر.

هل يجب أن أستخدم التدفق دائمًا لاستدعاءات LLM API؟

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

ماذا يعني finish_reason: error في استجابة متدفقة؟

إنها إشارة نهائية ترفقها بعض البوابات بالجزء الأخي من تدفق فشل في منتصف الطريق، تمييزاً عن القيم العادية مثل stop أو length. عالجه كتوليد فاشل، حتى لو كانت حالة HTTP للطلب 200.

المقالات الموصى بها