LLM으로 합성 데이터 생성: 파인튜닝·테스트·RAG 데이터셋 만들기

AI 기술

합성 데이터LLM 파인튜닝Self-InstructRAG 평가데이터 생성

이 글은 누구를 위한 것인가

  • 도메인 특화 LLM 파인튜닝에 필요한 데이터가 부족한 팀
  • RAG 시스템 품질 평가를 위한 QA 데이터셋이 필요한 엔지니어
  • 데이터 없이 모델 성능을 높이고 싶은 AI 팀

들어가며

"파인튜닝을 하고 싶은데 훈련 데이터가 없다." AI 팀이 가장 자주 맞닥뜨리는 문제다. 실제 사용자 데이터는 개인정보 이슈가 있고, 전문가 라벨링은 비싸고 느리다.

LLM으로 LLM의 훈련 데이터를 생성하는 역설적인 접근 — 합성 데이터 생성이 이 문제의 해법이다. GPT-4, Claude로 생성한 합성 데이터로 소형 모델을 파인튜닝하는 사례가 이미 활발하다.

이 글은 bluefoxdev.kr의 LLM 데이터 파이프라인 가이드 를 참고하고, 합성 데이터 생성 실전 구현 관점에서 확장하여 작성했습니다.


1. 합성 데이터 활용 시나리오

[합성 데이터가 필요한 상황]

파인튜닝용:
  ├── 도메인 특화 Q&A 쌍 (의료, 법률, 금융)
  ├── 응답 스타일 데이터 (톤앤매너, 길이)
  └── 거부 응답 데이터 (안전 정렬용)

RAG 평가용:
  ├── 문서 → QA 쌍 자동 생성
  ├── 어려운 질문 (멀티홉, 추론 필요)
  └── 부정 사례 (답이 문서에 없는 질문)

테스트용:
  ├── 엣지 케이스 시나리오
  ├── 다양한 언어/방언 변형
  └── 오타·노이즈 포함 입력

증강(Augmentation)용:
  ├── 기존 데이터 paraphrasing
  ├── 언어 번역 (한국어 → 영어 쌍 생성)
  └── 난이도별 변형 (쉬운 버전, 어려운 버전)

2. Self-Instruct 기반 데이터 생성

import anthropic
import json
import random

client = anthropic.Anthropic()

# 시드 예시 (소량의 인간 작성 데이터)
SEED_EXAMPLES = [
    {
        "instruction": "다음 텍스트를 3줄로 요약하세요.",
        "input": "긴 텍스트...",
        "output": "요약 결과..."
    },
    {
        "instruction": "주어진 키워드로 이메일 제목을 5개 작성하세요.",
        "input": "키워드: 할인, 여름, 신상품",
        "output": "1. 여름 신상품 특별 할인..."
    },
]

GENERATE_INSTRUCTION_PROMPT = """
다음은 AI 어시스턴트에게 주는 지시문(instruction) 예시들입니다:

{examples}

위 예시들을 참고하여 새로운 지시문 {n}개를 생성하세요.
각 지시문은 다양하고 구체적이어야 합니다.
JSON 배열 형식으로만 응답하세요:
["지시문1", "지시문2", ...]
"""

def generate_instructions(n: int = 20) -> list[str]:
    """Self-Instruct: 시드에서 새 지시문 생성"""
    
    # 랜덤 시드 선택
    selected = random.sample(SEED_EXAMPLES, min(3, len(SEED_EXAMPLES)))
    examples_text = "\n".join(
        f"- {ex['instruction']}" for ex in selected
    )
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2000,
        messages=[{
            "role": "user",
            "content": GENERATE_INSTRUCTION_PROMPT.format(
                examples=examples_text,
                n=n
            )
        }]
    )
    
    try:
        instructions = json.loads(response.content[0].text)
        return instructions
    except json.JSONDecodeError:
        return []

GENERATE_RESPONSE_PROMPT = """
다음 지시문에 대한 고품질 응답을 생성하세요.

지시문: {instruction}
입력 (있을 경우): {input}

응답은 정확하고, 완전하며, 도움이 되어야 합니다.
응답만 출력하세요.
"""

