DSPy is (really) All You Need - Kevin Madura, AlixPartners

개요

본 강연은 AlixPartners의 기술 컨설턴트 Kevin Madura가 DSPy 프레임워크의 실전 활용법을 다룬다. DSPy는 LLM을 1급 객체(first-class citizen)로 취급하는 선언형 프로그래밍 프레임워크로, 프롬프트 엔지니어링이 아닌 프로그래밍 패러다임을 통해 AI 애플리케이션을 구축할 수 있게 한다. 강연자는 문서 분류, PDF 처리, 멀티모달 작업, 웹 연구 에이전트 등 실제 컨설팅 프로젝트에서 활용한 사례를 중심으로 DSPy의 핵심 개념과 장점을 설명한다.

DSPy의 핵심 가치 제안

프로그래밍 패러다임으로서의 DSPy

DSPy의 가장 중요한 특징은 “프롬프트 엔지니어링”에서 “프로그램 개발”로의 패러다임 전환이다. 전통적인 LLM 활용 방식은 문자열 기반 프롬프트를 반복적으로 조정하는 것이었다면, DSPy는 LLM 호출을 일반적인 함수 호출처럼 취급한다. 이는 단순히 choices[0].message.content와 같은 저수준 API 조작이나 JSON 파싱을 반복하는 대신, 의도(intent)를 선언하고 입출력 타입을 정의하는 것에 집중할 수 있게 한다.

강연자는 자신이 매일 코드를 작성하는 엔지니어가 아닌 기술 컨설턴트로서, 법률 회사의 조사, 프로세스 개선, AI 도입 전략 수립, 수만 건의 계약서 분석 등 다양한 문제를 마주한다고 설명한다. DSPy는 이러한 **빠른 반복 개발(rapid iteration)**이 필요한 환경에서 특히 유용하다. 프롬프트를 조정하는 대신 적절한 Python 프로그램을 구축하며, DSPy는 이를 위한 효율적인 도구를 제공한다.

시스템 사고(Systems Mindset)와 이식성(Transferability)

DSPy의 창시자 Omar Khattab은 A16Z 팟캐스트에서 DSPy가 “시스템 사고”로 설계되었다고 설명했다. 이는 의도를 표현하는 방식이 **이식 가능(transferable)**하다는 의미다. 시스템이나 프로그램의 설계는 모델 성능만큼 빠르게 변하지 않는다. 거의 매일 새로운 모델이 출시되고 성능이 개선되는 상황에서, DSPy는 제어 흐름(control flow)과 의도는 유지하면서도 모델을 자유롭게 교체할 수 있는 구조를 제공한다.

이러한 추상화 수준은 몇 가지 핵심 이점을 제공한다:

  • 편리성(Convenience): JSON 파싱이나 문자열 조작 같은 반복 작업이 불필요
  • 적절한 추상화 레벨: 필요시 내부 동작을 이해하고 조정할 수 있으면서도, 핵심 로직에 집중 가능
  • 정밀성(Precision) 유지: 높은 수준의 제어와 정밀함을 유지
  • 모델 및 패러다임 변화에 대한 견고성: 프로그램 로직은 유지하면서 LLM만 교체 가능

다른 라이브러리와의 비교

강연자는 Pydantic AI, LangChain, Agno 등 다른 우수한 라이브러리들도 존재함을 인정한다. DSPy가 모든 사용 사례에 완벽한 것은 아니며, 처음에는 작동 방식을 이해하는 데 시간이 걸릴 수 있다. 그러나 LangChain에 비해 방해 요소가 적고, 다른 패러다임을 제공한다는 점이 차별점이다. 강연의 목적은 DSPy의 모든 세부 사항을 다루는 것이 아니라, 실제로 직면한 문제들을 어떻게 해결했는지, 왜 DSPy를 선택하게 되었는지를 공유하는 것이다.

DSPy의 6가지 핵심 개념

1. Signatures (시그니처)

Signatures는 함수 호출이 무엇을 할지 명시하는 방법이다. 입력과 출력을 정의하며, 둘 다 타입을 지정할 수 있다. “어떻게(how)” 구현할지는 LLM에게 맡긴다.

가장 흥미로운 점은 필드 이름 자체가 프롬프트의 일부로 작동한다는 것이다. 다음은 클래스 기반 시그니처의 예시다:

class SentimentClassifier(dspy.Signature):
    """Classify the sentiment of the given text"""
    text: str = dspy.InputField(desc="Text to analyze")
    sentiment: int = dspy.OutputField(desc="Sentiment score from 1-10")

