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は、チャンク分割器、埋め込みプロバイダー、ベクトルストア、オプションの再ランキングモデルという4つのコンポーネントを組み合わせます。

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がドキュメントを、50文字の重複を持つ256文字のチャンクに分割します
  2. MockEmbedderが各チャンクを64次元のベクトルに変換します
  3. InMemoryVectorStoreがベクトルを保存し、コサイン類似度で検索します
  4. query()が質問を埋め込みに変換し、最も近いチャンクを見つけて、スコア順に返します

ステップ 2: エージェントにRAGを追加する

RAGの真の力が発揮されるのは、エージェントがこれをツールとして使用するときです。RagToolはパイプラインをadk_core::Toolとしてラップします。エージェントは情報が必要になるたびにrag_searchを呼び出します。

GeminiをバックエンドとするエージェントでRagToolを使用すると、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(())
}

ユーザーが「返品ポリシーはどうなっていますか?」と尋ねた場合、エージェントは次の処理を行います。

  1. ナレッジベースを検索する必要があると判断する
  2. {"query": "return policy"}を使用してrag_searchを呼び出す
  3. スコア付きで関連するチャンクを受け取る
  4. チャンクをコンテキストとして使用し、自然な回答を生成する

ステップ3:チャンク分割戦略を選択する

ドキュメントの分割方法は、検索品質に影響します。adk-ragには3つのチャンク分割器が用意されています。

チャンカー最適な用途分割方法
FixedSizeChunker一般的なテキスト、ログ重複を含め、N文字ごと
RecursiveChunker記事、ドキュメント、コードコメント段落 → 文 → 単語
MarkdownChunkerMarkdownファイル、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隣接するチャンク間で共有する文字数chunk_sizeの10~20%にすると、境界での情報損失を防止できます
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専用ベクトル DB を使用する本番環境
LanceDBVectorStorelancedb組み込みベクトル DB(サーバー不要)
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 に切り替える

  • 関数ツール - カスタムツールの作成
  • LlmAgent - エージェントへのツールの追加
  • メモリ - 長期メモリ(RAGとは異なります)

前へ: ← UI ツール | 次へ: セッション →