Study/RAG(Retrieval-Augmented Generation)

2. RAG 단계별 기술과 OpenAI API

bluebamus 2026. 2. 18.

02_RAG_단계별_기술과_OpenAI_API.md
0.10MB
02_RAG_단계별_기술과_OpenAI_API.ipynb
0.12MB
02_RAG_단계별_기술과_OpenAI_API_new.md
0.08MB

 

1. OpenAI API 기반 RAG 기술 매트릭스

    - OpenAI API를 활용한 RAG 파이프라인의 각 단계에서 사용 가능한 모든 기법과 기술을 정리한다. RAG 파이프라인은 크게 **인덱싱 단계**(문서 로딩 → 청킹 → 임베딩 → 벡터 DB 저장)와 **검색-생성 단계**(질의 변환 → 검색 → 생성)로 나뉘며, OpenAI API는 각 단계에서 다양한 방식으로 활용된다.

 

   1.1. RAG 파이프라인에서 OpenAI API의 역할 개요

[인덱싱 단계 - 오프라인]
┌─────────────┐    ┌─────────────┐    ┌──────────────────┐    ┌─────────────┐
│  문서 로딩  │ →  │   청킹      │ →  │    임베딩        │ →  │  벡터 DB    │
│             │    │             │    │                  │    │   저장      │
│ GPT Vision  │    │ GPT 지능형  │    │ text-embedding-  │    │ 메타데이터  │
│ 으로 OCR    │    │ 청킹 가능   │    │ 3-large/small    │    │ 필터링      │
└─────────────┘    └─────────────┘    └──────────────────┘    └─────────────┘

[검색-생성 단계 - 온라인]
┌─────────────┐    ┌─────────────┐    ┌──────────────────┐    ┌─────────────┐
│  질의 변환  │ →  │   검색      │ →  │    생성           │ →  │  응답 반환  │
│             │    │             │    │                  │    │             │
│ GPT로 질의  │    │ Multi-Query │    │ Chat Completion  │    │ Structured  │
│ 재구성      │    │ Self-Query  │    │ Function Calling │    │ Output      │
└─────────────┘    └─────────────┘    └──────────────────┘    └─────────────┘

 

   1.2. 단계별 OpenAI API 활용 기술 매트릭스

RAG 단계 OpenAI API 기술 활용 목적 비용 수준
문서 로딩 GPT-4o Vision 이미지/스캔 문서 OCR 높음
문서 로딩 GPT-4o-mini 비정형 문서 구조화 낮음
청킹 GPT-4o-mini 지능형 의미 단위 분할 중간
임베딩 text-embedding-3-large 고품질 벡터 생성 낮음
임베딩 text-embedding-3-small 비용 최적화 벡터 생성 매우 낮음
임베딩 Batch API 대량 임베딩 50% 할인 매우 낮음
검색 Multi-Query (GPT) 질의 변형 생성 낮음
검색 Self-Query (GPT) 메타데이터 필터 자동 추출 낮음
질의 변환 GPT-4o-mini HyDE, 질의 분해, Step-back 낮음
생성 GPT-4o / GPT-4o-mini 최종 답변 생성 중간~높음
생성 Structured Output JSON/Pydantic 출력 중간
생성 Function Calling 도구 호출 연동 중간
생성 Streaming 실시간 응답 출력 중간
평가 GPT-4o LLM-as-Judge 평가 높음


      - 실무 권장: 비용을 최적화하려면 단순 작업(질의 변환, HyDE, 문서 구조화)에는 `gpt-4o-mini`를 사용하고, 최종 답변 생성과 평가처럼 품질이 중요한 단계에만 `gpt-4o`를 사용한다. 임베딩은 프로토타입에서 `text-embedding-3-small`, 프로덕션에서 `text-embedding-3-large`를 권장한다.

 

   1.3. 실무 비용 산정 예시

      - RAG 파이프라인의 실제 비용을 산정하는 방법을 예시로 설명한다. 비용은 **인덱싱 단계**(일회성)와 **검색-생성 단계**(질의당)로 나누어 계산한다.

 

      1.3.1. 인덱싱 비용 (일회성)

         -10,000건 세법 문서(문서당 평균 2,000자 ≈ 800토큰) 인덱싱 시:

작업 모델 토큰 수 단가 (1M 토큰) 비용
문서 구조화 gpt-4o-mini 8M (입력) + 8M (출력) $0.15 / $0.60 $1.20 + $4.80 = $6.00
메타데이터 태깅 gpt-4o-mini 8M (입력) + 0.5M (출력) $0.15 / $0.60 $1.20 + $0.30 = $1.50
임베딩 text-embedding-3-large 8M $0.13 $1.04
합계       $8.54
Batch API 적용 시     50% 할인 $4.27


         - 10,000건 세법 문서의 전체 인덱싱 비용이 $10 미만이다. Batch API를 활용하면 $5 미만으로 절감할 수 있다.

 

      1.3.2. 검색-생성 비용 (질의당)

         - 사용자 질의 1건 처리 시 (검색 4건 문서, 평균 답변 500토큰):

작업 모델 입력 토큰 출력 토큰 비용
질의 임베딩 text-embedding-3-large 50 - $0.00
답변 생성 (mini) gpt-4o-mini 3,000 500 $0.00
답변 생성 (4o) gpt-4o 3,000 500 $0.01
월간 질의량 gpt-4o-mini 월 비용 gpt-4o 월 비용
1,000건 $0.75 $12.50
10,000건 $7.50 $125.00
100,000건 $75.00 $1,250.00

 

         - 핵심: gpt-4o-mini는 gpt-4o 대비 약 17배 저렴하다. 대부분의 RAG 질의는 gpt-4o-mini로 충분하며, 고품질이 필요한 질의에만 선택적으로 gpt-4o를 사용하는 라우팅 전략이 가장 비용 효율적이다.

 

2. 단계 1: 문서 로딩 - OpenAI 관련 기술

   2.1. 문서 전처리와 구조화

      - OpenAI API는 문서 로딩 자체에는 직접 관여하지 않지만, 로딩 후 전처리에 활용할 수 있다. 일반적으로 문서 로딩은 LangChain의 로더(`Docx2txtLoader`, `PyPDFLoader` 등)가 담당하며, OpenAI API는 로딩된 텍스트를 구조화하거나 이미지에서 텍스트를 추출하는 데 사용된다.

 

      2.1.1. 문서 전처리에 OpenAI API를 활용하는 이유

         - 실무에서 로딩된 원본 텍스트는 다음과 같은 문제를 갖는 경우가 많다:

문제 설명 OpenAI API 해결 방법
비정형 구조 표, 목록, 섹션이 일반 텍스트로 뭉개져 있음 GPT로 마크다운 구조 변환
이미지 내 텍스트 스캔 문서, 캡처 이미지의 텍스트를 추출 불가 GPT Vision으로 OCR
혼재된 언어 영문/한문/특수기호가 섞인 문서 GPT로 정규화 및 번역
메타데이터 부재 카테고리, 날짜 등 메타데이터가 없음 GPT로 자동 분류/태깅


      2.1.2. GPT 기반 문서 구조화

         - 비정형 텍스트를 마크다운 형식으로 변환하면 후속 청킹 단계에서 `MarkdownHeaderTextSplitter`를 활용할 수 있어 청킹 품질이 향상된다.

from openai import OpenAI

 OpenAI 클라이언트 초기화 (환경변수 OPENAI_API_KEY에서 자동으로 키를 읽음)
client = OpenAI()

def structure_document_with_gpt(raw_text: str) -> str:
    """비정형 문서를 마크다운 구조로 변환하는 함수.

    로딩된 원본 텍스트가 표, 목록, 섹션 구분 없이 일반 텍스트로 되어 있을 때,
    GPT를 활용하여 마크다운 형식으로 구조화한다.
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",     #   단순 구조화 작업이므로 비용 효율적인 mini 모델 사용
        messages=[
            {
                "role": "system",
                "content": (
                    "주어진 텍스트를 마크다운 형식으로 구조화하세요. "
                    "표는 마크다운 테이블로, 목록은 불릿 포인트로, "
                    "섹션은 헤더로 변환하세요. "
                    "원본 내용을 변경하지 말고 구조만 변환하세요."
                )
            },
            {"role": "user", "content": raw_text}
        ],
        temperature=0  # 구조화 작업이므로 결정적 출력을 위해 0으로 설정
    )
    return response.choices[0].message.content

 

         - 파라미터 정의 기준:

            - `model="gpt-4o-mini"`: 문서 구조화는 복잡한 추론이 필요 없으므로 비용 효율적인 모델을 선택한다. `gpt-4o`를 사용하면 품질은 약간 향상되지만 비용이 약 17배 증가한다.

            - `temperature=0`: 구조화 작업은 창의성이 아닌 정확성이 중요하므로 0으로 설정하여 결정적(deterministic) 출력을 생성한다. temperature를 높이면 같은 입력에 대해 다른 구조화 결과가 나올 수 있다.


      2.1.3. GPT 기반 메타데이터 자동 태깅

         - 로딩된 문서에 카테고리, 핵심 키워드 등 메타데이터를 자동으로 부여하면 벡터 DB에서 메타데이터 필터링 검색이 가능해진다.

import json
from openai import OpenAI

client = OpenAI()

def auto_tag_document(text: str) -> dict:
    """문서에 카테고리, 키워드 등 메타데이터를 자동 부여하는 함수.

    셀프 쿼리 검색(Self-Query Retriever) 등에서 메타데이터 기반
    필터링을 활용하려면 문서에 적절한 메타데이터가 있어야 한다.
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "주어진 문서를 분석하여 다음 JSON 형식으로 메타데이터를 생성하세요:\n"
                    '{"category": "카테고리", "keywords": ["키워드1", "키워드2"], '
                    '"document_type": "법률|가이드|FAQ|기타", "year": 연도(숫자)}'
                )
            },
            {"role": "user", "content": text[:2000]}  # 비용 절약을 위해 앞부분만 분석
        ],
        temperature=0,
        response_format={"type": "json_object"} #  JSON 모드 활성화로 유효한 JSON 보장
    )
    return json.loads(response.choices[0].message.content)

# 사용 예시: LangChain Document에 메타데이터 추가
from langchain_core.documents import Document

