한줄 요약: 프롬프트 엔지니어링을 수작업에서 프로그래밍으로 전환 — LM 호출을 선언적으로 정의하고 컴파일러가 자동으로 프롬프트/파인튜닝을 최적화한다.
| 항목 | 내용 |
|---|---|
| 저자 | Omar Khattab, Arnav Singhvi, Paridhi Maheshwari, Zhiyuan Zhang, Keshav Santhanam, Sri Vardhamanan, Saiful Haq, Ashutosh Sharma, Thomas T. Joshi, Hanna Moazam, Heather Miller, Matei Zaharia, Christopher Potts |
| 소속 | Stanford University |
| 발표 | ICLR 2024 |
| 링크 | arxiv.org/abs/2310.03714 |
| 키워드 | Prompt Optimization, Compiler, Declarative Programming, LM Pipeline |
현재 LLM 애플리케이션 개발의 문제:
전형적인 RAG 파이프라인의 프롬프트:
"You are a helpful assistant. Given the context below, answer the question.
Be concise. If you don't know, say 'I don't know'.
Context: {context}
Question: {question}
Answer:"
문제점:
1. 취약성: 단어 하나 바꾸면 성능 급변 ("Be concise" 제거 → 성능 10% 하락)
2. 모델 종속: GPT-4에서 튜닝한 프롬프트가 Llama에서는 작동 안 함
3. 파이프라인 복잡도: RAG = 검색 + 재랭킹 + 생성, 각 단계마다 프롬프트 튜닝 필요
4. 비체계적: 프롬프트 변경의 전체 파이프라인 영향을 예측 불가
핵심 제안: 프롬프트를 직접 쓰는 대신, 무엇을 할지(what)를 선언하고 어떻게 할지(how)는 컴파일러에 맡긴다.
# 기존: 장황한 프롬프트 직접 작성
prompt = "Given a question, search for relevant passages and provide a concise answer..."
# DSPy: 입출력만 선언
class GenerateAnswer(dspy.Signature):
"""Answer questions with short factoid answers."""
context = dspy.InputField(desc="relevant passages")
question = dspy.InputField()
answer = dspy.OutputField(desc="often between 1 and 5 words")
# 내장 모듈
dspy.Predict(signature) # 기본 LM 호출
dspy.ChainOfThought(sig) # 자동으로 "reasoning" 단계 추가
dspy.ReAct(sig, tools=[...]) # ReAct 패턴 자동 구현
dspy.Retrieve(k=3) # 검색 모듈
# RAG 파이프라인 정의
class RAG(dspy.Module):
def __init__(self):
self.retrieve = dspy.Retrieve(k=3)
self.generate = dspy.ChainOfThought(GenerateAnswer)
def forward(self, question):
context = self.retrieve(question).passages
return self.generate(context=context, question=question)
컴파일 과정:
1. 학습 데이터 (질문, 정답) 쌍 준비
2. Teleprompter가 자동으로:
- Few-shot 예시 선택 (BootstrapFewShot)
- 또는 instruction 최적화 (COPRO, MIPRO)
- 또는 파인튜닝 데이터 생성 (BootstrapFinetune)
3. 각 모듈의 프롬프트를 독립적으로 최적화
4. 전체 파이프라인의 end-to-end 메트릭 기반 평가
teleprompter = BootstrapFewShotWithRandomSearch(
metric=answer_exact_match,
max_bootstrapped_demos=4,
num_candidate_programs=16
)
compiled_rag = teleprompter.compile(RAG(), trainset=trainset)
개발자가 정의하는 것: 컴파일러가 결정하는 것:
- 입출력 스키마 - 프롬프트 텍스트
- 모듈 구조 - Few-shot 예시 선택
- 평가 메트릭 - Instruction 문구
- 파이프라인 흐름 - 파인튜닝 여부/데이터
→ "무엇(what)"과 "어떻게(how)"의 분리
→ 모델을 바꿔도 re-compile만 하면 됨
| 방법 | 정확도 |
|---|---|
| 표준 프롬프트 | 33.8% |
| 수작업 CoT 프롬프트 | 44.2% |
| DSPy BootstrapFewShot | 49.1% |
| DSPy MIPRO | 52.7% |
| 방법 | F1 Score |
|---|---|
| 표준 RAG | 36.2 |
| 수작업 최적화 RAG | 41.5 |
| DSPy compiled RAG | 51.3 |
| DSPy compiled Baleen | 57.0 |
→ 수작업 프롬프트 대비 15-20%p 향상 — 자동 최적화가 사람보다 우수
GPT-3.5에서 최적화한 프롬프트를 Llama-2-13B에 적용:
수작업 프롬프트: 성능 -23% 하락
DSPy re-compile: 성능 -3% 하락
→ 모델 전환 시 re-compile만으로 적응
DSPy의 핵심 통찰은 "프롬프트는 코드가 아니라 설정(config)이어야 한다"는 것이다. 현재 대부분의 LLM 애플리케이션에서 프롬프트는 하드코딩된 문자열이고, 모델이나 태스크가 바뀌면 사람이 다시 튜닝해야 한다. DSPy는 이를 컴파일러에 위임함으로써, 프롬프트를 "최적화 가능한 파라미터"로 취급한다.
실전에서의 주의점: DSPy는 명확한 평가 메트릭이 있는 태스크에서 강력하다 (QA, 분류, 추출). 하지만 "좋은 글쓰기"처럼 메트릭 정의가 모호한 태스크에서는 컴파일러의 효과가 제한적이다.
LangChain이 "LLM 호출을 쉽게 체이닝"하는 도구라면, DSPy는 "LLM 호출을 자동으로 최적화"하는 도구다. 이 둘은 경쟁보다 상보적이며, 장기적으로 DSPy의 선언적 접근이 더 확장 가능해 보인다.
관련 논문: ColBERT, ARES, Demonstrate-Search-Predict, Self-Instruct