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

الوحدة 2 — النماذج والتعليمات ومحلّلات المخرجات

تعمل السلاسل بثلاث لَبِنات فقط: قالب تعليمة، ثمّ نموذج محادثة، ثمّ محلِّل مخرجات. تُثبّت هذه الوحدة الثلاث بدقّة، لأنّ 80٪ من الأخطاء في تطبيقات LLM لا تأتي من الوكلاء المعقّدين بل من مخرَج غير قابل للتحليل أو تعليمة غامضة.

واجهة نموذج المحادثة

يعرض BaseChatModel واجهة موحّدة تستقبل قائمة رسائل وتُرجع رسالة. لكلّ مزوّد فئة خاصّة (ChatOpenAI، ChatAnthropic، ChatMistralAI، ChatOllama)، والتبديل بينها يقتصر على السطر الذي يُنشئ الكائن:

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

modele = ChatOpenAI(model="gpt-4o-mini", temperature=0)
reponse = modele.invoke([
SystemMessage(content="أنت خبير في سياسة استرداد النفقات."),
HumanMessage(content="ما سقف عشاء العمل خارج الدولة؟"),
])
print(reponse.content)

يمنح temperature=0 سلوكًا شبه حتميّ مفيدًا للاختبار. أمّا invoke فتُرجع سلسلة، وstream تُبَثّ الرموز، وbatch تُنفّذ عدّة استفسارات بالتوازي.

قالب التعليمة: لماذا لا نُنشئ السلاسل يدويًا

المغرى الأوّل أن نبني التعليمة بـ f-string. لا تفعل ذلك. يوفّر ChatPromptTemplate أربع فوائد لا تُوجَد في تسلسل السلاسل:

  1. رسائل مُميّزة: system/human/ai تعمل بشكل مختلف عند بعض المزوّدين.
  2. حقن المتغيّرات المُتحقَّق منه: متغيّر ناقص يرفع خطأً واضحًا قبل استدعاء النموذج.
  3. قابليّة التشغيل: القالب Runnable، يمكن تركيبه بـ| مع النموذج والمحلِّل.
  4. دعم أمثلة قليلة اللقطات (few-shot) والرسائل المتعدّدة: قسم لاحق يبني عليها.
from langchain_core.prompts import ChatPromptTemplate

tpl = ChatPromptTemplate.from_messages([
("system", "أنت مساعد إدارة نفقات. جواب مختصر."),
("human", "المبلغ: {montant} {devise}\nالفئة: {categorie}\nهل يُستَرَدّ؟"),
])
messages = tpl.format_messages(montant=47, devise="EUR", categorie="عشاء عمل")

يعرض format_messages قائمة الرسائل النهائيّة قبل أن ترسلها إلى النموذج، وهذه أداة تنقيح لا تُقدَّر بثمن.

محلّلات المخرجات: من نصّ حرّ إلى بنية

يُرجع النموذج نصًّا حرًّا افتراضيًّا، لكنّ التطبيق يحتاج قرارًا: هل يُسترَدّ المبلغ؟ ما السقف المتبقّي؟ هذه بيانات مُهيكلة، لا فقرة عربيّة. لدينا مقاربتان.

PydanticOutputParser: المحلِّل الكلاسيكيّ

يُنشئ محلِّل Pydantic تعليمات إضافيّة تصف JSON المطلوب، ثم يقرأ الجواب:

from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

class DecisionRemboursement(BaseModel):
est_admis: bool = Field(description="هل النفقة قابلة للاسترداد؟")
plafond_eur: float = Field(description="السقف بيورو حسب السياسة")
justification: str

parser = PydanticOutputParser(pydantic_object=DecisionRemboursement)
tpl = ChatPromptTemplate.from_messages([
("system", "أنت مساعد. أجب حصريًّا بـJSON.\n{format_instructions}"),
("human", "المبلغ {montant} EUR في {categorie}."),
]).partial(format_instructions=parser.get_format_instructions())

chaine = tpl | modele | parser
decision = chaine.invoke({"montant": 47, "categorie": "عشاء عمل"})

النقطة الحاسمة: يُحقن format_instructions تلقائيًّا شرحًا للنموذج بشكل JSON. ولا يحتاج القارئ منّا لكتابة أيّ مخطّط يدويّ.

المخرجات المُبنيّة أصلًا

النماذج الحديثة (OpenAI، Anthropic، Mistral) تدعم structured outputs أصلًا عبر JSON schema أو استدعاء أدوات مُقنّع. تستعملها LangChain في سطر واحد:

modele_structuré = modele.with_structured_output(DecisionRemboursement)
decision = modele_structuré.invoke("المبلغ 47 EUR في عشاء عمل.")

هذه الطريقة مفضّلة حين يدعمها المزوّد: النموذج لا يستطيع فيزيائيًّا إرجاع JSON غير صالح، ويختفي احتمال خطأ التحليل. أمّا PydanticOutputParser فيبقى ضروريًّا للمزوّدين المفتوحين الذين لا يدعمون هذه الميزة.

معالجة أخطاء التحليل

حتّى مع temperature=0 قد يُرجع نموذج مفتوح المصدر جوابًا غير قابل للتحليل. يُغلَّف المحلِّل بـOutputFixingParser الذي يُعيد إرسال الجواب الفاسد إلى النموذج مع رسالة الخطأ ليُصحّحه:

from langchain.output_parsers import OutputFixingParser
parser_robuste = OutputFixingParser.from_llm(parser=parser, llm=modele)

هذه إعادة نداء إضافيّة، فلا تُستعمَل إلّا حين تُلاحَظ أخطاء فعليّة، وتُسجَّل في التتبّع (الوحدة 9). كثرة إعادة المحاولات مؤشّر على تعليمة غامضة، والحلّ يكون في تحسين الوصف أو استعمال with_structured_output.

الفخّ الشائع

تعليمات JSON محشورة في رسالة human بدل system تُنتج جوابًا مختلطًا: نصّ عربيّ ثمّ JSON. ضع دائمًا القيد البُنيويّ في رسالة system أو في partial.

الخلاصة

  • ChatPromptTemplate يعطينا تركيبًا، وتحقّقًا من المتغيّرات، ورسائل مُميّزة تفوق ما توفّره f-string بكثير.
  • with_structured_output هو الخيار الأوّل لمخرجات مُبنيّة؛ يمنع الأخطاء فيزيائيًّا حين يدعمه المزوّد.
  • PydanticOutputParser بديل قويّ لكلّ نموذج مفتوح لا يدعم البنية الأصليّة.
  • OutputFixingParser مُنقذ آخر لا أوّل، وكثرة تدخّله علامة على تعليمة تحتاج مراجعة، لا مشكلة تُداوى بإعادة المحاولة.

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

الوحدة التالية تركّز على تركيب هذه القوالب مع النماذج والمحلّلات ضمن سلاسل معاد استعمالها عبر LCEL.