이 코드는 단순히 타입이 지정된 Python 코드일 뿐 아니라, 프롬프트로도 기능한다. 파라미터 이름(text, sentiment)과 설명(desc)이 모두 모델에 전달된다. 따라서 모델이 직관적으로 이해할 수 있도록 파라미터 이름을 신중하게 선택하는 것이 중요하다.

더욱 놀라운 것은 **단축 표기법(shorthand)**이다:

"text -> sentiment: int"

이는 위의 클래스와 동일한 기능을 수행한다. 처음에는 낯설 수 있지만, 프로토타이핑과 빠른 실험에 매우 유용하다. 작동하는지 확인한 후 필요하다면 상세한 지시사항을 추가하거나 모듈로 발전시킬 수 있다.

질문: 기존에 훌륭한 프롬프트를 이미 가지고 있다면?

답변: Docstring이나 다른 방법을 통해 커스텀 프롬프트나 시스템 지시사항을 주입할 수 있다. 최종적으로는 문자열이므로, 기존 프롬프트를 시작점으로 활용하고 나머지 시스템을 구축하는 것이 가능하다.

2. Modules (모듈)

Modules는 DSPy 프로그램의 기본 추상화 계층이다. 하나 이상의 시그니처와 추가 로직을 포함하여 프로그램을 논리적으로 구조화한다. PyTorch의 방법론을 기반으로 설계되었다.

DSPy는 여러 내장 모듈을 제공하는데, 이들은 기본적으로 **다양한 프롬프팅 기법(prompting techniques)**의 모음이다:

  • dspy.Predict: 기본 LLM 호출
  • dspy.ChainOfThought: “단계별로 생각해봅시다(let’s think step-by-step)” 같은 문구를 자동으로 추가 (최신 모델에서는 덜 필요하지만 프롬프팅 기법의 좋은 예시)
  • dspy.ReAct: 도구를 모델에 노출하는 방식으로, 시그니처와 Python 함수를 받아 도구 호출을 처리
  • dspy.ProgramOfThought: 모델이 코드로 사고하도록 강제하며, Python 인터프리터가 내장되어 있음 (고도로 기술적인 문제에 유용)

모듈 예시:

class TimeEntryFormatter(dspy.Module):
    def __init__(self):
        super().__init__()
        self.change_tense = dspy.Predict("time_entry -> formatted_entry")
    
    def forward(self, time_entry):
        # LLM 호출
        result = self.change_tense(time_entry=time_entry)
        
        # 추가적인 비즈니스 로직
        if result.formatted_entry:
            return result.formatted_entry.strip()
        return time_entry

이 예시는 실제 클라이언트 사례에서 가져온 것으로, 수십만 개의 시간 기록(time entries)이 동일한 형식을 따르도록 보장해야 했다. LLM 호출과 하드코딩된 비즈니스 로직을 자연스럽게 혼합할 수 있다는 점이 핵심이다.

3. Tools (도구)

DSPy에서 도구는 일반 Python 함수일 뿐이다. DSPy는 내부적으로 LiteLLM을 사용하므로 둘 사이에 약간의 연결이 필요하지만, 기본적으로 다른 곳에서 사용하는 도구를 DSPy에서도 사용할 수 있다.

def get_weather(city: str) -> str:
    """Get weather for a city"""
    # 구현...
    return f"Weather in {city}: Sunny"
 
def search_web(query: str) -> str:
    """Search the web"""
    # 구현...
    return "Search results..."
 
# ReAct 에이전트에 도구 제공
agent = dspy.ReAct(
    "question -> answer",
    tools=[get_weather, search_web],
    max_iters=5
)
 
result = agent(question="What's the weather like in Tokyo?")

이 코드는 모델에 프롬프트와 도구를 제공하고, 모델이 필요에 따라 도구를 호출하도록 한다.

4. Adapters (어댑터)

Adapters는 시그니처와 실제 LLM 호출 사이에 위치하는 프롬프트 포매터다. 시그니처, 입력, 기타 속성을 특정 메시지 형식으로 변환한다.

프롬프트는 결국 LLM에 전송되는 문자열이다. 시그니처는 더 높은 수준에서 의도를 표현하는 방법이며, 어댑터는 이 둘 사이를 번역한다. 연구에 따르면 특정 모델은 XML, BAML, JSON 등 특정 형식에서 더 나은 성능을 보일 수 있다. 어댑터는 이러한 형식을 원하는 대로 쉽게 변경할 수 있게 한다.

