Community코딩 & 개발github.com

rag-구현

벡터 데이터베이스 선택, 청킹 전략, 임베딩 모델 및 검색 최적화를 포함한 RAG 시스템 구현에 대한 포괄적인 가이드입니다. RAG 시스템 구축, 시맨틱 검색 구현, 검색 품질 최적화 또는 RAG 성능 문제 디버깅 시 사용하세요.

rag-구현란 무엇인가요?

이 스킬은 Retrieval-Augmented Generation (RAG) 시스템을 처음부터 끝까지 구축하기 위한 포괄적인 참조 가이드를 제공합니다. 벡터 데이터베이스 선택(Qdrant, Pinecone, Chroma, Weaviate, Milvus), 다양한 청킹 전략(고정 크기, 의미론적, 계층적, 슬라이딩 윈도우), 임베딩 모델(OpenAI, Sentence Transformers, Cohere), 하이브리드 검색, 쿼리 확장, 재순위화 및 메타데이터 필터링과 같은 검색 최적화 기술을 다룹니다. 또한 검색된 컨텍스트 최적화 및 인용 추적을 위한 컨텍스트 관리 패턴도 포함합니다. 이 스킬은 LangChain, LlamaIndex, Haystack과 같은 프레임워크와 잘 작동하며, Claude Code, Cursor 및 기타 AI 에이전트 플랫폼에서 프롬프트 또는 지식 파일로 사용할 수 있습니다. SKILL.md에는 실제 코드 예제, 구성 비교표 및 프로덕션 배포를 위한 실용적인 조언이 포함되어 있습니다.

지원 대상~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/applied-artificial-intelligence/claude-code-toolkit/tree/main/skills/rag-implementation

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

RAG 구현 패턴

Retrieval-Augmented Generation (RAG) 시스템 구현에 대한 포괄적인 가이드. 벡터 데이터베이스 선택, 청킹 전략, 임베딩 모델, 검색 최적화 및 프로덕션 배포 패턴을 다룹니다.


빠른 참조

이 스킬을 사용해야 하는 경우:

  • RAG/시맨틱 검색 시스템 구축
  • 문서 검색 파이프라인 구현
  • 벡터 데이터베이스 성능 최적화
  • 검색 품질 문제 디버깅
  • 벡터 데이터베이스 옵션 선택
  • 청킹 전략 설계
  • 하이브리드 검색 구현

다루는 기술:

  • 벡터 DB: Qdrant, Pinecone, Chroma, Weaviate, Milvus
  • 임베딩: OpenAI, Sentence Transformers, Cohere
  • 프레임워크: LangChain, LlamaIndex, Haystack

1부: 벡터 데이터베이스 선택

데이터베이스 비교 매트릭스

데이터베이스최적 사용 사례배포 방식성능비용
Qdrant자체 호스팅, 프로덕션Docker/K8s우수 (Rust)무료 (자체 호스팅)
Pinecone관리형, 빠른 프로토타이핑클라우드우수사용량 기반 과금
Chroma로컬 개발, 임베디드인프로세스좋음 (Python)무료
Weaviate복잡한 스키마, GraphQLDocker/클라우드우수 (Go)무료 + 클라우드
Milvus대규모 분산 환경K8s우수 (C++)무료 (자체 호스팅)

Qdrant 설정 (프로덕션 권장)

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

# 클라이언트 초기화 (로컬 또는 클라우드)
client = QdrantClient(url="http://localhost:6333")  # 또는 클라우드 URL

# 컬렉션 생성
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1536,  # OpenAI text-embedding-3-small 차원
        distance=Distance.COSINE  # 또는 DOT, EUCLID
    )
)

# 페이로드와 함께 벡터 삽입
client.upsert(
    collection_name="documents",
    points=[
        PointStruct(
            id=1,
            vector=[0.1, 0.2, ...],  # 1536 차원
            payload={
                "text": "문서 내용",
                "source": "doc.pdf",
                "page": 1,
                "metadata": {...}
            }
        )
    ]
)

