0Pricing
AI Engineering Academy · درس

تنفيذ ميزات RAG والوكيل الأساسية

أنشئ خط أنابيب لاستيعاب المستندات، وفهرسة مخزن المتجهات، والاسترجاع مع إعادة الترتيب، وتكامل أدوات الوكيل، باتباع الأنماط التي تعلمتها طوال المسار.

تنفيذ ميزات RAG والوكيل الأساسية درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ترتيب التنفيذ والتبعيات

ابنوا نظامكم من الأسفل إلى الأعلى: ابدؤوا بالمكونات التي لا تعتمد على تبعيات خارجية، ثم أضيفوا طبقات المكونات التي تعتمد عليها. بالنسبة إلى نظام RAG + agent، يكون الترتيب: (1) مخطط مخزن المتجهات، (2) مسار الإدخال، (3) المسترجع، (4) سلسلة أسئلة وأجوبة أساسية، (5) نقطة نهاية البث، (6) الوكيل مع الأدوات، (7) طبقة التخزين المؤقت، (8) إضافة أدوات التتبع. اختبروا كل مكون بمعزل قبل دمجه في المسار.

# Build order:
IMPL_ORDER = [
    'pgvector_schema',      # prerequisite for everything
    'document_ingestion',   # populate the vector store
    'hybrid_retriever',     # test retrieval in isolation
    'qa_chain_basic',       # integrate LLM with retrieval
    'streaming_endpoint',   # expose via API
    'function_calling',     # add agent tool calls
    'semantic_cache',       # reduce repeat API calls
    'langsmith_tracing',    # add after core works
    'injection_filter',     # harden before load testing
]

إعداد مخزن المتجهات

أنشئوا جدول pgvector بالمخطط الصحيح قبل إدخال المستندات. أدرجوا أعمدة للمتجه، ونص المقطع، وجميع حقول البيانات الوصفية، وطابعًا زمنيًا updated_at لإعادة الفهرسة الانتقائية. أنشئوا فهرس متجهات (HNSW أو IVFFlat) على عمود التضمين فورًا؛ فإضافة الفهرس بعد وجود ملايين الصفوف أبطأ بكثير من إضافته مسبقًا إلى جدول فارغ.

