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

الوحدة 5 — المخازن المتّجهية والمسترجِعات

نبني في هذه الوحدة الطرف الأيسر لمساعد النفقات: كيف يجد النصّ ذا الصلة قبل التوليد؟ بعد أن حمّلنا المستندات وقطّعناها في الوحدة 4، نحوّل المقاطع إلى متّجهات، ونُخزّنها في قاعدة متّجهيّة، ونبني مسترجِعًا يعطينا المقاطع الخمسة الأكثر ملاءمة لكلّ سؤال.

من نصّ إلى متّجه

نموذج التضمين (embedding model) يُحوّل نصًّا إلى شعاع أعداد فاصلة في فضاء بأبعاد عالية (768، 1024، 3072). المسافة بين شعاعين تعكس القرب الدلاليّ لا المعجميّ: «سقف عشاء العمل» و«الحدّ الأقصى للمأدبة المهنيّة» يُنتجان شعاعين قريبين رغم اختلاف الكلمات.

from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

قاعدة صارمة: يُستَعمَل نفس نموذج التضمين للفهرسة والاستفسار. تغييره يعني إعادة فهرسة الكلّ، لأنّ الشعاعين لا يعيشان في الفضاء نفسه. النسيان هنا يُنتج مسترجِعًا يُرجع مقاطع عشوائيّة تمامًا.

VectorStore: الواجهة الموحّدة

يعرض VectorStore أربع عمليّات أساسيّة: add_documents، similarity_search، similarity_search_with_score، as_retriever. اختيار المخزن مسألة تشغيليّة لا وظيفيّة:

  • Chroma: سريع للتطوير، يحفظ على القرص، مثاليّ حتّى مليون مقطع تقريبًا.
  • FAISS: مكتبة Meta، أسرع في الذاكرة، لا يحفظ إلّا يدويًّا، يُعتمد للأداء البحت.
  • Qdrant، Weaviate، Pinecone، pgvector: مخازن إنتاج ذات مرشّحات مُهيكلة وتوسّع أفقيّ.
from langchain_chroma import Chroma

magasin = Chroma.from_documents(
documents=morceaux,
embedding=embeddings,
collection_name="politique_notes_frais",
persist_directory="./index_chroma",
)

يحفظ Chroma الفهرس على القرص، فلا نُعيد الحساب في كلّ تشغيل. هذه نقطة سعيدة كثيرًا مع سياسة تتغيّر مرّة في السنة.

المسترجِع الأساسيّ: عتبة وk

as_retriever(search_kwargs={"k": 5}) يُعيد أوّل خمسة مقاطع مرتّبة بالتشابه. لكن ما اختيار k؟ الجواب: يعتمد على تعليمة النموذج. تعليمة صريحة «أجب باستعمال المصدر الأكثر ملاءمة» تحتمل k=3؛ تعليمة «قارن بين المصادر المختلفة» تحتاج k=8 أو أكثر. البدء بـk=5 قاعدة سليمة.

الخيار الثاني هو العتبة: نُقصي أيّ مقطع تشابهه أقلّ من قيمة معيّنة. يمنع هذا مقاطع لا صلة لها من الوصول للنموذج حين لا توجد إجابة فعليّة في الفهرس.

recuperateur = magasin.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.5, "k": 5},
)

الفخّ: عتبة مرتفعة جدًّا تُفرِغ الاسترجاع تمامًا في نصف الأسئلة؛ عتبة منخفضة تُدخل ضجيجًا يخدم توهّم النموذج. تُضبَط بجدول تقييم صغير (الوحدة 9).

مسترجِع الاستفسارات المتعدّدة

مشكلة معروفة: صياغة السؤال تُغيّر مقاطع الاسترجاع. يسأل مستخدم عن «سقف الغداء»، وسياسة الاسترداد تقول «الحدّ الأقصى للوجبة». المتّجهان قريبان، لكن ليسا كافيَي القرب دائمًا.

الحلّ: نطلب من نموذج لغويّ أن يُنتج ثلاث صياغات للسؤال، ونستَرجع بكلّ منها، ونُوحّد النتائج:

from langchain.retrievers.multi_query import MultiQueryRetriever

multi = MultiQueryRetriever.from_llm(
retriever=magasin.as_retriever(search_kwargs={"k": 3}),
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
)

الكلفة: ثلاثة نداءات نموذج إضافيّة لكلّ سؤال. الفائدة: تغطية أوسع للأسئلة التي لا تستعمل معجم السياسة. يُعتمَد للأسئلة المفتوحة، ويُتَجنَّب لأسئلة بحث دقيقة.

البحث الهجين

البحث المتّجهيّ ممتاز في المعنى، سيّئ في الحرفيّات. سؤال يحتوي «قسم 3.2» أو «رقم فاتورة FR-2026-001» يحتاج مطابقة كلمة تقليديّة (BM25 مثلًا). EnsembleRetriever يمزج مسترجِعَين متّجهيًّا ومعجميًّا:

from langchain.retrievers import EnsembleRetriever, BM25Retriever

bm25 = BM25Retriever.from_documents(morceaux); bm25.k = 5
hybride = EnsembleRetriever(retrievers=[bm25, magasin.as_retriever()], weights=[0.4, 0.6])

الأوزان تُضبَط تجريبيًّا، لكن نقطة انطلاق [0.4, 0.6] نصيحة معياريّة.

سلسلة RAG المصغّرة

كلّ ما سبق يُدمج في مسار كامل باستعمال LCEL:

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

def formater(docs): return "\n\n".join(d.page_content for d in docs)

tpl = ChatPromptTemplate.from_messages([
("system", "أجب باستعمال المصادر أدناه فقط. إن لم تجد الجواب، قل ذلك."),
("human", "المصادر:\n{contexte}\n\nالسؤال: {question}"),
])

rag = (
{"contexte": recuperateur | formater, "question": RunnablePassthrough()}
| tpl | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser()
)
print(rag.invoke("ما سقف عشاء العمل؟"))

النقطة الحاسمة في التعليمة: «إن لم تجد الجواب، قل ذلك». بدونها يخترع النموذج جوابًا حين تكون المقاطع غير كافية، والوحدة 9 ستكشف هذا في التقييم.

الخلاصة

  • يُحوّل نموذج التضمين النصّ إلى شعاع؛ يجب استعمال نفس النموذج للفهرسة والاستفسار.
  • VectorStore واجهة موحّدة؛ Chroma للتطوير، FAISS للأداء، Qdrant/pgvector للإنتاج.
  • عتبة الاسترجاع تمنع مقاطع بلا صلة من تسمّم الجواب، وتُضبَط بجدول تقييم.
  • MultiQueryRetriever يُغطّي تنوّع الصياغات، وEnsembleRetriever يمزج البحث المتّجهيّ والمعجميّ للمعرّفات الحرفيّة.