DSPy 3.3.0: GEPA와 dspy.Flex로 프롬프트와 코드 흐름을 동시에 최적화하는 방법
요약
2026년 8월 3일 출시된 DSPy 3.3.0은 LLM 파이프라인 최적화의 범위를 한 단계 확장했다. 기존 DSPy 최적화기는 시그니처의 프롬프트 지시문만 다시 쓸 수 있었다. 3.3.0의 핵심 추가 기능인 dspy.Flex는 파이썬 코드 흐름까지 최적화 대상으로 삼는다. 중간 Predictor를 나누거나, 결정론적 단계를 순수 파이썬으로 이동하거나, 도우미 함수를 새로 만드는 방식으로 전체 모듈 소스를 재작성할 수 있다.
핵심 요약:
- GEPA: 실패 추적(오류 메시지·실패한 파스·평가 로그)을 분석해 반영 모델이 수정 프롬프트를 생성하는 유전적 탐색 최적화기. GRPO 대비 35× 적은 롤아웃으로 최대 20% 성능 향상.
- dspy.Flex: GEPA가 프롬프트뿐 아니라 모듈 소스 코드까지 재작성하는 DSPy 최초의 프리미티브.
- dspy.ReActV2: 기존 문자열 파싱 기반 ReAct를 제공자 네이티브 함수 호출 API로 재구성. 병렬 도구 호출과
dspy.History타입 지원. - 타입 지정 LM 인터페이스: OpenAI·Anthropic·Google 등을 원시 문자열 없이 타입 어댑터 계층으로 추상화.
배경: DSPy란 무엇인가
DSPy(Demonstrate-Search-Predict, Stanford NLP)는 LLM 파이프라인을 프로그래밍하는 프레임워크다. 일반적인 프롬프트 엔지니어링과 다른 점은 두 가지다.
- 선언적 시그니처:
dspy.Signature로 입력·출력 타입을 선언하면 구체적인 프롬프트 문자열은 최적화기가 생성한다. - 컴파일(최적화):
dspy.Compile(program, optimizer, metric)를 호출하면 최적화기가 프로그램을 실행하며 평가 메트릭을 최대화하는 프롬프트·가중치·제어 흐름을 탐색한다.
이 접근의 핵심 가치는 프롬프트를 손으로 반복하지 않아도 된다는 것이다. 무엇이 "정답"인지(평가 메트릭)를 정의하면 DSPy가 어떻게 물어볼지(프롬프트)를 찾아낸다.
GEPA: 반영·돌연변이 기반 프롬프트 최적화
기존 접근의 한계
DSPy 3.2 이전에는 GRPO(Gradient-based Prompt Optimization)가 주요 최적화기였다. GRPO는 샘플링을 반복하며 그래디언트 추정치로 프롬프트를 갱신한다. 문제는 롤아웃 비용이다. 고품질 수렴을 위해 수백 번의 전체 파이프라인 실행이 필요하다.
GEPA의 설계
GEPA(Generalized Error-driven Prompt Augmentation)는 ICLR 2026에서 발표된 알고리즘을 DSPy에 통합한 것이다. 핵심 아이디어는 실패 추적을 돌연변이 연산자로 사용한다는 점이다.
최적화 루프:
- 평가: 현재 프롬프트로 프로그램을 실행, 실패한 사례(오류 메시지, 파스 실패, 메트릭 하한 미달)를 수집.
- 반영: 반영 모델(별도 LM 호출)이 실패 이유를 진단하고, 실패 패턴을 수정하는 새 프롬프트 지시문 후보를 생성.
- 선택: 유전적 탐색으로 성능이 개선된 후보만 다음 세대로 전달.
import dspy
# 평가 메트릭 정의
def accuracy_metric(example, prediction, trace=None):
return example.answer == prediction.answer
# 프로그램 선언
class ExtractAnswer(dspy.Module):
def __init__(self):
self.predict = dspy.Predict("context, question -> answer")
def forward(self, context, question):
return self.predict(context=context, question=question)
# GEPA 최적화
from dspy.teleprompt import GEPA
optimizer = GEPA(metric=accuracy_metric, max_iters=10)
optimized_program = optimizer.compile(ExtractAnswer(), trainset=train_data)GRPO 대비 35× 적은 롤아웃으로 같거나 더 높은 성능을 달성한다. 데이터가 작거나 API 호출 비용이 높은 상황에서 실질적인 차이를 만든다.
dspy.Flex: 코드 흐름까지 최적화하는 첫 번째 프리미티브
Flex의 핵심 개념
기존 DSPy 최적화는 모듈 구조(Predictor 수, 파이썬 로직)는 고정하고 프롬프트 지시문만 바꿀 수 있었다. dspy.Flex는 이 제약을 제거한다.
dspy.Flex는 모듈 구현 자체가 최적화 대상이다. GEPA는 Flex가 적용된 모듈에 대해 다음을 시도한다:
- 하나의 Predictor를 여러 개로 분리
- LM 호출이 필요 없는 단계를 순수 파이썬으로 이동
- 도우미 함수 또는 서브모듈 추가
- 태스크를 더 잘 수행하는 완전히 새로운 제어 흐름 작성
from dspy import Flex
class SmartSummarizer(Flex):
"""이 모듈의 구현 코드는 GEPA가 최적화한다."""
signature = "long_document -> concise_summary, key_points: list[str]"
# GEPA + Flex: 프롬프트와 코드를 함께 탐색
optimizer = GEPA(metric=summarization_metric, max_iters=15)
optimized = optimizer.compile(SmartSummarizer(), trainset=train_data)최적화 후 optimized의 소스를 보면 원래 선언한 Flex가 아니라, GEPA가 생성한 완전히 다른 구현이 들어 있을 수 있다.
언제 Flex를 사용할까
| 상황 | Flex 사용 여부 |
|---|---|
| 프롬프트 지시문 조정으로 충분한 경우 | 필요 없음 (dspy.Predict + GEPA) |
| 태스크를 더 작은 단계로 분해하면 나아질 것 같은 경우 | 권장 |
| 파이프라인 구조 자체가 불확실한 경우 | 권장 |
| 결정론적 로직과 LM 호출의 비율이 적절한지 모를 경우 | 권장 |
dspy.ReActV2: 도구 호출 에이전트 재설계
기존 ReAct의 문제
기존 dspy.ReAct는 Thought-Action-Observation 사이클을 프롬프트로 유도하고 문자열을 파싱하는 방식으로 구현됐다. 이 방식은 세 가지 문제를 낳았다.
- 파싱 오류 — LM이 정확한 형식을 지키지 않으면 툴 호출이 실패.
- 병렬 호출 불가 — 순차적 Thought-Action 루프이므로 여러 도구를 동시에 부를 수 없음.
- 대화 이력 관리 — 타입 없는 문자열 누적으로 컨텍스트 관리가 어려움.
ReActV2의 설계
dspy.ReActV2는 제공자의 네이티브 함수 호출 API를 사용한다. 추론 결과는 JSON으로 직렬화되고 도구 정의는 OpenAI/Anthropic/Google의 tool schema로 자동 변환된다.
import dspy
def search_database(query: str) -> list[dict]:
"""데이터베이스에서 관련 레코드를 검색합니다."""
return db.search(query)
def calculate_metric(data: list[dict], metric: str) -> float:
"""주어진 데이터에 대해 지표를 계산합니다."""
return compute(data, metric)
agent = dspy.ReActV2(
"question -> answer",
tools=[search_database, calculate_metric],
max_iters=5,
)
# 병렬 도구 호출: 두 도구를 동시에 호출할 수 있음
result = agent(question="2025년 4분기 매출과 비용을 분석해줘")dspy.History 타입은 멀티턴 대화 이력을 구조화된 방식으로 관리한다. 이전 도구 호출 결과, 오류, Observation이 모두 타입 정보와 함께 보존된다.
아키텍처 다이어그램: DSPy 3.3.0 최적화 흐름
타입 지정 LM 인터페이스
DSPy 3.3.0은 LM 추상화 레이어를 정비했다. 기존에는 모델별 원시 문자열 응답을 파싱해야 했지만, 새 인터페이스는 타입 어댑터를 통해 제공자 간 동작을 표준화한다.
import dspy
# 제공자 전환: 코드 변경 없이 모델만 교체
lm = dspy.LM("openai/gpt-5.6-terra")
# lm = dspy.LM("anthropic/claude-sonnet-5")
# lm = dspy.LM("google/gemini-3.7-flash")
dspy.configure(lm=lm)
# 이하 시그니처·모듈 코드는 동일
class Classify(dspy.Module):
def __init__(self):
self.classify = dspy.Predict("text -> category: Literal['spam', 'ham']")
def forward(self, text: str) -> str:
return self.classify(text=text).category제공자 간 추상화의 실질적 이점은 프롬프트·코드를 모델에 종속되지 않게 설계할 수 있다는 점이다. GEPA가 최적화한 프로그램도 LM만 교체하면 다른 제공자에서 그대로 실행된다.
운영 고려사항
GEPA 최적화 비용
GEPA는 GRPO보다 롤아웃이 적지만, 반영 LM 호출이 추가된다. 실질적인 비용 구성:
| 항목 | GRPO | GEPA |
|---|---|---|
| 프로그램 롤아웃 수 | 높음 | 낮음 (35× 절감) |
| 반영 LM 호출 | 없음 | 이터레이션당 1회 |
| 총 API 비용 | 높음 | 중간 |
| 수렴 속도 | 느림 | 빠름 |
데이터셋이 수십~수백 샘플 수준이라면 GEPA가 더 경제적이다.
dspy.Flex 적용 범위
dspy.Flex는 GEPA가 임의 코드를 생성하므로 최적화 결과를 배포 전에 반드시 검토해야 한다. 특히:
- 생성된 코드에 원치 않는 부수 효과(파일 I/O, 외부 API 호출 등)가 없는지 확인
- 최적화된 코드는 버전 관리에 커밋해 재현 가능하게 유지
- 프로덕션 환경에서는
dspy.Flex최적화를 CI 파이프라인에 포함
데이터 엔지니어 활용 시나리오
| 태스크 | DSPy 적용 방식 |
|---|---|
| 비정형 로그 → 구조화 데이터 추출 | dspy.Predict + GEPA로 추출 정확도 자동 최적화 |
| 자연어 쿼리 → SQL 변환 | dspy.ChainOfThought + Flex로 쿼리 분해 전략 탐색 |
| 데이터 품질 이상 설명 생성 | dspy.ReActV2 + 검색 도구로 이상 원인 추적 |
| 멀티소스 데이터 조합 분석 | dspy.ReActV2 + 병렬 DB 조회 도구 |
제한사항과 열린 질문
- dspy.Flex는 실험적: 생성 코드의 안전성과 결정론적 동작이 보장되지 않는다. 프로덕션 적용 전 철저한 테스트 필요.
- 반영 LM 의존성: GEPA의 반영 단계 품질은 반영 LM의 성능에 의존한다. 약한 모델로 반영하면 수렴 품질이 낮아진다.
- 최적화 재현성: 유전적 탐색에 무작위성이 포함되어 동일 seed에서도 결과가 달라질 수 있다.
Open question:
dspy.Flex가 생성한 코드의 복잡도가 어느 수준 이상이 되면 유지보수 부담이 인간 작성 코드보다 커지는지 실증 사례가 아직 충분하지 않다.
References
- DSPy 3.3.0 Release Notes: https://github.com/stanfordnlp/dspy/releases/tag/3.3.0
- DSPy 공식 GEPA 문서: https://dspy.ai/api/optimizers/GEPA/overview/
- DSPy Flex 모듈 문서: https://dspy.ai/api/modules/Flex/
- X 발표 (Isaac Miller, DSPy maintainer): https://x.com/isaacbmiller1/status/2084410370282631534
- Hugging Face DSPy+GEPA 쿡북: https://huggingface.co/learn/cookbook/dspy_gepa
- GEPA 논문 (ICLR 2026): https://arxiv.org/abs/GEPA-iclr2026