DSPy: 옵티마이저 인 더 루프
수동 프롬프트 엔지니어링의 한계
섹션 제목: “수동 프롬프트 엔지니어링의 한계”루프를 설계할 때 가장 시간이 많이 드는 작업 중 하나가 프롬프트 작성이다. “단계적으로 생각하라”, “JSON 형식으로 출력하라”, “예시 세 개를 포함하라” — 이 지시들은 모두 사람이 직접 작성하고, 직접 테스트하고, 직접 수정한다. 모델이 바뀌면 처음부터 다시 해야 한다. 작업이 조금만 달라져도 수십 줄의 프롬프트를 뜯어고쳐야 한다.
DSPy(Declarative Self-improving Python) 는 이 문제에 정면으로 도전한다. 핵심 아이디어는 간단하다. 프롬프트를 코드처럼 파라미터화하고, 데이터와 메트릭을 이용해 컴파일러가 자동으로 최적화하게 하라.
DSPy의 세 가지 추상화
섹션 제목: “DSPy의 세 가지 추상화”1. 서명(Signature) — “무엇을” 할지 선언
섹션 제목: “1. 서명(Signature) — “무엇을” 할지 선언”서명은 입출력 필드의 타입 어노테이션이다. “어떻게” 할지는 명시하지 않는다.
개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
import dspy
class QuestionAnswering(dspy.Signature): """컨텍스트를 바탕으로 질문에 답합니다.""" context: str = dspy.InputField() question: str = dspy.InputField() answer: str = dspy.OutputField()
# 체인-오브-쏘트 추론이 필요한 수학 문제 서명class MathSolver(dspy.Signature): """수학 문제를 단계별로 풀어 최종 답을 구합니다.""" problem: str = dspy.InputField() reasoning: str = dspy.OutputField(desc="단계별 풀이 과정") answer: float = dspy.OutputField()서명을 쓰는 사람은 “이 입력으로 저 출력을 만들어라”고만 선언한다. 중간의 CoT 프롬프트, 예시, 지시문은 컴파일러가 결정한다.
2. 모듈(Module) — 재사용 가능한 추론 블록
섹션 제목: “2. 모듈(Module) — 재사용 가능한 추론 블록”dspy.Predict, dspy.ChainOfThought, dspy.ReAct 등이 기본 모듈이다. 모듈은 서명을 받아 실행 가능한 호출 단위로 만든다.
개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
# 단순 예측predict = dspy.Predict(QuestionAnswering)
# CoT 추론 포함cot = dspy.ChainOfThought(MathSolver)
# 여러 모듈을 조합해 파이프라인 구성class RAGPipeline(dspy.Module): def __init__(self): self.retrieve = dspy.Retrieve(k=3) self.answer = dspy.ChainOfThought(QuestionAnswering)
def forward(self, question): context = self.retrieve(question).passages return self.answer(context="\n".join(context), question=question)3. 컴파일러(Compiler) — “어떻게” 할지를 데이터로 학습
섹션 제목: “3. 컴파일러(Compiler) — “어떻게” 할지를 데이터로 학습”컴파일러는 학습 예제(입력-출력 쌍)와 메트릭(성공 판별 함수)을 받아, 모듈의 내부 프롬프트를 최적화한다.
┌────────────────────────────────────────────────────────────────┐│ DSPy 컴파일 루프 │├────────────────────────────────────────────────────────────────┤│ ││ 학습 예제 ──▶ ┌──────────────┐ ││ (Q, A 쌍) │ 옵티마이저 │ ◀── 메트릭 함수 (성공 판별) ││ │ │ ││ │ ① 후보 프롬프트 생성 ││ │ ② 전체 파이프라인 실행 ││ │ ③ 메트릭으로 품질 평가 ││ │ ④ 더 나은 프롬프트 선택·저장 ││ └──────┬───────┘ ││ │ ││ ▼ ││ 최적화된 프롬프트가 내장된 모듈 ││ (배포 준비 완료) │└────────────────────────────────────────────────────────────────┘두 가지 핵심 옵티마이저
섹션 제목: “두 가지 핵심 옵티마이저”BootstrapFewShot: 가장 단순한 옵티마이저. 학습 예제에서 파이프라인을 실행해 성공한 경로를 수집하고, 그것을 몇 샷(few-shot) 예시로 자동 삽입한다. 사람이 직접 예시를 고르는 수고를 줄이면서, 실제로 성공한 추론 경로만 예시로 사용한다는 장점이 있다.
MIPROv2: 더 정교한 옵티마이저. 지시문(instruction)과 예시를 함께 최적화하며, 베이지안 최적화로 후보 프롬프트를 효율적으로 탐색한다. 더 많은 LLM 호출이 필요하지만 복잡한 파이프라인에서 더 큰 개선을 보인다.
개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
# 컴파일 과정 예시teleprompter = dspy.MIPROv2(metric=exact_match, auto="medium")optimized_pipeline = teleprompter.compile( RAGPipeline(), trainset=train_examples,)# 이제 optimized_pipeline 내부에는 컴파일러가 찾은# 최적 지시문과 예시가 포함되어 있다DSPy 루프의 의미: 옵티마이저 인 더 루프
섹션 제목: “DSPy 루프의 의미: 옵티마이저 인 더 루프”DSPy 컴파일은 그 자체가 외부 최적화 루프다. 학습 예제에 대해 파이프라인을 실행하고, 메트릭으로 평가하고, 프롬프트를 조정하고, 다시 실행한다. 이 루프는 사람의 개입 없이 자동으로 돌아간다.
| 비교 항목 | 전통적 프롬프트 엔지니어링 | DSPy |
|---|---|---|
| 프롬프트 작성 | 사람이 직접 수기 작성 | 컴파일러가 자동 생성 |
| 모델 변경 시 | 처음부터 재작성 | 재컴파일로 자동 적응 |
| 최적화 기준 | 직관·경험 | 정의된 메트릭 함수 |
| 재현 가능성 | 낮음 (암묵지 의존) | 높음 (코드로 명세) |
| 적합한 규모 | 소규모 파이프라인 | 다단계 복잡 파이프라인 |
적합한 사용 시나리오와 주의점
섹션 제목: “적합한 사용 시나리오와 주의점”DSPy가 강점을 발휘하는 상황은 평가 메트릭을 코드로 정의할 수 있고, 충분한 학습 예제(수십~수백 개)가 있을 때다. 정보 추출, QA, 요약처럼 정답이 명확한 작업이 대표적이다.
반면 주의해야 할 상황도 있다. 첫째, 컴파일은 많은 LLM 호출을 소비한다 — 복잡한 파이프라인과 대규모 학습셋은 비용이 크다. 둘째, 메트릭을 잘못 정의하면 Goodhart’s Law에 걸린다: 컴파일러가 메트릭 함수를 해킹하는 방향으로 프롬프트를 최적화할 수 있다. 셋째, 창의적 글쓰기처럼 성공을 수치로 표현하기 어려운 작업에는 적합하지 않다.
DSPy는 루프의 정책(π)을 수동으로 작성하는 부담을 줄인다. 하지만 정책이 아닌 에이전트의 스캐폴드 코드 자체를 자동화하려면? 다음 챕터 AlphaEvolve가 그 방향을 보여준다.
참고 자료
- Khattab et al. — DSPy: Compiling Declarative Language Model Calls (arXiv:2310.03714) — 접속 2026-06-30
- Anthropic — Building Effective AI Agents — 접속 2026-06-30