# 검색
results = client.search(
    collection_name="documents",
    query_vector=[0.1, 0.2, ...],
    limit=5,
    score_threshold=0.7  # 최소 유사도
)

Pinecone 설정 (관리형 서비스)

from pinecone import Pinecone, ServerlessSpec

# 초기화
pc = Pinecone(api_key="your-key")

# 인덱스 생성
pc.create_index(
    name="documents",
    dimension=1536,
    metric="cosine",
    spec=ServerlessSpec(cloud="aws", region="us-east-1")
)

# 인덱스 가져오기
index = pc.Index("documents")

# 벡터 업서트
index.upsert(vectors=[
    ("doc1", [0.1, 0.2, ...], {"text": "...", "source": "..."})
])

# 쿼리
results = index.query(
    vector=[0.1, 0.2, ...],
    top_k=5,
    include_metadata=True
)

2부: 청킹 전략

전략 1: 고정 크기 청킹 (간단, 빠름)

def fixed_size_chunking(text: str, chunk_size: int = 512, overlap: int = 50) -> list[str]:
    """
    텍스트를 중첩(overlap)이 있는 고정 크기 청크로 분할합니다.

    장점: 간단하고 예측 가능한 청크 크기
    단점: 문장 중간에 끊길 수 있으며 의미적 경계가 불량함
    """
    words = text.split()
    chunks = []

    for i in range(0, len(words), chunk_size - overlap):
        chunk = ' '.join(words[i:i + chunk_size])
        chunks.append(chunk)

    return chunks

# 사용법
chunks = fixed_size_chunking(document, chunk_size=512, overlap=50)

사용 시기:

  • 간단한 문서 (로그, 대본)
  • 프로토타이핑/MVP
  • 일관된 토큰 예산 필요 시

전략 2: 의미적 청킹 (더 나은 품질)

from langchain.text_splitter import RecursiveCharacterTextSplitter

def semantic_chunking(text: str, chunk_size: int = 1000, overlap: int = 200) -> list[str]:
    """
    의미적 경계 (문단, 문장)를 기준으로 분할합니다.

    장점: 의미 보존, 더 나은 검색 품질
    단점: 가변 청크 크기, 느린 처리 속도
    """
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=overlap,
        separators=["\n\n", "\n", ". ", " ", ""],  # 우선 순위 순서
        length_function=len
    )

    return splitter.split_text(text)

# 사용법
chunks = semantic_chunking(document, chunk_size=1000, overlap=200)

사용 시기:

  • 긴 형식의 문서 (기사, 책, 보고서)
  • 품질 > 속도
  • 자연어 콘텐츠

전략 3: 계층적 청킹 (구조화된 문서에 최적)

def hierarchical_chunking(document: dict) -> list[dict]:
    """
    문서 구조 (섹션, 하위 섹션)를 기반으로 청킹합니다.

    장점: 계층 구조 보존, 부모-자식 검색 가능
    단점: 구조화된 입력 필요, 더 복잡함
    """
    chunks = []

    for section in document['sections']:
        # 부모 청크 (섹션 요약)
        chunks.append({
            'text': section['title'] + '\n' + section['summary'],
            'type': 'parent',
            'section_id': section['id']
        })

        # 자식 청크 (문단)
        for para in section['paragraphs']:
            chunks.append({
                'text': para,
                'type': 'child',
                'parent_id': section['id']
            })

    return chunks

사용 시기:

  • 기술 문서
  • 목차가 있는 책
  • 법률 문서
  • 컨텍스트 계층 구조 보존 필요 시

전략 4: 슬라이딩 윈도우 (최대 컨텍스트 보존)