예를 들어, 기본 JSON 어댑터는 다음과 같은 프롬프트를 생성한다:

Your input fields are:
1. clinical_note (str)
2. patient_info (PatientDetails): {복잡한 JSON 스키마}

Your output fields are:
...

반면 BAML 어댑터를 사용하면 더 깔끔한 형식으로 변환된다:

input:
  clinical_note: str
  patient_info:
    name: str
    age: int
    address:
      street: str
      city: str

Twitter에서 활동하는 개발자 Prashanth의 테스트에 따르면, 사용 사례에 따라 5-10% 성능 향상을 확인했다. 프로그램의 나머지 부분은 전혀 변경하지 않고, 어댑터만 지정하면 된다.

5. Optimizers (옵티마이저)

옵티마이저는 DSPy의 가장 흥미롭고 논란이 있는 부분이다. 많은 사람들이 DSPy를 들으면 옵티마이저를 먼저 떠올리지만, Omar Khattab의 말을 인용하자면: “DSPy는 옵티마이저가 아니다. 프로그래밍 추상화의 집합일 뿐이다. 단지 최적화할 수 있을 뿐이다.”

옵티마이저의 핵심 가치는 **모델 간 이식성(transferability)**이다. 예를 들어:

  • GPT-4o-mini로 분류 작업이 잘 작동하지만, 하루에 수백만 번 실행하여 비용이 높다
  • GPT-4o-1-nano로 시도하면 성능이 70%로 떨어진다
  • Nano에 대해 옵티마이저를 실행하면 성능을 87%까지 복구할 수 있다
  • 사용 사례에 따라 충분할 수 있으며, 비용은 몇 배나 줄어든다

옵티마이저는 내부적으로 프롬프트 문자열을 반복적으로 최적화하거나 조정한다. 여러 모듈로 구성된 프로그램을 최적화하면, DSPy가 자동으로 다양한 구성 요소를 최적화하여 입출력 성능을 향상시킨다.

Andrej Karpathy와 Dwarkesh의 대화에서 영감을 받은 통찰: Karpathy는 “LLM as a judge”가 문제가 될 수 있다고 지적했다. 평가받는 모델이 적대적 예제(adversarial examples)를 찾아 판단 모델을 속일 수 있기 때문이다. 이를 역으로 생각하면, DSPy 옵티마이저는 바로 이 속성을 활용한다. 모델의 “틈새와 구석(nooks and crannies)“을 찾아 데이터셋에 대한 성능을 향상시킨다.

최신 연구 결과: Chris Potts의 최근 발표에 따르면, MIPRO(DSPy 옵티마이저)가 GRPO와 같은 파인튜닝 방법과 동등하거나 그 이상의 성능을 보인다. 프롬프트 최적화는 활발한 연구 분야이며, 인프라 설정 없이 빠르게 시도해볼 가치가 있다.

옵티마이저 비용에 대한 질문: DSPy가 많은 LLM 호출로 인해 비쌀 수 있다는 우려에 대해, 강연자는 이것이 DSPy 고유의 문제가 아니라고 답한다. 함수를 비동기적으로 수백만 번 호출하면 당연히 비용이 발생한다. DSPy가 프롬프트에 약간의 오버헤드를 추가할 수는 있지만, 주요 비용 요인은 아니다. 필요하다면 **압축 어댑터(compressed adapter)**를 작성하여 모델에 전송되는 내용을 줄일 수 있다.

6. Metrics (메트릭)

Metrics는 옵티마이저와 함께 사용되어 프로그램의 성공을 정의한다. 옵티마이저는 메트릭을 사용하여 올바른 경로를 찾고 있는지 판단한다.

메트릭은 엄격한 검사일 수도 있고:

def exact_match(example, prediction):
    return example.answer == prediction.answer

LLM을 judge로 사용하는 주관적인 평가일 수도 있다:

def quality_check(example, prediction):
    judge = dspy.Predict("criteria, generated_text -> passes: bool")
    result = judge(
        criteria="Text must be professional and error-free",
        generated_text=prediction.text
    )
    return result.passes

메트릭은 여러 개를 정의할 수 있으며, 옵티마이저는 이를 사용하여 프롬프트 조정이 성능을 개선했는지 저하시켰는지 직접적인 피드백을 받는다.

실전 사용 사례

