RAG (التوليد المعزَّز بالاسترجاع)

امنح وكلاءك قاعدة معرفية حتى يتمكنوا من الإجابة عن الأسئلة باستخدام بياناتك الخاصة.


ما هو RAG؟

يتيح RAG لوكيلك البحث عن المعلومات ذات الصلة في مستنداتك قبل الإجابة عن سؤال. وبدلًا من الاعتماد فقط على ما تم تدريب LLM عليه، يبحث الوكيل في بياناتك ويستخدم النتائج كسياق.

تتم العملية كما يلي:

  1. الاستيعاب — تُقسَّم المستندات إلى أجزاء، وتُحوَّل إلى تضمينات متجهية، ثم تُخزَّن
  2. الاستعلام — يُحوَّل السؤال إلى تضمين، وتتم مطابقته مع الأجزاء المخزَّنة استنادًا إلى التشابه
  3. التوليد — تُمرَّر الأجزاء الأكثر صلة إلى LLM كسياق لإجابته

وهذا يعني أن وكيلك يستطيع الإجابة عن الأسئلة المتعلقة بوثائق المنتجات، وسياسات الشركة، وقواعد التعليمات البرمجية، أو أي نص تقدمه له.

أبرز الميزات:

  • 📄 استيعاب أي نص — وثائق المنتجات، وmarkdown، والتعليمات البرمجية، والسياسات
  • 🔍 البحث الدلالي — العثور على المحتوى ذي الصلة حسب المعنى، وليس الكلمات المفتاحية فقط
  • 🤖 الاسترجاع الوكيلي — يقرر الوكيل متى يبحث عبر RagTool
  • 🔌 واجهات خلفية قابلة للتبديل — تبديل موفري التضمينات ومخازن المتجهات دون تغيير التعليمات البرمجية

التثبيت

[dependencies]
# Core only (in-memory store, all chunkers, no external deps)
adk-rag = "2.1.0"

# With Gemini embeddings (recommended for getting started)
adk-rag = { version = "2.1.0", features = ["gemini"] }

الخطوة 1: بناء مسار معالجة

يربط RagPipeline أربعة مكونات معًا: مقسّمًا إلى أجزاء، وموفرًا للتضمينات، ومخزنًا للمتجهات، ومعيد ترتيب اختياري.

use std::collections::HashMap;
use std::sync::Arc;
use adk_rag::{
    Document, FixedSizeChunker, InMemoryVectorStore,
    RagConfig, RagPipeline, EmbeddingProvider,
};

// Mock embedder for demos — no API key needed.
// In production, use GeminiEmbeddingProvider or OpenAIEmbeddingProvider.
struct MockEmbedder;

#[async_trait::async_trait]
impl EmbeddingProvider for MockEmbedder {
    async fn embed(&self, text: &str) -> adk_rag::Result<Vec<f32>> {
        let hash = text.bytes().fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
        let mut v = vec![0.0f32; 64];
        for (i, x) in v.iter_mut().enumerate() {
            *x = ((hash.wrapping_add(i as u64)) as f32).sin();
        }
        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
        if norm > 0.0 { v.iter_mut().for_each(|x| *x /= norm); }
        Ok(v)
    }
    fn dimensions(&self) -> usize { 64 }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pipeline = RagPipeline::builder()
        .config(RagConfig::builder()
            .chunk_size(256)
            .chunk_overlap(50)
            .top_k(3)
            .build()?)
        .embedding_provider(Arc::new(MockEmbedder))
        .vector_store(Arc::new(InMemoryVectorStore::new()))
        .chunker(Arc::new(FixedSizeChunker::new(256, 50)))
        .build()?;

    // Create a collection and ingest a document
    pipeline.create_collection("docs").await?;
    pipeline.ingest("docs", &Document {
        id: "intro".into(),
        text: "Rust is a systems programming language focused on safety and speed.".into(),
        metadata: HashMap::from([("topic".into(), "rust".into())]),
        source_uri: None,
    }).await?;

    // Query
    let results = pipeline.query("docs", "safe programming").await?;
    for r in &results {
        println!("[{:.3}] {}", r.score, r.chunk.text);
    }
    Ok(())
}

