RAG (Geração Aumentada por Recuperação)

Forneça aos seus agentes uma base de conhecimento para que possam responder a perguntas usando seus próprios dados.


O que é RAG?

RAG permite que seu agente consulte informações relevantes em seus documentos antes de responder a uma pergunta. Em vez de depender apenas daquilo com que o LLM foi treinado, o agente pesquisa seus dados e usa os resultados como contexto.

O fluxo é:

  1. Ingestão — Os documentos são divididos em partes, convertidos em embeddings vetoriais e armazenados
  2. Consulta — Uma pergunta é convertida em embedding e comparada às partes armazenadas por similaridade
  3. Geração — As partes mais relevantes são passadas ao LLM como contexto para sua resposta

Isso significa que seu agente pode responder a perguntas sobre documentação de produtos, políticas da empresa, bases de código ou qualquer texto que você fornecer.

Principais destaques:

  • 📄 Ingestão de qualquer texto — documentação de produtos, markdown, código, políticas
  • 🔍 Pesquisa semântica — encontre conteúdo relevante pelo significado, não apenas por palavras-chave
  • 🤖 Recuperação agêntica — o agente decide quando pesquisar por meio de RagTool
  • 🔌 Backends intercambiáveis — troque provedores de embeddings e armazenamentos vetoriais sem alterar o código

Instalação

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

Etapa 1: Crie um pipeline

Um RagPipeline conecta quatro componentes: um particionador, um provedor de embeddings, um armazenamento vetorial e um reranker opcional.

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

Como funciona:

  1. FixedSizeChunker divide o documento em partes de 256 caracteres, com sobreposição de 50 caracteres
  2. MockEmbedder converte cada parte em um vetor de 64 dimensões
  3. InMemoryVectorStore armazena os vetores e pesquisa usando similaridade de cosseno
  4. query() converte a pergunta em embedding, encontra as partes mais próximas e as retorna classificadas por pontuação

Etapa 2: Adicione RAG a um agente

O verdadeiro poder de RAG aparece quando um agente o utiliza como uma ferramenta. RagTool encapsula o pipeline como um adk_core::Tool — o agente chama rag_search sempre que precisa de informações.

Quando você usa RagTool com agentes baseados no Gemini, ADK normaliza automaticamente o resultado da ferramenta em uma resposta de função compatível com o Gemini. Isso é importante porque rag_search naturalmente retorna uma lista de partes, enquanto o Gemini espera que functionResponse.response seja um objeto JSON na transmissão.

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

Quando um usuário pergunta "Qual é a sua política de devolução?", o agente:

  1. Decide que precisa pesquisar na base de conhecimento
  2. Chama rag_search com {"query": "return policy"}
  3. Recebe as partes relevantes com suas pontuações
  4. Usa as partes como contexto para gerar uma resposta natural

Etapa 3: Escolha uma estratégia de divisão em partes

A forma como você divide os documentos afeta a qualidade da recuperação. adk-rag fornece três divisores em partes:

Divisor de blocosMelhor paraComo divide
FixedSizeChunkerTextos gerais, registrosA cada N caracteres com sobreposição
RecursiveChunkerArtigos, documentação, comentários de códigoParágrafos → frases → palavras
MarkdownChunkerArquivos Markdown, READMEsPor cabeçalhos, preservando a hierarquia das seções
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 é a melhor opção padrão — ela tenta primeiro as quebras de parágrafo, depois os limites das frases e, por fim, os limites das palavras, produzindo fragmentos mais naturais do que a divisão em tamanhos fixos.

MarkdownChunker adiciona um campo de metadados header_path a cada fragmento (por exemplo, "Getting Started > Installation"), o que ajuda o agente a citar seções específicas.


Configuração

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()?;
ParâmetroO que controlaOrientação
chunk_sizeMáximo de caracteres por fragmento200–500 para a maioria dos casos de uso. Menor = mais preciso, maior = mais contexto
chunk_overlapCaracteres compartilhados entre fragmentos adjacentes10–20% de chunk_size evita a perda de informações nos limites
top_kNúmero de resultados retornadosMais resultados = mais contexto para o LLM, mas maior uso de tokens
similarity_thresholdPontuação mínima para incluir0,0 retorna tudo; 0,3–0,7 filtra correspondências fracas

Provedores de Embeddings

ProvedorSinalizador de recursoModeloRequer
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");

Você também pode implementar EmbeddingProvider para qualquer serviço de embeddings personalizado.


Backends de armazenamento vetorial

BackendSinalizador de recursoMais adequado para
InMemoryVectorStore(padrão)Desenvolvimento, testes, conjuntos de dados pequenos
QdrantVectorStoreqdrantProdução com banco de dados vetorial dedicado
LanceDBVectorStorelancedbBanco de dados vetorial integrado (nenhum servidor necessário)
PgVectorStorepgvectorQuando você já usa 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?;

Reranqueador personalizado

O NoOpReranker padrão repassa os resultados sem alterações. Escreva um reranqueador personalizado para melhorar a precisão:

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

Várias coleções

Use coleções separadas para diferentes domínios de conhecimento. O agente pode pesquisar coleções específicas ou você pode criar várias instâncias de 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()?;

O agente também pode substituir a coleção no momento da consulta, passando "collection": "faq" na chamada da ferramenta.


Sinalizadores de recursos

Inclua apenas as dependências de que você precisa:

RecursoHabilitaDependência extra
(padrão)Traits principais, InMemoryVectorStore, todos os chunkersnenhuma
geminiGeminiEmbeddingProvideradk-gemini
openaiOpenAIEmbeddingProviderreqwest
qdrantQdrantVectorStoreqdrant-client
lancedbLanceDBVectorStorelancedb, arrow
pgvectorPgVectorStoresqlx
fullTodas as opções acimatodos
# 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"] }

Observação: O recurso lancedb requer que protoc esteja instalado. Instale com brew install protobuf (macOS) ou apt install protobuf-compiler (Ubuntu).


Arquitetura

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

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

O RagPipeline orquestra ambos os fluxos. O RagTool encapsula o pipeline como um adk_core::Tool para que os agentes o chamem sob demanda.


Executar exemplos

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

Práticas recomendadas

PráticaMotivo
Use RecursiveChunker como padrãoProduz limites naturais para os blocos
Mantenha os blocos entre 200 e 500 caracteresEquilibra precisão e contexto
Use embeddings reais em produçãoUse geradores de embeddings simulados apenas para testes
Defina similarity_threshold > 0Filtra ruído irrelevante
Separe as coleções por domínioMelhora a precisão e permite que os agentes direcionem as pesquisas
Use InMemoryVectorStore apenas para desenvolvimentoMude para Qdrant/pgvector em produção


Anterior: ← Ferramentas de UI | Próximo: Sessões →