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, vor der Beantwortung einer Frage relevante Informationen aus Ihren Dokumenten abzurufen. Statt sich ausschließlich auf das zu verlassen, worauf der LLM trainiert wurde, durchsucht der Agent Ihre Daten und verwendet die Ergebnisse als Kontext.

Der Ablauf:

  1. Aufnahme — Dokumente werden in Abschnitte aufgeteilt, in Vektoreinbettungen umgewandelt und gespeichert
  2. Abfrage — Eine Frage wird eingebettet und anhand der Ähnlichkeit mit gespeicherten Abschnitten abgeglichen
  3. Generierung — Die relevantesten Abschnitte werden als Kontext für die Antwort an den LLM übergeben

Dadurch kann Ihr Agent Fragen zu Produktdokumentationen, Unternehmensrichtlinien, Codebasen oder beliebigen Texten beantworten, die Sie ihm zuführen.

Wichtige Merkmale:

  • 📄 Beliebigen Text aufnehmen — Produktdokumentationen, Markdown, Code, Richtlinien
  • 🔍 Semantische Suche — Relevante Inhalte anhand ihrer Bedeutung statt nur anhand von Schlüsselwörtern finden
  • 🤖 Agentengesteuerter Abruf — Der Agent entscheidet über RagTool, wann gesucht werden soll
  • 🔌 Austauschbare Backends — Einbettungsanbieter und Vektorspeicher austauschen, ohne den Code zu ändern

Installation

[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"] }

Schritt 1: Eine Pipeline erstellen

Eine RagPipeline verbindet vier Komponenten: einen Chunker, einen Einbettungsanbieter, einen Vektorspeicher und optional einen 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(())
}

Funktionsweise:

  1. FixedSizeChunker teilt das Dokument in Abschnitte mit 256 Zeichen und einer Überlappung von 50 Zeichen auf
  2. MockEmbedder wandelt jeden Abschnitt in einen 64-dimensionalen Vektor um
  3. InMemoryVectorStore speichert die Vektoren und führt die Suche anhand der Kosinusähnlichkeit durch
  4. query() bettet die Frage ein, findet die ähnlichsten Abschnitte und gibt sie nach Punktzahl sortiert 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 kapselt die Pipeline als adk_core::Tool – der Agent ruft rag_search auf, wann immer er Informationen benötigt.

Wenn Sie RagTool mit Gemini-basierten Agenten verwenden, normalisiert ADK das Tool-Ergebnis automatisch in eine mit Gemini kompatible Funktionsantwort. Das ist wichtig, weil rag_search von Natur aus eine Liste von Chunks zurückgibt, während Gemini erwartet, dass functionResponse.response über die Leitung ein JSON-Objekt 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-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(())
}

Wenn ein Benutzer fragt: „Wie lautet Ihre Rückgaberegelung?“, geht der Agent folgendermaßen vor:

  1. Er entscheidet, dass er die Wissensdatenbank durchsuchen muss
  2. Er ruft rag_search mit {"query": "return policy"} auf
  3. Er erhält die relevanten Chunks mit Bewertungen zurück
  4. Er verwendet die Chunks als Kontext, um eine natürlich formulierte Antwort zu generieren

Schritt 3: Eine Chunking-Strategie auswählen

Wie Sie Dokumente aufteilen, beeinflusst die Qualität des Abrufs. adk-rag stellt drei Chunker bereit:

ChunkerAm besten geeignet fürAufteilung
FixedSizeChunkerAllgemeine Texte, ProtokolleAlle N Zeichen mit Überlappung
RecursiveChunkerArtikel, Dokumentationen, CodekommentareAbsätze → Sätze → Wörter
MarkdownChunkerMarkdown-Dateien, READMEsNach Überschriften, wobei die Abschnittshierarchie erhalten bleibt
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 – zunächst werden Absatzumbrüche berücksichtigt, dann Satzgrenzen und anschließend Wortgrenzen. Dadurch entstehen natürlichere Abschnitte als bei einer Aufteilung in Blöcke fester Größe.

MarkdownChunker fügt jedem Abschnitt ein Metadatenfeld header_path hinzu (z. B. "Getting Started > Installation"), wodurch der Agent bestimmte Abschnitte zitieren kann.


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 steuertHinweise
chunk_sizeMaximale Zeichenanzahl pro Abschnitt200–500 für die meisten Anwendungsfälle. Kleiner = präziser, größer = mehr Kontext
chunk_overlapGemeinsame Zeichen zwischen benachbarten Abschnitten10–20 % von chunk_size verhindert den Verlust von Informationen an den Abschnittsgrenzen
top_kAnzahl der zurückgegebenen ErgebnisseMehr Ergebnisse = mehr Kontext für LLM, aber höherer Token-Verbrauch
similarity_thresholdMindestpunktzahl für die Aufnahme0,0 gibt alles zurück; 0,3–0,7 filtert schwache Übereinstimmungen heraus

Embedding-Anbieter

AnbieterFunktionsflagModellErfordert
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");

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


Vektorstore-Backends

BackendFeature-FlagAm besten geeignet für
InMemoryVectorStore(Standard)Entwicklung, Tests, kleine Datensätze
QdrantVectorStoreqdrantProduktion mit dedizierter Vektordatenbank
LanceDBVectorStorelancedbEingebettete Vektordatenbank (kein Server erforderlich)
PgVectorStorepgvectorWenn Sie PostgreSQL bereits 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 standardmäßige NoOpReranker leitet 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 Instanzen von RagTool 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 zur Abfragezeit auch überschreiben, indem "collection": "faq" im Tool-Aufruf übergeben wird.


Feature-Flags

Binden Sie nur die Abhängigkeiten ein, die Sie benötigen:

FunktionAktiviertZusätzliche Abhängigkeit
(Standard)Kernmerkmale, InMemoryVectorStore, alle Chunkerkeine
geminiGeminiEmbeddingProvideradk-gemini
openaiOpenAIEmbeddingProviderreqwest
qdrantQdrantVectorStoreqdrant-client
lancedbLanceDBVectorStorelancedb, arrow
pgvectorPgVectorStoresqlx
fullAlle oben genanntenalle
# 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"] }

Hinweis: Die lancedb-Funktion erfordert, dass protoc installiert ist. Installation 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

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


Beispiele ausführen

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

Bewährte Vorgehensweisen

MethodeWarum
RecursiveChunker als Standard verwendenErzeugt natürliche Chunk-Grenzen
Chunks mit 200–500 Zeichen beibehaltenGleicht Präzision und Kontext aus
Verwende echte Embeddings in der ProduktionMock-Embeddings sind nur zum Testen vorgesehen
Setze similarity_threshold > 0Filtert irrelevantes Rauschen heraus
Trenne Sammlungen nach DomäneVerbessert die Präzision und ermöglicht es Agenten, Suchvorgänge gezielt auszurichten
Verwende InMemoryVectorStore nur für die EntwicklungWechsle in der Produktion zu Qdrant/pgvector

  • Funktionstools – Benutzerdefinierte Tools erstellen
  • LlmAgent – Tools zu Agenten hinzufügen
  • Speicher – Langzeitspeicher (unterscheidet sich von RAG)

Zurück: ← UI-Tools | Weiter: Sitzungen →