1. 간단한 감성 분석 (Sentiment Classification)

# 시그니처 정의
sentiment_sig = dspy.Signature(
    "text -> sentiment: int",
    instructions="Lower numbers (1-3) are negative, higher (8-10) are positive"
)
 
# 모듈 생성
classifier = dspy.Predict(sentiment_sig)
 
# 사용
result1 = classifier(text="This hotel stinks")
print(result1.sentiment)  # 2
 
result2 = classifier(text="I'm feeling pretty happy")
print(result2.sentiment)  # 8

특별해 보이지 않을 수 있지만, 중요한 점은:

  • 단축 문자열 시그니처 사용
  • 타입 지정 (문자열 입력, 정수 출력)
  • docstring을 통한 커스텀 지시사항 추가
  • 빠른 스캐폴딩과 실험이 가능

DSPy는 사용량 추적 정보를 자동으로 제공한다 (LiteLLM을 통해):

dspy.inspect_history()[-1]  # 마지막 호출의 토큰 사용량, 비용 등

2. 멀티모달 PDF 분석 (Form 4 SEC Filing)

Attachments 라이브러리를 사용하면 다양한 파일 형식을 LLM 친화적 형식으로 쉽게 변환할 수 있다:

from attachments import Attachment
 
# PDF를 자동으로 이미지, 텍스트, 마크다운으로 변환
doc = Attachment("https://example.com/form4.pdf")
 
# 간단한 RAG (Poor man's RAG)
rag = dspy.ChainOfThought("document, question -> answer")
result = rag(document=doc, question="How many shares were sold in total?")
print(result.answer)

Attachments는 PDF Plumber를 사용해 마크다운을 생성하고, 이미지를 추출하며, OCR을 수행한다. 사용자는 이러한 세부 사항을 신경 쓸 필요가 없다.

3. 구조화된 데이터 추출

동적 구조 추출 - 모델이 구조를 결정하게 함:

# 모델이 중요한 정보의 구조를 결정
analyzer = dspy.ChainOfThought("document -> key_info: dict")
result = analyzer(document=doc)
print(result.key_info)
# {'filing_date': '2024-12-15', 'shares_sold': 50000, ...}

고정 스키마 추출 - Pydantic 모델 정의:

class Transaction(BaseModel):
    date: str
    shares: int
    price: float
 
class DocumentSchema(BaseModel):
    filing_date: str
    form_type: str
    transactions: List[Transaction]
 
# 시그니처에 스키마 지정
extractor = dspy.Predict("document -> schema: DocumentSchema")
result = extractor(document=doc)
print(result.schema.transactions[0].shares)

이는 Structured Outputs와 유사하지만 DSPy 생태계 내에서 작동한다.

4. 프롬프트 검사 (Inspect History)

무슨 일이 일어나는지 궁금하다면 inspect_history()를 사용:

dspy.inspect_history()[-1]

이는 다음을 보여준다:

  • 생성된 시스템 메시지
  • 입력/출력 필드가 어떻게 포맷되었는지
  • 실제 문서 내용
  • 원시 응답
  • 토큰 사용량

디버깅과 이해를 위한 완전한 투명성을 제공한다.

5. 어댑터 비교 (JSON vs BAML)

class PatientDetails(BaseModel):
    name: str
    age: int
    address: str
 
# JSON 어댑터 (기본)
with dspy.context(lm=smart_llm):
    result1 = dspy.Predict("clinical_note -> patient_info: PatientDetails")(
        clinical_note=note
    )
 
# BAML 어댑터
with dspy.context(lm=smart_llm, adapter=dspy.BAMLAdapter()):
    result2 = dspy.Predict("clinical_note -> patient_info: PatientDetails")(
        clinical_note=note
    )
 
# 결과는 동일하지만 프롬프트 형식이 다름
dspy.inspect_history(2)  # 두 가지 프롬프트 비교

BAML 어댑터는 더 깔끔하고 직관적인 형식을 생성하여 복잡한 사용 사례에서 5-10% 성능 향상을 가져올 수 있다.

6. 멀티모달 이미지 분석

# 이미지 로드
image = Attachment("parking_sign.jpg")
 
# 질문
analyzer = dspy.ChainOfThought(
    "image -> can_park: bool, when_to_leave: str"
)
result = analyzer(image=image)
 
print(f"Can park: {result.can_park}")
print(f"Leave by: {result.when_to_leave}")

이미지, 오디오 등 다양한 모달리티를 동일한 방식으로 처리할 수 있다.

