RAG(Retrieval-Augmented Generation) 시스템의 검색 정확도는Embedding 모델의 품질에 직접적으로 좌우됩니다. 문서를 벡터로 변환하고, 사용자 쿼리와 매칭시키는 이 핵심 단계에서 잘못된 모델 선택은 전체 시스템의 응답 품질을 좌절시킵니다.

저는 실제 프로덕션 RAG 시스템을 구축하며 여러 Embedding 모델을 비교 테스트했습니다. 이번 가이드에서는 HolySheep API를 활용한 Embedding 최적화 전략과 비용 효율적인 모델 선택 방법을 상세히 다룹니다.

HolySheep API vs 공식 API vs 다른 릴레이 서비스 비교

비교 항목 HolySheep API 공식 OpenAI API 공식 Azure OpenAI 기타 중계 서비스
text-embedding-3-small $0.10/MTok $0.02/MTok $0.02/MTok $0.05~0.15/MTok
text-embedding-3-large $0.10/MTok $0.13/MTok $0.13/MTok $0.20~0.40/MTok
결제 방식 ローカル 결제 (해외 카드 불필요) 국제 신용카드 필수 국제 신용카드 필수 다양함 (불안정)
다중 모델 지원 ✅ OpenAI + Voyage + Cohere + Jina ❌ OpenAI만 ❌ OpenAI만 ⚠️ 제한적
단일 API 키 통합 ✅ 모든 모델 1개 키 ❌ 각 서비스별 키 필요 ❌ 별도 키 관리 ⚠️ 서비스별 분리
신뢰성/가동률 ✅ 99.9% 이상 ⚠️ 지역별 차이 ✅ 기업용 SLA ⚠️ 불안정
한국어 지원 ✅ 최적화됨 ⚠️ 보통 ⚠️ 보통 ⚠️ 다양
무료 크레딧 ✅ 가입 시 제공 ❌ 없음 ❌ 없음 ⚠️ 드묿�

RAG 시스템에서 Embedding이 중요한 이유

RAG 파이프라인은 크게 3단계로 구성됩니다:

  1. 색인(Indexing) 단계: 문서를 청크로 분할하고 Embedding 모델로 벡터 변환 후 벡터 DB에 저장
  2. 검색(Retrieval) 단계: 사용자 쿼리를 Embedding으로 변환하고 유사도最高的 문서를 검색
  3. 생성(Generation) 단계: 검색된 문서와 쿼리를 LLM에 전달하여 답변 생성

Embedding 품질이 낮으면 Retrieval 단계에서 관련 없는 문서를 가져오고, 이는 최종 답변의 정확성을 심각하게 저하시킵니다.

HolySheep API를 활용한 Embedding 구현

1. 기본 Embedding API 호출

import requests
import os

HolySheep API 설정

HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" def get_embedding(text, model="text-embedding-3-small"): """ HolySheep API를 사용한 텍스트 Embedding 생성 """ url = f"{HOLYSHEEP_BASE_URL}/embeddings" headers = { "Authorization": f"Bearer {HOLYSHEEP_API_KEY}", "Content-Type": "application/json" } payload = { "input": text, "model": model } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: result = response.json() return result["data"][0]["embedding"] else: raise Exception(f"Embedding API 오류: {response.status_code} - {response.text}")

사용 예시

try: # 짧은 텍스트 임베딩 short_embedding = get_embedding("안녕하세요, RAG 시스템입니다.") print(f"짧은 텍스트 Embedding 차원: {len(short_embedding)}") # 긴 문서 임베딩 (자동 청킹) long_embedding = get_embedding("""RAG(Retrieval-Augmented Generation)는 검색 증강 생성 기술입니다. 대규모 언어 모델과 외부 지식 베이스를 결합하여 최신 정보와 정확한 답변을 생성합니다.""") print(f"긴 문서 Embedding 차원: {len(long_embedding)}") except Exception as e: print(f"오류 발생: {e}")

2. 문서 배치 처리 및 벡터 DB 저장

import requests
import numpy as np
from typing import List, Dict
import json

HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY"
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"

class DocumentEmbedder:
    """
    HolySheep API를 활용한 문서 임베딩 및 벡터 DB 저장 클래스
    """
    
    def __init__(self, api_key: str, model: str = "text-embedding-3-small"):
        self.api_key = api_key
        self.model = model
        self.base_url = HOLYSHEEP_BASE_URL
        self.vector_store = []  # 간단한 인메모리 벡터 저장소
    
    def _batch_embed(self, texts: List[str], batch_size: int = 100) -> List[List[float]]:
        """
        배치 단위로 Embedding 생성 (API 호출 최적화)
        """
        all_embeddings = []
        
        for i in range(0, len(texts), batch_size):
            batch = texts[i:i + batch_size]
            
            url = f"{self.base_url}/embeddings"
            headers = {
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            }
            
            payload = {
                "input": batch,
                "model": self.model
            }
            
            response = requests.post(url, headers=headers, json=payload)
            
            if response.status_code != 200:
                raise Exception(f"배치 임베딩 실패: {response.status_code}")
            
            result = response.json()
            batch_embeddings = [item["embedding"] for item in result["data"]]
            all_embeddings.extend(batch_embeddings)
            
            print(f"배치 {i//batch_size + 1} 완료: {len(batch)}개 문서 처리")
        
        return all_embeddings
    
    def index_documents(self, documents: List[Dict[str, str]], 
                        chunk_size: int = 500, 
                        overlap: int = 50):
        """
        문서를 청크 분할하고 임베딩 후 벡터 DB에 저장
        
        Args:
            documents: [{"id": "doc1", "content": "문서 내용", "metadata": {...}}]
            chunk_size: 청크 크기 (토큰 기준)
            overlap: 청크 간 중첩 크기
        """
        # 문서를 청크로 분할
        chunks = []
        chunk_ids = []
        doc_ids = []
        
        for doc in documents:
            content = doc["content"]
            # 간단한 청킹 (실제로는 tiktoken 사용 권장)
            words = content.split()
            for i in range(0, len(words), chunk_size - overlap):
                chunk_words = words[i:i + chunk_size]
                if len(chunk_words) < 10:  # 너무 짧은 청크 제외
                    continue
                chunks.append(" ".join(chunk_words))
                chunk_ids.append(f"{doc['id']}_chunk_{len(chunk_ids)}")
                doc_ids.append(doc["id"])
        
        print(f"총 {len(chunks)}개 청크 생성됨")
        
        # 배치 임베딩 생성
        embeddings = self._batch_embed(chunks)
        
        # 벡터 DB에 저장
        for idx, (chunk, embedding, chunk_id, doc_id) in enumerate(zip(
            chunks, embeddings, chunk_ids, doc_ids
        )):
            self.vector_store.append({
                "id": chunk_id,
                "document_id": doc_id,
                "content": chunk,
                "embedding": np.array(embedding),
                "metadata": documents[idx % len(documents)].get("metadata", {})
            })
        
        print(f"벡터 DB에 {len(self.vector_store)}개 임베딩 저장 완료")
        return chunk_ids
    
    def search(self, query: str, top_k: int = 5) -> List[Dict]:
        """
        쿼리와 유사한 상위 k개 문서 검색
        """
        # 쿼리 임베딩
        query_embedding = self._batch_embed([query])[0]
        query_vector = np.array(query_embedding)
        
        # 코사인 유사도 계산
        results = []
        for item in self.vector_store:
            doc_vector = item["embedding"]
            similarity = np.dot(query_vector, doc_vector) / (
                np.linalg.norm(query_vector) * np.linalg.norm(doc_vector)
            )
            results.append({
                "id": item["id"],
                "content": item["content"],
                "similarity": float(similarity),
                "metadata": item["metadata"]
            })
        
        # 유사도 순 정렬
        results.sort(key=lambda x: x["similarity"], reverse=True)
        return results[:top_k]


사용 예시

if __name__ == "__main__": embedder = DocumentEmbedder( api_key=HOLYSHEEP_API_KEY, model="text-embedding-3-small" # 또는 "text-embedding-3-large" ) # 샘플 문서 sample_docs = [ { "id": "doc_001", "content": "HolySheep AI는 글로벌 AI API 게이트웨이입니다. " "다양한 AI 모델을 단일 API 키로 통합하여 사용할 수 있습니다. " "해외 신용카드 없이 로컬 결제가 지원되어 개발자에게 편의성을 제공합니다.", "metadata": {"category": "AI 서비스", "source": "공식 문서"} }, { "id": "doc_002", "content": "RAG 시스템 구축 시 Embedding 모델 선택이 중요합니다. " "text-embedding-3-small은 비용 효율적이면서도 좋은 성능을 제공합니다. " "정확도가 중요한 경우 text-embedding-3-large 사용을 권장합니다.", "metadata": {"category": "기술 가이드", "source": "기술 블로그"} }, { "id": "doc_003", "content": "한국어 기반 RAG 시스템에서는 multilingual Embedding 모델이 " "효과적입니다. Voyage-multilingual-3이나 Cohere embed-multilingual-v3.0 " "등을 고려할 수 있습니다.", "metadata": {"category": "최적화", "source": "개발 경험"} } ] # 문서 색인 chunk_ids = embedder.index_documents(sample_docs) # 검색 테스트 query = "HolySheep AI의 장점은 무엇인가요?" results = embedder.search(query, top_k=2) print("\n=== 검색 결과 ===") for i, result in enumerate(results, 1): print(f"{i}. [유사도: {result['similarity']:.4f}] {result['content'][:100]}...")

Embedding 모델 선택 가이드

한국어 RAG에 최적화된 모델 비교

모델명 차원 한국어 성능 가격 (HolySheep) 권장 용도
text-embedding-3-small 1536 ⭐⭐⭐⭐ (우수) $0.10/MTok 일반 용도, 비용 최적화首选
text-embedding-3-large 3072 ⭐⭐⭐⭐⭐ (최상) $0.10/MTok 높은 정확도 필요 시
voyage-3 1024 ⭐⭐⭐⭐ (우수) $0.12/MTok 다국어 지원, 긴 컨텍스트
voyage-multilingual-3 1024 ⭐⭐⭐⭐⭐ (한국어 최적) $0.12/MTok 한국어 중심 다국어 RAG
embed-english-v3.0 1024 ⭐⭐ (영어 중심) $0.10/MTok 영어 문서만 처리 시
embed-multilingual-v3.0 1024 ⭐⭐⭐⭐ (다국어) $0.10/MTok 다국어 혼합 문서
jina-embeddings-v3 1024 ⭐⭐⭐⭐ (우수) $0.11/MTok 장문 처리, 구조화 검색

사용 사례별 권장 선택

# HolySheep API에서 다양한 Embedding 모델 사용 예시

MODELS_CONFIG = {
    # 1. 비용 최적화 + 일반 용도
    "cost_optimized": {
        "model": "text-embedding-3-small",
        "dimension": 1536,
        "use_case": "블로그, 뉴스, 일반 문서 RAG"
    },
    
    # 2. 최고 정확도
    "high_accuracy": {
        "model": "text-embedding-3-large",
        "dimension": 3072,
        "use_case": "법률 문서, 의료 기록, 기술 문서"
    },
    
    # 3. 한국어 최적화
    "korean_focused": {
        "model": "voyage-multilingual-3",
        "dimension": 1024,
        "use_case": "한국어 중심 기술 문서, 한국어 QA"
    },
    
    # 4. 다국어 지원
    "multilingual": {
        "model": "embed-multilingual-v3.0",
        "dimension": 1024,
        "use_case": "다국어 고객 지원, 글로벌 문서"
    },
    
    # 5. 장문 처리
    "long_context": {
        "model": "jina-embeddings-v3",
        "dimension": 1024,
        "use_case": "긴 계약서, 논문, 도서"
    }
}

def select_embedding_model(use_case: str) -> dict:
    """
    사용 사례에 따른 최적 Embedding 모델 선택
    
    Args:
        use_case: "korean_rag", "legal_doc", "general", "multilingual", "long_doc"
    """
    model_map = {
        "korean_rag": "voyage-multilingual-3",
        "legal_doc": "text-embedding-3-large",
        "technical": "text-embedding-3-large",
        "general": "text-embedding-3-small",
        "multilingual": "embed-multilingual-v3.0",
        "long_doc": "jina-embeddings-v3"
    }
    
    selected_model = model_map.get(use_case, "text-embedding-3-small")
    
    print(f"선택된 모델: {selected_model}")
    return {"model": selected_model}

모델 성능 테스트

def benchmark_models(texts: List[str]): """여러 모델의 성능 비교 벤치마크""" results = {} for config_name, config in MODELS_CONFIG.items(): model = config["model"] try: # HolySheep API 호출 embeddings = get_embedding_batch(texts, model=model) results[config_name] = { "model": model, "embedding_count": len(embeddings), "dimension": len(embeddings[0]) if embeddings else 0, "status": "success" } except Exception as e: results[config_name] = { "model": model, "status": "failed", "error": str(e) } return results

성능 최적화 전략

1. 청킹 전략 최적화

import re
from typing import List, Tuple

def smart_chunking(text: str, 
                   chunk_size: int = 512,
                   overlap: int = 128,
                   model: str = "text-embedding-3-small") -> List[Tuple[str, dict]]:
    """
    스마트 청킹: 문단 구조를 유지하며 의미적으로 분리
    
    Args:
        text: 원본 텍스트
        chunk_size: 목표 청크 크기 (토큰)
        overlap: 청크 간 중첩 (토큰)
        model: 토큰 카운트용 모델
    
    Returns:
        [(청크_텍스트, 메타데이터), ...]
    """
    
    # 문단 단위로 분리
    paragraphs = re.split(r'\n\n+', text)
    
    chunks = []
    current_chunk = []
    current_tokens = 0
    
    for para in paragraphs:
        # 문단 토큰估算 (간단히 단어 수 기반)
        para_tokens = len(para.split()) * 1.3  # 대략적 변환
        
        # 현재 청크에 추가 가능 여부 확인
        if current_tokens + para_tokens <= chunk_size:
            current_chunk.append(para)
            current_tokens += para_tokens
        else:
            # 현재 청크 저장
            if current_chunk:
                chunks.append((
                    "\n\n".join(current_chunk),
                    {"token_count": int(current_tokens)}
                ))
            
            # 중첩 처리
            if overlap > 0 and len(current_chunk) > 1:
                # 이전 청크의 마지막 문단 포함
                overlap_texts = current_chunk[-1:]
                current_chunk = overlap_texts + [para]
                current_tokens = sum(len(t.split()) * 1.3 for t in current_chunk)
            else:
                current_chunk = [para]
                current_tokens = para_tokens
    
    # 마지막 청크 추가
    if current_chunk:
        chunks.append((
            "\n\n".join(current_chunk),
            {"token_count": int(current_tokens)}
        ))
    
    return chunks


계층적 청킹 (Hierarchical Chunking)

def hierarchical_chunking(text: str, max_level1: int = 1000, max_level2: int = 500) -> List[dict]: """ 계층적 청킹: 큰 단위(L1)와 작은 단위(L2) 모두 저장 L1: 섹션 전체 (상위 수준 검색용) L2: 개별 문단 (세밀한 검색용) """ # 섹션 분리 sections = re.split(r'#{1,3}\s+', text) sections = [s.strip() for s in sections if s.strip()] result = { "level1_chunks": [], # 섹션 단위 "level2_chunks": [] # 문단 단위 } for section in sections: # L1: 섹션 전체 if len(section.split()) > 50: # 최소 크기 이상 result["level1_chunks"].append({ "content": section, "type": "section" }) # L2: 문단 단위 분리 paragraphs = section.split('\n\n') for para in paragraphs: if len(para.split()) > 20: result["level2_chunks"].append({ "content": para, "type": "paragraph", "parent": section[:100] # 상위 섹션 참조 }) return result

2. 하이브리드 검색 구현

from sklearn.feature_extraction.text import TfidfVectorizer
from rank_bm25 import BM25Okapi
import numpy as np

class HybridSearchEngine:
    """
    벡터 검색 + BM25 키워드 검색 결합
    
    HolySheep API의 Embedding + 전통적 키워드 검색으로
    검색 정확도 향상
    """
    
    def __init__(self, embedder: DocumentEmbedder):
        self.embedder = embedder
        self.tokenized_corpus = []
        self.bm25 = None
        
    def _prepare_keyword_search(self):
        """BM25 인덱스 구축"""
        # 토큰화 (간단한 토크나이저)
        self.tokenized_corpus = [
            doc["content"].split() 
            for doc in self.embedder.vector_store
        ]
        self.bm25 = BM25Okapi(self.tokenized_corpus)
    
    def hybrid_search(self, 
                      query: str, 
                      top_k: int = 5,
                      vector_weight: float = 0.7,
                      keyword_weight: float = 0.3) -> List[dict]:
        """
        하이브리드 검색 수행
        
        Args:
            query: 검색 쿼리
            top_k: 반환 결과 수
            vector_weight: 벡터 검색 가중치
            keyword_weight: BM25 가중치
        """
        # 1. 벡터 검색
        vector_results = self.embedder.search(query, top_k=top_k * 2)
        vector_scores = {r["id"]: r["similarity"] for r in vector_results}
        
        # 2. BM25 검색
        if self.bm25 is None:
            self._prepare_keyword_search()
        
        query_tokens = query.split()
        bm25_scores = self.bm25.get_scores(query_tokens)
        
        # BM25 점수 정규화
        max_bm25 = max(bm25_scores) if max(bm25_scores) > 0 else 1
        normalized_bm25 = {i: s/max_bm25 for i, s in enumerate(bm25_scores)}
        
        # 3. 점수 결합
        combined_scores = []
        for i, doc in enumerate(self.embedder.vector_store):
            vec_score = vector_scores.get(doc["id"], 0)
            kw_score = normalized_bm25.get(i, 0)
            
            final_score = (vector_weight * vec_score + 
                          keyword_weight * kw_score)
            
            combined_scores.append({
                "id": doc["id"],
                "content": doc["content"],
                "vector_score": vec_score,
                "keyword_score": kw_score,
                "combined_score": final_score,
                "metadata": doc.get("metadata", {})
            })
        
        # 정렬 및 반환
        combined_scores.sort(key=lambda x: x["combined_score"], reverse=True)
        return combined_scores[:top_k]

자주 발생하는 오류 해결

1. API Rate Limit 초과 오류

import time
import requests
from ratelimit import limits, sleep_and_retry

문제: Batch Embedding 시 rate limit 도달

해결: 요청 제한 및 재시도 로직 구현

@sleep_and_retry @limits(calls=100, period=60) # 분당 100회 제한 def rate_limited_embedding(text: str, model: str = "text-embedding-3-small"): """ Rate Limit 적용된 Embedding 함수 """ url = "https://api.holysheep.ai/v1/embeddings" headers = { "Authorization": f"Bearer {HOLYSHEEP_API_KEY}", "Content-Type": "application/json" } payload = { "input": text, "model": model } max_retries = 5 for attempt in range(max_retries): try: response = requests.post(url, headers=headers, json=payload) if response.status_code == 429: # Rate limit 도달 시 대기 retry_after = int(response.headers.get("Retry-After", 60)) print(f"Rate limit 도달. {retry_after}초 후 재시도...") time.sleep(retry_after) continue response.raise_for_status() return response.json()["data"][0]["embedding"] except requests.exceptions.RequestException as e: if attempt == max_retries - 1: raise Exception(f"API 호출 실패 (최대 재시도 횟수 초과): {e}") wait_time = 2 ** attempt # 지수 백오프 print(f"재시도 중... ({attempt + 1}/{max_retries})") time.sleep(wait_time)

배치 처리 시 권장 방식

def batch_embedding_with_backoff(texts: List[str], batch_size: int = 50, delay_between_batches: float = 1.0): """ 배치 Embedding 처리 (Rate Limit 안전) """ all_embeddings = [] total_batches = (len(texts) + batch_size - 1) // batch_size for i in range(0, len(texts), batch_size): batch_num = i // batch_size + 1 batch = texts[i:i + batch_size] print(f"배치 {batch_num}/{total_batches} 처리 중...") try: # 배치 Embedding 호출 response = call_batch_embedding_api(batch) batch_embeddings = [item["embedding"] for item in response["data"]] all_embeddings.extend(batch_embeddings) # 배치 간 딜레이 (Rate Limit 방지) if batch_num < total_batches: time.sleep(delay_between_batches) except Exception as e: print(f"배치 {batch_num} 실패: {e}") # 실패 시 개별 처리로 폴백 for text in batch: try: emb = rate_limited_embedding(text) all_embeddings.append(emb) except Exception as sub_error: print(f"텍스트 처리 실패: {sub_error}") all_embeddings.append(None) return all_embeddings

2. 임베딩 차원 불일치 오류

# 문제: 서로 다른 모델의 임베딩 차원이 달라 벡터DB 저장 실패

해결: 차원 정규화 및 호환성 처리

class DimensionNormalizer: """ 다양한 Embedding 모델의 차원 정규화 """ # 모델별 원본 차원 MODEL_DIMENSIONS = { "text-embedding-3-small": 1536, "text-embedding-3-large": 3072, "voyage-3": 1024, "voyage-multilingual-3": 1024, "embed-english-v3.0": 1024, "embed-multilingual-v3.0": 1024, "jina-embeddings-v3": 1024 } @staticmethod def normalize_embedding(embedding: List[float], target_dim: int = 1536) -> List[float]: """ Embedding 차원 정규화 (패딩 또는 트렁케이션) Args: embedding: 원본 임베딩 벡터 target_dim: 목표 차원 (기본값: 1536) Returns: 정규화된 임베딩 벡터 """ current_dim = len(embedding) if current_dim == target_dim: return embedding normalized = np.zeros(target_dim) if current_dim < target_dim: # 패딩: 앞부분만 복사 normalized[:current_dim] = embedding else: # 트렁케이션: 앞부분만 유지 normalized = embedding[:target_dim] return normalized.tolist() @staticmethod def dynamic_normalize(embedding: List[float], target_dim: int = None) -> List[float]: """ 목표 차원이 None이면 가장 작은 공통 차원으로 정규화 """ if target_dim is None: # 모든 지원 모델의 최소 차원(1024) 사용 target_dim = 1024 return DimensionNormalizer.normalize_embedding(embedding, target_dim)

벡터DB 저장 시 차원 호환성 보장

def store_with_dimension_check(vector_store: List[dict], target_dim: int = 1536): """ 벡터 저장소 내 모든 임베딩의 차원 통일 """ normalizer = DimensionNormalizer() normalized_store = [] for item in vector_store: normalized_embedding = normalizer.normalize_embedding( item["embedding"], target_dim ) normalized_item = { **item, "embedding": normalized_embedding, "original_dimension": len(item["embedding"]), "normalized_dimension": target_dim } normalized_store.append(normalized_item) return normalized_store

3. 한국어 텍스트 토큰화 오류

# 문제: 한국어 텍스트가 잘리거나 잘못 토큰화됨

해결: 한국어 최적화 전처리 및 청킹

import re def korean_text_preprocessor(text: str) -> str: """ 한국어 텍스트 전처리 - 불필요한 공백 제거 - 한글/숫자/영어 붙임 처리 - 특수문자 적절히 처리 """ # 한글, 숫자, 영어를 붙임 (띄어쓰기 정리) text = re.sub(r'([가-힣0-9a-zA-Z])\s+([가-힣0-9a-zA-Z])', r'\1\2', text) # 다중 공백 정리 text = re.sub(r'\s+', ' ', text) # 문장 끝 특수문자 정리 text = re.sub(r'\s+([.,!?])', r'\1', text) return text.strip() def korean_aware_chunking(text: str, max_chars: int = 1000, overlap_chars: int = 200) -> List[str]: """ 한국어 특성을 고려한 청킹 - 문장 단위 분리 - 의미 경계 인식 - 적절한 중첩 """ # 한국어 문장 분리 패턴 sentence_endings = r'([.!?]|\n)' # 문장 분리 sentences = re.split(sentence_endings, text) # 문장 + 구두점 재결합 reconstructed = [] for i in range(0, len(sentences) - 1, 2): if sentences[i].strip(): sentence = sentences[i] + (sentences[i + 1] if i + 1 < len(sentences) else "") reconstructed.append(sentence.strip()) # 청크 생성 chunks = [] current_chunk = [] current_len = 0 for sentence in reconstructed: sentence_len = len(sentence) if current_len + sentence_len <= max_chars: current_chunk.append(sentence) current_len += sentence_len else: # 현재 청크 저장 if current_chunk: chunks.append(' '.join(current_chunk)) # 중첩 처리 if overlap_chars > 0 and len(current_chunk) > 1: # 마지막 문장부터 시작 overlap_text = ' '.join(current_chunk[-2:]) if len(current_chunk) >= 2 else current_chunk[-1] if len(overlap_text) <= overlap_chars: current_chunk =