DSPy 옵티마이저

DSPy 옵티마이저 (이전 이름: 텔레프롬프터)

DSPy 옵티마이저는 DSPy 프로그램의 파라미터, 즉 프롬프트나 LM 가중치를 조절해서 여러분이 지정한 메트릭(예: 정확도)을 최대화하는 알고리즘이에요.

전형적인 DSPy 옵티마이저는 세 가지를 입력으로 받아요.

  • DSPy 프로그램. 단일 모듈일 수도 있고(dspy.Predict), 여러 모듈로 이뤄진 복잡한 프로그램일 수도 있어요.
  • 메트릭. 프로그램의 출력을 평가해 점수를 매기는 함수예요. 점수는 높을수록 좋아요.
  • 몇 개의 훈련 입력. 아주 작아도(5~10개) 되고, 불완전해도 돼요(라벨 없이 입력만 있어도 됨).

데이터가 많다면 DSPy가 그걸 활용해요. 하지만 적게 시작해도 강력한 결과를 얻을 수 있어요.

참고: 예전에는 텔레프롬프터라고 불렀어요. 공식 명칭을 바꾸는 중이라, 라이브러리와 문서 전반에 그 변경이 반영될 거예요.

DSPy 옵티마이저는 무엇을, 어떻게 튜닝하나요?

옵티마이저마다 프로그램의 품질을 높이는 방식이 달라요. 크게 세 갈래로 나눠볼 수 있어요.

  • 좋은 few-shot 예시를 합성하는 옵티마이저 — 예: dspy.BootstrapRS1
  • 더 나은 자연어 명령어를 제안하고 탐색하는 옵티마이저 — 예: dspy.MIPROv2,2 dspy.GEPA3
  • 모듈용 데이터셋을 만들어 LM 가중치를 미세조정하는 옵티마이저 — 예: dspy.BootstrapFinetune4

옵티마이저 한 개의 동작 예시는?

dspy.MIPROv2를 예로 들어볼게요. MIPRO는 세 단계로 돌아가요.

  1. 부트스트래핑 단계. 아직 최적화되지 않은 프로그램을 서로 다른 입력들에 대해 여러 번 실행해서, 각 모듈의 입출력 동작 trace를 모아요. 그중 메트릭이 높게 채점한 궤적에만 있는 trace만 남겨요.
  2. 근거 있는 제안 단계(grounded proposal stage). 프로그램 코드, 데이터, 프로그램을 돌려 얻은 trace를 미리 훑어보고, 프로그램 안의 모든 프롬프트에 대해 후보 명령어를 여러 개 초안으로 작성해요.
  3. 이산 탐색 단계(discrete search stage). 훈련 셋에서 미니배치를 샘플링해서, 파이프라인의 각 프롬프트를 만들 명령어와 trace의 조합을 제안하고, 미니배치에서 후보 프로그램을 평가해요. 그 점수를 바탕으로 서로게이트 모델을 갱신해서, 시간이 지날수록 제안이 더 좋아지게 해요.

DSPy 옵티마이저가 강력한 이유 중 하나는 합성(compose)이 가능하다는 점이에요. dspy.MIPROv2를 돌린 결과 프로그램을 다시 dspy.MIPROv2의 입력으로 쓰거나, dspy.BootstrapFinetune에 넘겨 더 나은 결과를 얻을 수 있어요. 이게 dspy.BetterTogether의 본질이기도 해요. 아니면 옵티마이저를 돌린 뒤 상위 5개 후보 프로그램을 뽑아 dspy.Ensemble로 묶을 수도 있어요. 이렇게 하면 추론 시점(inference-time) 연산(예: 앙상블)과 DSPy만의 고유한 추론 전(pre-inference) 연산(즉 최적화 예산)을 매우 체계적으로 확장할 수 있어요.

현재 어떤 DSPy 옵티마이저가 있나요?

옵티마이저는 dspy.<OptimizerName> 형태로 접근해요(예: dspy.MIPROv2, dspy.BootstrapFewShot).

자동 Few-Shot 학습

