
언어 모델을 위한 프로그래밍 프레임워크, DSPy
최근 언어 모델(LM)을 활용한 파이프라인 구축이 활발해지고 있지만, 대부분 수동으로 작성한 프롬프트 템플릿에 의존하고 있다. Stanford University를 중심으로 한 연구팀은 이러한 문제를 해결하기 위해 DSPy라는 새로운 프로그래밍 모델을 제안했다.
왜 체계적인 LM 프로그래밍이 필요할까?
현재 LM 파이프라인 개발의 문제점:
- 수동 프롬프트 엔지니어링의 한계: 긴 문자열로 된 프롬프트를 시행착오를 통해 만들어야 함
- 일반화의 어려움: 특정 작업이나 모델에 최적화된 프롬프트가 다른 상황에서는 작동하지 않음
- 복잡한 파이프라인 구축의 어려움: 여러 LM 호출이 상호작용해야 하는 경우 각각을 최적화하기 어려움
DSPy는 이를 "프롬프트 문자열 조작"에서 "프로그래밍"으로 패러다임을 전환하여 해결한다.
DSPy의 핵심 아이디어
DSPy는 언어 모델 파이프라인을 text transformation graph로 추상화하고, 이를 자동으로 최적화하는 혁신적인 접근법을 제시한다.
1. 세 가지 핵심 추상화
Signatures: 프롬프트를 넘어선 선언적 인터페이스
Signatures는 "무엇을 해야 하는지"를 선언하는 자연어 타입 시스템이다. 기존의 프롬프트 템플릿과 달리, DSPy는 작업의 의도를 추상화한다.
# 간단한 형태
qa = dspy.Predict("question -> answer")
# 고급 형태 (명시적 지시사항 포함)
class GenerateSearchQuery(dspy.Signature):
"""Write a simple search query that will help answer a complex question."""
context = dspy.InputField(desc="may contain relevant facts")
question = dspy.InputField()
query = dspy.OutputField(dtype=dspy.SearchQuery)
- 자동 프롬프트 생성: 필드 이름과 설명을 바탕으로 LM에 맞는 지시사항 자동 생성
- 구조화된 입출력: 문자열 조작 없이 깔끔한 데이터 처리
- 재사용성: 다양한 모듈과 LM에서 동일한 Signatures 활용 가능
Modules: 프롬프팅 기법의 파라미터화
Modules은 특정 프롬프팅 기법을 일반화하고 파라미터화한 구성 요소다. PyTorch의 신경망 레이어처럼 작동한다.
# ChainOfThought 모듈의 내부 구현 (단순화)
class ChainOfThought(dspy.Module):
def __init__(self, signature):
# 시그니처를 자동으로 확장: "inputs -> outputs"를
# "inputs -> rationale, outputs"로 변환
rationale_field = dspy.OutputField(
prefix="Reasoning: Let's think step by step."
)
signature = dspy.Signature(signature).prepend_output_field(rationale_field)
self.predict = dspy.Predict(signature)
def forward(self, **kwargs):
return self.predict(**kwargs)
주요 내장 Module들은 다음과 같다.
- Predict: 기본 예측 모듈
- ChainOfThought: 단계별 추론 생성
- ReAct: 도구 사용과 추론을 결합
- MultiChainComparison: 여러 추론 체인을 비교하여 최선 선택
- ProgramOfThought: 코드 생성을 통한 문제 해결
각 모듈은 파라미터화를 통해 구현되어 있다.
- 사용할 LM
- 프롬프트 지시사항
- Few-shot 데모
파라미터 중, Few-shot 데모가 제일 중요하다.
Teleprompters: 자동 프롬프트 최적화
Teleprompters는 DSPy 프로그램을 컴파일하여 최적화하는 역할을 한다. "프롬프트를 원격으로 자동 생성"한다는 의미를 담고 있다.
# 기본 부트스트래핑 예시
teleprompter = dspy.BootstrapFewShot(metric=dspy.evaluate.answer_exact_match)
compiled_rag = teleprompter.compile(RAG(), trainset=qa_trainset)
# 고급 파인튜닝 예시
finetuning_teleprompter = BootstrapFinetune(metric=custom_metric)
compiled_model = finetuning_teleprompter.compile(
RAG(),
teacher=compiled_rag, # 큰 모델을 교사로 사용
trainset=unlabeled_questions,
target='google/flan-t5-large' # 작은 모델로 증류
)
2. 컴파일 프로세스
DSPy의 컴파일은 세 단계로 진행되며, 각 단계가 유기적으로 연결된다:
Stage 1: 후보 생성 (Candidate Generation)
# 의사 코드로 본 BootstrapFewShot의 작동 방식
for example in trainset:
with dspy.context(compile=True): # 추적 모드 활성화
prediction = teacher_program(**example.inputs())
trace = dspy.settings.trace # 모든 중간 단계 기록
if metric(example, prediction, trace): # 성공적인 실행만 선택
for module, inputs, outputs in trace:
# 각 모듈에 대한 데모 저장
module.demonstrations.append(
dspy.Example(inputs=inputs, outputs=outputs)
)
- 자동 라벨 생성: 중간 단계의 라벨이 없어도 전체 파이프라인이 성공하면 역추적하여 생성
- 품질 필터링: 메트릭을 통과한 경우만 데모로 사용
- 다중 시도: 높은 temperature로 여러 번 실행하여 다양한 해결책 탐색
Stage 2: 파라미터 최적화
여러 최적화 전략 (random search or Tree structured Parzen Estimator, ...) 를 사용할 수 있다.
# 랜덤 서치
optimizer = BootstrapFewShotWithRandomSearch(metric=metric, trials=16)
# Optuna를 활용한 베이지안 최적화
optimizer = BootstrapFewShotWithOptuna(metric=metric, trials=100)
# 각 모듈에 대해 최적의 데모 조합 선택
best_program = optimizer.compile(student, trainset=train, valset=val)
Stage 3: 고차원 프로그램 최적화
프로그램의 flow 자체를 변경한다. 제일 단순한 예로 ensembles 방식이 있다.
# 앙상블 생성
ensemble = dspy.Ensemble(reduce_fn=dspy.majority_vote)
compiled_ensemble = ensemble.compile(bootstrap.programs[:7])
# 동적 백트래킹 (향후 기능)
# 실행 중 실패 시 자동으로 다른 경로 탐색
3. 프로그래밍 모델의 표현력
DSPy는 복잡한 파이프라인을 간결하게 표현할 수 있다:
class MultiHopQA(dspy.Module):
def __init__(self, passages_per_hop=3):
self.retrieve = dspy.Retrieve(k=passages_per_hop)
self.generate_query = dspy.ChainOfThought("context, question -> query")
self.generate_answer = dspy.ChainOfThought("context, question -> answer")
def forward(self, question):
context = []
# 2-hop 검색 및 추론
for hop in range(2):
query = self.generate_query(
context=context,
question=question
).query
context += self.retrieve(query).passages
return self.generate_answer(context=context, question=question)
이 간단한 코드가 컴파일되면:
- 각 단계에 최적화된 프롬프트 생성
- 효과적인 few-shot 예시 자동 선택
- 작업과 모델에 맞는 지시사항 조정
4. 핵심 설계 철학
모듈성 (Modularity)
- 각 구성 요소가 독립적으로 최적화 가능
- 레고 블록처럼 조합하여 복잡한 시스템 구축
선언적 프로그래밍 (Declarative Programming)
- "어떻게"가 아닌 "무엇을" 중심으로 사고
- 구현 세부사항은 컴파일러가 처리
자동 적응 (Automatic Adaptation)
- 다른 LM으로 전환 시 재컴파일만으로 최적화
- 작업 변경 시에도 동일한 모듈 구조 유지 가능
이러한 설계를 통해 DSPy는 프롬프트 엔지니어링의 수작업을 대폭 줄이면서도, 더 높은 성능과 재사용성을 달성한다.
주요 성능 및 실험 결과
GSM8K (수학 문제)
- GPT-3.5: 기본 25.2% → DSPy 최적화 후 81.6%
- Llama2-13b: 기본 9.4% → DSPy 최적화 후 46.9%
- 인간이 작성한 Chain of Thought 프롬프트와 동등하거나 더 나은 성능
HotPotQA (다중 홉 질의응답)
- GPT-3.5: 복잡한 멀티홉 프로그램으로 45.6% 정답률 달성
- T5-Large (770M): 200개의 라벨 데이터만으로 39.3% 성능
- 작은 모델도 효과적으로 활용 가능함을 입증
주요 발견사항
- 짧은 DSPy 프로그램(몇 줄)이 복잡한 수동 프롬프트를 대체
- 컴파일 시간은 수 분에서 수십 분 수준
- 라벨이 없는 중간 단계도 자동으로 부트스트래핑
의미와 전망
개발 패러다임의 전환
- 프롬프트 엔지니어링에서 모듈 조합으로
- "무엇을(what)" 하는지 선언하면, "어떻게(how)" 하는지는 컴파일러가 최적화
실용적 이점
- 다양한 LM과 작업에 자동 적응
- 작은 모델도 효과적으로 활용 가능
- 복잡한 파이프라인을 체계적으로 탐색 가능
향후 발전 방향
- 더 정교한 최적화 전략 개발
- 강화학습, 베이지안 최적화 등 고급 기법 통합
- 더 복잡한 에이전트 시스템으로 확장
결론
DSPy는 언어 모델 프로그래밍에 체계적이고 모듈화된 접근법을 제시한다. 수동 프롬프트 엔지니어링의 한계를 극복하고, 자동 최적화를 통해 높은 성능을 달성할 수 있음을 보여준다. 이는 AI 시스템 개발의 생산성과 신뢰성을 크게 향상시킬 수 있는 중요한 진전이다.
'인공지능 논문 정리' 카테고리의 다른 글
| Scaling up Test-Time Compute with Latent Reasoning: A Recurrent Depth Approach (2) | 2025.03.09 |
|---|---|
| Towards System 2 Reasoning in LLMs: Learning How to Think With Meta Chain-of-Thought 내용 정리 (0) | 2025.03.09 |
| Diverse Preference Optimization 내용 정리 (0) | 2025.02.10 |
| Thoughts Are All Over the Place: On the Underthinking of o1-Like LLMs 내용 정리 (1) | 2025.02.07 |
| s1: Simple test-time scaling 내용 정리 (2) | 2025.02.06 |