الدرس 7 من 9
RAG: جاوب من مستنداتك
أكتر تطبيق مطلوب في سوق الشغل دلوقتي.
المشكلة
Claude مش عارف سياسة الاسترجاع في متجرك، ولا لايحة شركتك، ولا الكتالوج بتاعك. لو سألته هيقول إنه مش عارف، أو أسوأ: يألّف. ومستحيل تحط كل مستنداتك في كل prompt، هتبقى غالية جداً وممكن أكبر من الـ context window.
الحل: امتحان «كتاب مفتوح»
بدل ما الموديل يجاوب من ذاكرته، ندورله على الفقرات المناسبة من مستنداتنا، ونديهاله مع السؤال، ونقوله جاوب من دول بس. زي طالب في امتحان open book: مش لازم يحفظ الكتاب، لازم يعرف يدور فيه.
- قطّع المستندات لفقرات صغيرة (chunks).
- اعمل embedding لكل فقرة وخزّنها (مرة واحدة).
- لما ييجي سؤال، اعمله embedding، وهات أقرب فقرات ليه بالـ cosine similarity.
- ابعت السؤال والفقرات للـ LLM، وقوله جاوب منها بس.
خطوة ١ و ٢ و ٣: البحث
from sentence_transformers import SentenceTransformer from sentence_transformers.util import cos_sim embedder = SentenceTransformer("BAAI/bge-m3") # the stronger model from lesson 2 # Our store's knowledge base (in real life: chunks from PDFs, pages, docs) chunks = [ "الاسترجاع: تقدر ترجع أي منتج خلال 14 يوم من الاستلام بشرط يكون بحالته الأصلية.", "المنتجات اللي بتوصل مكسورة أو فيها عيب بنبدلها فوراً ومصاريف الشحن علينا، ابعت صورة على واتساب خلال 48 ساعة.", "الشحن للقاهرة والجيزة بياخد من يوم ليومين، وباقي المحافظات من 3 لـ 5 أيام.", "الشحن مجاني للطلبات فوق 1000 جنيه، وأقل من كده 50 جنيه.", "طرق الدفع: فيزا، كاش عند الاستلام، فودافون كاش، وإنستاباي.", "ساعات خدمة العملاء: من 9 الصبح لـ 11 بالليل كل الأيام ما عدا الجمعة.", ] chunk_vectors = embedder.encode(chunks) # step 2: done once, then stored def retrieve(question, k=2): scores = cos_sim(embedder.encode(question), chunk_vectors)[0] top = scores.argsort(descending=True)[:k] # step 3: k most similar chunks return [(chunks[i], scores[i].item()) for i in top] for chunk, score in retrieve("المنتج وصلني مكسور أعمل إيه؟"): print(f"{score:.2f} {chunk}")
خطوة ٤: التوليد
from dotenv import load_dotenv import anthropic from rag_retrieve import retrieve # the function from the previous file load_dotenv() client = anthropic.Anthropic() def answer(question): context = "\n".join(f"<doc>{c}</doc>" for c, _ in retrieve(question, k=3)) reply = client.messages.create( model="claude-sonnet-5", max_tokens=400, system=( "You are a customer service assistant for an Egyptian online store. " "Answer ONLY using the documents provided. If the answer is not in them, " "say you don't know and suggest contacting customer service. " "Reply in friendly Egyptian Arabic." ), messages=[{"role": "user", "content": f"<documents>\n{context}\n</documents>\n\nQuestion: {question}"}], ) return reply.content[0].text print(answer("المنتج وصلني مكسور أعمل إيه؟")) print(answer("عندكم فرع في دبي؟")) # not in the docs -> should say it doesn't know
لما المستندات تبقى آلاف: Vector Database
مع ٦ فقرات القايمة كفاية. مع مليون فقرة محتاج Vector Database: قاعدة بيانات متخصصة بتخزن الـ embeddings وتدور فيها بسرعة جداً. أشهرها للبداية Chroma لأنها بتشتغل على جهازك من غير سيرفر:
import chromadb client = chromadb.PersistentClient(path="./store_db") # saved on disk collection = client.get_or_create_collection("help_center") collection.add( ids=["refund", "damaged", "shipping"], documents=[ "تقدر ترجع أي منتج خلال 14 يوم من الاستلام.", "المنتجات المكسورة بنبدلها فوراً ومصاريف الشحن علينا.", "الشحن للقاهرة والجيزة من يوم ليومين.", ], ) # Chroma creates the embeddings for you by default results = collection.query(query_texts=["الطلب وصل بايظ"], n_results=1) print(results["documents"][0])
💡 جودة الـ RAG في التفاصيلحجم الـ chunk (صغير أوي بيفقد السياق، وكبير أوي بيجيب كلام مالوش لازمة)، وموديل الـ embeddings (لازم يفهم لغة مستنداتك كويس)، وعدد الفقرات اللي بتبعتها. اعمل قايمة ٢٠ سؤال حقيقي بإجاباتهم، واختبر عليهم كل ما تغير حاجة.