RAG (검색 증강 생성)
에이전트에 지식 기반을 제공하여 자체 데이터를 사용해 질문에 답변할 수 있도록 하세요.
RAG란 무엇인가요?
RAG을 사용하면 에이전트가 질문에 답변하기 전에 문서에서 관련 정보를 조회할 수 있습니다. 에이전트는 LLM이 학습한 내용에만 의존하는 대신, 자체 데이터를 검색하고 그 결과를 컨텍스트로 사용합니다.
흐름은 다음과 같습니다.
- 수집 — 문서를 청크로 나누고, 벡터 임베딩으로 변환한 후 저장합니다.
- 쿼리 — 질문을 임베딩하고 유사도를 기준으로 저장된 청크와 매칭합니다.
- 생성 — 가장 관련성 높은 청크를 답변의 컨텍스트로 LLM에 전달합니다.
이를 통해 에이전트는 제품 문서, 회사 정책, 코드베이스 또는 입력한 모든 텍스트에 관한 질문에 답변할 수 있습니다.
주요 특징:
- 📄 모든 텍스트 수집 — 제품 문서, 마크다운, 코드, 정책
- 🔍 시맨틱 검색 — 단순한 키워드가 아니라 의미를 기준으로 관련 콘텐츠 검색
- 🤖 에이전트 기반 검색 — 에이전트가
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은 청커, 임베딩 제공자, 벡터 저장소 및 선택적 재순위 지정기라는 네 가지 구성 요소를 연결합니다.
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은 세 가지 청크 분할기를 제공합니다:
| 분할기 | 적합한 용도 | 분할 방식 |
|---|---|---|
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로 전환 |