이 옵티마이저들은 시그니처를 확장해서, 모델에 보내는 프롬프트 안에 최적화된 예시를 자동 생성·포함시켜 few-shot 학습을 구현해요.

  1. LabeledFewShot: 라벨이 달린 입출력 데이터 포인트에서 간단히 few-shot 예시(demo)를 만들어요. 프롬프트에 넣을 예시 개수인 k와, 그만큼을 무작위로 뽑을 trainset이 필요해요.

  2. BootstrapFewShot: 기본값으로 여러분의 프로그램을 쓰는 teacher 모듈로, 프로그램의 각 단계에 대한 완전한 demo를 만들어내고 trainset의 라벨 예시도 함께 써요. 파라미터로 max_labeled_demos(trainset에서 무작위로 뽑는 demo 수)와 max_bootstrapped_demos(teacher가 추가 생성하는 예시 수)가 있어요. 부트스트래핑 과정에서 메트릭으로 demo를 검증해서, "컴파일된" 프롬프트에는 메트릭을 통과한 것만 넣어요. 고급: 구조가 호환되는 다른 DSPy 프로그램을 teacher로 쓸 수 있어서, 더 어려운 태스크에 대응할 수 있어요.

  3. BootstrapFewShotWithRandomSearch: BootstrapFewShot를 생성된 demo에 대해 무작위 탐색을 하며 여러 번 적용하고, 최적화 과정에서 가장 좋은 프로그램을 골라요. 파라미터는 BootstrapFewShot과 같되, num_candidate_programs가 추가돼요. 이건(컴파일 안 된 프로그램, LabeledFewShot 최적화 프로그램, 예시를 섞지 않은 BootstrapFewShot 컴파일 프로그램, 그리고 예시 셋을 무작위화한 BootstrapFewShot 컴파일 프로그램들을 포함한) 후보 중에서 최적화 동안 평가할 무작위 프로그램 수를 지정해요.

  4. KNNFewShot: k-Nearest Neighbors 알고리즘으로 주어진 입력 예시와 가장 가까운 훈련 예시 demo를 찾아요. 이 최근접 이웃 demo를 BootstrapFewShot 최적화 과정의 trainset으로 써요.

자동 명령어 최적화

이 옵티마이저들은 프롬프트에 최적의 명령어를 만들고, MIPROv2의 경우엔 few-shot demo 셋까지 함께 최적화해요.

  1. COPRO: 각 단계에 새 명령어를 생성·개선하고, 좌표 상승(coordinate ascent, 메트릭 함수와 trainset을 쓰는 hill-climbing)으로 최적화해요. 파라미터 depth는 옵티마이저가 프롬프트 개선을 반복하는 횟수예요.

  2. MIPROv2: 각 단계에서 명령어 few-shot 예시를 모두 생성해요. 명령어 생성은 데이터 인식(data-aware)이면서 demo 인식(demonstration-aware)이에요. 베이지안 최적화로 모듈들에 걸친 생성 명령어/demo 공간을 효과적으로 탐색해요.

  3. SIMBA: 확률적 미니배치 샘플링으로 출력 변동이 큰 어려운 예시를 찾아내고, LLM으로 실패를 내성적으로 분석해 자기 반성적 개선 규칙을 만들거나 성공한 demo를 추가해요.

  4. GEPA: LM으로 DSPy 프로그램의 궤적을 되돌아보게 해서, 무엇이 잘됐고 무엇이 안 됐는지 파악하고 그 격차를 메울 프롬프트를 제안하게 해요. 또한 도메인 특화 텍스트 피드백을 활용해 DSPy 프로그램을 빠르게 개선할 수도 있어요. 자세한 튜토리얼은 dspy.GEPA Tutorials에서 볼 수 있어요.

자동 미세조정

이 옵티마이저는 기반 LLM을 미세조정하는 데 쓰여요.

  1. BootstrapFinetune: 프롬프트 기반 DSPy 프로그램을 가중치 업데이트로 증류해요. 결과는 같은 단계를 갖되, 각 단계를 프롬프트된 LM이 아니라 미세조정된 모델이 수행하는 DSPy 프로그램이에요. 완전한 예시는 분류 미세조정 튜토리얼을 참고하세요.