raw_doc = Document(page_content="2024년 소득세법 개정안...", metadata={"source": "tax_law.pdf"})
tags = auto_tag_document(raw_doc.page_content)
raw_doc.metadata.update(tags)  # 기존 메타데이터에 자동 태깅 결과를 병합
 → metadata: {"source": "tax_law.pdf", "category": "소득세", "keywords": [...], ...}

 

         - 실무 권장: 대량 문서의 메타데이터 태깅에는 Batch API를 활용하면 50% 비용 절감이 가능하다. 태깅 품질이 검색 필터링의 정확도에 직접 영향을 미치므로, 태깅 결과를 샘플링하여 검수하는 과정을 권장한다.

 

      2.1.4. OCR + GPT Vision 활용 (이미지 문서)

         - 스캔된 문서, 캡처 이미지, 손글씨 등 텍스트 추출이 어려운 이미지 기반 문서에서 텍스트를 추출할 때 GPT Vision을 활용한다. 기존 OCR 도구(Tesseract 등)보다 복잡한 레이아웃과 다국어 텍스트에서 월등한 성능을 보인다.

import base64
from openai import OpenAI

client = OpenAI()

def extract_text_from_image(image_path: str) -> str:
    """이미지에서 텍스트를 추출하고 마크다운으로 구조화하는 함수.

    GPT-4o의 Vision 기능을 활용하여 이미지 내 텍스트를 인식하고,
    표, 목록, 섹션 등의 구조를 보존하여 마크다운 형식으로 반환한다.
    """
     이미지를 Base64로 인코딩 (API에 전송하기 위한 형식)
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode("utf-8")

    response = client.chat.completions.create(
        model="gpt-4o",   Vision 기능은 gpt-4o에서 최고 품질 (gpt-4o-mini도 지원하지만 품질 차이 있음)
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": (
                        "이 이미지의 모든 텍스트를 추출하고 마크다운으로 구조화하세요. "
                        "표가 있으면 마크다운 테이블로, 목록은 불릿 포인트로 변환하세요. "
                        "이미지에서 텍스트가 보이지 않으면 '텍스트 없음'이라고 답하세요."
                    )
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{image_data}",
                        "detail": "high"   high: 고해상도 분석 (토큰 사용량 증가), low: 저해상도 (빠르고 저렴)
                    }
                }
            ]
        }],
        temperature=0,    #  OCR은 정확성이 중요하므로 0으로 설정
        max_tokens=4096   #  긴 문서 이미지의 경우 충분한 토큰 확보
    )
    return response.choices[0].message.content


         - 파라미터 정의 기준:

            - `model="gpt-4o"`: Vision OCR은 이미지 인식 정확도가 중요하므로 최고 성능 모델을 사용한다. `gpt-4o-mini`도 Vision을 지원하지만 복잡한 표나 작은 글씨 인식 정확도가 낮다.

         -  `detail="high"`: 고해상도 모드는 이미지를 512x512 타일로 분할하여 분석하므로 정확도가 높지만 토큰 사용량이 증가한다. 간단한 이미지는 `"low"`로 설정하여 비용을 절약할 수 있다.

         -  `max_tokens=4096`: 이미지에서 추출되는 텍스트 양이 예측 불가능하므로 넉넉하게 설정한다.

 

      2.1.5. GPT Vision 이미지 입력 방식 비교

방식 형식 장점 단점
Base64 인코딩 data:image/png;base64,... 로컬 파일 직접 사용 가능 요청 크기가 커짐
URL 참조 [https://example.com/image.png](https://example.com/image.png) 요청 크기 작음 이미지가 공개 URL에 있어야 함
# URL 방식 예시 (공개 접근 가능한 이미지)
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "이 이미지의 텍스트를 추출하세요."},
            {
                "type": "image_url",
                "image_url": {
                    "url": "https://example.com/tax_document_scan.png",  # 공개 URL
                    "detail": "high"
                }
            }
        ]
    }],
    temperature=0
)


   2.2. 문서 전처리 전략 비교