def sliding_window_chunking(text: str, window_size: int = 512, stride: int = 256) -> list[str]:
    """
    최대 컨텍스트를 위한 중첩 윈도우입니다.

    장점: 경계에서 정보 손실 없음
    단점: 저장 공간 오버헤드 (중복 콘텐츠)
    """
    words = text.split()
    chunks = []

    for i in range(0, len(words) - window_size + 1, stride):
        chunk = ' '.join(words[i:i + window_size])
        chunks.append(chunk)

    return chunks

사용 시기:

  • 중요한 검색 정확도 필요 시
  • 짧은 쿼리가 더 넓은 컨텍스트 필요 시
  • 저장 비용이 문제되지 않을 때

3부: 임베딩 모델

모델 선택 가이드

모델차원속도품질비용사용 사례
OpenAI text-embedding-3-small1536빠름우수$0.02/1M 토큰프로덕션, 일반 목적
OpenAI text-embedding-3-large3072중간최고$0.13/1M 토큰고품질 검색
all-MiniLM-L6-v2384매우 빠름좋음무료자체 호스팅, 프로토타이핑
all-mpnet-base-v2768빠름매우 좋음무료자체 호스팅, 품질
Cohere embed-english-v3.01024빠름우수$0.10/1M 토큰시맨틱 검색 중심

OpenAI 임베딩 (권장)

from openai import OpenAI

client = OpenAI(api_key="your-key")

def get_embeddings(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
    """
    OpenAI를 사용하여 임베딩을 생성합니다.

    배치 크기: 요청당 최대 2048개 입력
    속도 제한: 티어 제한 확인
    """
    response = client.embeddings.create(
        model=model,
        input=texts
    )

    return [item.embedding for item in response.data]

# 사용법
chunks = ["청크 1", "청크 2", ...]
embeddings = get_embeddings(chunks)

Sentence Transformers (자체 호스팅)

from sentence_transformers import SentenceTransformer

# 모델 로드 (첫 다운로드 후 캐시됨)
model = SentenceTransformer('all-MiniLM-L6-v2')

def get_embeddings_local(texts: list[str]) -> list[list[float]]:
    """
    로컬에서 임베딩을 생성합니다 (API 비용 없음).

    100개 이상의 배치에는 GPU 권장
    소규모 배치에는 CPU 사용 가능
    """
    return model.encode(texts, show_progress_bar=True).tolist()

# 사용법
embeddings = get_embeddings_local(chunks)

4부: 검색 최적화

기술 1: 하이브리드 검색 (밀집 + 희소)

from qdrant_client.models import Filter, FieldCondition, MatchValue

def hybrid_search(query: str, query_vector: list[float], top_k: int = 10):
    """
    밀집(벡터) 검색과 희소(키워드) 검색을 결합합니다.

    밀집: 시맨틱 유사도
    희소: 정확한 키워드 일치
    """
    # 밀집 검색
    dense_results = client.search(
        collection_name="documents",
        query_vector=query_vector,
        limit=top_k * 2  # 더 많은 후보 확보
    )

    # 희소 검색 (메타데이터를 통한 BM25)
    sparse_results = client.search(
        collection_name="documents",
        query_filter=Filter(
            must=[
                FieldCondition(
                    key="text",
                    match=MatchValue(value=query)
                )
            ]
        ),
        limit=top_k * 2
    )

    # 병합 및 재순위화
    combined = merge_results(dense_results, sparse_results, weights=(0.7, 0.3))
    return combined[:top_k]

기술 2: 쿼리 확장

def expand_query(query: str) -> list[str]:
    """
    더 나은 재현율을 위해 쿼리 변형을 생성합니다.

    기술:
    - 동의어 확장
    - 질문 재구성
    - 엔티티 추출
    """
    from openai import OpenAI
    client = OpenAI()

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": "사용자 쿼리의 대체 표현 3개를 생성하세요."
        }, {
            "role": "user",
            "content": query
        }]
    )

    expanded = [query] + response.choices[0].message.content.split('\n')
    return expanded