def generate_response(instruction: str, input_text: str = "") -> str:
    """지시문에 대한 응답 생성"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": GENERATE_RESPONSE_PROMPT.format(
                instruction=instruction,
                input=input_text or "없음"
            )
        }]
    )
    
    return response.content[0].text

def build_synthetic_dataset(target_size: int = 1000) -> list[dict]:
    """전체 합성 데이터셋 생성"""
    dataset = []
    
    while len(dataset) < target_size:
        # 새 지시문 생성
        instructions = generate_instructions(n=20)
        
        for instruction in instructions:
            if len(dataset) >= target_size:
                break
            
            response = generate_response(instruction)
            
            dataset.append({
                "instruction": instruction,
                "input": "",
                "output": response,
                "source": "synthetic_self_instruct"
            })
        
        print(f"생성 완료: {len(dataset)}/{target_size}")
    
    return dataset

3. RAG 평가용 QA 데이터셋 생성

QA_GENERATION_PROMPT = """
다음 문서를 읽고 Q&A 쌍을 생성하세요.

[문서]
{document}

요구사항:
1. 쉬운 질문 {easy_n}개: 문서에서 직접 찾을 수 있는 사실 질문
2. 어려운 질문 {hard_n}개: 추론이나 종합이 필요한 질문
3. 답 없는 질문 {unanswerable_n}개: 이 문서만으로는 답할 수 없는 질문

JSON 형식으로만 출력하세요:
{{
  "easy": [{{"question": "...", "answer": "...", "type": "easy"}}],
  "hard": [{{"question": "...", "answer": "...", "type": "hard"}}],
  "unanswerable": [{{"question": "...", "answer": null, "type": "unanswerable"}}]
}}
"""

def generate_qa_from_document(document: str) -> dict:
    """문서에서 QA 쌍 자동 생성"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2000,
        messages=[{
            "role": "user",
            "content": QA_GENERATION_PROMPT.format(
                document=document,
                easy_n=3,
                hard_n=2,
                unanswerable_n=1
            )
        }]
    )
    
    try:
        qa_pairs = json.loads(response.content[0].text)
        return qa_pairs
    except json.JSONDecodeError:
        return {"easy": [], "hard": [], "unanswerable": []}

def build_rag_eval_dataset(documents: list[str]) -> list[dict]:
    """RAG 평가 데이터셋 구축"""
    eval_dataset = []
    
    for i, doc in enumerate(documents):
        print(f"문서 {i+1}/{len(documents)} 처리 중...")
        qa = generate_qa_from_document(doc)
        
        for difficulty, pairs in qa.items():
            for pair in pairs:
                eval_dataset.append({
                    "document_id": i,
                    "document": doc,
                    "question": pair["question"],
                    "ground_truth": pair["answer"],
                    "difficulty": difficulty,
                })
    
    return eval_dataset

4. 데이터 품질 필터링

QUALITY_FILTER_PROMPT = """
다음 Q&A 쌍의 품질을 평가하세요.

질문: {question}
답변: {answer}

평가 기준:
- clarity: 질문이 명확한가 (1-5)
- accuracy: 답변이 정확한가 (1-5)
- specificity: 충분히 구체적인가 (1-5)
- keep: true/false (데이터셋에 포함할 것인가)
- reason: 제외 이유 (keep=false일 때만)

JSON으로만 응답하세요.
"""

def filter_quality(dataset: list[dict], min_score: float = 3.5) -> list[dict]:
    """품질 기준 미달 데이터 필터링"""
    filtered = []
    removed = 0
    
    for item in dataset:
        response = client.messages.create(
            model="claude-haiku-4-5-20251001",  # 필터링은 빠른 모델로
            max_tokens=150,
            messages=[{
                "role": "user",
                "content": QUALITY_FILTER_PROMPT.format(
                    question=item["instruction"],
                    answer=item["output"]
                )
            }]
        )
        
        try:
            eval_result = json.loads(response.content[0].text)
            avg_score = (
                eval_result["clarity"] +
                eval_result["accuracy"] +
                eval_result["specificity"]
            ) / 3
            
            if eval_result.get("keep", True) and avg_score >= min_score:
                item["quality_score"] = round(avg_score, 2)
                filtered.append(item)
            else:
                removed += 1
        except (json.JSONDecodeError, KeyError):
            filtered.append(item)  # 파싱 실패 시 포함 유지
    
    print(f"필터링: {len(dataset)}개 → {len(filtered)}개 ({removed}개 제거)")
    return filtered

마무리

합성 데이터는 실제 데이터를 완전히 대체할 수 없다. 하지만 데이터 부족 문제를 해결하거나, 레어 케이스를 보충하거나, 평가 데이터셋을 빠르게 구축하는 데 매우 효과적이다.

핵심은 품질 필터링이다. LLM-as-Judge로 자동 필터링을 구축하면, 대량 생성 후 고품질 데이터만 선별하는 파이프라인을 만들 수 있다. 생성 모델과 평가 모델을 분리하는 것이 좋다.