مقدمة LCEL

LCEL (لغة سلسلة التعبيرات) هي أداة تنسيق سلسلة الأوامر القوية التي تمكّنك من بناء سلاسل مهام معقدة من المكونات الأساسية، كما تدعم ميزات جاهزة للاستخدام مثل معالجة البث، والمعالجة المتوازية، وتسجيل الأحداث.

مثال بسيط: تعليمة + نموذج + محلل إخراج

في هذا المثال، سنقدم كيفية استخدام LCEL (لغة سلسلة التعبيرات) لربط ثلاث مكونات - قالب السؤال، والنموذج، ومحلل الإخراج - معًا لتشكيل سلاسل عمل كاملة لتنفيذ مهمة "سرد النكات". يبرز الكود كيفية إنشاء سلاسل واستخدام رمز الخط العمودي | لربط مكونات مختلفة، كما يقدم دور كل مكون بجانب نتائج الإخراج.

أولاً، دعونا نرى كيفية ربط قالب السؤال والنموذج لإنشاء نكتة حول موضوع محدد:

تثبيت التبعيات

%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template("أخبرني نكتة حول {topic}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

chain.invoke({"topic": "الآيس كريم"})

الناتج

"لماذا لا تدعو الحفلات الآيس كريم؟ لأنه يذوب عندما يكون حارًا!"

في هذا الكود، نستخدم LCEL لربط مكونات مختلفة في سلسلة:

chain = prompt | model | output_parser

رمز | هنا مشابه لـ مشغل أنابيب UNIX، الذي يربط مكونات مختلفة معًا ويمرر إخراج مكون واحد كمدخل للمكون التالي.

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

1. قالب السؤال

prompt هو BasePromptTemplate الذي يقبل قاموس المتغيرات النموذجية ويولد PromptValue. PromptValue هو كائن مغلف يحتوي على السؤال، والذي يمكن تمريره إلى LLM (كإدخال بشكل سلسلة) أو ChatModel (كإدخال بشكل تسلسل رسائل). يمكن استخدامه مع أي نوع من النماذج اللغوية لأنه يحدد المنطق لتوليد BaseMessage وتوليد السلاسل.

prompt_value = prompt.invoke({"topic": "الآيس كريم"})
prompt_value

الناتج

ChatPromptValue(messages=[HumanMessage(content='أخبرني نكتة حول الآيس كريم')])

أدناه، نحول النتيجة المنسقة للسؤال إلى صيغة الرسالة المستخدمة من قبل نماذج الدردشة:

prompt_value.to_messages()

الناتج

[HumanMessage(content='أخبرني نكتة حول الآيس كريم')]

يمكن أيضًا تحويلها مباشرة إلى سلسلة:

prompt_value.to_string()

الناتج

'إنسان: أخبرني نكتة حول الآيس كريم.'

2. النموذج

بعد ذلك، قم بتمرير PromptValue إلى الـ model. في هذا المثال، نموذجنا هو ChatModel، مما يعني أنه سيولّد BaseMessage.

حاول استدعاء الـ model مباشرةً:

message = model.invoke(prompt_value)
message

الناتج:

AIMessage(content="لماذا لا يتم دعوة الآيس كريم إلى الحفلات؟\n\nلأنها دائماً تنقطع عندما تصبح الأمور ساخنة!")

إذا تم تعريف نموذجنا كنوع LLM، فإنه سيخرج سلسلة نصية.

from langchain_openai.llms import OpenAI

llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)

النموذج يعيد:

'\n\nروبوت: لماذا توقفت شاحنة الآيس كريم عن العمل؟ لأنها خضعت لانصهار!'

3. محلل الإخراج

أخيرًا، قم بتمرير الإخراج من الـ model إلى output_parser، والذي هو BaseOutputParser، وهذا يعني أنه يقبل سلسلة أو BaseMessage كإدخال. StrOutputParser يحول بشكل خاص أي إدخال إلى سلسلة بسيطة.

output_parser.invoke(message)
لماذا لا يتم دعوة الآيس كريم إلى الحفلات؟\n\nلأنها دائماً تنقطع عندما تصبح الأمور ساخنة!

4. العملية بأكملها

عملية التنفيذ كالتالي:

  1. استدعاء chain.invoke({"topic": "ice cream"})، وهو ما يعادل بدء سير العمل الذي قمنا بتعريفه وتمرير المعلمة {"topic": "ice cream"} لتوليد نكتة عن "الآيس كريم".
  2. تمرير معلمة الاستدعاء {"topic": "ice cream"} إلى المكون الأول في السلسلة، prompt، الذي يقوم بتنسيق قالب النص المطلوب للحصول على الموجه قل لي نكتة صغيرة عن الآيس كريم.
  3. تمرير الموجه قل لي نكتة صغيرة عن الآيس كريم إلى model (نموذج gpt4).
  4. تمرير النتيجة المُرجعة من model إلى قارئ الإخراج output_parser، الذي يقوم بتنسيق نتيجة النموذج وإرجاع المحتوى النهائي.

إذا كنت مهتمًا بنتائج أي مكون، يمكنك اختبار نسخة أصغر من السلسلة في أي وقت، مثل prompt أو prompt | model، لرؤية النتائج الوسيطة:

input = {"topic": "ice cream"}

prompt.invoke(input)

(prompt | model).invoke(input)

مثال بحث RAG

الآن، دعنا نشرح مثالًا أكثر تعقيدًا لمثال LCEL. سنقوم بتشغيل مثال لاسترجاع محسّن لتوليد سلاسل، من أجل إضافة بعض المعلومات الأساسية عند الإجابة على الأسئلة.

from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

vectorstore = DocArrayInMemorySearch.from_texts(
    ["harrison worked at kensho", "bears like to eat honey"],
    embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()

template = """أجب على السؤال بناءً فقط على السياق التالي:
{context}

السؤال: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

chain.invoke("أين عمل هاريسون؟")

في هذه الحالة، السلسلة المتكونة هي:

chain = setup_and_retrieval | prompt | model | output_parser

ببساطة، القالب المذكور أعلاه يقبل context و question كقيم يتم استبدالها في الموجه. قبل بناء القالب الموجه، نريد استرداد مستندات ذات صلة لاستخدامها كجزء من السياق.

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

retriever.invoke("أين عمل هاريسون؟")

ثم، نستخدم RunnableParallel لإعداد المدخل للموجه، البحث عن المستندات باستخدام الاسترجاع، وتمرير سؤال المستخدم باستخدام RunnablePassthrough:

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)

لختصار، السلسلة الكاملة هي:

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

تكون العملية كالتالي:

  1. أولاً، إنشاء كائن RunnableParallel يحتوي على مدخلين. المدخل الأول context سيحتوي على نتائج الوثائق التي تم استخراجها بواسطة الاسترجاع. المدخل الثاني question سيحتوي على سؤال المستخدم الأصلي. لتمرير السؤال، نستخدم RunnablePassthrough لنسخ هذا المدخل.
  2. تمرير القاموس من الخطوة السابقة إلى مكون الـ prompt. يقبل الإدخال من المستخدم (أي، question) بالإضافة إلى المستندات المُسترجعة (أي، context)، ويُنشئ موجهًا ويُخرج PromptValue.
  3. المكون model يأخذ الموجه الذي تم إنشاؤه ويمرره إلى نموذج LLM التابع لشركة OpenAI للتقييم. الإخراج المُنتج بواسطة النموذج هو كائن ChatMessage.
  4. أخيرًا، المكون output_parser يأخذ ChatMessage، يحوله إلى سلسلة Python، ويُرجعه من طريقة invoke.