انتقل إلى المحتوى الرئيسي

الوحدة 3 — تركيب السلاسل

بعد لَبِنات الوحدة 2، ندخل جوهر LangChain: لغة التعبير عن السلاسل (LangChain Expression Language، أو LCEL). فكرة واحدة تكرِّر نفسها: كلّ مكوّن Runnable، وكلّ تركيب Runnable. فلا تعلّم بنيات جديدة، بل تُركّب.

مشغِّل التركيب

المشغِّل | يُنشئ سلسلة تنفيذ متسلسلة، تشبه أنبوب shell:

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

modele = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tpl = ChatPromptTemplate.from_template("لخِّص السياسة التالية في نقطتين:\n{texte}")
chaine = tpl | modele | StrOutputParser()

resume = chaine.invoke({"texte": "سياسة الاسترداد لعام 2026: ..."})

كلّ خطوة تستقبل مخرَج الخطوة السابقة. القالب يستقبل قاموسًا ويُنتج قائمة رسائل؛ النموذج يستقبل الرسائل ويُنتج AIMessage؛ المحلِّل يستقبله ويُنتج سلسلة. الأنواع تُتحقَّق أثناء البناء لا في وقت التشغيل، وهذا يمنع فئة كاملة من الأخطاء.

أربع طرائق تنفيذ مجّانيّة

كلّ سلسلة تُبنى بـLCEL تحصل تلقائيًّا على أربع طرائق تنفيذ دون كتابة سطر إضافيّ:

  • invoke(x) — استدعاء متزامن، الجواب كاملًا في النهاية.
  • stream(x) — بثّ الرموز كما ينتجها النموذج، مفيد للواجهات الحيّة.
  • batch([x1, x2, ...]) — تنفيذ عدّة استفسارات بالتوازي مع تحكّم بالتزامن.
  • ainvoke، astream، abatch — النسخ غير المتزامنة للاستعمال في asyncio أو FastAPI.

كتابة هذه القدرات يدويًّا على نداء SDK أوّليّ يستغرق أيّامًا. هذه هي الفائدة الحقيقيّة الأولى لـLCEL.

RunnablePassthrough: تمرير قيمة دون تعديل

في مسار RAG، نحتاج أن نُمرّر السؤال إلى المسترجِع وأن نحتفظ به ليصل إلى قالب التعليمة. هنا يظهر RunnablePassthrough:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel

def recuperer(question: str) -> str:
return "المقاطع ذات الصلة بـ" + question # مثال مُبسَّط، الوحدة 5 تُفصّل

chaine_rag = (
RunnableParallel(
contexte=lambda x: recuperer(x["question"]),
question=RunnablePassthrough() | (lambda x: x["question"]),
)
| ChatPromptTemplate.from_template("سياق:\n{contexte}\n\nسؤال: {question}")
| modele
| StrOutputParser()
)

يمرِّر RunnablePassthrough القاموس كما هو، بينما تُنفَّذ الفروع الأخرى بالتوازي. هذه بنية RAG المعياريّة في LCEL.

RunnableParallel: التنفيذ المتوازي

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

نتيجة مباشرة: مسار RAG جيّد لا يكون سلسلة صرفة، بل تفرّعًا موازيًّا في البداية (استرجاع، ذاكرة، سياق ثابت) يُدمج ثمّ يُغذّي النموذج.

التفريع الشرطيّ

يستقبل مساعد النفقات نوعين من الطلبات: سؤال عن السياسة (يستدعي RAG)، أو طلب حساب سقف (يستدعي أداة). التفريع يُنجَز بـRunnableBranch:

from langchain_core.runnables import RunnableBranch

def classer(x): return "politique" if "سياسة" in x["question"] else "calcul"

chaine = RunnableBranch(
(lambda x: classer(x) == "politique", chaine_rag),
chaine_calcul, # الفرع الافتراضيّ
)

القرار السريع يتّخذه مصنّف بسيط (كلمات مفتاحيّة، نموذج صغير للتصنيف، أو حتّى قواعد regex). ثم يُوجَّه الطلب إلى السلسلة المناسبة. في الوحدة 8 سنرى الوكيل الذي يقرأ هذا القرار من مخرج النموذج مباشرة.

البثّ: من مسألة تجربة إلى قيد معماريّ

مسألة تجربة المستخدم: الجواب الكامل يتأخّر خمس ثوانٍ؛ الجواب المتدفّق يبدأ في نصف ثانية. لكنّ البثّ ليس قرارًا بريئًا: تدفّق مخرَج JSON مُبنيّ صعب (لا يمكن تحليل نصف JSON)؛ وتدفّق مخرج بعد أداة تُنفَّذ بعد اكتمال قرار النموذج يتطلّب مخطّط تحكّم مختلف (الوحدة 8).

القاعدة: للنصّ الحرّ استعمل البثّ؛ للمخرَج المُبنيّ والوكلاء يبقى invoke أبسط والانتظار مقبول.

الفخّ: التوازي الخاطئ

خطأ متكرّر: بناء RunnableParallel لعمليّتين إحداهما تعتمد على الأخرى.

# خطأ: recherche_seconde يحتاج فعليًّا نتيجة recherche_premiere
mauvais = RunnableParallel(
a=recherche_premiere,
b=lambda x: recherche_seconde(x["a"]), # x["a"] غير متاح هنا
)

يُنفَّذ b بالتوازي مع a، فيرى قاموس المُدخل الأصليّ لا مخرَج a. الحلّ: تسلسل بـ| بين ما يعتمد، وتوازٍ بين ما هو مستقلّ. قاعدة: يقتصر التوازي على العمليّات المستقلّة تمامًا.

الخيط الأحمر

مساعد النفقات الآن يستقبل سؤالًا، يُمرَّر بالتوازي إلى «استخراج المبلغ» (regex) و«استخراج الفئة» (مصنّف صغير)، تُدمج النتيجتان مع السؤال، ويُوجَّه القرار عبر RunnableBranch إلى فرع «سياسة» أو فرع «حساب». نفس البنية تُوسَّع في الوحدات القادمة.

قاعدة قرار

اسأل: «هل الخطوتان تستطيعان أن تُنفَّذا في نفس اللحظة بلا معلومات إضافيّة؟». إن نعم، RunnableParallel. إن لا، |.

الخلاصة

  • | يُركّب متسلسلًا، وRunnableParallel يُركّب متوازيًّا؛ ولكلّ سلسلة أربع طرائق تنفيذ مجّانيّة.
  • RunnablePassthrough يحفظ المُدخل الأصليّ في مسار متفرّع، وهو عمود بنية RAG المعياريّة.
  • RunnableBranch يفرّع الطلبات حسب مصنّف بسيط قبل أن نحتاج إلى وكيل كامل.
  • التوازي يوفّر وقتًا حين تكون الخطوات مستقلّة؛ وإلا فهو مصدر أخطاء صامتة، وخطوة سيّئة تحوَّل إلى تسلسل.