Ollama: خط أنابيب توليد معزز بالاسترجاع RAG
تمنح توليد معزز بالاسترجاع RAG الذكاء الاصطناعي خزنة معرفية — بدلاً من تخمين الإجابات، يسترجع الحقيقة من المستندات.
💡 نصيحة: المبدأ الأساسي لـ RAG هو "استرجع أولاً، ثم تولّد" — يجيب النموذج فقط بناءً على أجزاء المستندات المسترجعة، وليس من الذاكرة. هذا يقلل بشكل كبير من "الأوهام" (اختلاق المعلومات)، لكن جودة الاسترجاع تحدد جودة الإجابة مباشرة. استراتيجية التقسيم واختيار نموذج التضمين هما المفتاح.
📋 المتطلبات المسبقة: يجب أن تتقن أولاً ما يلي
- الدرس 13: تكامل LangChain
1. ما ستتعلمه
- بنية RAG: تحميل المستندات → التقسيم → التضمين → التخزين المتجهي → الاسترجاع → التوليد
- استخدام نماذج تضمينات Ollama
- اختيار قاعدة البيانات المتجهية: Chroma / FAISS / Qdrant
- استراتيجيات تقسيم المستندات وضبط المعاملات
- SupportBot V3 الخاص بـ Alice: قاعدة معرفية المنتجات بتوليد معزز بالاسترجاع
2. قصة حقيقية من رائدة أعمال SaaS
⚠️ تحذير: لا يمكن لتوليد معزز بالاسترجاع RAG القضاء على الأوهام — بل يمكنه فقط تقليل احتمالها بشكل كبير. إذا كانت أجزاء المستندات المسترجعة غير ذات صلة أو غير كافية، قد لا يزال النموذج "يختلق" إجابات. الاستراتيجية الرئيسية: تقييد النموذج بالإجابة فقط ضمن نطاق نتائج الاسترجاع، وإضافة إلى موجه النظام: "إذا لم تكن المعلومات ذات الصلة في المستندات، أجب بـ 'لا أستطيع تأكيد ذلك'."
(1) المشكلة: النموذج لا يعرف تفاصيل المنتج
يمكن لـ SupportBot الخاص بـ Alice التعامل مع خدمة العملاء العامة، لكنه لا يعرف سياسات الإرجاع المحددة للمنتج، أو شروط الضمان، أو تفاصيل التسعير. النموذج "يختلق" المعلومات بشكل متكرر، مما يؤدي إلى شكاوى مستمرة من العملاء.
(2) الحل: RAG يمكّن النموذج من الإجابة بناءً على مستندات حقيقية
استيراد أدلة المنتجات والأسئلة الشائعة ووثائق السياسات إلى قاعدة بيانات متجهية، واسترجاع الأجزاء ذات الصلة، والسماح للنموذج بتوليد إجابات بناءً على الحقائق:
PYTHON
# RAG: Retrieve relevant docs, then generate answer
docs = retriever.invoke("What is the warranty for headphones?")
answer = llm.invoke(f"Based on: {docs}\nAnswer: What is the warranty?")
3. بنية RAG بالتفصيل
💡 نصيحة: المبدأ الأساسي لـ RAG هو "استرجع أولاً، ثم تولّد" — يجيب النموذج فقط بناءً على أجزاء المستندات المسترجعة، وليس من الذاكرة. هذا يقلل بشكل كبير من "الأوهام" (اختلاق المعلومات)، لكن جودة الاسترجاع تحدد جودة الإجابة مباشرة. استراتيجية التقسيم واختيار نموذج التضمين هما المفتاح.
(1) تدفق خط الأنابيب من البداية إلى النهاية
flowchart LR
A[المستندات] --> B[المُحمِّل]
B --> C[المُقسِّم]
C --> D[نموذج التضمين<br/>nomic-embed-text]
D --> E[المخزن المتجهي<br/>Chroma/FAISS]
F[استعلام المستخدم] --> G[تضمين الاستعلام]
G --> E
E --> H[أجزاء Top-K]
H --> I[توليد النموذج اللغوي الكبير<br/>qwen2.5]
I --> J[الإجابة]
(2) تفصيل مرحلة بمرحلة
| المرحلة | الأداة | الإدخال | الإخراج |
|---|---|---|---|
| التحميل | PyPDFLoader / TextLoader | PDF/MD/TXT | قائمة مستندات |
| التقسيم | RecursiveCharacterTextSplitter | مستند | قائمة أجزاء |
| التضمين | OllamaEmbeddings | نص الجزء | متجه (768 بُعد) |
| التخزين | Chroma / FAISS | متجه + نص | فهرس |
| الاسترجاع | بحث التشابه | متجه الاستعلام | أجزاء Top-K |
| التوليد | ChatOllama | استعلام + أجزاء | إجابة |
4. تضمينات Ollama
⚠️ ملاحظة: اختيار نموذج التضمين يؤثر مباشرة على جودة الاسترجاع — nomic-embed-text (768 بُعدًا) يعمل بشكل جيد باللغة الإنجليزية لكنه متوسط باللغة الصينية، وmxbai-embed-large (1024 بُعدًا) لديه دقة أعلى لكن حجمه مضاعف. للسيناريوهات الصينية، اختبر نماذج متعددة واختر الأفضل — لا تفترض أن "الأكبر دائمًا أفضل".
(1) مقارنة نماذج التضمين
| النموذج | الأبعاد | الحجم | السرعة | حالة الاستخدام |
|---|---|---|---|---|
| nomic-embed-text | 768 | 274 ميجابايت | سريع | الاستخدام العام الإنجليزي |
| mxbai-embed-large | 1024 | 670 ميجابايت | متوسط | دقة عالية |
| all-minilm | 384 | 46 ميجابايت | سريع جدًا | خفيف الوزن |
(2) دليل اختيار نموذج التضمين
| السيناريو | النموذج المُوصى به | السبب |
|---|---|---|
| RAG العام | nomic-embed-text | أفضل قيمة |
| الاسترجاع عالي الدقة | mxbai-embed-large | أبعاد أعلى، تمييز أفضل |
| الموارد المحدودة | all-minilm | الأصغر والأسرع |
| غالبًا صيني | nomic-embed-text | أداء صيني مقبول |
(1) ▶ مثال: أساسيات تضمينات Ollama
PYTHON
from langchain_ollama import OllamaEmbeddings
# Initialize embeddings model
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Embed a single text
vector = embeddings.embed_query("What is the return policy?")
print(f"Dimension: {len(vector)}") # 768
# Embed multiple texts
vectors = embeddings.embed_documents([
"Free shipping on orders over $50",
"30-day return policy for all products",
"Warranty covers manufacturing defects for 1 year"
])
print(f"Embedded {len(vectors)} documents")
الإخراج:
TEXT
# Execution successful
5. تحميل المستندات وتقسيمها
(1) محمّلات المستندات
| المُحمِّل | التنسيق | التثبيت |
|---|---|---|
| PyPDFLoader | pip install pypdf | |
| TextLoader | TXT | مُضمَّن |
| UnstructuredMarkdownLoader | Markdown | pip install unstructured |
| CSVLoader | CSV | مُضمَّن |
| UnstructuredHTMLLoader | HTML/URL | pip install unstructured |
(2) مقارنة استراتيجيات التقسيم
| الاستراتيجية | المعاملات | المستندات المناسبة | المزايا/العيوب |
|---|---|---|---|
| حجم ثابت | chunk_size=500, overlap=50 | عام | بسيط لكن قد يكسر الدلالات |
| أحرف متكررة | chunk_size=500, overlap=50, فواصل | مستندات منظمة | يحافظ على حدود الفقرات |
| تقسيم دلالي | similarity_threshold | نصوص طويلة | أفضل جودة لكن مكلف حسابيًا |
💡 نصيحة: RecursiveCharacterTextSplitter هو الخيار المفضل. chunk_size=500-1000 وoverlap=50-100 هي القيم المثلى تجريبيًا.
(2) ▶ مثال: تحميل المستندات وتقسيمها
PYTHON
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load text document
loader = TextLoader("product_faq.txt")
docs = loader.load()
print(f"Loaded {len(docs)} document(s)")
# Split into chunks
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
print(f"First chunk preview: {chunks[0].page_content[:100]}...")
الإخراج:
TEXT
# Execution successful
6. اختيار قاعدة البيانات المتجهية
(1) مقارنة ثلاثة مخازن متجهية رئيسية
| البُعد | Chroma | FAISS | Qdrant |
|---|---|---|---|
| النوع | مُضمَّن | في الذاكرة | عميل/خادم |
| الثبات | ✅ ملفات محلية | ❌ الذاكرة فقط | ✅ قرص/سحابة |
| التثبيت | pip install | pip install + faiss-cpu | دوكر / pip |
| الأفضل لـ | التطوير/النطاق الصغير | استرجاع عالي السرعة | الإنتاج |
| التصفية | ✅ تصفية البيانات الوصفية | ❌ | ✅ تصفية متقدمة |
| الموزع | ❌ | ❌ | ✅ |
(3) ▶ مثال: التخزين والاسترجاع المتجهي بـ Chroma
PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Prepare documents
documents = [
"Return policy: 30 days from purchase, items must be in original condition.",
"Shipping: Free for orders over $50. Standard delivery takes 3-5 business days.",
"Warranty: All electronics have a 1-year manufacturer warranty.",
"International shipping: Available to 50+ countries. Import duties may apply.",
"Refund process: Full refund within 5-7 business days after we receive the return."
]
# Split documents
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = splitter.create_documents(documents)
# Create vector store
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# Similarity search
results = vectorstore.similarity_search("How do I get a refund?", k=2)
for doc in results:
print(f"- {doc.page_content}")
الإخراج:
TEXT
# Execution successful
(4) ▶ مثال: التخزين المتجهي بـ FAISS
PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Create FAISS index
texts = [
"Product A: Wireless headphones, $89.99, Bluetooth 5.3",
"Product B: Laptop stand, $34.99, Adjustable height",
"Product C: USB-C cable, $12.99, 2-meter length"
]
vectorstore = FAISS.from_texts(texts, embeddings)
# Save and load
vectorstore.save_local("./faiss_index")
loaded_vs = FAISS.load_local("./faiss_index", embeddings,
allow_dangerous_deserialization=True)
# Search
results = loaded_vs.similarity_search("headphones", k=2)
for doc in results:
print(doc.page_content)
الإخراج:
TEXT
# Execution successful
7. خط أنابيب RAG الكامل و SupportBot V3
(5) ▶ مثال: خط أنابيب RAG الكامل
PYTHON
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Build retriever
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# Build RAG chain
llm = ChatOllama(model="qwen2.5", temperature=0.3)
prompt = ChatPromptTemplate.from_messages([
("system", "You are SupportBot. Answer based ONLY on the following context. "
"If the answer is not in the context, say 'I don't have that information. "
"Let me connect you with a human agent.'\n\nContext:\n{context}"),
("human", "{question}")
])
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Ask questions
answer = rag_chain.invoke("What is the return policy?")
print(answer)
الإخراج:
TEXT
# Function defined successfully
8. مثال شامل: نظام خدمة عملاء SupportBot V3 بتوليد معزز بالاسترجاع
PYTHON
# ============================================
# Comprehensive: SupportBot V3 with RAG
# Product knowledge base powered customer service
# ============================================
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from pathlib import Path
class SupportBotV3:
def __init__(self, kb_path: str = "./knowledge_base",
model: str = "qwen2.5",
embed_model: str = "nomic-embed-text"):
self.llm = ChatOllama(model=model, temperature=0.3)
self.embeddings = OllamaEmbeddings(model=embed_model)
self.kb_path = kb_path
self.vectorstore = None
self.rag_chain = None
def build_knowledge_base(self, doc_dir: str):
"""Load documents and build vector store."""
docs = []
for f in Path(doc_dir).glob("*.txt"):
loader = TextLoader(str(f))
docs.extend(loader.load())
for f in Path(doc_dir).glob("*.md"):
loader = TextLoader(str(f))
docs.extend(loader.load())
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50
)
chunks = splitter.split_documents(docs)
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
persist_directory=self.kb_path
)
print(f"Indexed {len(chunks)} chunks from {len(docs)} documents")
def setup_rag_chain(self):
"""Build the RAG chain."""
if not self.vectorstore:
self.vectorstore = Chroma(
persist_directory=self.kb_path,
embedding_function=self.embeddings
)
retriever = self.vectorstore.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_messages([
("system", (
"You are SupportBot for GlobalShop e-commerce. "
"Answer based ONLY on the provided context. "
"If not in context, say: 'Let me connect you with a human agent.' "
"Be concise (2-3 sentences).\n\nContext:\n{context}"
)),
("human", "{question}")
])
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
self.rag_chain = (
{"context": retriever | format_docs,
"question": RunnablePassthrough()}
| prompt | self.llm | StrOutputParser()
)
def ask(self, question: str) -> str:
"""Ask a question using RAG."""
if not self.rag_chain:
self.setup_rag_chain()
return self.rag_chain.invoke(question)
# Usage
if __name__ == "__main__":
bot = SupportBotV3()
# First time: build knowledge base
# bot.build_knowledge_base("./docs")
# Ask questions
questions = [
"What is the return policy for electronics?",
"How long does international shipping take?",
"Do you offer free returns?",
]
for q in questions:
answer = bot.ask(q)
print(f"Q: {q}")
print(f"A: {answer}\n")
❓ أسئلة شائعة
س هل أختار RAG أم الضبط الدقيق؟
ج RAG مناسب للسيناريوهات التي تتطلب تحديث المعرفة بشكل متكرر (أسئلة شائعة للمنتجات، وثائق السياسات) دون إعادة التدريب. الضبط الدقيق لتغيير أنماط سلوك النموذج. 90% من السيناريوهات المؤسسية تخدمها RAG بشكل أفضل.
س كيف أختار chunk_size؟
ج 500-1000 حرف هي نقطة بداية جيدة. المستندات القصيرة (الأسئلة الشائعة) تستخدم 200-500؛ المستندات الطويلة (الأدلة) تستخدم 500-1000. عيّن التداخل overlap إلى 10-20% من chunk_size.
س ماذا أفعل إذا كانت نتائج الاسترجاع غير دقيقة؟
ج جرّب: 1) التبديل إلى نموذج تضمين بأبعاد أعلى (mxbai-embed-large)؛ 2) زيادة قيمة k (3→5)؛ 3) استخدام بحث MMR لمزيد من التنوع؛ 4) تحسين جودة تقسيم المستندات.
س كيف أختار بين Chroma وFAISS؟
ج استخدم Chroma أثناء التطوير (ثبات تلقائي، دعم التصفية). استخدم FAISS عندما تحتاج سرعة قصوى وكل البيانات تناسب الذاكرة. استخدم Qdrant للإنتاج (موزع، تصفية متقدمة).
س كيف أقدر استهلاك رموز RAG؟
ج كل استعلام يستهلك = رموز الاستعلام + رموز أجزاء top-k + رموز الإجابة المولدة. مع k=3 وchunk_size=500، الإدخال تقريبًا 1500-2000 رمز. الاستنتاج المحلي ليس له تكلفة رموز.
س كيف أقيّم جودة RAG؟
ج استخدم إطار عمل RAGAS لتقييم 4 مقاييس: الإخلاص (هل الإجابة مخلصة للسياق)، صلة الإجابة (هل الإجابة في الموضوع)، دقة السياق (دقة الاسترجاع)، واستدعاء السياق.
📖 ملخص
- خط أنابيب RAG ذو ست خطوات: تحميل → تقسيم → تضمين → تخزين → استرجاع → توليد
- nomic-embed-text هو نموذج التضمين الافتراضي لـ Ollama، 768 بُعدًا، أفضل قيمة
- RecursiveCharacterTextSplitter هو أداة التقسيم المفضلة؛ chunk_size=500 نقطة بداية جيدة
- Chroma للتطوير، FAISS للاسترجاع عالي السرعة، Qdrant للإنتاج
- سلسلة RAG المُكوَّنة بـ LCEL: retriever | format | prompt | llm | parser
- يستخدم SupportBot V3 توليد معزز بالاسترجاع لتأسيس الإجابات بالأدلة، مما يخلص من مشكلة "الاختلاق"
📝 تمارين
- أساسي (⭐): استخدم OllamaEmbeddings لتخزين 5 نصوص أسئلة شائعة للمنتجات في Chroma، وقم بإجراء بحث تشابه واحد.
- متوسط (⭐⭐): ابنِ خط أنابيب RAG كامل — من تحميل ملف نصي، التقسيم، التضمين، التخزين إلى الاسترجاع-التوليد، واختبره بـ 3 أسئلة.
- متقدم (⭐⭐⭐): ابنِ قاعدة معرفية لـ SupportBot V3 — حضّر 10+ مستندات منتجات، ابنِ نظام RAG، وقارن جودة الإجابة مع وبدون RAG لنفس الأسئلة.