الواجهة الموحّدة للنماذج في لانغ تشين: كيف تتحدّث طبقة نموذجك إلى أي مزوّد دون إعادة كتابة
تشرح init_chat_model وصيغة «provider:model» التي تفصل المزوّد عن اسم النموذج، فتنتقل بين OpenAI وAnthropic وGoogle وOllama بتغيير سلسلة نصّية واحدة لا سطراً من المنطق. كما تغطي الرسائل، وخاصية content_blocks المدعومة اليوم في خمس حزم تكامل فقط لا كل المزوّدين، والبثّ عبر stream_events بنسخة v3، مع تمرين عملي يبني طبقة نموذج قابلة للتبديل عندك.

في أول مقال عملي في هذه السلسلة، سنبني طبقة نموذج واحدة في مشروعك ثم نبدّل المزوّد بسطر واحد. الفكرة كلها في صيغة نصّية واحدة: init_chat_model("openai:gpt-5.5") — النقطتان تفصلان المزوّد عن اسم النموذج، وما كتبته فوقهما من منطق يبقى كما هو.
قبل أن نبدأ: إن كنت تقرأ درساً قديماً يشرح langchain.chains أو create_react_agent، فهو يصف عالماً انتهى مع الإصدار v1. كما في المقالين السابقين من هذه السلسلة، سنكتب هنا على واجهة v1 الحالية فقط.
ستة مصطلحات تختصر المقالة:
- المزوّد (provider): شركة أو منصة تستضيف نماذج لغوية وتعرّضها عبر واجهة برمجية — مثل OpenAI وAnthropic وGoogle وOllama.
- الواجهة الموحّدة للنماذج: طبقة واحدة في لانغ تشين (LangChain) تُخفي تفاصيل كل مزوّد وتُبقي الشكل واحداً.
init_chat_model: دالة إنشاء النموذج عبر هذه الواجهة.- الرسالة (message): وحدة السياق: دورها (
systemأوuserأوassistantأوtool) ومحتواها. - كتلة المحتوى (content block): تمثيل قياسي لمحتوى الرسالة داخل الخاصية
content_blocks. - البثّ (streaming): استلام المخرجات أجزاءً أثناء توليدها بدل انتظار الردّ كاملاً.
ما المشكلة التي تحلّها الواجهة الموحّدة للنماذج؟
الجواب: أن كل مزوّد يعرّض واجهته البرمجية الخاصة به، فلولا طبقة توحيد لكنت تكتب منطقاً جديداً لكل مزوّد؛ ولانغ تشين تضع طبقة واحدة يبرمج عليها الجميع. تعريف المزوّد في الوثائق نفسها: «شركة أو منصة تستضيف نماذج ذكاء اصطناعي وتعرّضها عبر واجهة برمجية». وتضيف: «معظم المزوّدين لديهم حزمة langchain-<provider> منفّذة لإحدى واجهات لانغ تشين القياسية — نماذج محادثة، وتضمينات، ومخازن متجهات وغيرها — مما يعطيك واجهة برمجية متّسقة بغضّ النظر عن المزوّد».
وداخل هذه الطبقة تكفي ثلاث عمليات: استدعاء (invoke) يعيد رسالة كاملة، وبثّ (stream) يعيدها قطعاً، ودفعة (batch) يرسل عدة طلبات معاً. والمعاملات نفسها — temperature وmax_tokens وtimeout وmax_retries — تُمرَّر إلى init_chat_model أياً كان المزوّد.
كيف تنسّق نموذجاً مع أي مزوّد؟ صيغة provider:model
الجواب: مرّر المزوّد واسم النموذج في سلسلة واحدة يفصل بينهما النقطتان، أو مرّرهما في وسيطين منفصلين. تعريف الوثائق لمعامل model هو الأدق: «يمكنك أيضاً تحديد النموذج ومزوّده في وسيطة واحدة باستخدام صيغة '{model_provider}:{model}'، مثل 'openai:o1'».
ما قبل النقطتين هو اسم المزوّد كما تستعمله حزمة التكامل، وما بعدهما هو اسم النموذج كما يسمّيه المزوّد بنفسه:
model = init_chat_model("openai:gpt-5.5")
model = init_chat_model("azure_openai:gpt-5.5")
model = init_chat_model("google_genai:gemini-3.7-flash")
كل مزوّد حزمة تُثبَّت وحدها. المزوّد هنا ليس خدمة سحابية بل حزمة بايثون، لذلك يتغيّر التثبيت بتغيّر المزوّد ويبقى الكود ثابتاً:
pip install -U "langchain[anthropic]"
pip install -U "langchain[openai]"
pip install -U "langchain[google-genai]"
ماذا يحدث لو حذفت المزوّد من السلسلة؟
الجواب: يعمل إذا كان اسم النموذج وحده يحدّد مزوّداً واحداً، لكن الصيغة الكاملة أوضح وأأمن في كود الإنتاج. الوثائق نفسها تستخدم الشكل المختصر في مثال Anthropic: init_chat_model("claude-sonnet-4-6") بلا نقطتين.
لكن حين لا يكفي الاسم وحده، تُمرَّر التسمية صراحةً في model_provider=. والنمط في الأمثلة واضح: أسماء المستودعات، ومعرّفات النشرات السحابية، والقيم العامة مثل auto:
model = init_chat_model(
"microsoft/Phi-3-mini-4k-instruct",
model_provider="huggingface",
temperature=0.7,
max_tokens=1024,
)
model = init_chat_model(
"us.anthropic.claude-sonnet-4-6",
model_provider="bedrock_converse",
)
model = init_chat_model(
"auto",
model_provider="openrouter",
)
وتنبيه صدق: الوثائق لا تشرح قاعدة الاستنتاج ولا شكل رسالة الخطأ عند غموض الاسم، فلا تبني منطقك على الاستنتاج. اجعل المزوّد صريحاً في كود الإنتاج، أو احفظ السلسلة كاملة في متغيّر بيئة — وهذا بالضبط ما سنفعله في التمرين.
هل يبقى كودك كما هو بعد التبديل؟
الجواب: نعم — الدالة نفسها والأدوات نفسها، وسطر model= وحده هو ما يتغيّر. في صفحة الوكلاء، حزمة create_agent مكرّرة حرفياً لكل مزوّد، والفرق كله في السلسلة:
from langchain.agents import create_agent
agent = create_agent(model="openai:gpt-5.5", tools=tools)
from langchain.agents import create_agent
agent = create_agent(model="ollama:north-mini-code-1.0", tools=tools)
وحقل model= يقبل شكلين: «سلسلة معرّف نموذج ("provider:model") أو نسخة نموذج مُهيّأة». أي يمكنك أيضاً بناء النموذج بنفسك وتمرير الكائن، وهو مفيد إن أردت ضبط معاملات إضافية:
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"It's always sunny in {city}!"
weather_agent = create_agent(
model=init_chat_model("claude-sonnet-4-6"),
tools=[get_weather],
name="weather_agent",
)
وملاحظة قصيرة مهمة: أسماء النماذج الجديدة تعمل فوراً بعد نشر المزوّد لها، «دون تحديث لانغ تشين»، لأن حزم التكامل تمرّر اسم النموذج مباشرةً إلى واجهة المزوّد. أي أن تأخّر الإصدار ليس مانعاً من تبديل النموذج.
ما الرسائل (messages) ولماذا هي الحقل الأساسي؟
الجواب: الرسائل هي وحدة السياق في لانغ تشين — مدخل النموذج ومخرجه معاً — ولهذا تُبنى عليها كل الطبقات الأعلى. وصفها الوثائق حرفياً: «الرسائل هي وحدة السياق الأساسية للنماذج في لانغ تشين؛ تمثّل مدخلات النماذج ومخرجاتها، حاملةً المحتوى والبيانات الوصفية اللازمة لتمثيل حالة المحادثة».
أنواعها الأربعة في كود واحد، وهذا مثال من صفحة النماذج:
from langchain.messages import HumanMessage, AIMessage, SystemMessage
conversation = [
SystemMessage("You are a helpful assistant that translates English to French."),
HumanMessage("Translate: I love programming."),
AIMessage("J'adore la programmation."),
HumanMessage("Translate: I love building applications.")
]
response = model.invoke(conversation)
SystemMessage هو الموجّه النظامي، وHumanMessage رسالة المستخدم، وAIMessage ردّ النموذج، وToolMessage نتيجة أداة يعود بها النموذج في دورة تالية (ولها نصيب من مقال الأدوات لاحقاً). ويمكن كتابة الأدوار كقاموس بدل الأصناف — نفس النتيجة تماماً:
conversation = [
{"role": "system", "content": "You are a helpful assistant that translates English to French."},
{"role": "user", "content": "Translate: I love programming."},
]
لماذا لا تُستبدل الرسائل القديمة أبداً؟
الجواب: لأن messages داخل AgentState حقل «إضافة فقط» — تُضاف إليه الرسائل الجديدة ولا يُستبدل شيء: «سجلّ المحادثة الكامل للمحادثة الحالية. إضافة فقط: الرسائل الجديدة تُضاف ولا تُستبدل أبداً».
وثلاثة آثار عملية تهمّك الآن:
- خطّافات الوسيطة تستقبل الحالة كاملةً وتُرجع تحديثات تُدمج فيها، فحذف رسالة قديمية يعني فقدان سياق قرار سابق.
- الذاكرة في مقالنا القادم ستبني على هذا السلوك نفسه:
thread_idيشير إلى محادثة واحدة، وسجلّها يتراكم عبر الجولات. - ضبط نافذة السياق ليس مهمة تُنجزها يدوياً بحذف رسائل؛ التلخيص (وظيفة جاهزة في لانغ تشين) يتولّى ذلك.
وفي لانغ غراف (LangGraph) سيظهر الحقل نفسه تحت اسم MessagesState مع دالة الدمج add_messages — وهذا جسر مقال الذاكرة إلى ما سنبنيه لاحقاً.
ما الجديد في v1: content_blocks — وهل يعمل مع كل المزوّدين؟
الجواب: content_blocks توحّد محتوى الرسالة — نصاً، أو استدعاء أداة، أو أثر استدلال — في تمثيل واحد، لكنها مدعومة اليوم في خمس حزم تكامل فقط، لا عند كل مزوّد.
الخاصية تحلّل حقل content كسولاً عند أول قراءة إلى تمثيل قياسي موحّد: فتحصل على كتلة reasoning سواء أتى أثر الاستدلال من Anthropic بصيغة thinking أو من OpenAI بصيغة reasoning — وهذا هو معنى «provider agnostic» في الوثائق.
والقائمة الفعلية للتكاملات المدعومة، كما تنصّ الوثائق حرفياً:
langchain-anthropiclangchain-awslangchain-openailangchain-google-genailangchain-ollama
وتضيف الوثائق أن «الدعم الأوسع لكتل المحتوى سيُطرح تدريجياً عبر مزوّدين أكثر». هذه نقطة صدق مهمة: لا تفترض أن أي حزمة تكامل تقرأها content_blocks، وتحقّق من القائمة قبل أن تبني عليها.
حلقة التكرار من مثال صفحة الإصدار، حرفياً كما ورد:
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-sonnet-4-6")
response = model.invoke("What's the capital of France?")
# Unified access to content blocks
for block in response.content_blocks:
if block["type"] == "reasoning":
print(f"Model reasoning: {block['reasoning']}")
elif block["type"] == "text":
print(f"Response: {block['text']}")
elif block["type"] == "tool_call":
print(f"Tool call: {block['name']}({block['args']})")
ولأن init_chat_model يُعيد نفس الواجهة، فالحلقة نفسها تعمل على أي مزوّد من الخمسة بلا تغيير في الكود.
كيف تبثّ مخرجات النموذج والوكيل؟
الجواب: للوكيل استخدم stream_events(..., version="v3") بلا تردّد؛ وللنموذج وحده يكفي .stream() مع جمع القطع.
توصية الوثائق واضحة: «في معظم حالات التطبيق والواجهة الأمامية، استخدم بثّ الأحداث عبر stream_events(..., version="v3"). يبثّ الأحداث كائن تشغيل بإسقاطات مُنمَّطة، فيمكن استهلاك كل إسقاط على حدة بدل تحليل أزواج أنماط البثّ».
from langchain.agents import create_agent
def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="gpt-5-nano",
tools=[get_weather],
)
stream = agent.stream_events({
"messages": [{"role": "user", "content": "What is the weather in SF?"}],
}, version="v3")
for message in stream.messages:
for delta in message.text:
print(delta, end="", flush=True)
final_state = stream.output
الإسقاطات التي ستحتاجها فعلياً:
stream.messages— رسائل النموذج، واحدة لكل نداء نموذج، وmessage.textفيها هو أجزاء النص ثم النص النهائي.stream.tool_calls— دورة حياة تنفيذ الأدوات: المدخلات، أجزاء المخرجات، المخرج النهائي، والأخطاء.stream.values— لقطات من حالة الوكيل أثناء التشغيل.stream.output— الحالة النهائية بعد انتهاء التشغيل.
هناك واجهات بثّ أقدم ما زالت تعمل إلى جانبها: .stream() للنموذج وحده، وstream_mode=، والأشكال التي تبدأ بحرف a للنسخ غير المتزامنة. لن نشرحها هنا — في هذه السلسلة نلتزم واجهة واحدة (stream_events(..., version="v3")) حتى تتعيّن عادة واحدة.
وعلى مستوى النموذج وحده، فالقطع تُجمع بالجمع لتبني الرسالة كاملة:
full = None # None | AIMessageChunk
for chunk in model.stream("What color is the sky?"):
full = chunk if full is None else full + chunk
print(full.text)
وتحذير من الوثائق يستحق القراءة قبل أي مشروع إنتاجي: البثّ يعمل فقط إذا كانت كل خطوات البرنامج قادرة على معالجة تدفّق القطع. تطبيق يخزّن الردّ كاملاً في الذاكرة قبل عرضه لا يستطيع البثّ أصلاً.
تمرين عملي: ابنِ طبقة نموذج قابلة للتبديل
الجواب: ملف واحد فيه دالة build_llm تقرأ معرّف النموذج من متغيّر بيئة، ثم تشغّله ثلاث مرات بثلاثة مزوّدين — بصفر أسطر معدّلة.
احفظ الآتي في llm_layer.py:
import os
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, SystemMessage
DEFAULT_MODEL = "anthropic:claude-sonnet-4-6"
def build_llm(model_id: str | None = None):
"""Return a chat model for any provider."""
model_id = model_id or os.environ.get("MODEL", DEFAULT_MODEL)
return init_chat_model(model_id, temperature=0.7, max_tokens=1000)
def answer(llm, question: str) -> str:
response = llm.invoke([
SystemMessage("You are a helpful assistant."),
HumanMessage(question),
])
return response.text
if __name__ == "__main__":
llm = build_llm()
print(answer(llm, "اشرح الفرق بين stream و invoke في سطرين."))
ثم شغّل الملف نفسه ثلاث مرات:
MODEL="anthropic:claude-sonnet-4-6" python llm_layer.py
MODEL="openai:gpt-5.5" python llm_layer.py
MODEL="google_genai:gemini-3.7-flash" python llm_layer.py
ماذا ستفعل بعد هذا بالضبط:
- قارن المخرجات على نفس السؤال بين المزوّدين، وسجّل الفروق في نبرة الإجابة والطول — لا في الشكل، فالشكل واحد.
- غيّر
temperatureوحده فيbuild_llmوأعد التشغيل على المزوّد نفسه، ولاحظ أثر العشوائية على الصياغة. - ارفع الصمود على الشبكة إن كنت تعمل خلف شبكة متقطّعة:
max_retries=6هو الافتراضي (إعادة محاولة بتباعد أُسّي مع اهتزاز)، والوثائق تنصح بزيادته على الشبكات غير الموثوقة. - مرّر الوكلاء لاحقاً بلا تعديل:
create_agent(model=build_llm())— نفس الدالة التي تخدم سطر الأوامر تخدم الوكيل.
ما الذي لا تغطيه هذه الطبقة — بصراحة
- لا توحّد القدرات، توحّد الشكل.
content_blocksعند خمسة تكاملات فقط، والمعاملات الخاصة بكل مزوّد تبقى موجودة. - البديل الاحتياطي (fallback) على مستوى الوكيل يحتاج طبقة إضافية؛ هذه المقالة تعطيك طبقة نموذج قابلة للتبديل يدوياً، لا تحويلاً تلقائياً عند فشل المزوّد.
- رسائل الوكيل تُبنى على
messagesلا على نصّ واحد. هنا استدعينا النموذج مباشرة؛ في الوكيل تُمرَّر الرسائل عبر الحالة التي شرحناها.
في المقال التالي ننتقل إلى ما فوق النموذج: الأدوات واستدعاؤها — كيف يقرّر النموذج متى ينفّذ دالة، ولماذا صار الـ docstring جزءاً من العقد لا زينة.
تحتاج وكيلاً يعمل لفريقك؟
نصمّم ونبني أنظمة وكلاء للشركات والأفراد فوق لانغ تشين (LangChain) ولانغ غراف (LangGraph) — من إثبات المفهوم في أسبوع إلى نظام إنتاجي يُشغَّل يومياً. ابدأ بطلبك من صفحة التواصل.
المصادر
كل ادعاء في المقال مرتبط بمصدره. الروابط تفتح في نافذة جديدة.
- 01LangChain v1 release notes ↗
docs.langchain.com
content_blocks تتيح الوصول إلى آثار الاستدلال والاستشهادات والأدوات المدمجة (بحث الويب، مفسّرات الشيفرة وغيرها) عبر الواجهة نفسها مهما كان المزوّد.
- 02Models — LangChain Python docs ↗
docs.langchain.com
أسماء النماذج الجديدة تعمل فوراً دون تحديث لانغ تشين، لأن حزم التكامل تمرّر اسم النموذج مباشرةً إلى واجهة المزوّد.
- 03LangChain Python integrations — providers overview ↗
docs.langchain.com
المزوّد هو شركة أو منصة تستضيف نماذج ذكاء اصطناعي وتعرّضها عبر واجهة برمجية، ومعظم المزوّدين لديهم حزمة langchain-<provider> منفّذة لإحدى واجهات لانغ تشين القياسية.
- 04Event streaming — LangChain Python docs ↗
docs.langchain.com
الوثائق توصي باستخدام بثّ الأحداث عبر stream_events(..., version="v3") في معظم حالات التطبيق والواجهة الأمامية.
- 05Agents — LangChain ↗
docs.langchain.com
حقل messages داخل AgentState هو سجلّ المحادثة الكامل للمحادثة الحالية وهو حقل يُضاف إليه فقط: الرسائل الجديدة تُضاف ولا تُستبدل أبداً.
- 06Agents — LangChain ↗
docs.langchain.com
حقل model في create_agent يقبل سلسلة معرّف نموذج بصيغة "provider:model" أو نسخة نموذج مُهيّأة.
- 07LangChain v1 release notes ↗
docs.langchain.com
خاصية content_blocks مدعومة اليوم في خمس تكاملات فقط: langchain-anthropic وlangchain-aws وlangchain-openai وlangchain-google-genai وlangchain-ollama، والدعم الأوسع سيُطرح تدريجياً.
- 08Models — LangChain Python docs ↗
docs.langchain.com
كل تكامل نموذج قد يضيف معاملات خاصة بوظيفية خاصة بالمزوّد.
- 09Models — LangChain Python docs ↗
docs.langchain.com
كل حزمة مزوّد تنفّذ الواجهة القياسية نفسها، فالتبديل بين المزوّدين لا يستلزم إعادة كتابة لمنطق التطبيق.
- 10Models — LangChain Python docs ↗
docs.langchain.com
يمكن تمرير المزوّد واسم النموذج في وسيطة واحدة بصيغة '{model_provider}:{model}'، مثل 'openai:o1'.