# 사용법
queries = expand_query("신경망을 학습하는 방법은?")
# → ["신경망을 학습하는 방법은?",
#    "신경망 학습 기술은 무엇인가?",
#    "신경망 최적화 방법",
#    "딥러닝 모델 학습"]

기술 3: 재순위화

from sentence_transformers import CrossEncoder

# 크로스 인코더 로드 (재순위화를 위해 바이 인코더보다 우수)
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

def rerank_results(query: str, results: list[dict], top_k: int = 5) -> list[dict]:
    """
    크로스 인코더를 사용하여 초기 결과를 재순위화합니다.

    초기 검색보다 정확하지만 느림
    상위 20-50개 후보에만 사용
    """
    # 각 쿼리-문서 쌍 점수 매기기
    pairs = [(query, result['text']) for result in results]
    scores = reranker.predict(pairs)

    # 점수를 결과와 결합
    for result, score in zip(results, scores):
        result['rerank_score'] = float(score)

    # 정렬 후 상위 top_k 반환
    reranked = sorted(results, key=lambda x: x['rerank_score'], reverse=True)
    return reranked[:top_k]

기술 4: 메타데이터 필터링

def filtered_search(
    query_vector: list[float],
    filters: dict,
    top_k: int = 5
):
    """
    메타데이터(날짜, 카테고리, 작성자 등)로 검색을 필터링합니다.

    사전 필터: 더 빠르지만 결과를 놓칠 수 있음
    사후 필터: 더 많은 결과를 제공하지만 느림
    """
    from qdrant_client.models import Filter, FieldCondition, Range

    # 필터 조건 생성
    conditions = []

    if 'date_range' in filters:
        conditions.append(
            FieldCondition(
                key="date",
                range=Range(
                    gte=filters['date_range']['start'],
                    lte=filters['date_range']['end']
                )
            )
        )

    if 'category' in filters:
        conditions.append(
            FieldCondition(
                key="category",
                match=MatchValue(value=filters['category'])
            )
        )

    # 필터로 검색
    results = client.search(
        collection_name="documents",
        query_vector=query_vector,
        query_filter=Filter(must=conditions) if conditions else None,
        limit=top_k
    )

    return results

5부: 컨텍스트 관리

패턴 1: 검색된 컨텍스트 최적화

def optimize_context(query: str, retrieved_docs: list[str], max_tokens: int = 4000) -> str:
    """
    검색된 컨텍스트를 LLM 컨텍스트 창에 맞게 최적화합니다.

    전략:
    1. 관련성 기반 자르기
    2. 추출 요약
    3. 중복 제거
    """
    # 관련성별 정렬
    sorted_docs = sorted(retrieved_docs, key=lambda d: d['score'], reverse=True)

    # 토큰 예산 내에서 컨텍스트 구축
    context_parts = []
    total_tokens = 0

    for doc in sorted_docs:
        doc_tokens = estimate_tokens(doc['text'])

        if total_tokens + doc_tokens <= max_tokens:
            context_parts.append(f"[출처: {doc['source']}]\n{doc['text']}")
            total_tokens += doc_tokens
        else:
            # 마지막 문서를 맞게 자르기
            remaining = max_tokens - total_tokens
            truncated = truncate_to_tokens(doc['text'], remaining)
            context_parts.append(f"[출처: {doc['source']}]\n{truncated}")
            break

    return "\n\n---\n\n".join(context_parts)

패턴 2: 인용 추적

