حلول RAG


إجابات أسئلة التحقق من المعرفة

  1. RAG يضيف المستندات ذات الصلة إلى الموجه (prompt) في وقت الاستعلام، فلا حاجة إلى تدريب، ويسهل تحديثه. أما الضبط الدقيق (fine-tuning) فيعيد تدريب النموذج على بياناتك، وهو مكلف وبطيء، لكنه يغيّر سلوك النموذج وأسلوبه.

  2. التقطيع (chunking) يجعل كل تضمين (embedding) مركّزًا على موضوع واحد. فالمستند الطويل الذي يتناول «الدروس الخصوصية والمساعدة المالية والإرشاد» ينتج تضمينًا ضبابيًا. لذلك يجزّئ التقطيع المستند بحيث تحصل «الدروس الخصوصية» على متجه خاص بها، وتحصل «المساعدة المالية» على متجه خاص بها، وهكذا.

  3. تشابه جيب التمام (cosine similarity) يقيس الزاوية بين متجهين. القيمة 1.0 تعني الاتجاه نفسه (أي معنى متشابه)، و0.0 تعني عدم وجود علاقة، و-1.0 تعني الاتجاه المعاكس.

  4. 384 بُعدًا توازن بين الدقة والسرعة. فكلما زاد عدد الأبعاد التقطت تفاصيل أدق، لكنها تستهلك مساحة تخزين وحوسبة أكبر. والعدد 384 مناسب للبحث على مستوى الجملة.

  5. يعود الذكاء الاصطناعي إلى معرفته العامة. يظل نظام RAG يعمل، لكنه لن يجد سياقًا ذا صلة. ويطلب الموجه من الذكاء الاصطناعي أن يذكر ذلك إذا لم يكن السياق مفيدًا.


حل التحدي 1

# بدّل إلى النموذج all-mpnet-base-v2 (768 بُعدًا)
MODEL_NAME = "all-mpnet-base-v2"
embedder = SentenceTransformer(MODEL_NAME)

# حدّث نموذج Document
from pgvector.sqlalchemy import Vector

class Document(db.Model):
    ...
    embedding = db.Column(Vector(768), nullable=False)

# أعد الفهرسة
db.drop_all()
db.create_all()
index_documents(db, Document)

حل التحدي 2

from sqlalchemy import text


def hybrid_search(db, query, table, top_k=5):
    query_vec = embed_text(query)

    # البحث المتجهي
    vector_results = (
        db.session.query(table, (1 - table.embedding.cosine_distance(query_vec)).label("score"))
        .order_by(table.embedding.cosine_distance(query_vec))
        .limit(top_k * 2)
        .all()
    )

    # البحث بالكلمات المفتاحية
    sql = text("""
        SELECT *, ts_rank(to_tsvector('english', content),
                 plainto_tsquery('english', :query)) as score
        FROM document
        WHERE to_tsvector('english', content) @@ plainto_tsquery('english', :query)
        ORDER BY score DESC
        LIMIT :limit
    """)
    kw_results = db.session.execute(sql, {"query": query, "limit": top_k * 2}).fetchall()

    # الدمج مع ترجيح النتائج
    seen = set()
    merged = []
    for r in vector_results:
        seen.add(r[0].id)
        merged.append((r[0], r.score * 0.7))

    for r in kw_results:
        weight = r.score * 0.3 if r.id not in seen else 0
        merged.append((r, weight))

    merged.sort(key=lambda x: x[1], reverse=True)
    return merged[:top_k]

حل التحدي 3

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")


def search_with_rerank(db, query, table, top_k=20, rerank_k=5):
    results = search_similar_chunks(db, query, table, top_k=top_k)
    pairs = [(query, r["content"]) for r in results]
    scores = reranker.predict(pairs)
    for r, s in zip(results, scores):
        r["rerank_score"] = float(s)
    results.sort(key=lambda r: r["rerank_score"], reverse=True)
    return results[:rerank_k]

حل التحدي 4

import requests
from bs4 import BeautifulSoup

def scrape_clark_news():
    url = "https://www.clark.edu/news-and-media/24-7/"
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")

    articles = []
    for item in soup.select(".story-item"):
        title_el = item.select_one("h3 a")
        if not title_el:
            continue
        title = title_el.get_text(strip=True)
        url_path = title_el.get("href", "")
        excerpt_el = item.select_one(".description")
        excerpt = excerpt_el.get_text(strip=True) if excerpt_el else ""

        articles.append({
            "title": title,
            "content": excerpt,
            "source": f"https://www.clark.edu{url_path}",
            "category": "news",
        })

    return articles

حل التحدي 5

عدّل نقطة النهاية /api/rag-chat لتضمّن سجل المحادثة:

@api.route("/rag-chat", methods=["POST"])
def rag_chat():
    data = request.get_json()
    user_message = data.get("message", "")

    if not user_message:
        return jsonify({"error": "Message is required"}), 400

    user_msg = Message(content=user_message, sender="user")
    db.session.add(user_msg)
    db.session.commit()

    try:
        results = search_similar_chunks(db, user_message, Document, top_k=5)
        context = build_rag_context(results)

        recent = Message.query.order_by(Message.timestamp.desc()).limit(6).all()
        recent.reverse()

        messages = [{"role": "system", "content": "You are a helpful assistant for Clark College. Use the context to answer."}]
        if context:
            messages.append({"role": "system", "content": f"Context:\n{context}"})
        for msg in recent[:-1]:
            role = "user" if msg.sender == "user" else "assistant"
            messages.append({"role": role, "content": msg.content})
        messages.append({"role": "user", "content": user_message})

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
        )
        ai_reply = response.choices[0].message.content

        ai_msg = Message(content=ai_reply, sender="ai")
        db.session.add(ai_msg)
        db.session.commit()

        return jsonify({
            "user_message": user_msg.to_dict(),
            "ai_message": ai_msg.to_dict(),
            "evidence": [{"title": r["title"], "similarity": round(r["similarity"], 3)} for r in results],
        })

    except Exception as e:
        return jsonify({"error": str(e)}), 500