7. 도구 호출 워크플로우 (Bio Agent)

def perplexity_search(query: str) -> str:
    """Search using Perplexity API"""
    # 구현...
 
def get_url_content(url: str) -> str:
    """Fetch URL content"""
    # 구현...
 
class BioAgent(dspy.Module):
    def __init__(self):
        super().__init__()
        self.lm = gemini_flash  # 특정 LLM 지정
        self.answer_gen = dspy.ReAct(
            "question -> answer",
            tools=[perplexity_search, get_url_content]
        )
    
    def forward(self, question):
        with dspy.context(lm=self.lm):
            return self.answer_gen(question=question)
 
agent = BioAgent()
result = agent(question="Has Satya Nadella been at Microsoft for over 10 years?")
print(result.answer)
print(result.trajectory)  # 어떤 도구가 호출되었는지, 관찰 결과 등

Trajectory는 디버깅에 매우 유용하다:

  • 어떤 도구가 호출되었는지
  • 전달된 인자
  • 각 호출의 관찰 결과(observations)

8. 복잡한 문서 처리 파이프라인

실제 컨설팅 프로젝트에서 사용한 예시: 다양한 파일 형식(SEC 서류, 계약서, 이미지 등)을 자동으로 분류하고 각각에 맞는 처리를 수행.

class DocumentType(str, Enum):
    SEC_FILING = "sec_filing"
    CONTRACT = "contract"
    PATENT = "patent"
    CITY_INFRASTRUCTURE = "city_infrastructure"
    OTHER = "other"
 
# 문서 분류
classifier = dspy.Predict("images: List[Image] -> doc_type: DocumentType")
 
def process_file(file_path: str):
    # Attachments로 파일 로드
    doc = Attachment(file_path)
    
    # 분류
    doc_type = classifier(images=doc.images[:3]).doc_type
    
    # 타입별 처리
    if doc_type == DocumentType.SEC_FILING:
        # Form 4 추출
        extractor = dspy.Predict("document -> schema: Form4Schema")
        return extractor(document=doc).schema
    
    elif doc_type == DocumentType.CONTRACT:
        # 재귀적 요약
        summarizer = RecursiveSummarizer()
        summary = summarizer(document=doc)
        
        # 문서 경계 탐지
        boundary_detector = dspy.ReAct(
            "pages -> structure: DocumentStructure",
            tools=[get_page_images]
        )
        boundaries = boundary_detector(pages=doc.pages)
        
        return {"summary": summary, "boundaries": boundaries}
    
    elif doc_type == DocumentType.CITY_INFRASTRUCTURE:
        # 이미지 분석
        with dspy.context(lm=gemini_flash):  # 이미지에는 Gemini 사용
            analyzer = dspy.ChainOfThought("image -> description: str")
            return analyzer(image=doc.images[0])

이 예시는 여러 핵심 개념을 보여준다:

  • 모델 라우팅: 작업 유형에 따라 다른 LLM 사용 (Gemini for images, GPT-4 for complex reasoning)
  • 조건부 로직: 분류 결과에 따른 다른 처리 경로
  • 도구 통합: 페이지 이미지 가져오기 등
  • 모듈 조합: 여러 DSPy 모듈을 하나의 워크플로우로 결합

9. 문서 경계 탐지 (Boundary Detection)

계약서에서 본문, 일정(Schedule), 첨부물(Exhibit)의 경계를 자동으로 탐지:

class DocumentStructure(BaseModel):
    sections: Dict[str, Tuple[int, int]]  # {"Main": (0, 6), "Schedule 1": (7, 8)}
 
# 1단계: 각 페이지 분류
page_classifier = dspy.Predict("image -> section_type: str")
classifications = [page_classifier(image=img) for img in doc.images]
 
# 2단계: 분류 결과를 구조로 변환
structure_builder = dspy.ReAct(
    "pages_with_classifications -> structure: DocumentStructure",
    tools=[get_page_images],  # 자체 검증을 위해
    instructions="Detect document boundaries and verify by reviewing images"
)
 
result = structure_builder(pages_with_classifications=classifications)
print(result.structure.sections)
# {"Main Document": (0, 6), "Schedule 1": (7, 8), "Schedule 2": (9, 9), ...}

흥미로운 점은 **자체 검증(self-reflection)**이다. ReAct 모듈은 생성한 경계가 올바른지 확인하기 위해 get_page_images 도구를 호출할 수 있다. 이는 **모델의 내성적 능력(introspective ability)**을 활용하는 좋은 예시다.