آلية العمل:

  1. يقسّم FixedSizeChunker المستند إلى أجزاء مكوّنة من 256 حرفًا مع تداخل يبلغ 50 حرفًا
  2. يحوّل MockEmbedder كل جزء إلى متجه ذي 64 بُعدًا
  3. يخزّن InMemoryVectorStore المتجهات ويبحث باستخدام تشابه جيب التمام
  4. يُضمّن query() السؤال، ويعثر على الأجزاء الأقرب، ثم يعيدها مرتبة حسب الدرجة

الخطوة 2: إضافة RAG إلى وكيل

تكمن القوة الحقيقية لـ RAG عندما يستخدمه agent كأداة. يغلّف RagTool خط الأنابيب باعتباره adk_core::Tool — ويستدعي agent ‏rag_search كلما احتاج إلى معلومات.

عند استخدام RagTool مع agents المدعومة من Gemini، يعمل ADK تلقائيًا على تسوية نتيجة الأداة إلى استجابة دالة متوافقة مع Gemini. وهذا مهم لأن rag_search يعيد طبيعيًا قائمة من المقاطع، بينما يتوقع Gemini أن تكون functionResponse.response كائن JSON على مستوى النقل.

use std::sync::Arc;
use adk_agent::LlmAgentBuilder;
use adk_model::gemini::GeminiModel;
use adk_rag::{
    Document, GeminiEmbeddingProvider, InMemoryVectorStore,
    RagConfig, RagPipeline, RagTool, RecursiveChunker,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let api_key = std::env::var("GOOGLE_API_KEY")?;

    // Build pipeline with real embeddings
    let pipeline = Arc::new(
        RagPipeline::builder()
            .config(RagConfig::builder().chunk_size(300).chunk_overlap(50).top_k(3).build()?)
            .embedding_provider(Arc::new(GeminiEmbeddingProvider::new(&api_key)?))
            .vector_store(Arc::new(InMemoryVectorStore::new()))
            .chunker(Arc::new(RecursiveChunker::new(300, 50)))
            .build()?,
    );

    // Ingest documents
    pipeline.create_collection("kb").await?;
    pipeline.ingest("kb", &Document {
        id: "returns".into(),
        text: "Our return policy allows returns within 30 days with a receipt.".into(),
        metadata: Default::default(),
        source_uri: None,
    }).await?;

    // Wrap pipeline as a tool and attach to an agent
    let agent = LlmAgentBuilder::new("support")
        .instruction("Answer questions using the rag_search tool. Cite your sources.")
        .model(Arc::new(GeminiModel::new(&api_key, "gemini-3.7-flash")?))
        .tool(Arc::new(RagTool::new(pipeline, "kb")))
        .build()?;

    // The agent now calls rag_search automatically when it needs knowledge base info
    adk_cli::console::run_console(Arc::new(agent), "app".into(), "user1".into()).await?;
    Ok(())
}

عندما يسأل المستخدم «ما سياسة الإرجاع لديكم؟»، يقوم agent بما يلي:

  1. يقرر أنه يحتاج إلى البحث في قاعدة المعرفة
  2. يستدعي rag_search باستخدام {"query": "return policy"}
  3. يحصل على المقاطع ذات الصلة مع درجاتها
  4. يستخدم المقاطع كسياق لإنشاء إجابة طبيعية

الخطوة 3: اختيار استراتيجية تقسيم المقاطع

يؤثر تقسيم المستندات في جودة الاسترجاع. يوفر adk-rag ثلاثة مقسّمات للمقاطع:

المجزئالأنسب لـكيفية التقسيم
FixedSizeChunkerالنصوص العامة، والسجلاتكل N من الأحرف مع تداخل
RecursiveChunkerالمقالات، والوثائق، وتعليقات التعليمات البرمجيةالفقرات ← الجمل ← الكلمات
MarkdownChunkerملفات Markdown وREADMEحسب العناوين، مع الحفاظ على التسلسل الهرمي للأقسام
use adk_rag::{FixedSizeChunker, RecursiveChunker, MarkdownChunker};

