0Pricing
AI Engineering Academy · درس

السلاسل المتفرعة والمتوازية

ابنوا تراكيب RunnableParallel وRunnableBranch لتشغيل سلاسل متعددة في الوقت نفسه أو توجيه الإدخال إلى سلاسل مختلفة بناءً على شروط ديناميكية.

السلاسل المتفرعة والمتوازية درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

أهمية التوازي والتفرع

غالبًا ما تحتاج مسارات معالجة LLM الواقعية إلى تنفيذ عدة مهام في الوقت نفسه أو توجيه الطلبات بطرق مختلفة استنادًا إلى المحتوى. تشغّل السلاسل المتوازية فروعًا متعددة في آن واحد، مما يقلل زمن الاستجابة عندما تكون المهام مستقلة. أما السلاسل المتفرعة فتوجه الإدخال إلى سلاسل متخصصة مختلفة استنادًا إلى شروط ديناميكية. ويدعم LCEL كلا النمطين أصليًا باستخدام RunnableParallel وRunnableBranch.

أساسيات RunnableParallel

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

from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model='gpt-4o-mini')
parser = StrOutputParser()

parallel = RunnableParallel(
    summary=(
        ChatPromptTemplate.from_template('Summarize: {text}') | model | parser
    ),
    keywords=(
        ChatPromptTemplate.from_template('Extract keywords from: {text}') | model | parser
    )
)

result = parallel.invoke({'text': 'Long document text here...'})
print(result['summary'])
print(result['keywords'])

التوازي باستخدام الصياغة المختصرة للقاموس

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

from langchain_core.runnables import RunnablePassthrough

# Dict shorthand creates RunnableParallel automatically
chain = (
    RunnablePassthrough.assign(
        sentiment=(
            ChatPromptTemplate.from_template('Sentiment of: {review}')
            | model | parser
        ),
        aspects=(
            ChatPromptTemplate.from_template('List aspects mentioned in: {review}')
            | model | parser
        )
    )
)

result = chain.invoke({'review': 'Great battery but poor camera quality.'})
print(result['sentiment'])
print(result['aspects'])

فهم RunnableBranch

يوجّه RunnableBranch الإدخال إلى سلاسل مختلفة استنادًا إلى شرط. وتقدم له قائمة من أزواج (condition, runnable) وRunnable افتراضيًا. ويقيّم الفرع الشروط بالترتيب ويشغّل أول فرع مطابق. ويتيح ذلك التوجيه القائم على النية — مثل إرسال استفسارات خدمة العملاء إلى سلسلة دعم، والأسئلة التقنية إلى سلسلة توثيق.

from langchain_core.runnables import RunnableBranch

technical_chain = (
    ChatPromptTemplate.from_template('Technical answer: {query}') | model | parser
)
general_chain = (
    ChatPromptTemplate.from_template('General answer: {query}') | model | parser
)

branch = RunnableBranch(
    (lambda x: 'error' in x['query'].lower() or 'bug' in x['query'].lower(),
     technical_chain),
    general_chain  # default branch
)

result = branch.invoke({'query': 'I got a TypeError in my code'})
# Routes to technical_chain because 'error' is in the query

التوجيه الدلالي باستخدام تصنيف LLM

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

from langchain_core.output_parsers import StrOutputParser

# Step 1: classify intent
classify_prompt = ChatPromptTemplate.from_template(
    'Classify this query as exactly one of: billing, technical, general.\nQuery: {query}'
)
classifier = classify_prompt | model | StrOutputParser()

# Step 2: route based on classification
def route(classification_result: dict):
    topic = classification_result['topic'].strip().lower()
    if topic == 'billing':
        return billing_chain
    elif topic == 'technical':
        return technical_chain
    return general_chain

full_chain = (
    RunnablePassthrough.assign(topic=lambda x: classifier.invoke(x))
    | RunnableLambda(route)
)

RAG المتوازي: مسترجعات متعددة

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

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

# Assume these retrievers are already set up
faq_retriever = faq_vectorstore.as_retriever(search_kwargs={'k': 3})
doc_retriever = doc_vectorstore.as_retriever(search_kwargs={'k': 3})

retrieval = RunnableParallel(
    faq_results=faq_retriever,
    doc_results=doc_retriever
)

def merge_docs(retrieved: dict) -> str:
    all_docs = retrieved['faq_results'] + retrieved['doc_results']
    return '\n\n'.join(d.page_content for d in all_docs)

pipeline = (
    retrieval
    | RunnableLambda(merge_docs)
    | ChatPromptTemplate.from_template('Context: {context}\nAnswer: {question}')
    | model | parser
)

السلاسل الشرطية باستخدام itemgetter

عندما تحتاج إلى اختيار مفتاح محدد من نتيجة متوازية أو تمرير جزء فقط من السياق إلى الخطوة التالية، تعمل operator.itemgetter في Python كمحدِّد خفيف الوزن من نوع Runnable. ويكون ذلك مفيدًا بعد خطوة متوازية عندما تنتج الفروع المختلفة مفاتيح مختلفة وتحتاج إلى استخراج المفتاح ذي الصلة فقط للمرحلة التالية من المعالجة.

