본문 바로가기

AI 개발

DSPy 완전 가이드 — 프롬프트를 손으로 짜지 말고 AI가 최적화하게 두는 법

반응형

LLM 앱 만들면 프롬프트 짜고, 테스트하고, 결과 별로면 수정하고, 또 테스트하고. 이 루프가 끝이 없습니다. DSPy는 이걸 뒤집습니다. 입력과 출력만 정의하면 최적 프롬프트를 AI가 찾아줍니다.

[핵심 요약]
→ DSPy: Declarative Self-improving Python — Stanford 연구팀 개발
→ 핵심 철학: "프롬프트를 짜는 것" → "프로그램을 선언하고 컴파일"
→ 3가지 핵심 구성: Signature(입출력 선언) + Module(추론 방식) + Optimizer(자동 최적화)
→ 주요 옵티마이저: BootstrapFewShot, MIPROv2, SIMBA, GEPA
→ 효과: 팩트 정확도 30~45% 향상, 환각률 25% 감소 (논문 기준)
→ 지원 모델: Claude, GPT, Gemini, Llama, Ollama 등 모든 LLM
→ 가장 잘 맞는 케이스: 반복 실행 태스크 + 측정 가능한 품질 기준 있을 때

프롬프트 엔지니어링 vs DSPy

기존 프롬프트 엔지니어링:
→ 프롬프트 직접 작성 (수백~수천 단어)
→ 결과 별로면 수동 수정
→ 모델 바꾸면 처음부터 다시
→ 어떤 프롬프트가 왜 잘 되는지 불명확
→ "감"에 의존한 반복 시행착오

DSPy:
→ 입력/출력 타입만 선언
→ 예시 데이터 + 평가 기준 정의
→ 옵티마이저가 자동으로 최적 프롬프트 탐색
→ 모델 바꿔도 자동 재최적화
→ 측정 가능하고 재현 가능한 개선

머신러닝 비유:

손으로 피처 엔지니어링 → AutoML이 자동화
손으로 프롬프트 작성   → DSPy가 자동화

실전 1 — 설치 및 기본 구조

pip install dspy
pip install anthropic  # 또는 openai

Signature — 입출력 선언

DSPy의 가장 기본 단위입니다. 무엇을 받아서 무엇을 출력하는지 선언합니다.

import dspy

# LM 설정
lm = dspy.LM("anthropic/claude-sonnet-4-5", api_key="your-key")
dspy.configure(lm=lm)

# ── Signature 정의 ──────────────────────────────────────
# 형식: "입력1, 입력2 -> 출력1, 출력2"
# 필드 설명이 곧 프롬프트 힌트가 됨

class SentimentClassifier(dspy.Signature):
    """고객 리뷰의 감정을 분류합니다."""
    review: str = dspy.InputField(desc="분석할 고객 리뷰 텍스트")
    sentiment: str = dspy.OutputField(desc="positive, negative, neutral 중 하나")
    confidence: float = dspy.OutputField(desc="0.0~1.0 사이 신뢰도")
    reason: str = dspy.OutputField(desc="분류 이유 한 줄 설명")

# ── Module로 실행 ────────────────────────────────────────
classifier = dspy.Predict(SentimentClassifier)

result = classifier(review="배송이 빠르고 제품도 마음에 들어요. 재구매 의향 있습니다.")

print(result.sentiment)   # positive
print(result.confidence)  # 0.95
print(result.reason)      # 긍정적 경험과 재구매 의향을 명확히 표현
[Signature의 역할]
→ 필드 이름 + desc가 자동으로 프롬프트에 반영됨
→ 타입 어노테이션으로 출력 검증
→ Docstring이 태스크 설명으로 사용됨
→ 프롬프트를 직접 작성하지 않아도 됨

실전 2 — Module: 추론 방식 선택

같은 Signature라도 추론 방식을 다르게 쓸 수 있습니다.

import dspy

class QASignature(dspy.Signature):
    """주어진 컨텍스트를 기반으로 질문에 답합니다."""
    context: str = dspy.InputField(desc="답변 근거가 되는 문서 내용")
    question: str = dspy.InputField(desc="사용자 질문")
    answer: str = dspy.OutputField(desc="간결하고 정확한 답변")

