RAG(検索拡張生成)
エージェントにナレッジベースを提供し、自分のデータを使って質問に回答できるようにします。
RAGとは?
RAGを使うと、エージェントは質問に回答する前に、ドキュメントから関連情報を検索できます。LLMが学習した内容だけに頼るのではなく、エージェントがデータを検索し、その結果をコンテキストとして使用します。
処理の流れは次のとおりです。
- 取り込み — ドキュメントをチャンクに分割し、ベクトル埋め込みに変換して保存します
- クエリ — 質問を埋め込みに変換し、類似度によって保存されたチャンクと照合します
- 生成 — 最も関連性の高いチャンクをコンテキストとして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(())
}
仕組み:
FixedSizeChunkerがドキュメントを、50文字の重複を持つ256文字のチャンクに分割しますMockEmbedderが各チャンクを64次元のベクトルに変換しますInMemoryVectorStoreがベクトルを保存し、コサイン類似度で検索します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(())
}
ユーザーが「返品ポリシーはどうなっていますか?」と尋ねた場合、エージェントは次の処理を行います。
- ナレッジベースを検索する必要があると判断する
{"query": "return policy"}を使用してrag_searchを呼び出す- スコア付きで関連するチャンクを受け取る
- チャンクをコンテキストとして使用し、自然な回答を生成する
ステップ3:チャンク分割戦略を選択する
ドキュメントの分割方法は、検索品質に影響します。adk-ragには3つのチャンク分割器が用意されています。
| チャンカー | 最適な用途 | 分割方法 |
|---|---|---|
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 | 隣接するチャンク間で共有する文字数 | chunk_sizeの10~20%にすると、境界での情報損失を防止できます |
top_k | 返される結果の数 | 結果が多いほど LLM のコンテキストが増えますが、トークン使用量も増加します |
similarity_threshold | 含める最小スコア | 0.0 はすべてを返し、0.3~0.7 は一致度の低い結果を除外します |
埋め込みプロバイダー
| プロバイダー | 機能フラグ | モデル | 必要条件 |
|---|---|---|---|
GeminiEmbeddingProvider | gemini | gemini-embedding-2 | GOOGLE_API_KEY |
OpenAIEmbeddingProvider | openai | text-embedding-3-small | OPENAI_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 | (デフォルト) | 開発、テスト、小規模なデータセット |
QdrantVectorStore | qdrant | 専用ベクトル DB を使用する本番環境 |
LanceDBVectorStore | lancedb | 組み込みベクトル DB(サーバー不要) |
PgVectorStore | pgvector | すでに 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、すべてのチャンク分割器 | なし |
gemini | GeminiEmbeddingProvider | adk-gemini |
openai | OpenAIEmbeddingProvider | reqwest |
qdrant | QdrantVectorStore | qdrant-client |
lancedb | LanceDBVectorStore | lancedb, arrow |
pgvector | PgVectorStore | sqlx |
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 に切り替える |