옵티마이저 실전

전형적인 워크플로우

  1. 프로그램 구성: 로직을 모듈로 분해
  2. 메트릭 정의: 성공의 윤곽 정의
  3. 최적화: 모든 것을 통합하여 최종 결과 도출

MIPRO 옵티마이저 예시

실제 클라이언트 사례: 수십만 개의 시간 기록(time entries)이 표준을 준수하는지 확인 (대문자, 마침표 등)

# 데이터셋 준비
train_set = [
    dspy.Example(
        entry="fixed bug in authentication module",
        corrected="Fixed bug in authentication module."
    ).with_inputs("entry"),
    # ... 더 많은 예시
]
 
# 메트릭 정의
def capitalization_metric(example, prediction):
    return prediction.corrected[0].isupper()
 
def punctuation_metric(example, prediction):
    return prediction.corrected.endswith('.')
 
def exact_match_metric(example, prediction):
    return example.corrected == prediction.corrected
 
# 프로그램
class TimeEntryCorrector(dspy.Module):
    def __init__(self):
        super().__init__()
        self.corrector = dspy.ChainOfThought("entry -> corrected")
    
    def forward(self, entry):
        return self.corrector(entry=entry)
 
# 최적화
from dspy.teleprompt import MIPRO
 
optimizer = MIPRO(
    metric=lambda ex, pred: (
        capitalization_metric(ex, pred) +
        punctuation_metric(ex, pred) +
        exact_match_metric(ex, pred)
    ) / 3,
    num_candidates=10,
    init_temperature=1.0
)
 
optimized_program = optimizer.compile(
    TimeEntryCorrector(),
    trainset=train_set
)

결과:

  • 기본 성능: 86%
  • 최적화 후: 89%
  • 메트릭별 분석도 제공되어 어느 메트릭에서 개선이 있었는지 확인 가능

옵티마이저 출력물

옵티마이저의 출력은 컴파일된 모듈이다. 이를 직렬화하여 저장하거나 나중에 로드할 수 있다:

# 저장
optimized_program.save("time_entry_corrector_v1")
 
# 로드
loaded_program = TimeEntryCorrector()
loaded_program.load("time_entry_corrector_v1")
 
# 내부 프롬프트 검사
dspy.inspect_history(loaded_program)

내부를 보면:

  • 최적화된 프롬프트 문자열
  • 입력/출력 정의
  • 추가된 지시사항 (예: “이름은 반드시 대문자로 시작해야 합니다”)

질문: 솔루션 객체가 최적화된 프롬프트 외에 무엇을 포함하는가?

답변: 근본적으로는 문자열(프롬프트)이 핵심이다. 하지만 DSPy 모듈 구조, 시그니처의 다양한 구성 요소(필드 설명 등)도 포함된다. 원한다면 최적화된 프롬프트를 덤프할 수 있다.

MIPRO와 교사 피드백 (Teacher Feedback)

MIPRO(Multi-prompt Instruction Proposal Optimizer)의 특별한 점은 교사 모델의 텍스트 피드백을 사용한다는 것이다:

def teacher_feedback_metric(example, prediction, trace=None):
    """
    단순히 맞다/틀리다가 아니라,
    WHY it's wrong과 WHAT it should be를 제공
    """
    if prediction.category != example.category:
        return {
            "score": 0,
            "feedback": f"분류가 틀렸습니다. '{prediction.category}'가 아니라 "
                       f"'{example.category}'이어야 합니다. 이 메시지는 "
                       f"{example.urgency} 긴급도를 나타냅니다."
        }
    return {"score": 1, "feedback": "정확합니다."}

이 피드백은 옵티마이저가 **파레토 프론티어(Pareto frontier)**를 찾아 반복적으로 프롬프트를 조정하는 데 사용된다. 논문에 자세한 내용이 있지만, 기본적으로 반복 루프를 더 촘촘하게 만든다.

온라인 최적화와 지연된 피드백

질문: 사용자 피드백(thumbs up/down)을 실시간으로 반영할 수 있는가?

답변: 기술적으로는 가능하다. 사용자 피드백을 데이터셋에 추가하고, 정기적으로 재최적화를 실행하는 방식으로 온라인 학습(online learning) 유사한 접근이 가능하다. 그러나 강연자는 특정 사용 사례(예: 분류)에서 ML 모델을 DSPy 최적화 프로그램으로 대체하는 것은 권장하지 않는다. 대신 더 작은 모델로 이식하여 비용을 절감하면서 성능을 유지하는 것이 가장 큰 이점이라고 본다.