// Fixed: simple, predictable chunks
let chunker = FixedSizeChunker::new(512, 100);

// Recursive: respects natural text boundaries
let chunker = RecursiveChunker::new(512, 100);

// Markdown: each section becomes a chunk with header_path metadata
let chunker = MarkdownChunker::new(512, 100);

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

يضيف MarkdownChunker حقل بيانات وصفية header_path إلى كل جزء (مثل "Getting Started > Installation")، مما يساعد الوكيل على الاستشهاد بأقسام محددة.


الإعداد

use adk_rag::RagConfig;

let config = RagConfig::builder()
    .chunk_size(256)            // max characters per chunk (default: 512)
    .chunk_overlap(50)          // overlap between chunks (default: 100)
    .top_k(5)                   // results to return (default: 10)
    .similarity_threshold(0.5)  // minimum score to include (default: 0.0)
    .build()?;
المعلَمةما الذي تتحكم فيهالإرشادات
chunk_sizeالحد الأقصى لعدد الأحرف في كل جزءمن 200 إلى 500 لمعظم حالات الاستخدام. الأقل = دقة أكبر، والأكثر = سياق أوسع
chunk_overlapالأحرف المشتركة بين الأجزاء المتجاورةتمنع نسبة 10–20% من chunk_size فقدان المعلومات عند الحدود
top_kعدد النتائج المُعادةنتائج أكثر = سياق أكبر لـ LLM ولكن استخدام أعلى للرموز
similarity_thresholdالحد الأدنى للدرجة للإدراج‏0.0 يعيد كل شيء؛ ‏0.3–0.7 يرشّح التطابقات الضعيفة

موفرو التضمين

المزوّدعلامة الميزةالنموذجيتطلّب
GeminiEmbeddingProvidergeminigemini-embedding-2GOOGLE_API_KEY
OpenAIEmbeddingProvideropenaitext-embedding-3-smallOPENAI_API_KEY
// Gemini
use adk_rag::GeminiEmbeddingProvider;
let embedder = GeminiEmbeddingProvider::new(&api_key)?;

// OpenAI
use adk_rag::OpenAIEmbeddingProvider;
let embedder = OpenAIEmbeddingProvider::new(&api_key, "text-embedding-3-small");

يمكنك أيضًا تنفيذ EmbeddingProvider لأي خدمة تضمين مخصصة.


الواجهات الخلفية لمخازن المتجهات

الواجهة الخلفيةعلامة الميزةالأفضل لـ
InMemoryVectorStore(افتراضي)التطوير والاختبار ومجموعات البيانات الصغيرة
QdrantVectorStoreqdrantبيئات الإنتاج مع قاعدة بيانات متجهات مخصصة
LanceDBVectorStorelancedbقاعدة بيانات متجهات مضمّنة (لا حاجة إلى خادم)
PgVectorStorepgvectorعندما تستخدم PostgreSQL بالفعل
// In-memory (no setup needed)
use adk_rag::InMemoryVectorStore;
let store = InMemoryVectorStore::new();

// Qdrant (requires running Qdrant server)
use adk_rag::QdrantVectorStore;
let store = QdrantVectorStore::new("http://localhost:6334").await?;

// pgvector (requires PostgreSQL with pgvector extension)
use adk_rag::PgVectorStore;
let store = PgVectorStore::new("postgres://user:pass@localhost/db").await?;

مُعيد ترتيب مخصّص

يمرّر NoOpReranker الافتراضي النتائج دون تغيير. اكتب مُعيد ترتيب مخصّصًا لتحسين الدقة:

use adk_rag::{Reranker, SearchResult};

struct KeywordBoostReranker { boost: f32 }

#[async_trait::async_trait]
impl Reranker for KeywordBoostReranker {
    async fn rerank(
        &self,
        query: &str,
        mut results: Vec<SearchResult>,
    ) -> adk_rag::Result<Vec<SearchResult>> {
        let keywords: Vec<String> = query.split_whitespace()
            .filter(|w| w.len() > 3)
            .map(|w| w.to_lowercase())
            .collect();

        for r in &mut results {
            let text = r.chunk.text.to_lowercase();
            let hits = keywords.iter().filter(|kw| text.contains(kw.as_str())).count();
            r.score += hits as f32 * self.boost;
        }
        results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
        Ok(results)
    }
}