# ── Predict: 단순 예측 ─────────────────────────────────
simple_qa = dspy.Predict(QASignature)

# ── ChainOfThought: 단계별 추론 (복잡한 문제에 유리) ───
cot_qa = dspy.ChainOfThought(QASignature)

# ── ReAct: 도구 사용 포함 에이전트 추론 ────────────────
def search_tool(query: str) -> str:
    """실제로는 검색 API 호출"""
    return f"검색 결과: {query}에 관한 정보..."

react_qa = dspy.ReAct(QASignature, tools=[search_tool])

# 사용
context = """
DSPy는 Stanford에서 개발한 LLM 프레임워크입니다.
2023년 처음 공개됐으며 자동 프롬프트 최적화가 핵심 기능입니다.
"""

result_simple = simple_qa(context=context, question="DSPy는 어디서 만들었나요?")
result_cot = cot_qa(context=context, question="DSPy의 가장 중요한 특징은?")

print(result_simple.answer)
print(result_cot.answer)
# ChainOfThought는 result_cot.reasoning으로 추론 과정도 확인 가능
print(result_cot.reasoning)
[Module 선택 가이드]
→ Predict: 단순 분류, 키워드 추출 등 직관적 태스크
→ ChainOfThought: 복잡한 추론, 수학 문제, 다단계 분석
→ ProgramOfThought: 코드 실행이 필요한 수치 계산
→ ReAct: 도구 사용 + 추론 반복이 필요한 에이전트 태스크
→ MultiChainComparison: 여러 추론 경로 비교 후 최선 선택

실전 3 — 핵심: Optimizer로 자동 최적화

여기가 DSPy의 핵심입니다. 예시 데이터 + 평가 기준을 주면 옵티마이저가 최적 프롬프트를 자동으로 찾습니다.

import dspy
from dspy.evaluate import Evaluate

# ── 1. 프로그램 정의 ────────────────────────────────────
class ReviewAnalyzer(dspy.Module):
    def __init__(self):
        self.classify = dspy.ChainOfThought(SentimentClassifier)

    def forward(self, review: str):
        return self.classify(review=review)

# ── 2. 학습 데이터 준비 ─────────────────────────────────
# 실제로는 수십~수백 개 권장
trainset = [
    dspy.Example(
        review="제품이 설명과 달라서 실망했습니다.",
        sentiment="negative"
    ).with_inputs("review"),
    dspy.Example(
        review="가격 대비 품질이 훌륭합니다!",
        sentiment="positive"
    ).with_inputs("review"),
    dspy.Example(
        review="그냥 보통이에요. 나쁘지는 않아요.",
        sentiment="neutral"
    ).with_inputs("review"),
    # ... 더 많은 예시
]

# ── 3. 평가 기준(Metric) 정의 ───────────────────────────
def sentiment_metric(example, prediction, trace=None) -> float:
    """
    예측이 정답과 일치하면 1.0, 아니면 0.0
    복잡한 태스크는 LLM-as-Judge 사용 가능
    """
    return float(example.sentiment == prediction.sentiment)

# ── 4. 옵티마이저 실행 ──────────────────────────────────
from dspy.teleprompt import BootstrapFewShot

# BootstrapFewShot: 가장 기본적인 옵티마이저
# 학습 데이터에서 자동으로 few-shot 예시 선택
optimizer = BootstrapFewShot(
    metric=sentiment_metric,
    max_bootstrapped_demos=4,   # 최대 few-shot 예시 수
    max_labeled_demos=16,       # 레이블된 데이터 최대 사용량
)

program = ReviewAnalyzer()

# 컴파일 — 이 과정에서 최적 프롬프트 탐색
optimized_program = optimizer.compile(
    program,
    trainset=trainset,
)

# ── 5. 결과 비교 ─────────────────────────────────────────
# 최적화 전
result_before = program(review="포장이 엉망이고 배송도 늦었어요.")
print(f"최적화 전: {result_before.sentiment}")

