RAG (Retrieval-Augmented Generation)

Geben Sie Ihren Agents eine Wissensbasis, damit sie Fragen mithilfe Ihrer eigenen Daten beantworten können.


Was ist RAG?

RAG ermöglicht es Ihrem Agent, relevante Informationen aus Ihren Dokumenten nachzuschlagen, bevor er eine Frage beantwortet. Anstatt sich nur auf das zu verlassen, worauf das LLM trainiert wurde, durchsucht der Agent Ihre Daten und verwendet die Ergebnisse als Kontext.

Der Ablauf ist:

  1. Ingest — Dokumente werden in chunks aufgeteilt, in vector embeddings umgewandelt und gespeichert
  2. Query — Eine Frage wird eingebettet und anhand von Ähnlichkeit mit gespeicherten chunks abgeglichen
  3. Generate — Die relevantesten chunks werden dem LLM als Kontext für seine Antwort übergeben

Das bedeutet, Ihr Agent kann Fragen zu Produktdokumenten, Unternehmensrichtlinien, Codebasen oder jedem Text beantworten, den Sie ihm zuführen.

Wichtige Highlights:

  • 📄 Beliebigen Text aufnehmen — Produktdokumente, Markdown, Code, Richtlinien
  • 🔍 Semantische Suche — relevante Inhalte nach Bedeutung finden, nicht nur nach Schlüsselwörtern
  • 🤖 Agentengesteuerte Abfrage — der Agent entscheidet, wann gesucht werden soll über RagTool
  • 🔌 Steckbare Backends — Embedding-Anbieter und Vektorspeicher austauschen, ohne den Code zu ändern

Installation

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

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

Schritt 1: Eine Pipeline erstellen

Ein RagPipeline verbindet vier Komponenten: einen Chunker, einen Embedding-Anbieter, einen Vektorspeicher und einen optionalen Reranker.

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(())
}

So funktioniert es:

  1. FixedSizeChunker teilt das Dokument in 256-Zeichen-Blöcke mit 50-Zeichen-Überlappung
  2. MockEmbedder wandelt jeden Block in einen 64-dimensionalen Vektor um
  3. InMemoryVectorStore speichert die Vektoren und sucht nach Kosinus-Ähnlichkeit
  4. query() bettet die Frage ein, findet die nächstgelegenen Blöcke und gibt sie nach Rangfolge zurück

Schritt 2: RAG zu einem Agent hinzufügen

Die wahre Stärke von RAG zeigt sich, wenn ein Agent es als Tool verwendet. RagTool verpackt die Pipeline als adk_core::Tool – der Agent ruft rag_search auf, wann immer er Informationen benötigt.

Wenn Sie RagTool mit Gemini-gestützten Agents verwenden, normalisiert ADK das Tool-Ergebnis automatisch in eine Gemini-kompatible Funktionsantwort. Dies ist wichtig, da rag_search natürlich eine Liste von Blöcken zurückgibt, während Gemini erwartet, dass functionResponse.response ein JSON-Objekt über die Leitung ist.

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-2.5-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(())
}

Wenn ein Benutzer fragt "Wie lautet Ihre Rückgaberichtlinie?", der Agent:

  1. Entscheidet, dass er die Wissensdatenbank durchsuchen muss
  2. Ruft rag_search mit {"query": "return policy"} auf
  3. Erhält die relevanten Chunks mit Bewertungen zurück
  4. Verwendet die Chunks als Kontext, um eine natürliche Antwort zu generieren

Schritt 3: Eine Chunking-Strategie wählen

Wie Sie Dokumente aufteilen, beeinflusst die Abrufqualität. adk-rag bietet drei Chunker:

ChunkerAm besten geeignet fürWie es aufteilt
FixedSizeChunkerAllgemeiner Text, ProtokolleAlle N Zeichen mit Überlappung
RecursiveChunkerArtikel, Dokumente, Code-KommentareAbsätze → Sätze → Wörter
MarkdownChunkerMarkdown-Dateien, READMEsNach Überschriften, unter Beibehaltung der Abschnittshierarchie
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 ist die beste Standardwahl — es versucht zuerst Absatzumbrüche, dann Satzgrenzen, dann Wortgrenzen, wodurch natürlichere Blöcke entstehen als bei einer Aufteilung mit fester Größe.

