حلول RAG
إجابات أسئلة التحقق من المعرفة
-
RAG يضيف المستندات ذات الصلة إلى الموجه (prompt) في وقت الاستعلام، فلا حاجة إلى تدريب، ويسهل تحديثه. أما الضبط الدقيق (fine-tuning) فيعيد تدريب النموذج على بياناتك، وهو مكلف وبطيء، لكنه يغيّر سلوك النموذج وأسلوبه.
-
التقطيع (chunking) يجعل كل تضمين (embedding) مركّزًا على موضوع واحد. فالمستند الطويل الذي يتناول «الدروس الخصوصية والمساعدة المالية والإرشاد» ينتج تضمينًا ضبابيًا. لذلك يجزّئ التقطيع المستند بحيث تحصل «الدروس الخصوصية» على متجه خاص بها، وتحصل «المساعدة المالية» على متجه خاص بها، وهكذا.
-
تشابه جيب التمام (cosine similarity) يقيس الزاوية بين متجهين. القيمة 1.0 تعني الاتجاه نفسه (أي معنى متشابه)، و0.0 تعني عدم وجود علاقة، و-1.0 تعني الاتجاه المعاكس.
-
384 بُعدًا توازن بين الدقة والسرعة. فكلما زاد عدد الأبعاد التقطت تفاصيل أدق، لكنها تستهلك مساحة تخزين وحوسبة أكبر. والعدد 384 مناسب للبحث على مستوى الجملة.
-
يعود الذكاء الاصطناعي إلى معرفته العامة. يظل نظام 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