이 글은 누구를 위한 것인가
- 도메인 특화 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로 자동 필터링을 구축하면, 대량 생성 후 고품질 데이터만 선별하는 파이프라인을 만들 수 있다. 생성 모델과 평가 모델을 분리하는 것이 좋다.