# 최적화 후
result_after = optimized_program(review="포장이 엉망이고 배송도 늦었어요.")
print(f"최적화 후: {result_after.sentiment}")

# 최적화된 프로그램 저장
optimized_program.save("optimized_classifier.json")

실전 4 — MIPROv2로 고급 최적화

BootstrapFewShot이 few-shot 예시만 최적화한다면, MIPROv2는 지시문(instructions)까지 자동으로 개선합니다.

from dspy.teleprompt import MIPROv2

# MIPROv2: 지시문 + few-shot 동시 최적화
# Bayesian Optimization으로 탐색 공간 효율화
optimizer = MIPROv2(
    metric=sentiment_metric,
    auto="medium",          # "light" / "medium" / "heavy" (탐색 깊이)
    num_threads=4,          # 병렬 실행
    prompt_model=dspy.LM("anthropic/claude-sonnet-4-5"),  # 지시문 생성용
    task_model=dspy.LM("anthropic/claude-haiku-4-5"),     # 실제 태스크용
)

# 검증셋도 준비
devset = [
    dspy.Example(review="완전 별로에요.", sentiment="negative").with_inputs("review"),
    dspy.Example(review="강력 추천합니다!", sentiment="positive").with_inputs("review"),
]

optimized = optimizer.compile(
    ReviewAnalyzer(),
    trainset=trainset,
    valset=devset,
    requires_permission_to_run=False,  # 자동 실행 허용
)

# 최적화된 프롬프트 확인 (어떻게 바꿨는지 투명하게 확인 가능)
optimized.inspect_history(n=3)
# ── RAG 파이프라인에 DSPy 적용 ────────────────────────

class RAGSignature(dspy.Signature):
    """검색된 문서를 기반으로 질문에 답합니다."""
    question: str = dspy.InputField()
    context: list[str] = dspy.InputField(desc="검색된 관련 문서들")
    answer: str = dspy.OutputField(desc="정확하고 간결한 답변")
    citations: list[int] = dspy.OutputField(desc="사용한 문서 인덱스 목록")

class RAGProgram(dspy.Module):
    def __init__(self, retriever):
        self.retriever = retriever
        self.generate = dspy.ChainOfThought(RAGSignature)

    def forward(self, question: str):
        # 1. 검색
        docs = self.retriever(question)
        context = [doc.text for doc in docs]

        # 2. 생성
        return self.generate(question=question, context=context)

# RAG 품질 평가 메트릭
def rag_metric(example, prediction, trace=None) -> float:
    # 정답 포함 여부 + 인용 정확도 복합 평가
    answer_correct = example.answer.lower() in prediction.answer.lower()
    has_citations = len(prediction.citations) > 0
    return float(answer_correct) * 0.7 + float(has_citations) * 0.3
[옵티마이저 선택 가이드]
→ BootstrapFewShot: 시작점. 데이터 적을 때 (10~50개)
→ MIPROv2: 지시문까지 최적화. 데이터 충분할 때 (50개+)
→ SIMBA: 어려운 예시 집중 학습. 엣지케이스 많을 때
→ GEPA: 실패 패턴 분석 기반. 반복 개선 필요할 때
→ BootstrapFinetune: 모델 자체를 파인튜닝. 최고 성능 필요 시

마무리

✅ DSPy가 빛나는 경우
→ 같은 태스크를 반복 실행하는 프로덕션 LLM 앱
→ "이 프롬프트가 왜 잘 되는지 모르겠다" 상황
→ 모델을 자주 교체하거나 여러 모델 비교가 필요할 때
→ 팩트 정확도, 감정 분류 등 측정 가능한 품질 기준이 있을 때
→ RAG 파이프라인 품질을 체계적으로 올리고 싶을 때

❌ DSPy가 안 맞는 경우
→ 1회성 또는 비정형 태스크 (매번 다른 요청)
→ 평가 기준을 수치로 표현하기 어려운 창의적 작업
→ 학습 데이터가 아예 없는 완전 새 태스크
→ 빠른 프로토타이핑이 목표 (오버엔지니어링)
→ 단순 챗봇 — 프롬프트 직접 짜는 게 더 빠름

 

반응형