고급 주제 및 실용 팁

컨텍스트 관리 (Context Management)

특정 호출에 다른 LLM 사용:

# 전역 LLM은 GPT-4
dspy.configure(lm=gpt4)
 
# 특정 호출에만 Gemini 사용
with dspy.context(lm=gemini_flash):
    result = image_analyzer(image=img)
 
# 어댑터도 변경 가능
with dspy.context(lm=gpt4, adapter=dspy.BAMLAdapter()):
    result = extractor(document=doc)

이는 워크로드별 모델 라우팅에 매우 유용하다:

  • 이미지 분석 → Gemini
  • 복잡한 추론 → GPT-4o
  • 간단한 분류 → GPT-4o-mini

캐싱 (Caching)

DSPy는 자동 캐싱을 제공한다:

  • 시그니처 정의가 변경되지 않으면 이전 결과를 재사용
  • 테스트 시 매우 유용
  • 비용 절감과 속도 향상
# 첫 호출 - 실제 API 호출
result1 = classifier(text="Hello")
 
# 두 번째 호출 - 캐시에서 즉시 반환
result2 = classifier(text="Hello")
 
# 입력 변경 - 새로운 API 호출
result3 = classifier(text="Hello ")  # 공백 추가

사용량 추적 (Usage Tracking)

모든 호출에 대한 자동 사용량 정보:

result = classifier(text="Some text")
 
# 사용량 확인
usage = dspy.inspect_history()[-1].usage
print(f"Tokens: {usage.total_tokens}")
print(f"Cost: ${usage.cost}")
print(f"Model: {usage.model}")

관찰성(observability)과 최적화에 중요하다.

Phoenix 통합 (Observability)

Arize Phoenix를 사용한 추적:

import phoenix as px
 
# Phoenix 세션 시작
px.launch_app()
 
# DSPy에서 자동 추적
# 모든 LLM 호출, 토큰 사용량, 레이턴시 등을 시각화

강연자는 발표 직전에 이를 추가했지만, 프로덕션 환경에서 모든 호출을 추적하고 디버그하는 데 유용하다.

모듈 구성 패턴

재귀적 요약 (Recursive Summarization):

class RecursiveSummarizer(dspy.Module):
    def __init__(self, chunk_size=1000):
        super().__init__()
        self.chunk_size = chunk_size
        self.summarize_chunk = dspy.ChainOfThought("text -> summary")
        self.combine_summaries = dspy.ChainOfThought("summaries -> final_summary")
    
    def forward(self, document):
        # 문서를 청크로 분할
        chunks = [document[i:i+self.chunk_size] 
                 for i in range(0, len(document), self.chunk_size)]
        
        # 각 청크 요약
        summaries = [self.summarize_chunk(text=chunk).summary 
                    for chunk in chunks]
        
        # 요약들을 결합
        final = self.combine_summaries(summaries="\n".join(summaries))
        return final.final_summary

조건부 모델 라우팅:

class SmartRouter(dspy.Module):
    def __init__(self):
        super().__init__()
        self.small_lm = gpt4_mini
        self.large_lm = gpt4
        
    def forward(self, task, complexity):
        if complexity == "simple":
            with dspy.context(lm=self.small_lm):
                return dspy.Predict("task -> result")(task=task)
        else:
            with dspy.context(lm=self.large_lm):
                return dspy.ChainOfThought("task -> result")(task=task)

에코시스템 및 커뮤니티

주요 인물 및 리소스

강연자가 추천하는 Twitter 계정:

  • Omar Khattab (@lateinteraction): DSPy 창시자
  • Chris Potts (@ChrisGPotts): 최신 연구 및 MIPRO 관련
  • Prashanth (@prashanth_): 어댑터 실험 및 실용적 팁
  • Maxime (@maximelabonne): Attachments 라이브러리 개발자

DSPy Hub 컨셉

강연자가 실험 중인 아이디어: DSPy Hub - 최적화된 프로그램의 저장소

# Hub에서 최적화된 프로그램 다운로드
from dspyhub import load_program
 
# 다른 전문가가 최적화한 분류기 로드
classifier = load_program("city-infrastructure-classifier-v2")
 
# 즉시 사용
result = classifier(image=my_image)