-- PostgreSQL schema with pgvector
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunks (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      TEXT NOT NULL,
    chunk_text  TEXT NOT NULL,
    embedding   VECTOR(1536) NOT NULL,
    source_file TEXT,
    page_number INT,
    section     TEXT,
    doc_type    TEXT,
    tenant_id   TEXT NOT NULL,  -- for data isolation
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_chunks_hnsw ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

CREATE INDEX idx_chunks_tenant ON document_chunks(tenant_id);

بناء مسار الإدخال

نفّذوا الإدخال في صورة دالة غير متزامنة واحدة تقبل مسار ملف أو عنوان URL وتعيد عدد المقاطع المفهرسة. استخدموا محمّلات المستندات في LangChain لأنواع الملفات المختلفة، ومقسّم النصوص التكراري للمقاطع. نفّذوا استدعاءات التضمين على دفعات للالتزام بحد الإدخال البالغ 2048 رمزًا، ولتقليل استدعاءات API من الآلاف إلى العشرات.

from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from openai import AsyncOpenAI

async def ingest_document(file_path: str, doc_id: str, tenant_id: str) -> int:
    loader = PyPDFLoader(file_path)
    pages = loader.load()
    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
    chunks = splitter.split_documents(pages)
    # Batch embed
    texts = [c.page_content for c in chunks]
    client = AsyncOpenAI()
    embeddings_response = await client.embeddings.create(
        model='text-embedding-3-small',
        input=texts
    )
    embeddings = [e.embedding for e in embeddings_response.data]
    await insert_chunks_to_pgvector(chunks, embeddings, doc_id, tenant_id)
    return len(chunks)

بناء المسترجع الهجين

اجمعوا بين البحث الكثيف بالمتجهات والبحث عن الكلمات المفتاحية باستخدام BM25، وادمجوا النتائج باستخدام دمج الرتب التبادلي. نفّذوا المسترجع في صورة فئة تحتوي على طريقة واحدة retrieve(query, tenant_id, top_k). نفّذوا البحثين بالتزامن داخلها باستخدام asyncio.gather، وادمجوا القوائم المرتبة باستخدام RRF، وأزيلوا التكرارات حسب معرّف المقطع، ثم أعيدوا أفضل نتائج top_k مع البيانات الوصفية لمصادرها.

import asyncio
from rank_bm25 import BM25Okapi

class HybridRetriever:
    def __init__(self, pool, k_rrf: int = 60):
        self.pool = pool
        self.k_rrf = k_rrf

    async def retrieve(self, query: str, tenant_id: str, top_k: int = 10) -> list:
        dense_results, sparse_results = await asyncio.gather(
            self._dense_search(query, tenant_id, top_k * 3),
            self._bm25_search(query, tenant_id, top_k * 3)
        )
        merged = self._rrf_merge(dense_results, sparse_results)
        return merged[:top_k]

    def _rrf_score(self, rank: int) -> float:
        return 1.0 / (self.k_rrf + rank + 1)

تنفيذ سلسلة الأسئلة والأجوبة الأساسية

ابنوا سلسلة الأسئلة والأجوبة الأساسية باستخدام LangChain LCEL. تأخذ السلسلة سؤالًا ومقاطع مسترجعة، وتنسّق مطالبة معززة تتضمن تعليمات باستخدام السياق المقدم فقط، ثم تبث الاستجابة. أضيفوا تعليمات صريحة للنموذج للاستشهاد بالمصادر حسب اسم المستند ورقم الصفحة، وللقول «لا أعرف» عندما لا تكون الإجابة موجودة في السياق المسترجع.

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

RAG_PROMPT = ChatPromptTemplate.from_messages([
    ('system', 'You are a precise assistant. Answer ONLY using the provided context. '
               'Cite sources as [DocName, p.N]. If the answer is not in the context, say "I don\'t have information about that."'),
    ('user', 'Context:\n{context}\n\nQuestion: {question}')
])

llm = ChatOpenAI(model='gpt-4o', temperature=0, streaming=True)
qa_chain = RAG_PROMPT | llm | StrOutputParser()

async def answer_question(question: str, chunks: list) -> str:
    context = '\n\n'.join(f'[{c["source"]}]\n{c["text"]}' for c in chunks)
    return await qa_chain.ainvoke({'context': context, 'question': question})

إضافة استدعاء الدوال إلى الوكيل

وسّعوا نظام الأسئلة والأجوبة الأساسي باستخدام استدعاء الدوال للتعامل مع الاستعلامات التي تتطلب بيانات آنية أو عمليات حسابية. حدّدوا أدوات للبحث في الويب عن المعلومات الحالية، وتشغيل استعلامات SQL على قاعدة بيانات آمنة للقراءة فقط، والبحث عن سجلات محددة حسب المعرّف. يقرر الوكيل الأدوات التي يستدعيها استنادًا إلى السؤال، وينفذها، ويدمج النتائج في الإجابة النهائية.

from openai import AsyncOpenAI
import json

TOOLS = [
    {
        'type': 'function',
        'function': {
            'name': 'search_knowledge_base',
            'description': 'Search the internal document knowledge base for relevant information',
            'parameters': {
                'type': 'object',
                'properties': {
                    'query': {'type': 'string', 'description': 'Search query'},
                    'top_k': {'type': 'integer', 'default': 5}
                },
                'required': ['query']
            }
        }
    }
]

async def agent_with_tools(question: str, tenant_id: str) -> str:
    client = AsyncOpenAI()
    messages = [{'role': 'user', 'content': question}]
    while True:
        resp = await client.chat.completions.create(
            model='gpt-4o', messages=messages, tools=TOOLS)
        if resp.choices[0].finish_reason != 'tool_calls':
            return resp.choices[0].message.content
        tool_call = resp.choices[0].message.tool_calls[0]
        args = json.loads(tool_call.function.arguments)
        result = await dispatch_tool(tool_call.function.name, args, tenant_id)
        messages.append({'role': 'tool', 'tool_call_id': tool_call.id, 'content': result})

بث استجابة الوكيل

غلّف الوكيل في StreamingResponse من FastAPI باستخدام الأحداث المرسلة من الخادم، بحيث تعرض الواجهة الأمامية الرموز فور وصولها. وبالنسبة إلى استجابات الوكيل التي تتضمن استدعاءات للأدوات، اعرض مؤشر تقدم أثناء تنفيذ الأداة («جارٍ البحث في قاعدة المعرفة...»)، ثم أرسل الإجابة النهائية رمزًا تلو الآخر. يمنع ذلك الصفحة من الظهور وكأنها متجمدة أثناء زمن الاستجابة الناتج عن تنفيذ الأداة.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def event_stream(question: str, tenant_id: str):
    yield 'data: {"type": "start"}\n\n'
    chunks = await retriever.retrieve(question, tenant_id)
    yield 'data: {"type": "retrieving", "count": ' + str(len(chunks)) + '}\n\n'
    async for token in qa_chain.astream({'context': format_context(chunks), 'question': question}):
        yield f'data: {{"type": "token", "content": {repr(token)}}}\n\n'
    yield 'data: {"type": "done"}\n\n'

@app.post('/query/stream')
async def stream_query(question: str, tenant_id: str):
    return StreamingResponse(event_stream(question, tenant_id), media_type='text/event-stream')

دمج Semantic Cache

أضف ذاكرة التخزين المؤقت الدلالية كخطوة تسبق الاسترجاع في مسار الاستعلام. قبل الوصول إلى المسترجِع وLLM، حوّل سؤال المستخدم إلى تضمين متجهِي وتحقق من الذاكرة المؤقتة. عند العثور على إدخال تتجاوز درجة تشابهه العتبة المحددة، أعد الإجابة المخزنة فورًا مع العلم cached: true. أما حالات عدم العثور على إدخال مطابق، فتتابع عبر المسار الكامل، ثم تُخزَّن الإجابة الناتجة في الذاكرة المؤقتة للاستعلامات المشابهة مستقبلًا.

async def query_pipeline(question: str, tenant_id: str) -> dict:
    # 1. Check semantic cache
    cache_hit = await semantic_cache.lookup(question, tenant_id, threshold=0.92)
    if cache_hit:
        return {'answer': cache_hit.answer, 'cached': True, 'sources': cache_hit.sources}

    # 2. Retrieve
    chunks = await retriever.retrieve(question, tenant_id, top_k=5)
    chunks = await reranker.rerank(question, chunks, top_n=3)

    # 3. Generate
    answer = await answer_question(question, chunks)
    sources = [c['source'] for c in chunks]

    # 4. Cache result
    await semantic_cache.store(question, tenant_id, answer, sources)

    return {'answer': answer, 'cached': False, 'sources': sources}

إضافة التتبّع باستخدام LangSmith

أضف قياسًا لمسار المعالجة باستخدام LangSmith من خلال ضبط متغيرين للبيئة. سيُتتبَّع كل استدعاء لسلسلة LangChain تلقائيًا، مع تسجيل أعداد الرموز وزمن الاستجابة والمدخلات والمخرجات وأي أخطاء. أما التعليمات البرمجية المخصصة التي لا تستخدم LangChain، مثل الاسترجاع وإعادة الترتيب، فغلّف استدعاءاتها بواسمات @traceable لإدراجها في التتبّع. يمنحك ذلك رؤية شاملة من البداية إلى النهاية لكل خطوة في المسار.

import os
from langsmith import traceable

os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_API_KEY'] = os.environ['LANGSMITH_API_KEY']
os.environ['LANGCHAIN_PROJECT'] = 'document-qa-production'

# Wrap non-LangChain steps with @traceable
@traceable(name='hybrid_retrieval')
async def traced_retrieval(question: str, tenant_id: str, top_k: int) -> list:
    return await retriever.retrieve(question, tenant_id, top_k)

@traceable(name='cohere_reranking')
async def traced_reranking(question: str, chunks: list) -> list:
    return await reranker.rerank(question, chunks)

# LangChain LCEL chains are automatically traced — no extra code needed

تشغيل اختبارات التكامل

اكتب اختبارات تكامل تختبر مسار الاستعلام الكامل، بدءًا من إدخال السؤال ووصولًا إلى الإجابة النهائية. استخدم مخزن متجهات صغيرًا للاختبار، يحتوي على مستندات معروفة، حتى تتمكن من كتابة تأكيدات حتمية بشأن المقاطع التي ينبغي استرجاعها ومحتوى الإجابة المتوقع. شغّل اختبارات التكامل في بيئة تجريبية مماثلة للبنية التحتية للإنتاج، ولكن باستخدام مخزن متجهات ومفتاح LLM منفصلين.

import pytest

@pytest.mark.asyncio
async def test_full_pipeline_returns_grounded_answer():
    # Setup: ingest known document
    await ingest_document('tests/fixtures/policy.pdf', 'policy_v1', 'test_tenant')

    # Query with a question that has a known answer in the document
    result = await query_pipeline(
        question='What is the cancellation policy?',
        tenant_id='test_tenant'
    )

    assert result['answer'] is not None
    assert len(result['answer']) > 50
    assert '24 hours' in result['answer']  # known fact in document
    assert 'policy.pdf' in str(result['sources'])
    assert result['cached'] is False  # fresh query

قياس جودة الاسترجاع الأساسية

قبل تحسين أي شيء، أنشئ خط أساس للاسترجاع. استخدم مجموعة اختبار التقييم لقياس معدل الإصابة، أي ما إذا كان المستند الصحيح يظهر ضمن النتائج الخمس الأولى، وMRR، أي مدى ارتفاع ترتيبه. شغّل هذا القياس بعد إعداد مخزن المتجهات الأولي وقبل إضافة إعادة الترتيب أو البحث الهجين. يوضح لك خط الأساس مقدار التحسن الذي يقدمه كل تحسين فعليًا، ويوفر دليلًا على الأساليب التي تستحق الإبقاء عليها.

async def measure_retrieval_baseline(test_cases: list) -> dict:
    hits = 0
    reciprocal_ranks = []
    for case in test_cases:
        results = await retriever.retrieve(case['question'], case['tenant_id'], top_k=5)
        result_docs = [r['doc_id'] for r in results]
        if case['relevant_doc'] in result_docs:
            hits += 1
            rank = result_docs.index(case['relevant_doc']) + 1
            reciprocal_ranks.append(1.0 / rank)
        else:
            reciprocal_ranks.append(0.0)
    return {
        'hit_rate_at_5': hits / len(test_cases),
        'mrr': sum(reciprocal_ranks) / len(reciprocal_ranks)
    }

تحقق سريع

اختبر مدى فهمك لبناء الميزات الأساسية لـRAG والوكلاء.

مراجعة الدرس

تعلمت في هذا الدرس أن ترتيب التنفيذ من الأسفل إلى الأعلى يضمن اختبار كل مكوّن بمعزل عن غيره قبل دمجه، وأن الاسترجاع الهجين باستخدام البحث الكثيف وBM25 بالتوازي ودمجهما عبر RRF يحقق أفضل جودة للاسترجاع، وأن التتبّع باستخدام LangSmith وواسمات @traceable يوفر رؤية كاملة لمسار المعالجة. بعد ذلك سنحصّن النظام باستخدام أنماط الأمان والتخزين المؤقت والموثوقية.

الأسئلة الشائعة

هل درس «تنفيذ ميزات RAG والوكيل الأساسية» مجاني؟

نعم — نص درس «تنفيذ ميزات RAG والوكيل الأساسية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ماذا ستتعلم في «تنفيذ ميزات RAG والوكيل الأساسية»؟

أنشئ خط أنابيب لاستيعاب المستندات، وفهرسة مخزن المتجهات، والاسترجاع مع إعادة الترتيب، وتكامل أدوات الوكيل، باتباع الأنماط التي تعلمتها طوال المسار. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟

لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «تنفيذ ميزات RAG والوكيل الأساسية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟

نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تصميم بنية الإنتاج
  2. تنفيذ ميزات RAG والوكيل الأساسية
  3. تقوية النظام: الأمان والتخزين المؤقت والموثوقية
  4. التقييم والنشر والمراجعة اللاحقة
← العودة إلى AI Engineering Academy