الاستعلام والاسترجاع والتوليد
اكتبوا pipeline للاستعلام يحوّل سؤال المستخدم إلى embedding، ويسترجع أفضل k من الأجزاء، وينسّق prompt معزّزًا، ويستدعي LLM، ويعيد إجابة موثّقة بالاستشهادات.
الاستعلام والاسترجاع والتوليد درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.
مسار الاستعلام: من البداية إلى النهاية
يمثل مسار الاستعلام الجزء المتصل بالإنترنت من RAG — أي الشيفرة التي تعمل في الوقت الفعلي عندما يطرح المستخدم سؤالًا. وهو يربط جميع المكونات التي أُنشئت أثناء الفهرسة: نموذج التضمين، ومخزن المتجهات، وقالب المطالبة، وLLM. يكتمل مسار استعلام جيد التنفيذ خلال أقل من 500ms لمعظم أحمال العمل، وينتج إجابات مستندة إلى السياق ومرفقة بالاستشهادات. في هذا الدرس، سنبني كل خطوة من الصفر.
الخطوة 1: تضمين استعلام المستخدم
تتمثل الخطوة الأولى في تحويل سؤال المستخدم المكتوب باللغة الطبيعية إلى تضمين متجه باستخدام النموذج نفسه المستخدم أثناء الفهرسة. يرمّز هذا التضمين المعنى الدلالي للسؤال، وستتم مقارنته بتضمينات أجزاء المستند في مخزن المتجهات. اجعل هذه الخطوة سريعة — استخدم نموذجًا خفيفًا مثل text-embedding-3-small وخزّن التضمينات مؤقتًا للاستعلامات المتطابقة المتكررة.
from openai import OpenAI
client = OpenAI()
def embed_query(question: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=[question]
)
return response.data[0].embedding
user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')الخطوة 2: استرجاع أفضل K من الأجزاء
أرسل متجه الاستعلام إلى مخزن المتجهات للعثور على الأجزاء K الأكثر تشابهًا دلاليًا. تُرتّب النتائج المعادة وفق درجة تشابه جيب التمام (عادةً من 0.0 إلى 1.0، وكلما ارتفعت كانت النتيجة أفضل). توازن قيمة K المثالية بين ثراء السياق وتكلفة نافذة السياق؛ وتُعد K=5 نقطة بداية شائعة. يمكنك أيضًا تطبيق مرشحات البيانات الوصفية هنا لقصر الاسترجاع على قسم أو نوع مستند أو نطاق زمني محدد.
def retrieve_chunks(query_vector, index, top_k=5, filters=None):
query_params = {
'vector': query_vector,
'top_k': top_k,
'include_metadata': True
}
if filters:
query_params['filter'] = filters
results = index.query(**query_params)
chunks = []
for match in results.matches:
chunks.append({
'score': match.score,
'text': match.metadata['text'],
'source': match.metadata.get('source', ''),
'page': match.metadata.get('page', '')
})
return chunksالخطوة 3: التصفية وفق عتبة الدرجة
ليست كل الأجزاء المسترجعة ذات صلة فعلية — فقد تحصل بعض الأجزاء على درجات تشابه منخفضة، لكنها تظل ضمن أفضل K لأن الاستعلام يقع خارج نطاق تغطية الفهرس. طبّق حدًا أدنى للدرجة لتصفية النتائج منخفضة الثقة. إذا كانت جميع الأجزاء المسترجعة أقل من العتبة، فأعد استجابة تفيد بعدم العثور على معلومات بدلًا من إرسال سياق غير ذي صلة إلى LLM، لأن ذلك سينتج إجابة أسوأ من الرفض بطريقة لائقة.
MIN_SCORE_THRESHOLD = 0.75
def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
relevant = [c for c in chunks if c['score'] >= threshold]
if not relevant:
print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
return relevant
retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
print('Responding: no relevant information found')الخطوة 4: تنسيق كتلة السياق
اجمع الأجزاء المسترجعة في كتلة سياق منظّمة سيقرأها LLM. ضع تسمية مصدر لكل جزء حتى يتمكن النموذج من الاستشهاد به بدقة. أضف فاصلًا بين الأجزاء لتوضيحها. حافظ على إجمالي السياق ضمن ميزانية الرموز — احسب الرموز باستخدام tiktoken، واقطع الأجزاء الأقل تسجيلًا أو احذفها إذا تجاوزت الحد. تُدرج كتلة السياق في المطالبة بين تعليمات النظام وسؤال المستخدم.
def format_context(chunks):
parts = []
for i, chunk in enumerate(chunks, start=1):
source_label = chunk['source']
if chunk.get('page'):
source_label += f", page {chunk['page']}"
parts.append(
f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
)
return '\n\n---\n\n'.join(parts)
context = format_context(filtered)
print(f'Context block: {len(context)} characters')الخطوة 5: إنشاء المطالبة المعزَّزة
اجمع كتلة السياق وتعليمات النظام وسؤال المستخدم في المطالبة النهائية. تخبر رسالة النظام النموذج باستخدام السياق المقدم فقط والاستشهاد بالمصادر. وتحتوي رسالة المستخدم على السياق المنسق متبوعًا بالسؤال. يمنع هذا الفصل الواضح النموذج من خلط محتوى السياق بالسؤال، ويجعل الحد الفاصل بين البيانات المسترجعة وإدخال المستخدم غير ملتبس.
def build_prompt(question, context):
system_message = (
'You are a helpful assistant. Answer the question using ONLY '
'the information in the provided documents. '
'Cite the document number(s) used, like [Doc 1]. '
'If the documents do not contain the answer, say so.'
)
user_message = (
f'Documents:\n\n{context}\n\n'
f'Question: {question}'
)
return system_message, user_messageالخطوة 6: استدعاء LLM والحصول على الإجابة
أرسل المطالبة المُجمّعة إلى LLM باستخدام Chat Completions API. استخدم درجة حرارة منخفضة (من 0.0 إلى 0.3) للأسئلة والأجوبة الواقعية، للحصول على إجابات متسقة ومستندة إلى السياق. تنتج درجات الحرارة الأعلى استجابات أكثر إبداعًا، لكنها تزيد خطر إضافة النموذج معلومات تتجاوز ما يرد في السياق. حلّل الاستجابة وأعد نص الإجابة والمصادر المسترجعة معًا، حتى يتمكن تطبيقك من عرض الاستشهادات للمستخدم.
def generate_answer(question, context, sources):
system_msg, user_msg = build_prompt(question, context)
response = client.chat.completions.create(
model='gpt-4o',
temperature=0.1, # low temperature for factual Q&A
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources,
'tokens_used': response.usage.total_tokens
}جمع المكونات كلها
يستدعي مسار الاستعلام الكامل هذه الخطوات بالتتابع. كل خطوة عبارة عن دالة نقية يمكنك اختبارها بصورة مستقلة، وتتدفق البيانات بسلاسة من خطوة إلى التالية. تؤدي إضافة التسجيل في كل خطوة إلى جعل المسار قابلًا للرصد — إذ يمكنك رؤية الأجزاء المسترجعة بالضبط، والدرجة التي حصلت عليها، وكيفية تجميع السياق، وعدد الرموز المستخدمة. تُعد هذه الرؤية ضرورية لتصحيح الأخطاء وتحسين جودة الاسترجاع.
def answer_question(user_question, vector_index):
# Step 1: Embed query
q_vector = embed_query(user_question)
# Step 2: Retrieve
chunks = retrieve_chunks(q_vector, vector_index, top_k=5)
# Step 3: Filter low-confidence matches
chunks = filter_by_score(chunks, threshold=0.70)
if not chunks:
return {'answer': 'I do not have information about that topic.', 'sources': []}
# Step 4 & 5: Format and build prompt
context = format_context(chunks)
sources = [c['source'] for c in chunks]
# Step 6: Generate
return generate_answer(user_question, context, sources)تحسين زمن الاستجابة
يحتوي مسار الاستعلام على خطوتين تعتمدان على عمليات الإدخال والإخراج: استدعاء التضمين واستدعاء LLM. شغّلهما من دون فترات انتظار غير ضرورية: فاستدعاء التضمين سريع (<100ms)، بينما استدعاء LLM بطيء (من 500ms إلى 3s). لتقليل زمن الاستجابة المُدرَك، بث استجابة LLM بحيث تظهر الرموز أثناء إنشائها بدلًا من انتظار الاستجابة الكاملة. خزّن تضمين الاستعلامات المتطابقة المتكررة مؤقتًا لتجنب استدعاءات API المكررة.
async def answer_question_streaming(question, index):
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
if not chunks:
yield 'I do not have information about that topic.'
return
context = format_context(chunks)
system_msg, user_msg = build_prompt(question, context)
stream = await client.chat.completions.create(
model='gpt-4o',
stream=True,
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
async for chunk in stream:
delta = chunk.choices[0].delta.content or ''
yield deltaالتسجيل من أجل قابلية الرصد
تحتاج مسارات RAG في بيئات الإنتاج إلى تسجيل منظّم حتى تتمكن من تشخيص حالات فشل الاسترجاع أو تقديم LLM إجابة سيئة. سجّل الاستعلام، ومعرّفات الأجزاء المسترجعة ودرجاتها، وعدد رموز السياق، والإجابة، وزمن الاستجابة لكل طلب. خزّن هذه السجلات في قاعدة بيانات أو منصة للرصد. عندما يبلغ المستخدمون عن إجابات سيئة، يمكنك إعادة تشغيل الاستعلام نفسه وفحص الأجزاء التي استُرجعت وسبب عدم كفايتها.
import time
import logging
import json
def answer_question_with_logging(question, index):
start = time.time()
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
context = format_context(chunks)
result = generate_answer(question, context, [c['source'] for c in chunks])
latency_ms = (time.time() - start) * 1000
log_entry = {
'question': question,
'num_chunks_retrieved': len(chunks),
'chunk_scores': [c['score'] for c in chunks],
'tokens_used': result.get('tokens_used'),
'latency_ms': round(latency_ms)
}
logging.info(json.dumps(log_entry))
return resultالتخزين المؤقت لتضمينات الاستعلامات
إذا كان تطبيقك يتلقى عددًا كبيرًا من الاستعلامات المتكررة أو المتشابهة جدًا — مثل روبوتات الأسئلة الشائعة التي يطرح مستخدموها الأسئلة نفسها غالبًا — فإن تخزين تضمينات الاستعلامات مؤقتًا يُعد تحسينًا بسيطًا وعالي التأثير. احسب تجزئة لسلسلة الاستعلام، وتحقق من ذاكرة Redis المؤقتة بحثًا عن التضمين المقابل، ولا تستدعِ API التضمين إلا عند عدم العثور عليه في الذاكرة المؤقتة. تُعد معدلات إصابة ذاكرة التضمين المؤقتة التي تتراوح بين 30% و60% شائعة في روبوتات الأسئلة الشائعة وروبوتات دعم المحادثة في بيئات الإنتاج، ما يلغي جزءًا كبيرًا من تكلفة API ويقلل زمن الاستجابة بمقدار يتراوح بين 50 و100ms لكل استعلام مستفيد من التخزين المؤقت.
import hashlib
import json
import redis
r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400 # 24 hours
def embed_query_cached(question):
cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached) # cache hit
# Cache miss: call the API
vector = embed_query(question)
r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
return vectorتحقق سريع
اختبر مدى استيعابك لمفاهيم هندسة الذكاء الاصطناعي الواردة في هذا الدرس.
مراجعة الدرس
تعلّمت في هذا الدرس: مسار الاستعلام ذي الخطوات الست (تضمين الاستعلام، واسترجاع الأجزاء، والتصفية وفق الدرجة، وتنسيق السياق، وإنشاء المطالبة، وإنشاء الإجابة)، والتصفية وفق عتبة الدرجة لمعالجة الاستعلامات الواقعة خارج نطاق تغطية الفهرس، وتحسينات بيئات الإنتاج، بما في ذلك بث الاستجابات، والتسجيل المنظّم، وتحسين زمن الاستجابة. بعد ذلك سنتعلم كيفية تقييم ما إذا كان نظام RAG الكامل يعمل فعلًا بصورة صحيحة.
الأسئلة الشائعة
هل درس «الاستعلام والاسترجاع والتوليد» مجاني؟
نعم — نص درس «الاستعلام والاسترجاع والتوليد» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.
ماذا ستتعلم في «الاستعلام والاسترجاع والتوليد»؟
اكتبوا pipeline للاستعلام يحوّل سؤال المستخدم إلى embedding، ويسترجع أفضل k من الأجزاء، وينسّق prompt معزّزًا، ويستدعي LLM، ويعيد إجابة موثّقة بالاستشهادات. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟
لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «الاستعلام والاسترجاع والتوليد»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟
نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- تحميل المستندات واستخراج النصوص
- استراتيجيات تقسيم النص: ثابت أم حسب الجمل أم تكراري
- الفهرسة: تحويل الأجزاء إلى Embeddings وتخزينها
- الاستعلام والاسترجاع والتوليد