MarkdownChunker fügt ein header_path Metadatenfeld zu jedem Chunk hinzu (z.B. "Getting Started > Installation"), was dem Agenten hilft, bestimmte Abschnitte zu zitieren.


Konfiguration

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()?;
ParameterWas es steuertAnleitung
chunk_sizeMax characters per chunk200–500 für die meisten Anwendungsfälle. Kleiner = präziser, größer = mehr Kontext
chunk_overlapGemeinsame Zeichen zwischen benachbarten Chunks10–20% der chunk_size verhindert Informationsverlust an den Grenzen
top_kAnzahl der zurückgegebenen ErgebnisseMehr Ergebnisse = mehr Kontext für das LLM, aber höherer Token-Verbrauch
similarity_thresholdMindestpunktzahl zur Aufnahme0.0 gibt alles zurück; 0.3–0.7 filtert schwache Übereinstimmungen

Embedding-Anbieter

AnbieterFeature-FlagModellErfordert
GeminiEmbeddingProvidergeminigemini-embedding-001GOOGLE_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");

Sie können auch EmbeddingProvider für jeden benutzerdefinierten Embedding-Dienst implementieren.


Vektorspeicher-Backends

BackendFeature-FlagAm besten geeignet für
InMemoryVectorStore(default)Entwicklung, Tests, kleine Datensätze
QdrantVectorStoreqdrantProduktion mit dedizierter Vektordatenbank
LanceDBVectorStorelancedbEingebettete Vektordatenbank (kein Server erforderlich)
PgVectorStorepgvectorWenn Sie bereits PostgreSQL verwenden
// 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?;

Benutzerdefinierter Reranker

Der Standard-NoOpReranker gibt Ergebnisse unverändert weiter. Schreiben Sie einen benutzerdefinierten Reranker, um die Präzision zu verbessern:

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()?;

Mehrere Sammlungen

Verwenden Sie separate Sammlungen für verschiedene Wissensdomänen. Der Agent kann bestimmte Sammlungen durchsuchen oder Sie können mehrere RagTool Instanzen erstellen:

// 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()?;

Der Agent kann die Sammlung auch zur Abfragezeit überschreiben, indem er "collection": "faq" im Tool-Aufruf übergibt.


Feature-Flags

Ziehen Sie nur die Abhängigkeiten, die Sie benötigen:

MerkmalErmöglichtZusätzliche Abhängigkeit
(Standard)Core traits, InMemoryVectorStore, alle chunkersnone
geminiGeminiEmbeddingProvideradk-gemini
openaiOpenAIEmbeddingProviderreqwest
qdrantQdrantVectorStoreqdrant-client
lancedbLanceDBVectorStorelancedb, arrow
pgvectorPgVectorStoresqlx
fullAlles oben Genanntealle
# Just core
adk-rag = "2.0.0"

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

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

Hinweis: Die lancedb-Funktion erfordert die Installation von protoc. Installieren Sie mit brew install protobuf (macOS) oder apt install protobuf-compiler (Ubuntu).


Architektur

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

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

Der RagPipeline orchestriert beide Abläufe. Der RagTool kapselt die Pipeline als adk_core::Tool, sodass Agents sie bei Bedarf aufrufen können.


Beispiele ausführen

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

Bewährte Verfahren

PraxisWarum
Verwenden Sie RecursiveChunker als StandardErzeugt natürliche Segmentgrenzen
Abschnitte von 200–500 Zeichen beibehaltenGleicht Präzision und Kontext aus
Verwenden Sie echte Einbettungen in der ProduktionMock-Einbetter sind nur zum Testen
Wenn similarity_threshold > 0Filtert irrelevantes Rauschen heraus
Sammlungen nach Domäne trennenVerbessert die Präzision und ermöglicht Agenten, Suchen gezielter durchzuführen
InMemoryVectorStore nur für die Entwicklung verwendenFür die Produktion auf Qdrant/pgvector wechseln
  • Function Tools - Benutzerdefinierte Tools erstellen
  • LlmAgent - Tools zu Agents hinzufügen
  • Memory - Langzeitgedächtnis (unterschiedlich zu RAG)

Zurück: ← UI Tools | Weiter: Sessions →