def generate_with_citations(query: str, context: str, sources: list[dict]) -> dict:
    """
    인용 추적과 함께 답변을 생성합니다.

    반환:
    - answer: 생성된 텍스트
    - citations: 사용된 출처 문서 목록
    """
    from openai import OpenAI
    client = OpenAI()

    # 출처 맵 생성
    source_map = {i+1: source for i, source in enumerate(sources)}
    numbered_context = "\n\n".join([
        f"[{i+1}] {source['text']}"
        for i, source in enumerate(sources)
    ])

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": "제공된 출처를 사용하여 답변하세요. 출처는 [1], [2] 등으로 인용하세요."
        }, {
            "role": "user",
            "content": f"컨텍스트:\n{numbered_context}\n\n질문: {query}"
        }]
    )

    answer = response.choices[0].message.content

    return {
        "answer": answer,
        "citations": extract_citations(answer, source_map)
    }

패턴 3: 대화형 검색 (멀티턴 RAG)

class ConversationRAG:
    """
    대화 컨텍스트로 검색을 향상시키는 멀티턴 RAG 시스템입니다.
    """
    def __init__(self, vector_store, llm_client):
        self.vector_store = vector_store
        self.llm = llm_client
        self.history = []

    def query(self, user_input: str, top_k: int = 5) -> str:
        # 히스토리로 쿼리 재구성
        query = self._rewrite_with_context(user_input)

        # 관련 문서 검색
        results = self.vector_store.search(query, top_k=top_k)

        # 컨텍스트와 히스토리로 답변 생성
        answer = self._generate_answer(query, results, self.history)

        # 히스토리 업데이트
        self.history.append({"user": user_input, "assistant": answer})

        return answer

6부: 프로덕션 배포

시스템 아키텍처

┌──────────┐     ┌──────────┐     ┌──────────┐
│  문서 처리  │────▶│  벡터 저장소  │────▶│  검색 엔진   │
└──────────┘     └──────────┘     └──────────┘
       │                              │
       ▼                              ▼
┌──────────┐                    ┌──────────┐
│  청킹 파이프라인│                    │  쿼리 최적화  │
└──────────┘                    └──────────┘
       │                              │
       ▼                              ▼
┌──────────┐                    ┌──────────┐
│  임베딩 모델 │                    │  LLM  추론 │
└──────────┘                    └──────────┘
       │                              │
       └──────────┬──────────┬──────────┘
                  ▼          ▼
            ┌──────────┐ ┌──────────┐
            │  캐싱 계층  │ │  모니터링   │
            └──────────┘ └──────────┘

성능 최적화 팁

  1. 벡터 인덱싱: 대규모 데이터셋의 경우 IVF (Inverted File) 또는 HNSW (Hierarchical Navigable Small World) 인덱스 사용
  2. 캐싱: 자주 액세스하는 쿼리 결과를 Redis에 캐싱
  3. 일괄 처리: 임베딩 요청을 배치로 묶어 API 호출 최소화
  4. 비동기: 비동기 클라이언트를 위해 asyncio 사용
  5. 압축: 차원 축소를 위해 벡터 양자화 또는 PCA 사용

모니터링 메트릭

메트릭목표임계값
검색 정밀도> 0.85< 0.70
검색 재현율> 0.80< 0.60
엔드투엔드 지연 시간< 2초> 5초
벡터 DB 가용성99.9%< 99.5%
토큰 사용량예산 내예산 초과 시 경고

모범 사례 요약

시작하기

  • Qdrant 또는 Pinecone으로 빠른 프로토타이핑부터 시작
  • 의미적 청킹과 기본 임베딩 사용
  • 검색 품질을 측정하는 평가 세트 구축

프로덕션 준비

  • 정밀도를 높이기 위해 하이브리드 검색 구현
  • 재순위화를 위해 크로스 인코더 사용
  • 지연 시간을 줄이기 위해 캐싱 추가

고급 최적화

  • 검색을 안내하는 메타데이터 필터링
  • 더 나은 재현율을 위해 쿼리 확장 사용
  • 컨텍스트를 LLM 내부에 맞게 최적화

일반적인 함정

  • ❌ 청킹 전략 무시
  • ❌ 검색 품질 평가 실패
  • ❌ 컨텍스트 창 제한 무시
  • ❌ 피드백 루프 없음

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