프로그램 변환

  1. Ensemble: DSPy 프로그램들의 집합을 앙상블해서, 전체를 쓰거나 무작위 부분집합을 뽑아 하나의 프로그램으로 만들어요.

메타 옵티마이저

  1. BetterTogether: 프롬프트 최적화와 가중치 최적화(미세조정)를 설정 가능한 순서로 결합하는 메타 옵티마이저예요. 프롬프트 최적화는 효과적인 태스크 분해와 추론 전략을 발견하고, 가중치 최적화는 모델이 그 패턴을 더 효율적으로 실행하도록 특화시켜요. 이 둘을 순서로(예: prompt → weight → prompt) 사용하면 각각이 서로의 개선 위에 세워질 수 있어요. 경험적으로 이 접근이 어느 한 전략만 쓸 때보다 자주 더 좋아요.

어떤 옵티마이저를 써야 하나요?

결국 '옳은' 옵티마이저와 최적 설정을 찾는 건 실험을 필요로 해요. DSPy에서의 성공은 여전히 반복 과정이에요. 가장 좋은 성능을 얻으려면 탐색하고 반복해야 해요.

그래도 시작할 때의 일반적 지침은 이래요.

  • 예시가 아주 적다면(약 10개), BootstrapFewShot부터 시작하세요.
  • 데이터가 더 많다면(50개 이상), BootstrapFewShotWithRandomSearch를 써보세요.
  • 명령어 최적화만 하고 싶다면(즉 프롬프트를 0-shot으로 유지하고 싶다면), MIPROv20-shot 최적화용으로 설정해서 쓰세요.
  • 더 긴 최적화 실행(예: 40 트라이얼 이상)에 추론 호출을 더 써도 되고 데이터도 충분하다면(과적합 방지를 위해 200개 이상), MIPROv2를 써보세요.
  • 큰 LM(예: 7B 파라미터 이상)으로 그중 하나를 이미 써봤고 효율적인 프로그램이 필요하다면, BootstrapFinetune으로 태스크에 맞는 작은 LM을 미세조정하세요.

옵티마이저는 어떻게 쓰나요?

모두 비슷한 일반 인터페이스를 공유하고, 키워드 인자(하이퍼파라미터)에서 조금씩 달라져요. 전체 목록은 API 레퍼런스에서 볼 수 있어요.

가장 흔한 BootstrapFewShotWithRandomSearch로 살펴볼게요.

import dspy

# 옵티마이저 설정: 프로그램 단계의 8-shot 예시를 "부트스트랩"(스스로 생성)하고 싶어요.
# 옵티마이저는 devset에서 최고 시도를 고르기 전에 이 과정을 10번(초기 시도 몇 회 포함) 반복해요.
config = dict(max_bootstrapped_demos=4, max_labeled_demos=4, num_candidate_programs=10, num_threads=4)

teleprompter = dspy.BootstrapFewShotWithRandomSearch(metric=YOUR_METRIC_HERE, **config)
optimized_program = teleprompter.compile(YOUR_PROGRAM_HERE, trainset=YOUR_TRAINSET_HERE)

참고: 전형적인 단순 최적화 실행은 대략 $2 정도의 비용과 10분가량의 시간이 들어요. 아주 큰 LM이나 아주 큰 데이터셋으로 옵티마이저를 돌릴 때는 주의하세요. 옵티마이저 실행은 LM·데이터셋·설정에 따라 몇 센트부터 수십 달러까지 비용이 달라질 수 있어요.

실행 예시들

ReAct 에이전트의 프롬프트 최적화 — 위키백과 검색으로 질문에 답하는 dspy.ReAct 에이전트를 만들고, HotPotQA 데이터셋에서 샘플링한 500개 QA 쌍으로 저렴한 light 모드의 dspy.MIPROv2로 최적화해요.

import dspy
from dspy.datasets import HotPotQA

dspy.configure(lm=dspy.LM('openai/gpt-4o-mini'))

