RAG (검색 증강 생성)

에이전트에 지식 기반을 제공하여 자체 데이터를 사용해 질문에 답변할 수 있도록 하세요.


RAG란 무엇인가요?

RAG을 사용하면 에이전트가 질문에 답변하기 전에 문서에서 관련 정보를 조회할 수 있습니다. 에이전트는 LLM이 학습한 내용에만 의존하는 대신, 자체 데이터를 검색하고 그 결과를 컨텍스트로 사용합니다.

흐름은 다음과 같습니다.

  1. 수집 — 문서를 청크로 나누고, 벡터 임베딩으로 변환한 후 저장합니다.
  2. 쿼리 — 질문을 임베딩하고 유사도를 기준으로 저장된 청크와 매칭합니다.
  3. 생성 — 가장 관련성 높은 청크를 답변의 컨텍스트로 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(())
}

작동 방식:

  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은 세 가지 청크 분할기를 제공합니다:

분할기적합한 용도분할 방식
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로 전환


이전: ← UI 도구 | 다음: 세션 →