DSPy GEPA: 프롬프트 최적화로 더 싸고 더 정확하게
DSPy GEPA: 프롬프트 최적화로 더 싸고 더 정확하게
"같은 프롬프트인데 모델마다 반응이 다르고, 다음 달 업데이트되면 또 안 되고" — 수동으로 프롬프트를 다듬는 일은 느리고, 그 노하우가 다른 모델에 전수되지도 않아요. DSPy가 제안하는 해법은 프롬프트 최적화예요. 그중에서도 반성(reflection) 기반의 GEPA 옵티마이저를 함께 보겠어요.
출처: DSPy GEPA 공식 문서
왜 프롬프트를 최적화하나
언어 모델은 같은 프롬프트에 제각각 반응해요. 어떤 모델에서 깔끔한 구조화 출력을 주는 요청이 다른 모델에선 장황하게 흘러갈 수 있고, 오늘 잘 되던 표현이 다음 달 제공자 업데이트로 깨질 수 있어요. 모델 하나만 붙잡아도 단어 선택과 지침의 조합은 사실상 무한하죠.
프롬프트 최적화는 이 수동 조정 루프를 대체해요. DSPy에 훈련 세트와 메트릭을 주면, 옵티마이저가 (LM을 써서) 지침의 변형을 만들고, 후보 지침으로 예시를 실행해, 가장 점수가 높은 프롬프트를 유지해요. 우리가 어떤 프롬프트 트릭이 특정 모델에 맞는지 알 필요가 없어요. 옵티마이저가 찾아주죠.
절약 효과는 클 수 있어요. 작고 저렴한 모델에서 최적화된 프로그램이, 같은 작업에서 수동 프롬프트된 큰 모델을 따라잡는 일도 흔해요. 신뢰성도 함께 좋아져요. 옵티마이저가 메트릭이 보상하는 쪽으로 선택하니까요.
실제 사례로, Shopify는 단일 프롬프트 GPT-5 작업을 DSPy로 옮기고 작은 Qwen 모델에 GEPA로 최적화해서 약 75배 저렴하고 2배 더 신뢰성 있는 솔루션을 얻었어요. Dropbox도 DSPy와 프롬프트 옵티마이저로 더 작은 모델로 옮겨 정확도를 두 배로 올리면서 "같은 비용으로 10~100배 많은 데이터"를 확보했다고 했어요.
무엇보다, 다음 주에 새 모델이 나오면 옵티마이저를 다시 돌려 "바꿀 가치가 있는지" 즉시 판단할 수 있다는 게 큰 장점이에요.
GEPA는 반성을 이용해 지침을 개선한다
DSPy에는 여러 프롬프트 옵티마이저가 있지만, 여기선 GEPA에 집중할게요. GEPA의 핵심 특징은 메트릭이 텍스트 피드백을 제공할 수 있다는 점이에요. 이 피드백을 LM이 받아 이후 지침을 만드는 데 써요. 메트릭을 이렇게 바꿔 보겠어요.
def haiku_score_gepa(example, prediction, trace=None, pred_name=None, pred_trace=None):
"""
Penalize verbatim use of the input season string.
A haiku should evoke the season through imagery, not name it
directly.
"""
text = prediction.haiku.lower()
if example.season.strip().lower() in text:
return dspy.Prediction(
score=0.0,
feedback="Don't reference the input season verbatim."
)
return dspy.Prediction(score=1.0, feedback=None)
점수만 반환하는 대신, 예측이 왜 실패했는지(또는 성공했는지)를 텍스트로 알려줄 수 있어요. 라벨링할 때 어노테이터가 각 레코드의 뉘앙스를 메모로 적어 두면, 그게 GEPA로 전달되어 이후 지침을 안내할 수 있어요. LM judge로 훈련할 때도 judge가 "왜 기준에 미달했는지" 상세 피드백을 줄 수 있어요.
하이쿠 메트릭 확장
모델에 더 어려운 과제를 주기 위해 메트릭을 여러 조건으로 확장했어요. 행 수, 각 행의 음절 수, 입력 반복 회피, 1인칭 회피, 품사 비율, 형용사·관사 사용 최소화, 현재 시제 등이에요. 대부분의 측정은 자연어 처리 라이브러리 spaCy로 도울 수 있어요. 전체 코드는 여기에서 확인할 수 있어요.
최적화 컴파일
메트릭이 준비됐으면 옵티마이저를 설정해요.
from haiku_metric import haiku_metric
reflection_lm = dspy.LM("openai/gpt-5.4")
optimizer = dspy.GEPA(
metric=haiku_metric,
reflection_lm=reflection_lm,
auto="light",
num_threads=2,
)
여기서 주목할 건 reflection_lm이에요. GEPA는 반성과 지침 작성에 쓰는 별도 LM을 선택할 수 있어요. 이 LM이 예시와 점수를 보고 프롬프트를 다시 써서 점수 개선을 시도해요. 작은 모델을 최적화할 땐 더 큰 모델을 reflection_lm으로 쓰는 게 좋아요. 더 나은 추론·프롬프팅 능력을 갖췄고, 최적화 중 몇 번만 호출되므로 비용도 큰 걱정이 아니에요.
auto 인자는 예산을 정해요. auto="light"는 후보 프롬프트 약 6개를 평가하고 멈추고, "medium"·"heavy"는 더 멀리 가요. num_threads는 추론 제공자에 따라 레이트 리밋을 피하도록 조정할 수 있어요. 이제 컴파일을 실행해요.
optimized_haiku_bot = optimizer.compile(haiku_bot, trainset=train, valset=val)
훈련·검증 세트 고르기
GEPA는 두 분할을 다른 일에 써요. trainset은 반성적 프롬프트 업데이트의 예시를 제공하고, valset은 Pareto 점수를 추적해 compile이 반환할 프로그램을 선택해요. 최종 테스트 세트는 따로 두어, 두 단계에 모두 영향받지 않은 예시로 최적화된 프로그램을 평가해야 해요.
보편적인 분할 비율은 없어요. 일반화를 위해 trainset에 최대한 많은 데이터를 두고 valset은 하류 분포를 대표하는 최소한의 샘플로 하는 게 좋아요. 모든 후보가 검증 예시로 점수 매겨지므로, 불필요하게 큰 valset은 고정된 메트릭 호출 예산 안에서 GEPA가 탐색할 후보 수를 줄여요.
valset을 생략하면 GEPA는 선택에 trainset을 재사용해요. 추론 시점 검색처럼 "지금 갖고 있는 예시에서 최고 출력을 찾는" 용도에는 유용하지만, 보이지 않는 데이터 성능이 중요하다면 별도 검증 세트를 넘기세요.
컴파일은 내부적으로 반복 루프로 도는데, 클라이언트 LM으로 프로그램을 실행하고 메트릭으로 점수를 매기고 → 반성 LM이 새 지침을 제안하고 → 그 지침으로 다시 실행·채점해 최고 점수 후보를 유지해요. 예산이 다하면 끝나요.
gpt-5.4-nano로 이 옵티마이저를 돌리고 gpt-5.4를 반성 LM으로 써보니, 전체 하이쿠 메트릭에서 점수가 78.1%에서 90.1%로 올랐어요. 기준선이었던 gpt-5.4의 82.4%보다 높죠. 즉 GEPA로 최적화된 작은 모델이, 최적화되지 않은 프론티어 모델보다 빠르고 싸고 더 나아요.
GEPA가 개선한 프롬프트
ReAct는 추론·도구 호출 결정을 하는 Predict 모듈과 최종 출력을 만드는 ChainOfThought 모듈로 구성돼요. GEPA는 이 두 모듈을 모두 최적화해요. 초기 지침이 "주어진 입력으로 고전 하이쿠를 써라"였다면, 컴파일 후에는 입력 항목, 출력 요구사항, 성공 기준, 장소·계절·기분 처리, 공통 실패 모드, 최종 품질 체크까지 담긴 훨씬 상세한 지침으로 바뀌어요.
흥미롭게도 같은 프로그램도 모델에 따라 다르게 최적화돼요. GEPA의 전체 메커니즘(Pareto 샘플링, 퍼-예측기 피드백, auto 예산 변환, detailed_results 감사 추적)은 GEPA in depth에서 볼 수 있어요.
더 알아보기
- 옵티마이저 고르기: Choosing an Optimizer
- GEPA 심화: GEPA in depth
- 프롬프트 대신 프로그래밍: Program, don't prompt