def search(query: str) -> list[str]:
    """Retrieves abstracts from Wikipedia."""
    results = dspy.ColBERTv2(url='http://20.102.90.50:2017/wiki17_abstracts')(query, k=3)
    return [x['text'] for x in results]

trainset = [x.with_inputs('question') for x in HotPotQA(train_seed=2024, train_size=500).train]
react = dspy.ReAct("question -> answer", tools=[search])

tp = dspy.MIPROv2(metric=dspy.evaluate.answer_exact_match, auto="light", num_threads=24)
optimized_react = tp.compile(react, trainset=trainset)

이와 비슷한 비공식 실행이 DSPy 2.5.29에서 ReAct의 점수를 24%에서 51%로 끌어올렸어요.

RAG의 프롬프트 최적화search용 검색 인덱스와 좋아하는 dspy.LM, 질문과 정답이 들어있는 작은 trainset만 있으면, 긴 출력을 내는 RAG 시스템을 DSPy 모듈로 구현된 내장 dspy.SemanticF1 메트릭으로 최적화할 수 있어요.

class RAG(dspy.Module):
    def __init__(self, num_docs=5):
        self.num_docs = num_docs
        self.respond = dspy.ChainOfThought('context, question -> response')

    def forward(self, question):
        context = search(question, k=self.num_docs)   # this snippet에 정의 안 됨, 위 링크 참고
        return self.respond(context=context, question=question)

tp = dspy.MIPROv2(metric=dspy.SemanticF1(), auto="medium", num_threads=24)
optimized_rag = tp.compile(RAG(), trainset=trainset, max_bootstrapped_demos=2, max_labeled_demos=2)

실행 가능한 완전한 RAG 예시는 이 튜토리얼에서 시작해 볼 수 있어요. StackExchange 커뮤니티 일부 영역에서 RAG 시스템의 품질을 53%에서 61%로 개선해요.

분류를 위한 가중치 최적화BootstrapFinetune으로 분류 모델의 가중치를 튜닝해요. (데이터셋 설정 코드는 접어두었어요.)

import dspy
lm=dspy.LM('openai/gpt-4o-mini-2024-07-18')

# 분류용 DSPy 모듈 정의. 훈련 시점에 hint가 있으면 그걸 사용해요.
signature = dspy.Signature("text, hint -> label").with_updated_fields('label', type_=Literal[tuple(CLASSES)])
classify = dspy.ChainOfThought(signature)
classify.set_lm(lm)

# BootstrapFinetune으로 최적화.
optimizer = dspy.BootstrapFinetune(metric=(lambda x, y, trace=None: x.label == y.label), num_threads=24)
optimized = optimizer.compile(classify, trainset=trainset)

optimized(text="What does a pending cash withdrawal mean?")

# 완전한 미세조정 튜토리얼: https://dspy.ai/tutorials/classification_finetuning/

(마지막 줄의 가능한 출력):

Prediction(
    reasoning='A pending cash withdrawal indicates that a request to withdraw cash has been initiated but has not yet been completed or processed. This status means that the transaction is still in progress and the funds have not yet been deducted from the account or made available to the user.',
    label='pending_cash_withdrawal'
)

이와 비슷한 비공식 실행이 DSPy 2.5.29에서 GPT-4o-mini의 점수를 66%에서 87%로 끌어올렸어요.

옵티마이저 출력 저장하고 불러오기

프로그램을 옵티마이저에 통과시킨 뒤에는 저장해두는 게 좋아요. 나중에 파일에서 프로그램을 불러와 추론에 쓸 수 있어요. 이때 loadsave 메서드를 사용해요.

optimized_program.save(YOUR_SAVE_PATH)

결과 파일은 일반 텍스트 JSON 형식이에요. 원본 프로그램의 모든 파라미터와 단계를 담고 있어서, 옵티마이저가 뭘 생성했는지 언제든 열어볼 수 있어요.

파일에서 프로그램을 불러오려면, 해당 클래스의 객체를 생성한 뒤 그 객체에 load 메서드를 호출하면 돼요.

loaded_program = YOUR_PROGRAM_CLASS()
loaded_program.load(path=YOUR_SAVE_PATH)