// Add to pipeline
let pipeline = RagPipeline::builder()
    .config(config)
    .embedding_provider(embedder)
    .vector_store(store)
    .chunker(chunker)
    .reranker(Arc::new(KeywordBoostReranker { boost: 0.1 }))
    .build()?;

مجموعات متعددة

استخدم مجموعات منفصلة لمجالات معرفية مختلفة. يمكن للوكيل البحث في مجموعات محددة، أو يمكنك إنشاء مثيلات متعددة من RagTool:

// Create collections for different content types
pipeline.create_collection("docs").await?;
pipeline.create_collection("faq").await?;
pipeline.create_collection("changelog").await?;

// Ingest into each
pipeline.ingest("docs", &setup_doc).await?;
pipeline.ingest("faq", &faq_doc).await?;
pipeline.ingest("changelog", &release_doc).await?;

// One tool per collection — the agent picks which to search
let docs_tool = RagTool::new(pipeline.clone(), "docs");
let faq_tool = RagTool::new(pipeline.clone(), "faq");

let agent = LlmAgentBuilder::new("support")
    .instruction("Search 'docs' for how-to questions, 'faq' for common questions.")
    .model(Arc::new(model))
    .tool(Arc::new(docs_tool))
    .tool(Arc::new(faq_tool))
    .build()?;

يمكن للوكيل أيضًا تجاوز المجموعة في وقت الاستعلام عن طريق تمرير "collection": "faq" في استدعاء الأداة.


علامات الميزات

لا تسحب سوى التبعيات التي تحتاج إليها:

الميزةتُمكّن منتبعية إضافية
(الافتراضي)السمات الأساسية، InMemoryVectorStore، جميع المقسِّماتلا توجد
geminiGeminiEmbeddingProvideradk-gemini
openaiOpenAIEmbeddingProviderreqwest
qdrantQdrantVectorStoreqdrant-client
lancedbLanceDBVectorStorelancedb, arrow
pgvectorPgVectorStoresqlx
fullكل ما سبقالكل
# Just core
adk-rag = "2.1.0"

# With Gemini embeddings
adk-rag = { version = "2.1.0", features = ["gemini"] }

# Everything
adk-rag = { version = "2.1.0", features = ["full"] }

ملاحظة: تتطلب ميزة lancedb تثبيت protoc. ثبّتها باستخدام brew install protobuf (macOS) أو apt install protobuf-compiler (Ubuntu).


البنية

                        Ingestion
Documents ──→ [Chunker] ──→ [EmbeddingProvider] ──→ [VectorStore]

                          Query
Question ──→ [EmbeddingProvider] ──→ [VectorStore search] ──→ [Reranker] ──→ Results
                                                                               │
                                                                               ▼
                                                                    Agent uses as context

ينسّق RagPipeline كلا التدفقين. ويغلّف RagTool خط المعالجة باعتباره adk_core::Tool، لكي تستدعيه الوكلاء عند الطلب.


تشغيل الأمثلة

cargo adk new rag_agent --template rag
cd rag_agent
cargo run

أفضل الممارسات

الممارسةالسبب
استخدم RecursiveChunker كإعداد افتراضيينتج حدودًا طبيعية للمقاطع
حافظ على المقاطع بطول 200–500 حرفيوازن بين الدقة والسياق
استخدم التضمينات الفعلية في بيئة الإنتاجمولدات التضمين الوهمية مخصصة للاختبار فقط
اضبط similarity_threshold على قيمة أكبر من 0يستبعد الضوضاء غير ذات الصلة
افصل المجموعات حسب المجاليحسّن الدقة ويتيح للوكلاء توجيه عمليات البحث
استخدم InMemoryVectorStore للتطوير فقطانتقل إلى Qdrant/pgvector في بيئة الإنتاج


السابق: ← أدوات واجهة المستخدم | التالي: الجلسات →