from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough

# After a parallel step, extract just the summary
chain = (
    RunnableParallel(
        summary=summary_chain,
        sentiment=sentiment_chain
    )
    | itemgetter('summary')  # pass only the summary onward
    | translate_chain
)

# itemgetter works because dict.__getitem__ is a valid transform

قياس التسريع الناتج عن التنفيذ المتوازي

الفائدة الأساسية من RunnableParallel هي تقليل الزمن الفعلي. فثلاثة استدعاءات متسلسلة لنموذج LLM، يستغرق كل منها ثانيتين، ستستغرق 6 ثوانٍ إجمالًا. أما عند تنفيذها بالتوازي، فستكتمل في نحو ثانيتين، أي مدة أبطأ فرع. لكن الاستدعاءات المتوازية تزيد استخدام الرموز المميزة دفعة واحدة، لذا راقب حدود معدل الطلبات. استخدم max_concurrency في batch() أو عيّن حدود معدل لكل مفتاح عند الحاجة.

import time

# Measure sequential time
start = time.time()
result1 = chain_a.invoke(input_data)
result2 = chain_b.invoke(input_data)
result3 = chain_c.invoke(input_data)
seq_time = time.time() - start
print(f'Sequential: {seq_time:.2f}s')

# Measure parallel time
start = time.time()
results = RunnableParallel(a=chain_a, b=chain_b, c=chain_c).invoke(input_data)
par_time = time.time() - start
print(f'Parallel: {par_time:.2f}s')
print(f'Speedup: {seq_time/par_time:.1f}x')

السلاسل المتوازية غير المتزامنة

للتنفيذ المتوازي غير الحاجب فعليًا في التطبيقات غير المتزامنة، استخدم RunnableParallel.ainvoke(). في الخلفية، يستخدم LCEL الدالة asyncio.gather() لتشغيل الفروع بشكل متزامن ضمن حلقة الأحداث. وهذا مهم بشكل خاص في خدمات FastAPI، حيث يكون كل معالج طلب عبارة عن coroutine؛ فاستخدام الواجهة غير المتزامنة يمنع حجب حلقة الأحداث حتى مع وجود عدة استدعاءات متزامنة لنموذج LLM.

import asyncio

async def analyze_document(text: str) -> dict:
    parallel = RunnableParallel(
        summary=summary_chain,
        keywords=keyword_chain,
        sentiment=sentiment_chain
    )
    # All three chains run concurrently with asyncio.gather internally
    result = await parallel.ainvoke({'text': text})
    return result

# In FastAPI:
from fastapi import FastAPI
app = FastAPI()

@app.post('/analyze')
async def analyze(request: dict):
    return await analyze_document(request['text'])

تضمين السلاسل المتوازية والمتسلسلة

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

# Full pipeline: classify → parallel retrieval → generate
pipeline = (
    RunnablePassthrough.assign(
        intent=classify_chain  # sequential: classify first
    )
    | RunnablePassthrough.assign(
        context=RunnableParallel(  # parallel: retrieve from both sources
            faq=faq_retriever,
            docs=doc_retriever
        )
    )
    | format_context_chain  # sequential: format merged context
    | generate_answer_chain  # sequential: call LLM
)

معالجة الأخطاء في الفروع

عندما يفشل أحد فروع RunnableParallel، يرفع الاستدعاء المتوازي بأكمله استثناءً افتراضيًا. استخدم .with_fallbacks() مع Runnable لكل فرع على حدة لمعالجة حالات فشل الفروع بسلاسة. أما في RunnableBranch، فلفّ كل فرع داخل try-except في RunnableLambda، أو استخدم سلسلة احتياطية افتراضية لمعالجة أخطاء التوجيه.

from langchain_core.runnables import RunnableParallel

# Wrap each branch with a fallback
safe_summary = summary_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Summary unavailable')
])
safe_sentiment = sentiment_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Sentiment unavailable')
])

robust_parallel = RunnableParallel(
    summary=safe_summary,
    sentiment=safe_sentiment
)

# Now one branch failing won't kill the entire parallel call

تحقق سريع

اختبر مدى فهمك للفروع والسلاسل المتوازية في LCEL.

مراجعة الدرس

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

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

هل درس «السلاسل المتفرعة والمتوازية» مجاني؟

نعم — نص درس «السلاسل المتفرعة والمتوازية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ماذا ستتعلم في «السلاسل المتفرعة والمتوازية»؟

ابنوا تراكيب RunnableParallel وRunnableBranch لتشغيل سلاسل متعددة في الوقت نفسه أو توجيه الإدخال إلى سلاسل مختلفة بناءً على شروط ديناميكية. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟

لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «السلاسل المتفرعة والمتوازية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟

نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. بنية LangChain وتجريداتها الأساسية
  2. بناء السلاسل باستخدام LCEL
  3. السلاسل المتفرعة والمتوازية
  4. بث المخرجات في LangChain
← العودة إلى AI Engineering Academy