Hugging Face와 유사한 개념으로:

  • 전문가가 특정 도메인에 대해 최적화한 프로그램 공유
  • 다른 프로젝트나 팀에서 재사용
  • 예: 청구서 탐지기(invoice detector), 계약서 분류기 등

그러나 강연자는 각 사용 사례가 **매우 특정적(hyperspecific)**일 수 있어 유용성에 대해 확신하지 못한다고 언급했다.

제한사항 및 고려사항

DSPy가 적합하지 않은 경우

  • 매우 간단한 일회성 작업: 한두 줄의 프롬프트로 충분한 경우
  • 극도로 최적화된 프로덕션 ML 파이프라인: 전통적인 ML 모델이 더 나을 수 있음
  • 실시간 스트리밍이 필수적인 경우: DSPy는 주로 배치/요청-응답 패러다임

학습 곡선

강연자 자신도 처음에는 DSPy의 작동 방식을 이해하는 데 시간이 걸렸다고 인정한다:

  • 특히 단축 문자열 시그니처가 낯설었음
  • 시스템 사고로의 전환이 필요
  • 하지만 일단 이해하면 매우 강력함

프로덕션 고려사항

강연에서 보여준 코드는 프로토타입 수준이며, 프로덕션에는 추가 작업이 필요:

  • 에러 핸들링
  • 재시도 로직
  • 모니터링 및 알림
  • 버전 관리
  • A/B 테스팅

핵심 교훈 및 권장사항

1. 프로그래밍 우선, 최적화는 보너스

DSPy는 옵티마이저가 아니다. 프로그래밍 추상화의 집합이다. 옵티마이저에 집착하지 말고, 모듈화된 프로그램을 구축하는 것에 집중하라. 최적화는 나중에 추가할 수 있는 강력한 도구일 뿐이다.

2. 빠른 반복이 핵심

DSPy의 진정한 가치는 빠른 실험과 반복이다:

  • 단축 시그니처로 빠르게 프로토타입 작성
  • 작동하면 상세한 지시사항 추가
  • 필요하면 모듈로 발전
  • 최종적으로 최적화

3. 작은 것부터 시작

기존 ML 모델이나 복잡한 파이프라인을 DSPy로 대체하려고 하지 말라. 대신:

  • 작은 사용 사례로 시작
  • DSPy의 사고방식에 익숙해지기
  • 점진적으로 더 복잡한 워크플로우로 확장

4. 모델 이식성 활용

DSPy의 가장 실용적인 이점:

  • 큰 모델에서 프로그램 개발
  • 최적화를 통해 작은 모델로 이식
  • 비용은 줄이고 성능은 대부분 유지

5. 데이터 품질이 중요

옵티마이저는 마법이 아니다:

  • 10-100개의 잘 구성된 예시가 필요
  • 메트릭이 실제로 원하는 것을 측정하는지 확인
  • 가비지 인, 가비지 아웃

6. 도구를 혼합하라

DSPy가 모든 것을 해결하지는 않는다:

  • 필요하면 Pydantic 사용
  • 필요하면 Structured Outputs 사용
  • DSPy를 더 큰 시스템의 일부로 활용
  • 올바른 도구를 올바른 작업에 사용

결론

DSPy는 LLM을 활용한 프로그램 개발 방식의 패러다임 전환을 제공한다. 프롬프트 엔지니어링의 임시방편적 특성에서 벗어나, 체계적이고 모듈화된 소프트�어 개발로 이동할 수 있게 한다.

핵심은 6가지 기본 요소다:

  1. Signatures: 의도 선언
  2. Modules: 로직 구조화
  3. Tools: Python 함수 통합
  4. Adapters: 프롬프트 형식 최적화
  5. Optimizers: 자동 성능 향상
  6. Metrics: 성공 정의

강연자의 경험상, DSPy의 진정한 가치는 옵티마이저가 아니라 프로그래밍 추상화에 있다. 이를 통해:

  • 빠른 반복과 실험
  • 명확한 코드 구조
  • 모델 간 이식성
  • 선택적 최적화

DSPy가 모든 사용 사례에 완벽한 것은 아니며, 학습 곡선이 있다. 하지만 시스템 사고를 받아들이고 기본 요소들을 이해한다면, LLM 기반 애플리케이션을 구축하는 매우 강력한 방법이 될 수 있다.

“구체적이고 모듈화된 방식으로 시작하고, 필요할 때만 최적화하라. DSPy는 그 여정을 지원하는 도구다.”