전처리 방법 적합 상황 비용 품질
GPT 구조화 비정형 텍스트 → 마크다운 낮음 (gpt-4o-mini) 높음
GPT Vision OCR 스캔 문서, 이미지 높음 (gpt-4o) 매우 높음
GPT 메타데이터 태깅 메타데이터 없는 문서 낮음 (gpt-4o-mini) 중간~높음
Tesseract OCR 단순 텍스트 이미지 무료 (로컬) 중간
전처리 없음 이미 구조화된 문서 없음 원본 그대로


      - 실무 권장: 모든 문서에 GPT 전처리를 적용하면 비용이 급증한다. 원본 문서의 품질을 먼저 확인하고, 구조화가 필요한 문서에만 선택적으로 적용한다. 대량 문서는 Batch API(50% 할인)를 활용한다.

 

   2.3. UnstructuredURLLoader (웹 페이지 로딩)

      - `WebBaseLoader`가 처리하기 어려운 복잡한 HTML 구조나 HTTP를 통해 제공되는 PDF를 로딩할 때 사용한다.

      - Unstructured` 라이브러리를 기반으로 하며, HTML뿐 아니라 PDF, 이메일, 이미지 등 다양한 포맷을 자동 감지하여 파싱한다.

 

      2.3.1. 동작 원리

         1) 지정된 URL에서 콘텐츠를 다운로드한다

         2) 콘텐츠 타입(HTML, PDF 등)을 자동 감지한다

         3) `Unstructured` 라이브러리의 파티셔닝 엔진으로 요소를 분리한다

         4) `mode` 설정에 따라 단일 문서 또는 요소별 문서로 반환한다

from langchain_community.document_loaders import UnstructuredURLLoader

 여러 URL을 한 번에 로딩 (세법 관련 페이지 예시)
urls = [
    "https://example.com/tax-guide-2024",  #   세법 가이드 페이지
    "https://example.com/income-tax-faq",  #   소득세 FAQ 페이지
]

loader = UnstructuredURLLoader(
    urls=urls,             #  로딩할 URL 목록
    mode="elements",        #  "single": 전체를 하나의 문서로, "elements": 요소별 분리
    show_progress_bar=True  #  로딩 진행률 표시 (대량 URL 처리 시 유용)
)

docs = loader.load()
print(f"로딩된 문서 수: {len(docs)}")
for doc in docs[:3]:
    # elements 모드에서는 각 요소의 타입(Title, NarrativeText, Table 등)이 메타데이터에 포함됨
    print(f"  타입: {doc.metadata.get('category', 'N/A')}, 길이: {len(doc.page_content)}")

 

      - 파라미터 정의 기준:

         - `mode="elements"`: 요소별로 분리하면 제목, 본문, 표 등이 구분되어 후속 청킹의 품질이 향상된다. `"single"`은 전체 페이지를 하나의 텍스트로 합치므로 구조 정보가 손실된다.
         - `show_progress_bar=True`: URL이 많거나 페이지가 무거울 때 진행률을 확인할 수 있다. `tqdm` 라이브러리가 필요하다.

 

      2.3.2. mode 옵션 상세 비교

mode 동작 장점 단점 적합 상황
"single" 전체 페이지를 하나의 Document로 단순, 문맥 보존 구조 정보 손실 짧은 페이지, 단순 텍스트
"elements" 제목/본문/표 등 요소별 Document 구조 보존, 정밀한 처리 Document 수 증가 복잡한 HTML, 표 포함 문서


         - WebBaseLoader vs UnstructuredURLLoader:

            - `WebBaseLoader`: BeautifulSoup 기반, 가볍고 빠름, 간단한 HTML에 적합. `pip install beautifulsoup4`만 필요

            - `UnstructuredURLLoader`: Unstructured 라이브러리 기반, 복잡한 HTML/PDF 처리 가능, 

              `pip install unstructured` 필요. 의존성이 무거우므로 필요한 경우에만 사용

 

         - 실무 권장: 단순한 웹 페이지는 `WebBaseLoader`로 충분하다. 표, 이미지, 복잡한 레이아웃이 있는 페이지에서 `WebBaseLoader`의 결과가 만족스럽지 않을 때 `UnstructuredURLLoader`를 사용한다. PDF URL의 경우 `UnstructuredURLLoader`가 자동으로 PDF 파싱을 처리해주므로 별도의 PDF 로더가 필요 없다.

 

3. 단계 2: 청킹 - 고급 분할 기법

   - 청킹(Chunking)은 로딩된 문서를 LLM의 컨텍스트 윈도우와 임베딩 모델의 토큰 제한에 맞게 작은 단위로 분할하는 과정이다. 청킹의 품질은 RAG 파이프라인 전체 성능에 직접적인 영향을 미친다. 청크가 너무 크면 검색 시 관련 없는 내용이 포함되고(노이즈 증가), 너무 작으면 문맥이 손실되어 답변 품질이 저하된다.

 

   3.1 기본 텍스트 분할기 옵션

      3.1.1. 텍스트 분할기 비교 총괄표

         - 분할기분할 기준장점단점적합 상황

분할기 분할 기준 장점 단점 적합 상황
RecursiveCharacterTextSplitter 다단계 구분자(문단→줄→문장→단어) 범용성 높음, 자연스러운 분할 의미 단위 보장 안 됨 일반 텍스트 (기본 선택)
TokenTextSplitter 토큰 수 정밀한 토큰 제어 문맥 경계 무시 가능 토큰 제한 정밀 관리
MarkdownHeaderTextSplitter 마크다운 헤더 계층 문서 구조 보존, 메타데이터 자동 생성 마크다운 전용 마크다운/기술 문서
HTMLHeaderTextSplitter HTML 헤더 태그 HTML 구조 보존 HTML 전용 웹 크롤링 데이터
SemanticChunker 임베딩 유사도 의미 단위 보존 비용/시간 증가, 크기 불균일 주제 변화 잦은 문서
GPT 지능형 청킹 LLM 판단 최고 품질 분할 높은 비용, 느린 속도 고품질 필수, 소량 문서

 

      3.1.2. RecursiveCharacterTextSplitter (가장 범용적)

         - 여러 구분자를 우선순위 순서대로 시도하여 가능한 한 자연스러운 경계(문단 → 줄 → 문장 → 단어)에서 분할한다. 대부분의 RAG 프로젝트에서 기본 선택으로 사용된다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

 기본 설정 - 세법 문서 등 일반 텍스트에 적합한 설정
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1500,        #  각 청크의 최대 문자 수 (한국어는 1자≈1토큰보다 약간 적음)
    chunk_overlap=200,      #  인접 청크 간 중복 문자 수 (문맥 연속성 보장, chunk_size의 10~15%)
    separators=["\n\n", "\n", ". ", " ", ""], #  분할 구분자 우선순위 (문단→줄→문장→단어→문자)
    length_function=len,    #  길이 측정 함수 (기본: 문자 수. 토큰 수로 변경 가능)
    is_separator_regex=False # True로 설정하면 separators를 정규표현식으로 해석
)

# 문서 분할 실행
chunks = splitter.split_documents(documents)
print(f"원본 문서: {len(documents)}개 → 청크: {len(chunks)}개")

 

         - 동작 원리:

            1) 첫 번째 구분자(`\n\n`, 빈 줄 = 문단 경계)로 텍스트 분할 시도

            2) 결과 청크가 `chunk_size`보다 크면 다음 구분자(`\n`, 줄바꿈)로 재분할

            3) 여전히 크면 `. `(마침표+공백, 문장 경계)로 재분할

            4) 모든 구분자를 소진할 때까지 반복 (최종적으로 `""`는 문자 단위 분할)

            5) 분할된 인접 청크 간에 `chunk_overlap`만큼 내용을 공유하여 문맥 연속성 보장

chunk_size = 1500, chunk_overlap = 200 일 때의 동작:

청크 1: [===========1500자===========]
청크 2:                    [==200==[===========1500자===========]
청크 3:                                          [==200==[===========1500자===========]

→ 중복 영역(overlap)이 문맥 연속성을 보장
→ 청크 1의 마지막 200자 = 청크 2의 처음 200자

 

         - 파라미터 정의 기준:

            - `chunk_size=1500`: OpenAI 임베딩 모델(text-embedding-3 시리즈)의 최대 토큰 제한이 8191이므로, 한국어 기준 1500자(약 500~750토큰)는 충분히 안전한 범위이다. 법률/세법 문서는 1000~2000이 적합하다.

            - `chunk_overlap=200`: chunk_size의 약 13%. 너무 작으면 청크 경계에서 문맥이 끊기고, 너무 크면 중복 저장으로 벡터 DB 용량이 증가한다.

            -`separators`: 한국어 문서에서는 `["\n\n", "\n", "다. ", ". ", " ", ""]`처럼 한국어 문장 종결 패턴을 추가하면 더 자연스러운 분할이 가능하다.

 

      3.1.3. TokenTextSplitter (토큰 정밀 제어)

         - 문자 수가 아닌 토큰 수를 기준으로 분할한다. OpenAI 모델의 토큰 제한에 정밀하게 맞추어야 할 때 사용한다. 특히 한국어는 문자 수와 토큰 수의 비율이 일정하지 않으므로(한글 1자가 1~3토큰), 정밀한 토큰 관리가 필요한 경우 유용하다.

from langchain_text_splitters import TokenTextSplitter

splitter = TokenTextSplitter(
    encoding_name="cl100k_base",   #토큰화 인코딩 방식
    # - cl100k_base: GPT-4, GPT-4o, GPT-3.5-turbo, text-embedding-3 #시리즈용
    # - o200k_base: GPT-4o 전용 대안 (더 효율적인 인코딩)
    # - p50k_base: 이전 세대 모델용 (GPT-3, Codex)
    chunk_size=500,               #  토큰 수 기준 (문자 수가 아님!)
    chunk_overlap=50              #  중복 토큰 수
)

chunks = splitter.split_documents(documents)

 

         - 동작 원리:

            - `tiktoken` 라이브러리를 사용하여 텍스트를 토큰으로 변환한 뒤, 정확한 토큰 수 기준으로 분할

            - 토큰 경계에서 분할하므로 단어 중간이 잘리는 경우는 없음 (토큰은 단어/서브워드 단위)

 

         - 파라미터 정의 기준:

            - `encoding_name="cl100k_base"`: 사용하는 임베딩/LLM 모델의 토크나이저와 반드시 일치시켜야 한다. 불일치 시 토큰 수 계산이 틀려져 임베딩 모델의 최대 토큰을 초과할 수 있다.

         - `chunk_size=500`: 토큰 기준이므로 문자 수 기준의 chunk_size와 직접 비교하면 안 된다. 한국어 기준 500토큰은 대략 250~500자에 해당한다.

 

      3.1.4. MarkdownHeaderTextSplitter (구조 보존 분할)

         - 마크다운 헤더 계층(``, ``, ``)을 기준으로 문서를 논리적 섹션으로 분할한다. 각 청크의 메타데이터에 해당 섹션의 헤더 정보가 자동으로 포함되므로, 검색 시 "어떤 섹션에서 검색된 내용인지" 추적할 수 있다.

from langchain_text_splitters import MarkdownHeaderTextSplitter

 분할 기준이 되는 헤더 레벨 정의
headers_to_split_on = [
    ("", "Header 1"),     # H1 헤더를 기준으로 분할하고, 메타데이터 키를 "Header 1"로 지정
    ("", "Header 2"),    # H2 헤더 기준
    ("", "Header 3"),   # H3 헤더 기준
]

splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers=False #  False: 헤더 텍스트를 본문에 유지, True: 메타데이터에만 저장
)

# 분할 실행
chunks = splitter.split_text(markdown_text)

# 결과 확인: 각 청크에 헤더 정보가 메타데이터로 포함됨
for chunk in chunks:
    print(f"메타데이터: {chunk.metadata}")
     → {"Header 1": "세법 가이드", "Header 2": "소득세", "Header 3": "근로소득"}
    print(f"내용: {chunk.page_content[:80]}")

 

         - 동작 원리:

            - 마크다운 헤더 계층을 파싱하여 각 섹션을 독립적인 Document 객체로 분할

            - 상위 헤더 정보가 하위 섹션의 메타데이터에 자동으로 계승됨 (H1 → H2 → H3 계층 유지)

            - 헤더 없는 긴 섹션은 분할되지 않으므로, `RecursiveCharacterTextSplitter`와 결합하여 2단계 분할을 권장

 

         -  파라미터 정의 기준:

            - `strip_headers=False`: 헤더 텍스트가 청크 본문에 포함되면 임베딩에 반영되어 검색 품질이 향상된다. `True`로 설정하면 메타데이터에만 저장되므로 본문이 깔끔해지지만 임베딩에 헤더 정보가 반영되지 않는다.

            - `headers_to_split_on`: 문서의 헤더 깊이에 따라 조절한다. 보통 H1~H3(3단계)을 사용하며, H4 이하까지 분할하면 청크가 지나치게 작아질 수 있다.

 

   3.2 시맨틱 청킹 (Semantic Chunking)

      - 문자 수나 구분자가 아닌, 텍스트의 의미적 유사도를 기반으로 분할 지점을 결정하는 고급 분할 기법이다. 인접 문장들의 임베딩을 비교하여 의미가 크게 바뀌는 지점에서 분할하므로, 주제가 자주 전환되는 문서에 특히 유용하다.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# 임베딩 모델 초기화 (시맨틱 청킹에는 임베딩 모델이 필수)
embedding = OpenAIEmbeddings(model="text-embedding-3-large")

 시맨틱 분할기 생성
semantic_splitter = SemanticChunker(
    embeddings=embedding,                      #  문장 간 유사도 계산에 사용할 임베딩 모델
    breakpoint_threshold_type="percentile",    #  분할 임계값 유형 (아래 표 참조)
    breakpoint_threshold_amount=95             #  임계값 수치 (유사도 하위 5% 지점에서 분할)
)

 분할 실행
chunks = semantic_splitter.split_documents(documents)

 

      - 동작 원리:

         1) 텍스트를 문장 단위로 분리한다

         2) 각 문장을 임베딩하여 벡터로 변환한다

         3) 인접 문장 쌍의 코사인 유사도를 계산한다

         4) 유사도가 임계값 이하로 떨어지는 지점(= 주제 전환 지점)에서 분할한다

         5) 결과적으로 의미적으로 연관된 문장들이 같은 청크에 포함됨

 

      - breakpoint_threshold_type 옵션 상세:

타입 알고리즘 설명 적합 상황 결과 특성
percentile 유사도 분포의 N 백분위수 유사도 하위 N% 지점에서 분할 균일한 청크 크기 원할 때 가장 안정적, 기본 권장
standard_deviation 평균 ± N 표준편차 평균에서 N 표준편차 이상 차이나는 지점에서 분할 명확한 주제 전환 포착 큰 주제 변화만 감지
interquartile 사분위수 범위(IQR) IQR 기반 이상치 탐지로 분할 극단적 주제 전환만 포착 가장 적은 분할, 큰 청크

 

      - 파라미터 정의 기준:

          - `breakpoint_threshold_amount=95` (percentile 방식): 값을 높이면(예: 98) 극단적 주제 변화에서만 분할하여 청크가 커지고, 낮추면(예: 80) 작은 변화에서도 분할하여 청크가 작아진다. 90~95가 일반적인 시작점이다.

          - `embeddings`: 시맨틱 청킹은 모든 문장을 임베딩하므로 API 비용이 발생한다. `text-embedding-3-small`을 사용하면 비용을 줄일 수 있다.

 

      - 실무 권장: 시맨틱 청킹은 품질이 높지만 비용과 처리 시간이 증가한다. 대부분의 경우 `RecursiveCharacterTextSplitter`로 충분하며, 주제 전환이 빈번하고 의미 단위 보존이 중요한 문서(연구 보고서, 뉴스 모음 등)에서 선택적으로 사용한다.

 

   3.3. GPT 기반 지능형 청킹

      - LLM의 문서 이해 능력을 활용하여 의미 단위로 분할하는 최고 품질의 청킹 기법이다. 도메인 전문 지식을 프롬프트에 반영할 수 있어, 도메인 특화 문서에서 가장 높은 품질을 기대할 수 있다. 단, LLM 호출 비용이 발생하므로 대량 문서에는 적합하지 않다.

from openai import OpenAI

client = OpenAI()

def intelligent_chunking(text: str, domain: str = "세법") -> list[str]:
    """GPT를 활용한 지능형 문서 분할 함수.

    도메인 전문 지식을 반영하여 의미적으로 독립적인 단위로 분할한다.
    각 청크는 독립적으로 이해 가능하고, 검색에 최적화된 크기로 생성된다.

    Args:
        text: 분할할 원본 텍스트
        domain: 문서의 도메인 (프롬프트에 반영되어 분할 품질 향상)

    Returns:
        분할된 청크 리스트
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini", # 비용 효율적 모델 사용 (청킹은 복잡한 추론 불필요)
        messages=[
            {
                "role": "system",
                "content": f"""당신은 {domain} 전문 문서 분석가입니다.
주어진 텍스트를 의미 단위로 분할하세요.

분할 규칙:
1. 각 청크는 독립적으로 이해 가능해야 합니다
2. 하나의 청크는 하나의 주제/개념만 다루어야 합니다
3. 각 청크는 200~500자 사이가 적절합니다
4. 청크 구분은 [CHUNK_BREAK]로 표시하세요
5. 원본 텍스트를 변경하지 마세요"""
            },
            {"role": "user", "content": text}
        ],
        temperature=0   일관된 분할 결과를 위해 0으로 설정
    )
    # [CHUNK_BREAK] 구분자로 분할하고 빈 청크 제거
    chunks = response.choices[0].message.content.split("[CHUNK_BREAK]")
    return [chunk.strip() for chunk in chunks if chunk.strip()]

 

      - 실무 권장: GPT 기반 지능형 청킹은 문서당 LLM 호출이 필요하므로 대량 문서에는 비용이 급증한다. 핵심 문서(법률 원문, 내부 규정 등) 소량에만 적용하고, 일반 문서는 `RecursiveCharacterTextSplitter`를 사용한다. Batch API를 활용하면 50% 비용 절감이 가능하다.

 

   3.4. 청킹 전략 선택 가이드

문서 유형 권장 전략 설정 예시 이유
일반 텍스트/보고서 RecursiveCharacterTextSplitter chunk_size=1000, overlap=150 범용성, 안정적 성능
마크다운/기술 문서 MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter 2단계 분할 구조 보존 + 크기 제어
웹 크롤링 HTML HTMLHeaderTextSplitter + RecursiveCharacterTextSplitter 2단계 분할 구조 보존 + 크기 제어
법률/세법 문서 RecursiveCharacterTextSplitter chunk_size=1500~2000, overlap=200 문맥 보존 중요
FAQ/정의집 RecursiveCharacterTextSplitter chunk_size=200~500, overlap=50 개별 항목 단위
토큰 정밀 제어 TokenTextSplitter chunk_size=500 (토큰) LLM 토큰 제한 관리
주제 변화 잦은 문서 SemanticChunker percentile=90~95 의미 단위 보존
핵심 소량 문서 GPT 지능형 청킹 gpt-4o-mini 최고 품질 분할

 

      - 실무 권장: 대부분의 프로덕션 환경에서 `RecursiveCharacterTextSplitter`가 기본 선택이다. chunk_size 500~1500, chunk_overlap 50~200으로 시작한 뒤, 검색 품질 평가(Retrieval Evaluation)를 통해 최적값을 튜닝한다. 구조화된 문서라면 구조 기반 분할 + 크기 기반 재분할의 복합 전략이 가장 좋은 성능을 보인다.

 

4. 단계 3: 임베딩 - OpenAI Embeddings API 상세

   - 임베딩(Embedding)은 텍스트를 고차원 벡터 공간에 매핑하여, 텍스트 간의 **의미적 유사도를 수치화**할 수 있게 하는 핵심 기술이다. "소득세"와 "인컴택스"는 다른 문자열이지만, 임베딩 벡터 공간에서는 가까운 위치에 배치된다. RAG 파이프라인에서는 문서 청크와 사용자 질의를 동일한 벡터 공간에 임베딩하여, 코사인 유사도로 관련 문서를 검색한다.

 

   4.1. OpenAI 임베딩 모델 상세 사양

모델 차원 최대 토큰 가격(1M 토큰) MTEB 점수 차원 축소 지원
text-embedding-3-large 3072 8191 $0.13 64.6 지원 (256~3072)
text-embedding-3-small 1536 8191 $0.02 62.3 지원 (256~1536)
text-embedding-ada-002 1536 8191 $0.10 61 미지원 (레거시)

 

      - 참고: MTEB(Massive Text Embedding Benchmark) 점수는 벤치마크 버전에 따라 다를 수 있으며, 위 값은 공식 발표 기준 근사치이다. `text-embedding-ada-002`는 레거시 모델이므로 신규 프로젝트에서는 `text-embedding-3-*` 시리즈를 사용하는 것을 권장한다. 가격은 OpenAI 정책에 따라 변경될 수 있으므로 공식 가격 페이지를 확인하라.

 

      4.1.1. 모델 선택 기준

기준 text-embedding-3-large text-embedding-3-small 선택 가이드
품질 최고 (MTEB 64.6) 높음 (MTEB 62.3) 품질 차이 약 3.7%
비용 $0.13/1M 토큰 $0.02/1M 토큰 small이 6.5배 저렴
차원 3072 1536 large가 2배 → 저장 비용 2배
검색 속도 상대적 느림 상대적 빠름 차원이 적을수록 빠름
적합 상황 프로덕션, 고품질 필수 프로토타입, 비용 민감 대부분 small로 시작


         - 실무 권장: `text-embedding-3-small`로 프로토타입을 구성하고 검색 품질을 평가한 뒤, 품질이 부족할 때 `text-embedding-3-large`로 전환한다. 임베딩 모델을 변경하면 벡터 DB 전체 인덱스 재구축이 필요하므로, 초기에 충분한 평가를 거쳐 결정하는 것이 중요하다.

 

   4.2. 차원 축소 (Dimensionality Reduction)

      - `text-embedding-3-*` 모델은 Matryoshka Representation Learning 기법을 사용하여, 원본 차원보다 낮은 차원의 벡터를 생성할 수 있다. 이 기법은 벡터의 앞부분에 가장 중요한 정보가 집중되도록 학습하여, 뒷부분을 잘라내도 품질 손실이 최소화된다.

from openai import OpenAI

client = OpenAI()

# 차원 축소 적용: 3072차원을 256차원으로 축소
response = client.embeddings.create(
    model="text-embedding-3-large",    # 원본 3072차원 모델
    input="소득세 계산 방법",            # 임베딩할 텍스트
    dimensions=256                      # 출력 벡터 차원 (3072 → 256으로 축소)
)

vector = response.data[0].embedding
print(f"벡터 차원: {len(vector)}")   # 256
print(f"벡터 샘플: {vector[:5]}")     # [0.0234, -0.0567, ...]

 

      - 파라미터 정의 기준:

         - `dimensions=256`: API 호출 시 `dimensions` 파라미터를 지정하면 서버 측에서 차원 축소된 벡터를 반환한다. 클라이언트에서 별도의 후처리가 필요 없다.

         - 차원 축소는 `text-embedding-3-*` 모델만 지원한다. `text-embedding-ada-002`는 미지원.

 

      - LangChain에서 차원 축소:

from langchain_openai import OpenAIEmbeddings

# LangChain의 OpenAIEmbeddings에서도 dimensions 파라미터를 지원
embedding = OpenAIEmbeddings(
    model="text-embedding-3-large",
    dimensions=1024   # 기본 3072에서 1024로 축소
)

# 이후 Chroma, Pinecone 등에 이 embedding 객체를 전달하면
# 모든 문서가 1024차원으로 임베딩됨
vector = embedding.embed_query("소득세 계산 방법")
print(f"벡터 차원: {len(vector)}")   # 1024

 

      - 차원별 성능-비용 트레이드오프:

차원 상대 품질 저장 공간 검색 속도 적합 상황
256 ~90% 01월 12일 매우 빠름 대규모 데이터, 비용 민감한 환경
512 ~93% 01월 06일 빠름 대규모 데이터, 적정 품질 필요
1024 ~97% 01월 03일 보통 균형 최적 (실무 권장)
3072 100% 1x (기준) 느림 최고 품질 필수 환경

 

      - 실무 권장: `text-embedding-3-large`를 1024차원으로 축소하면, `text-embedding-3-small`(1536차원)보다 적은 차원으로도 더 높은 품질을 얻을 수 있다. 비용은 임베딩 생성 시에는 large 가격이 적용되지만, 벡터 DB 저장/검색 비용이 절감된다.

 

   4.3 배치 임베딩 최적화

      - 대량 문서를 임베딩할 때는 한 번에 하나씩 API를 호출하는 대신, 여러 텍스트를 배치(batch)로 묶어 한 번의 API 호출로 처리해야 한다. LangChain의 `OpenAIEmbeddings`는 내부적으로 배치 처리를 자동으로 수행한다.

from langchain_openai import OpenAIEmbeddings

embedding = OpenAIEmbeddings(
    model="text-embedding-3-large",
    chunk_size=1000   # 한 번의 API 호출에 포함할 텍스트 수 (기본값: 1000)
)

# 대량 문서 임베딩 시 자동 배치 처리
texts = ["문서1 내용", "문서2 내용", "...", "문서10000 내용"]   # 10,000개 텍스트
vectors = embedding.embed_documents(texts)
# → 내부적으로 1000개씩 10번의 API 호출로 자동 분할 처리
# → Rate limit 에러를 방지하기 위해 배치 간 적절한 간격을 유지

 

      - 파라미터 정의 기준:

         - `chunk_size=1000`: 한 번의 API 호출에 포함할 텍스트 수이다. OpenAI의 임베딩 API는 한 번에 최대 2048개의 텍스트를 처리할 수 있지만, 너무 크면 요청 크기 제한(수 MB)을 초과할 수 있으므로 1000이 안전한 기본값이다.

         - Rate limit에 걸리면 LangChain이 자동으로 재시도(retry)하지만, 대량 처리 시에는 `chunk_size`를 줄이거나 API 티어를 업그레이드하는 것을 고려한다.

 

   4.4. 배치 API 활용 (비용 절감)

      - OpenAI Batch API를 사용하면 임베딩과 Chat Completion을 **50% 할인된 가격**에 이용할 수 있다. 단, 응답이 비동기적으로 최대 24시간 이내에 반환된다. 실시간 응답이 필요 없는 대량 문서 인덱싱, 정기 재인덱싱 등에 적합하다.


      4.4.1. Batch API 동작 흐름

[1. 요청 파일 생성]       [2. 파일 업로드]        [3. 배치 실행]
 .jsonl 형식으로    →    files.create()로   →   batches.create()로
 요청 목록 작성          서버에 업로드            배치 작업 시작

[4. 상태 확인]           [5. 결과 다운로드]
 batches.retrieve()  →   완료 후 output_file_id로
 주기적 폴링              결과 파일 다운로드
from openai import OpenAI
import json

client = OpenAI()

# === 1단계: 배치 요청 파일 생성 (.jsonl 형식) ===
# 각 줄에 하나의 API 요청을 JSON 형태로 기록
batch_requests = []
for i, text in enumerate(texts):
    batch_requests.append({
        "custom_id": f"embed-{i}",     # 결과에서 이 ID로 요청-응답 매칭
        "method": "POST",               # HTTP 메서드
        "url": "/v1/embeddings",        # 엔드포인트 경로
        "body": {
            "model": "text-embedding-3-large",
            "input": text                # 임베딩할 텍스트
        }
    })

# .jsonl 파일로 저장 (각 줄이 하나의 JSON 객체)
with open("batch_input.jsonl", "w", encoding="utf-8") as f:
    for req in batch_requests:
        f.write(json.dumps(req, ensure_ascii=False) + "\n")

# === 2단계: 배치 파일 업로드 ===
batch_input_file = client.files.create(
    file=open("batch_input.jsonl", "rb"),
    purpose="batch"   # 배치 처리 용도로 업로드
)

# === 3단계: 배치 작업 실행 ===
batch = client.batches.create(
    input_file_id=batch_input_file.id,    # 업로드한 파일 ID
    endpoint="/v1/embeddings",             # 사용할 API 엔드포인트
    completion_window="24h"                # 완료 기한 (현재 24h만 지원)
)

# === 4단계: 상태 확인 (폴링) ===
import time
while True:
    status = client.batches.retrieve(batch.id)
    print(f"상태: {status.status}")   # validating → in_progress → completed
    if status.status == "completed":
        break
    elif status.status == "failed":
        print(f"실패 사유: {status.errors}")
        break
    time.sleep(60)   # 1분 간격으로 상태 확인

# === 5단계: 결과 다운로드 ===
if status.status == "completed":
    result_file = client.files.content(status.output_file_id)
    results = [json.loads(line) for line in result_file.text.strip().split("\n")]
    for r in results[:3]:
        print(f"ID: {r['custom_id']}, 벡터 차원: {len(r['response']['body']['data'][0]['embedding'])}")


         - 파라미터 정의 기준:

            - `custom_id`: 결과 파일에서 요청-응답을 매칭하는 데 사용된다. 고유한 ID를 부여해야 한다.

            - `completion_window="24h"`: 현재 OpenAI Batch API는 24시간 완료 기한만 지원한다. 실제로는 대부분 수 시간 내에 완료된다.

            - 50% 할인이 적용되므로, `text-embedding-3-large`의 경우 1M 토큰당 $0.13 → $0.065로 절감된다.

 

         - 적합 상황과 부적합 상황:

상황 적합 여부 이유
대량 문서 초기 인덱싱 적합 실시간 응답 불필요, 비용 50% 절감
정기 재인덱싱 (야간 배치) 적합 스케줄링 가능, 비용 절감
대량 메타데이터 태깅 적합 Chat Completion도 배치 가능
사용자 질의 임베딩 부적합 실시간 응답 필요
실시간 RAG 파이프라인 부적합 지연 시간이 허용 불가


   4.5. 임베딩 캐싱

      - 동일한 텍스트를 반복 임베딩하면 불필요한 API 비용이 발생한다. 캐싱을 적용하면 이미 임베딩한 텍스트의 벡터를 로컬에 저장하여 재사용할 수 있다. 문서가 자주 변경되지 않는 환경에서 특히 유용하다.

from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore
from langchain_openai import OpenAIEmbeddings

# 파일 시스템 기반 캐시 저장소 생성
store = LocalFileStore("./embedding_cache/")

# 캐시가 적용된 임베딩 객체 생성
cached_embedding = CacheBackedEmbeddings.from_bytes_store(
    underlying_embeddings=OpenAIEmbeddings(model="text-embedding-3-large"),   # 실제 임베딩 모델
    document_embedding_cache=store,   # 캐시 저장소
    namespace="tax_docs"              # 캐시 네임스페이스 (프로젝트별 구분)
)

# 첫 번째 호출: API 호출하여 임베딩 생성 + 캐시에 저장
vector1 = cached_embedding.embed_documents(["소득세 계산 방법"])

# 두 번째 호출: 캐시에서 즉시 반환 (API 호출 없음, 비용 0)
vector2 = cached_embedding.embed_documents(["소득세 계산 방법"])

# vector1 == vector2 (동일한 벡터)

 

      - 파라미터 정의 기준:

          - `namespace="tax_docs"`: 프로젝트나 문서 세트별로 캐시를 구분한다. 다른 프로젝트의 캐시와 충돌을 방지한다.

          - `LocalFileStore`: 로컬 파일 시스템에 캐시를 저장한다. Redis, InMemoryStore 등 다른 저장소도 사용 가능하다.

 

      - 실무 권장**: 벡터 DB에 문서를 추가할 때 `CacheBackedEmbeddings`를 사용하면, 동일 문서가 중복 추가되더라도 API 비용이 발생하지 않는다. 단, 캐시는 텍스트 해시 기반이므로 문서 내용이 한 글자라도 변경되면 새로 임베딩한다.

 

5. 단계 4: 벡터 DB - 저장 및 인덱싱 기술

   - 벡터 DB는 임베딩된 벡터를 저장하고, 유사도 검색을 수행하는 특수 목적의 데이터베이스이다. 전통적인 관계형 DB(MySQL, PostgreSQL)는 정확한 값 매칭(WHERE 조건)에 최적화되어 있지만, 벡터 DB는 **근사 최근접 이웃 검색(ANN: Approximate Nearest Neighbor)**에 최적화되어 있다. 수백만~수억 개의 고차원 벡터에서 밀리초 단위로 유사한 벡터를 검색할 수 있다.

 

   5.1. 인덱싱 알고리즘

      - 벡터 DB가 빠르게 유사 벡터를 검색하려면, 모든 벡터를 일일이 비교하는 대신 인덱스 구조를 활용해야 한다. 인덱싱 알고리즘은 검색 속도, 정확도, 메모리 사용량 사이의 트레이드오프를 결정한다.

알고리즘 원리 검색 속도 정확도 메모리 적합 상황
Flat (브루트포스) 모든 벡터와 비교 느림 (O(n)) 100% (정확) 낮음 소규모 데이터 (<10만건), 정확도 최우선
IVF (역파일 인덱스) 클러스터링 후 해당 클러스터만 검색 빠름 ~95% 중간 중규모 데이터 (10만~100만건)
HNSW 계층적 그래프 구조로 탐색 매우 빠름 ~99% 높음 대규모 데이터, 높은 정확도 필요
PQ (양자화) 벡터를 서브벡터로 분할 후 코드북 압축 빠름 ~90% 매우 낮음 초대규모 데이터, 메모리 제한 환경

 

      5.1.1. 인덱싱 알고리즘 동작 원리 상세

         5.1.1.1. Flat (브루트포스)

            - 인덱스 없이 질의 벡터와 모든 저장된 벡터를 하나하나 비교한다

            - 정확도 100%를 보장하지만, 데이터가 커지면 검색 시간이 선형적으로 증가한다

            - Chroma의 기본 설정이며, 소규모(~10만건) 데이터에서는 충분히 빠르다

 

         5.1.1.2. HNSW (Hierarchical Navigable Small World)
            - 벡터 간 연결 관계를 그래프로 구축하고, 계층적으로 탐색하여 빠르게 근접 벡터를 찾는다

            - 대부분의 프로덕션 벡터 DB(Pinecone, Qdrant, Weaviate)의 기본 알고리즘이다             -

            - 메모리 사용량이 높지만, 정확도와 속도의 균형이 가장 우수하다

 

         5.1.1.3. 실무 권장**: 소규모 프로토타입에서는 Flat(Chroma 기본)으로 충분하다. 프로덕션에서 10만건 이상의 벡터를 다루면 HNSW 기반 벡터 DB(Pinecone, Qdrant 등)를 사용한다. 인덱싱 알고리즘은 대부분 벡터 DB가 내부적으로 관리하므로, 직접 설정할 필요 없이 DB 선택으로 결정된다.

 

      5.1.2. 주요 벡터 DB 비교

특성 Chroma Pinecone Qdrant Weaviate FAISS
배포 방식 로컬/Docker 클라우드 (관리형) 셀프호스트/클라우드 셀프호스트/클라우드 로컬 (라이브러리)
기본 인덱스 Flat (HNSW 선택 가능) HNSW HNSW HNSW Flat/IVF/HNSW
확장성 ~100만건 수억건+ 수천만건+ 수천만건+ ~1억건
메타데이터 필터링 지원 ($and, $or) 지원 지원 (고급 필터) 지원 (GraphQL) 미지원
비용 무료 (오픈소스) $0.04/벡터/월~ 무료 (오픈소스) 무료 (오픈소스) 무료 (라이브러리)
LangChain 통합 완벽 완벽 완벽 완벽 완벽
적합 상황 프로토타입, 소규모 대규모 프로덕션 성능+비용 균형 하이브리드 검색 연구, 벤치마크


         - 실무 선택 가이드**: 프로토타입 → Chroma, 프로덕션(관리형 원하면) → Pinecone, 프로덕션(비용 절약) → Qdrant 셀프호스트, 하이브리드 검색 필요 → Weaviate, 순수 벡터 연산만 → FAISS

 

   5.2. 메타데이터 필터링

      - 벡터 유사도 검색에 메타데이터 조건을 추가하면, 검색 범위를 좁혀 정확도를 높일 수 있다. 예를 들어 "2024년 소득세 관련 문서만 검색"하려면 벡터 유사도 검색에 `year=2024` 필터를 추가한다.

 

      5.2.1. 기본 필터링

Chroma 메타데이터 필터: 특정 소스 파일의 문서만 검색

results = db.similarity_search(
    query="소득세",            # 검색 질의
    k=4,                      # 반환할 문서 수
    filter={"source": "tax_law_2024.docx"}  # 메타데이터 필터 조건
)


      5.2.2. 복합 필터링

# $and 연산자: 모든 조건을 동시에 만족하는 문서만 검색
results = db.similarity_search(
    query="소득세",
    k=4,
    filter={
        "$and": [
            {"category": "income_tax"},       # 카테고리가 소득세이고
            {"year": {"$gte": 2023}}           # 연도가 2023 이상인 문서
        ]
    }
)

# $or 연산자: 조건 중 하나라도 만족하는 문서 검색
results = db.similarity_search(
    query="세율",
    k=4,
    filter={
        "$or": [
            {"category": "income_tax"},       # 소득세 카테고리이거나
            {"category": "corporate_tax"}     # 법인세 카테고리인 문서
        ]
    }
)


         5.2.2.1. Chroma 필터 연산자 정리

연산자 의미 예시
$eq 같음 (기본) {"year": 2024} 또는 {"year": {"$eq": 2024}}
$ne 같지 않음 {"category": {"$ne": "draft"}}
$gt 초과 {"year": {"$gt": 2022}}
$gte 이상 {"year": {"$gte": 2023}}
$lt 미만 {"year": {"$lt": 2025}}
$lte 이하 {"year": {"$lte": 2024}}
$in 포함 {"category": {"$in": ["income_tax", "vat"]}}
$nin 미포함 {"status": {"$nin": ["draft", "archived"]}}
$and 모든 조건 충족 위 복합 필터 예시 참조
$or 하나 이상 충족 위 복합 필터 예시 참조

 

            - 실무 권장: 메타데이터 필터링을 활용하려면 문서 로딩/청킹 단계에서 적절한 메타데이터(카테고리, 연도, 소스, 버전 등)를 Document 객체에 부여해야 한다. Self-Query Retriever를 사용하면 사용자 질의에서 필터 조건을 자동 추출할 수 있다.

 

   5.3. 문서 업데이트 전략

      - 프로덕션 환경에서는 문서가 주기적으로 추가, 수정, 삭제된다. 벡터 DB의 문서를 효율적으로 업데이트하는 전략이 필요하다.

from langchain_community.vectorstores import Chroma

# === 방법 1: 전체 재구성 (간단하지만 비효율) ===
# 모든 문서를 삭제하고 처음부터 다시 인덱싱
# 문서 수가 적거나 임베딩 모델/청킹 전략이 변경되었을 때 사용
import shutil
shutil.rmtree("./chroma_db")   # 기존 DB 디렉토리 전체 삭제
db = Chroma.from_documents(
    new_docs,                        # 새 문서 리스트
    embedding,                       # 임베딩 모델
    persist_directory="./chroma_db"  # 저장 경로
)

# === 방법 2: 증분 추가 (새 문서만 추가) ===
# 기존 문서는 유지하고 새 문서만 벡터 DB에 추가
# 가장 빈번하게 사용되는 방식
db.add_documents(new_docs)   # 기존 DB에 새 문서 추가

# === 방법 3: ID 기반 업데이트 (Chroma 내부 API) ===
# 특정 문서의 내용이나 메타데이터를 업데이트
# 문서 ID를 알고 있어야 하므로, 추가 시 ID를 기록해두어야 함
db._collection.update(
    ids=["doc_001"],                       # 업데이트할 문서 ID
    documents=["업데이트된 내용"],           # 새 텍스트 내용
    metadatas=[{"version": 2}]             # 새 메타데이터
)

# === 방법 4: 삭제 후 추가 (가장 안전한 업데이트) ===
# 기존 문서를 삭제하고 새 버전을 추가
# ID 기반 업데이트가 지원되지 않는 벡터 DB에서도 사용 가능
db._collection.delete(ids=["doc_001"])     # 기존 문서 삭제
db.add_documents([new_doc])                # 새 버전 추가


      5.3.1. 업데이트 전략 비교

전략 장점 단점 적합 상황
전체 재구성 일관성 보장, 구현 간단 비용/시간 증가, 다운타임 임베딩 모델 변경, 전면 개편
증분 추가 빠름, 비용 최소 중복 가능, 삭제된 문서 잔존 새 문서만 추가할 때
ID 기반 업데이트 정밀한 업데이트 ID 관리 필요, DB별 API 차이 특정 문서 내용 변경
삭제 후 추가 안전, 범용적 삭제-추가 사이 검색 불가 문서 버전 교체

 

         - 실무 권장: 일상적인 운영에서는 증분 추가를 기본으로 사용하고, 중복 문서가 축적되면 주기적으로 전체 재구성*을 수행한다. 문서 ID를 체계적으로 관리하면 ID 기반 업데이트로 정밀한 운영이 가능하다. 임베딩 모델이나 청킹 전략을 변경할 때는 반드시 전체 재구성이 필요하다.

 

6. 단계 5: 생성 - OpenAI Chat Completion 활용

   - 검색된 문서를 컨텍스트로 구성하여 LLM에 전달하고, 최종 답변을 생성하는 단계이다. OpenAI Chat Completion API의 다양한 파라미터와 기능을 활용하여 답변의 품질, 형식, 신뢰도를 제어할 수 있다.

 

   6.1. OpenAI Chat API 파라미터 상세

      - OpenAI Chat Completion API는 다양한 파라미터를 통해 생성 동작을 세밀하게 제어할 수 있다. RAG에서는 사실 기반의 정확한 답변이 필요하므로, 파라미터를 적절히 조정하는 것이 중요하다.

from openai import OpenAI

client = OpenAI()

# 검색된 문서와 사용자 질의 (실제 RAG에서는 벡터 DB 검색 결과를 사용)
context = "소득세법 제55조: 거주자의 종합소득에 대한 세율은 ... 1,200만원 이하: 6%, ..."
question = "근로소득세 계산 방법은?"

response = client.chat.completions.create(
    # === 필수 파라미터 ===
    model="gpt-4o-mini",            # 사용할 모델 (아래 7장 모델 선택 가이드 참조)
    messages=[
        {
            "role": "system",
            "content": "당신은 한국 세법 전문가입니다. 제공된 문서를 근거로 정확히 답변하세요."
        },
        {
            "role": "user",
            "content": f"다음 문서를 참고하여 질문에 답하세요.\n\n"
                       f"문서:\n{context}\n\n"
                       f"질문: {question}"
        }
    ],

    # === 생성 제어 파라미터 ===
    temperature=0,                   # 생성 다양성: 0=결정적(항상 같은 답), 2=최대 랜덤
    max_tokens=1000,                 # 최대 생성 토큰 수 (답변 길이 제한)
    top_p=1.0,                       # 누적 확률 기반 토큰 선택 (0~1, 낮을수록 보수적)
    frequency_penalty=0.0,           # 이미 나온 토큰의 재등장 억제 (-2.0~2.0)
    presence_penalty=0.0,            # 새로운 주제 토큰 유도 (-2.0~2.0)

    # === 출력 제어 파라미터 ===
    stop=["\n\n"],                   # 이 문자열을 만나면 생성 중단 (리스트로 여러 개 지정 가능)
    seed=42,                         # 재현성을 위한 시드값 (베타, 완전한 재현은 미보장)
    response_format={"type": "json_object"},   # JSON 모드: 유효한 JSON만 출력하도록 강제
)

# 응답 추출
answer = response.choices[0].message.content
usage = response.usage
print(f"답변: {answer}")
print(f"토큰 사용량 - 입력: {usage.prompt_tokens}, 출력: {usage.completion_tokens}")


      6.1.1. 파라미터 상세 설명

         6.1.1.1. temperature와 top_p의 관계:

            - `temperature`와 `top_p`는 모두 생성의 다양성(랜덤성)을 제어한다

            - OpenAI 공식 권장: 둘 중 하나만 조정하고 다른 하나는 기본값으로 둔다

            - RAG에서는 `temperature=0~0.3`이 기본이며, `top_p`는 1.0 그대로 둔다

파라미터 범위 RAG 권장값 설명
temperature 0~2 0~0.3 사실 기반 답변에는 낮은 값. 0이면 항상 같은 답
max_tokens 1~ 500~2000 답변 길이에 따라 조정. 짧은 Q&A는 500, 상세 분석은 2000
top_p 0~1 0.9~1.0 기본값 1.0 유지 권장. temperature와 동시 조정 비권장
frequency_penalty -2~2 0~0.5 같은 표현 반복 방지. 0.5 이상이면 답변이 부자연스러워질 수 있음
presence_penalty -2~2 0~0.3 다양한 주제 유도. RAG에서는 문서 근거 답변이 중요하므로 낮게 유지
stop 문자열 리스트 상황별 답변 종료 지점 제어. 후속 불필요한 텍스트 생성 방지
seed 정수 42 (선택) 재현성 필요 시 설정. 완전한 재현은 미보장 (베타 기능)


         6.1.1.2. frequency_penalty vs presence_penalty:

파라미터 동작 효과 예시
frequency_penalty 이미 등장한 횟수에 비례하여 패널티 같은 단어 반복 억제 "세금세금세금" 방지
presence_penalty 등장 여부에만 패널티 (횟수 무관) 새로운 토픽 유도 다양한 관점 답변

 

         6.1.1.3. 파라미터 정의 기준:

            - RAG에서는 `temperature=0`, `max_tokens=1000`으로 시작한다. 답변이 너무 짧으면 max_tokens를 늘리고, 다양한 표현이 필요하면 temperature를 0.1~0.3으로 올린다.

            - `response_format={"type": "json_object"}`: JSON 모드를 활성화하면 반드시 유효한 JSON을 출력하지만, 프롬프트에도 JSON 형식을 명시해야 한다. 프롬프트에 JSON 언급이 없으면 무한 루프에 빠질 수 있다.

 

         6.1.2. RAG 프롬프트 설계 가이드

            - RAG에서 프롬프트 구성은 답변 품질에 직접적인 영향을 미친다. 검색된 문서를 어떻게 LLM에 전달하느냐에 따라 환각(hallucination)이 줄어들고 정확도가 향상된다.

 

            6.1.2.1. 권장 프롬프트 구조:

# RAG 프롬프트 설계 - 환각 방지 및 출처 기반 답변 유도
system_prompt = """당신은 한국 세법 전문가입니다. 아래 규칙을 반드시 따르세요:

1. 제공된 [참고 문서]만을 근거로 답변하세요.
2. 문서에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.
3. 답변 시 근거가 되는 문서의 내용을 인용하세요.
4. 불확실한 정보는 "~일 수 있습니다"로 표현하세요."""

user_prompt = """[참고 문서]
{context}

[질문]
{question}

위 참고 문서를 근거로 질문에 답변하세요."""

# 사용 예시: 검색된 문서와 질의를 프롬프트에 삽입
filled_prompt = user_prompt.format(
    context="소득세법 제55조: 거주자의 종합소득에 대한 세율은...",
    question="근로소득세 계산 방법은?"
)


            6.1.2.2. 프롬프트 설계 핵심 원칙:

원칙 설명 효과
문서 우선 지시 "제공된 문서만을 근거로" 명시 환각 방지
불확실성 표현 모르는 경우의 답변 형식 지정 거짓 답변 방지
인용 유도 근거 문서 인용 요청 검증 가능한 답변
컨텍스트 위치 질문보다 문서를 먼저 배치 LLM의 문서 참조율 향상
역할 지정 도메인 전문가 역할 부여 전문적 답변 유도

 

          6.1.2.3. 실무 권장:

            - 프롬프트에 "문서에 없으면 모른다고 답하라"는 지시를 반드시 포함한다. 이 지시 하나로 환각률이 크게 감소한다. 컨텍스트는 질문 앞에 배치하면 LLM이 문서를 더 잘 참조한다.

 

   6.2. LangChain ChatOpenAI 상세 설정

      - LangChain의 `ChatOpenAI`는 OpenAI Chat API를 래핑하여 LangChain 생태계(체인, 프롬프트, 출력 파서 등)와 통합할 수 있게 한다.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    # === 모델 설정 ===
    model="gpt-4o-mini",        # 모델 선택
    temperature=0,               # 생성 다양성

    # === 생성 제어 ===
    max_tokens=1000,             # 최대 생성 토큰
    model_kwargs={               # OpenAI API에 직접 전달되는 추가 파라미터
        "top_p": 0.95,           # 누적 확률 기반 선택
        "frequency_penalty": 0.1,    # 반복 방지 (약간)
        "presence_penalty": 0.1,     # 새 주제 유도 (약간)
        "seed": 42                  # 재현성 시드
    },

    # === 네트워크 설정 ===
    request_timeout=30,          # API 타임아웃 (초). 네트워크가 느린 환경에서 늘림
    max_retries=2,               # 실패 시 재시도 횟수 (Rate limit 에러 등)

    # === 스트리밍 설정 ===
    streaming=True,              # 스트리밍 활성화 (토큰 단위로 실시간 출력)
)

 

      6.2.1. 파라미터 정의 기준:

         - `model_kwargs`: LangChain의 `ChatOpenAI`가 직접 지원하지 않는 OpenAI API 파라미터를 전달할 때 사용한다. 단, LangChain v0.3+에서는 `top_p`, `frequency_penalty`, `presence_penalty`, `seed` 등을 직접 파라미터로 전달하는 것을 권장한다 (`model_kwargs` 사용 시 경고 발생).

         - `request_timeout=30`: 기본값은 보통 60초이지만, RAG 파이프라인에서는 30초면 충분하다. 타임아웃이 발생하면 `max_retries`에 따라 자동 재시도한다.

         - `streaming=True`: 스트리밍을 활성화하면 첫 토큰이 빠르게 반환되어 사용자 경험이 향상된다. Streamlit, FastAPI 등과 연동할 때 필수적이다.

 

      6.2.2. LangChain v0.3+ 직접 파라미터 방식 (권장):

# model_kwargs 대신 직접 파라미터 전달 (경고 없음)
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    max_tokens=1000,
    top_p=0.95,
    frequency_penalty=0.1,
    presence_penalty=0.1,
    seed=42,
    request_timeout=30,
    max_retries=2,
    streaming=True,
)


   6.3. 구조화된 출력 (Structured Output)

      - LLM의 출력을 Pydantic 모델로 정의된 구조화된 형식으로 받을 수 있다. 답변 내용뿐만 아니라 신뢰도, 출처, 계산 결과 등 부가 정보를 구조화하여 프로그래밍적으로 처리할 수 있다.

from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI

# 참고: langchain_core.pydantic_v1은 deprecated.
# LangChain v0.3+ 에서는 pydantic v2를 직접 사용 권장

# 출력 스키마 정의: 답변의 구조를 Pydantic 모델로 명시
class TaxAnswer(BaseModel):
    """세법 질의에 대한 구조화된 답변 형식"""
    answer: str = Field(description="질문에 대한 답변 텍스트")
    confidence: float = Field(description="답변 신뢰도 (0.0~1.0, 1.0이 최고)")
    sources: list[str] = Field(description="참조한 문서 또는 법률 조항 목록")
    tax_amount: float = Field(default=0, description="계산된 세액 (원). 세액 계산 질의가 아니면 0")

# LLM에 구조화된 출력 적용
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(TaxAnswer)

# 사용: 반환값이 TaxAnswer 인스턴스로 자동 파싱됨
result = structured_llm.invoke("근로소득세 계산 방법을 알려주세요.")
print(f"답변: {result.answer}")
print(f"신뢰도: {result.confidence}")
print(f"출처: {result.sources}")
print(f"세액: {result.tax_amount:,}원")
# → TaxAnswer(answer="근로소득세는...", confidence=0.85, sources=["소득세법 제47조"], tax_amount=0)


      6.3.1. 실무 권장:

         - 구조화된 출력은 RAG 파이프라인의 후처리를 자동화할 때 유용하다. 예를 들어 `confidence`가 0.5 미만이면 "답변을 확신할 수 없습니다"로 처리하거나, `sources`를 사용자에게 출처로 표시할 수 있다. Pydantic v2의 `Field(description=...)`을 상세히 작성하면 LLM의 출력 품질이 향상된다.

 

   6.4. 스트리밍 (Streaming)

      - LLM의 응답을 토큰 단위로 실시간 출력하는 기능이다. 전체 답변이 완성될 때까지 기다리지 않고, 생성되는 즉시 사용자에게 표시할 수 있어 체감 응답 속도가 크게 향상된다. Streamlit, FastAPI 등 웹 애플리케이션과 연동할 때 필수적이다.

# === 방법 1: LangChain 체인 스트리밍 ===
# RAG 체인의 최종 출력을 스트리밍
for chunk in rag_chain.stream({"input": "근로소득세 계산"}):
    if "answer" in chunk:   # answer 키가 있는 청크만 출력
        print(chunk["answer"], end="", flush=True)
        # flush=True: 버퍼 없이 즉시 출력 (실시간 표시)

# === 방법 2: OpenAI API 직접 스트리밍 ===
# LangChain 없이 OpenAI API를 직접 사용하는 경우
stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "세법 전문가입니다."},
        {"role": "user", "content": "근로소득세 계산 방법은?"}
    ],
    stream=True   # 스트리밍 활성화
)

# 각 청크에서 생성된 토큰을 실시간으로 추출
for chunk in stream:
    # delta.content에 새로 생성된 텍스트가 포함됨 (None일 수도 있음)
    content = chunk.choices[0].delta.content
    if content:
        print(content, end="", flush=True)


      6.4.1. 실무 권장:

         - 사용자 인터페이스가 있는 RAG 애플리케이션에서는 스트리밍을 기본 활성화한다. 스트리밍은 전체 응답 시간을 줄이지는 않지만, 첫 토큰 표시까지의 시간(TTFT: Time to First Token)을 크게 단축하여 사용자 경험을 개선한다.

 

   6.5. Function Calling (도구 호출)

      - LLM이 사전 정의된 함수(도구)를 자동으로 호출할 수 있는 기능이다. RAG에서는 검색된 문서 기반 답변 외에, 세금 계산, 데이터 조회 등 추가 작업이 필요할 때 활용한다. LLM이 질의를 분석하여 적절한 함수와 인자를 자동 선택한다.

# === 1단계: 도구(함수) 정의 ===
tools = [
    {
        "type": "function",
        "function": {
            "name": "calculate_income_tax",          # 함수 이름
            "description": "근로소득세를 계산합니다",   # LLM이 함수 선택 시 참고하는 설명
            "parameters": {
                "type": "object",
                "properties": {
                    "annual_income": {
                        "type": "number",
                        "description": "연간 총급여 (원)"    # 파라미터 설명
                    },
                    "deductions": {
                        "type": "number",
                        "description": "공제 금액 (원)"
                    }
                },
                "required": ["annual_income"]   # 필수 파라미터
            }
        }
    }
]

# === 2단계: LLM에 도구와 함께 질의 전달 ===
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "연봉 5000만원일 때 소득세는?"}],
    tools=tools,
    tool_choice="auto"   # LLM이 도구 호출 여부를 자동 판단
    # "auto": 필요할 때만 호출, "none": 호출 안 함
    # "required": 반드시 호출, {"type": "function", "function": {"name": "..."}}: 특정 함수 지정
)

# === 3단계: 도구 호출 결과 확인 ===
message = response.choices[0].message
if message.tool_calls:
    # LLM이 함수 호출을 결정한 경우
    tool_call = message.tool_calls[0]
    print(f"호출할 함수: {tool_call.function.name}")
    print(f"전달할 인자: {tool_call.function.arguments}")
    # → 호출할 함수: calculate_income_tax
    # → 전달할 인자: {"annual_income": 50000000}

    # 4단계: 실제 함수를 호출하고 결과를 LLM에 다시 전달
    import json
    args = json.loads(tool_call.function.arguments)

    # 실제 세금 계산 함수 (2024년 소득세 간이세액표 기준)
    def calculate_income_tax(annual_income: float, deductions: float = 0) -> float:
        """근로소득세 간이 계산 (2024년 기준)"""
        taxable = annual_income - deductions
        brackets = [
            (14_000_000, 0.06), (50_000_000, 0.15), (88_000_000, 0.24),
            (150_000_000, 0.35), (300_000_000, 0.38), (500_000_000, 0.40),
            (1_000_000_000, 0.42), (float('inf'), 0.45)
        ]
        tax, prev_limit = 0, 0
        for limit, rate in brackets:
            if taxable <= limit:
                tax += (taxable - prev_limit) * rate
                break
            tax += (limit - prev_limit) * rate
            prev_limit = limit
        return tax

    tax_result = calculate_income_tax(args)

    # 5단계: 함수 결과를 포함하여 LLM에 재질의 (최종 자연어 답변 생성)
    follow_up = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "user", "content": "연봉 5000만원일 때 소득세는?"},
            message,   # LLM의 도구 호출 결정
            {
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps({"tax_amount": tax_result})
            }
        ]
    )
    print(follow_up.choices[0].message.content)

 

      6.5.1. 파라미터 정의 기준:

         - `tool_choice="auto"`: 대부분의 경우 `"auto"`가 적합하다. LLM이 질의 내용을 분석하여 함수 호출이 필요한지 자동 판단한다. `"required"`로 설정하면 항상 함수를 호출하므로, 함수 호출이 반드시 필요한 파이프라인에서 사용한다.

         - `description`: 함수와 파라미터의 설명을 상세히 작성할수록 LLM의 함수 선택과 인자 추출 정확도가 향상된다.

 

      6.5.2. 실무 권장:

         - RAG + Function Calling을 결합하면, "검색 기반 지식 답변 + 계산/조회 기능"을 하나의 파이프라인으로 구성할 수 있다. 예를 들어 세법 질의에서 법률 지식은 RAG로, 세금 계산은 Function Calling으로 처리하여 정확한 답변을 제공한다.

 

7. OpenAI API 모델별 RAG 활용 가이드

   7.1. 모델 선택 기준

      - OpenAI는 다양한 모델을 제공하며, RAG 파이프라인의 각 단계에서 적합한 모델이 다르다. 비용, 성능, 토큰 제한을 종합적으로 고려하여 단계별로 최적의 모델을 선택해야 한다.

모델 토큰 제한 비용(입력/출력 per 1M) 특징 적합 용도
gpt-4o 128K $2.50 / $10.00 최신 플래그십, 멀티모달(텍스트+이미지+오디오) 복합 추론, 최종 답변, 평가
gpt-4o-mini 128K $0.15 / $0.60 gpt-4o의 경량 버전, 뛰어난 비용 대비 성능 일반 RAG, 질의 변환, 전처리
gpt-4-turbo 128K $10 / $30 이전 세대 고성능 모델 레거시 호환 (신규 비권장)
o1 200K $15 / $60 심층 추론 특화 (Chain-of-Thought 내장) 복잡한 논리적 추론, 수학적 분석
o1-mini 128K $3 / $12 o1의 경량 버전, 추론 비용 최적화 중간 수준 추론 작업
o3-mini 200K $1.10 / $4.40 최신 추론 모델, 비용 효율적 추론 필요하지만 비용 민감한 환경

 

      - 참고: 위 가격은 작성 시점 기준이며 OpenAI 정책에 따라 변경될 수 있다. 최신 가격은 [OpenAI Pricing](https://openai.com/pricing) 페이지를 확인하라. Batch API를 사용하면 50% 할인된 가격으로 이용 가능하다.

 

      7.1.1. gpt-4o vs gpt-4o-mini 상세 비교

비교 항목 gpt-4o gpt-4o-mini 차이
입력 비용 $2.50/1M $0.15/1M mini가 약 17배 저렴
출력 비용 $10.00/1M $0.60/1M mini가 약 17배 저렴
추론 능력 매우 높음 높음 복합 추론에서 4o 우위
속도 보통 빠름 mini가 약 2배 빠름
멀티모달 텍스트+이미지+오디오 텍스트+이미지 4o가 오디오 추가 지원
RAG 기본 모델 고품질 필요 시 대부분의 경우 mini를 기본으로 사용

 

      7.1.2. o1/o3 시리즈 (추론 모델)의 RAG 활용

         - o1, o3 시리즈는 내부적으로 Chain-of-Thought(사고 과정)를 수행하는 추론 특화 모델이다. 일반 RAG에서는 불필요하지만, 다음과 같은 상황에서 유용하다:

상황 모델 이유
복수 문서 간 모순 판단 o1 또는 o3-mini 논리적 비교/분석 필요
세금 계산식 추론 o3-mini 수학적 추론 포함
법률 해석 (판례 적용) o1 심층적 법적 추론 필요
단순 Q&A gpt-4o-mini 추론 모델 불필요, 비용 낭비

 

         - 실무 권장: 대부분의 RAG 파이프라인에서는 `gpt-4o-mini`가 비용 대비 성능이 가장 우수하다. `gpt-4o`는 최종 답변의 품질이 중요하거나, LLM-as-Judge 평가에서 사용한다. 추론 모델(o1, o3)은 일반 RAG에서는 과잉이므로, 복합 추론이 필수적인 특수 상황에서만 고려한다.

 

   7.2. RAG 단계별 권장 모델

      - 각 RAG 단계의 작업 특성에 맞는 최적 모델을 정리한다. 비용을 최소화하면서도 각 단계에서 필요한 품질을 확보하는 것이 핵심이다.

단계 작업 권장 모델 이유 비용 영향
문서 전처리 (구조화) 텍스트 → 마크다운 변환 gpt-4o-mini 단순 변환 작업, 비용 절약 낮음
문서 전처리 (OCR) 이미지 → 텍스트 추출 gpt-4o Vision 정확도 중요 높음
메타데이터 태깅 카테고리/키워드 추출 gpt-4o-mini 단순 분류 작업 낮음
지능형 청킹 의미 단위 문서 분할 gpt-4o-mini 분할 품질은 충분 중간
임베딩 텍스트 → 벡터 text-embedding-3-large 검색 품질의 핵심 낮음
질의 변환 (사전/HyDE) 질의 최적화 gpt-4o-mini 빠른 응답, 단순 작업 낮음
질의 분해 복합 질의 분해 gpt-4o-mini 또는 gpt-4o 복합 추론 시 gpt-4o 낮음~중간
HyDE 가상 문서 생성 가상 답변 생성 gpt-4o-mini 검색용이므로 높은 품질 불필요 낮음
최종 답변 생성 컨텍스트 기반 답변 gpt-4o-mini 또는 gpt-4o 품질 요구에 따라 선택 중간~높음
구조화된 출력 JSON/Pydantic 출력 gpt-4o-mini 구조 추출은 충분 낮음
평가 (LLM-as-Judge) 답변 품질 평가 gpt-4o 정확한 평가 필요 높음
환각 검출 답변-문서 일치 검증 gpt-4o 신뢰도 높은 판단 필요 높음

 

      7.2.1. 비용 최적화 전략

[비용 최적화 모델 배치 전략]

단순 작업 (비용 낮게)          품질 중요 작업 (비용 높게)
┌──────────────────┐          ┌──────────────────┐
│   gpt-4o-mini    │          │     gpt-4o       │
│                  │          │                  │
│ - 질의 변환      │          │ - 최종 답변 생성  │
│ - HyDE 생성      │          │ - 평가/검증       │
│ - 메타데이터 태깅│          │ - OCR (Vision)    │
│ - 문서 구조화    │          │ - 복합 추론       │
│ - 지능형 청킹    │          │                  │
└──────────────────┘          └──────────────────┘

임베딩 작업 (별도 모델)         대량 배치 작업
┌──────────────────┐          ┌──────────────────┐
│ text-embedding-  │          │   Batch API      │
│ 3-large/small    │          │   (50% 할인)     │
│                  │          │                  │
│ - 문서 임베딩     │          │ - 초기 인덱싱    │
│ - 질의 임베딩     │          │ - 재인덱싱       │
│ - 시맨틱 청킹     │          │ - 대량 태깅      │
└──────────────────┘          └──────────────────┘


         - 실무 권장: RAG 파이프라인의 비용 대부분은 최종 답변 생성에서 발생한다 (사용자 질의마다 호출). 전처리/인덱싱 단계는 한 번만 실행되므로 비용 비중이 낮다. 따라서 가장 빈번하게 호출되는 답변 생성 단계의 모델 선택이 전체 비용에 가장 큰 영향을 미친다. `gpt-4o-mini`로 시작하여 품질이 부족한 경우에만 `gpt-4o`로 전환하는 것을 권장한다.

 

   7.3. OpenAI API 에러 처리 가이드

      - 프로덕션 RAG 파이프라인에서 OpenAI API 호출 시 발생할 수 있는 주요 에러와 대응 방법을 정리한다.

from openai import OpenAI, APIConnectionError, RateLimitError, APIStatusError
import time

client = OpenAI()

def call_with_retry(func, max_retries=3, base_delay=1):
    """지수 백오프(Exponential Backoff) 재시도 패턴.

    OpenAI API에서 Rate Limit, 네트워크 에러 등 일시적 장애 발생 시
    점진적으로 대기 시간을 늘리며 재시도한다.
    """
    for attempt in range(max_retries):
        try:
            return func()
        except RateLimitError as e:
            # Rate Limit 초과: 대기 후 재시도 (가장 빈번한 에러)
            delay = base_delay * (2 ** attempt)   # 1초 → 2초 → 4초
            print(f"Rate limit 초과. {delay}초 후 재시도 ({attempt + 1}/{max_retries})")
            time.sleep(delay)
        except APIConnectionError as e:
            # 네트워크 연결 실패: 대기 후 재시도
            delay = base_delay * (2 ** attempt)
            print(f"연결 실패. {delay}초 후 재시도 ({attempt + 1}/{max_retries})")
            time.sleep(delay)
        except APIStatusError as e:
            if e.status_code == 429:   # Rate limit (RateLimitError와 동일)
                delay = base_delay * (2 ** attempt)
                time.sleep(delay)
            elif e.status_code == 500:   # 서버 에러: 재시도 가능
                delay = base_delay * (2 ** attempt)
                time.sleep(delay)
            else:   # 400, 401, 403 등 클라이언트 에러: 재시도 불가
                raise   # 즉시 에러 전파
    raise Exception(f"{max_retries}회 재시도 후 실패")


      7.3.1. 주요 에러 유형별 대응

에러 코드 원인 재시도 가능 대응 방법
429 Rate Limit 초과 가능 지수 백오프 재시도, API 티어 업그레이드
400 잘못된 요청 (토큰 초과 등) 불가 입력 길이 줄이기, 프롬프트 수정
401 인증 실패 불가 API 키 확인, 환경변수 점검
403 권한 없음 불가 API 키 권한 확인, 모델 접근 권한 점검
500+ 서버 에러 가능 지수 백오프 재시도
연결 에러 네트워크 문제 가능 네트워크 확인, 프록시 설정 점검


         - 실무 권장: LangChain의 `ChatOpenAI`는 `max_retries` 파라미터로 자동 재시도를 지원한다. 직접 OpenAI API를 호출하는 경우에만 위의 재시도 패턴을 사용한다. Rate Limit이 빈번하면 OpenAI의 API 사용량 티어(Tier 1 → 5)를 업그레이드한다.

 

댓글