
객체 지향 추상화는 사라지지 않았습니다. 프롬프트 속으로 이동했을 뿐입니다.
요약
프롬프트 엔지니어링의 비구조적인 문제를 해결하기 위해 DSPy 프레임워크가 제안하는 객체 지향적 접근 방식을 설명합니다. 시그니처를 통해 인터페이스와 구현을 분리하고, 컴파일러 방식을 통해 모델 교체 시에도 안정적인 성능을 유지하는 방법을 다룹니다.
핵심 포인트
- 프롬프트를 단순 문자열이 아닌 인터페이스(Signature)로 정의하여 추상화 구현
- DSPy를 통해 모델 변경 시에도 프롬프트를 수동 수정 없이 컴파일 가능
- 입출력 계약과 실행 전략을 분리하여 코드의 유지보수성 향상
- 레이블된 예시와 메트릭을 기반으로 최적의 프롬프트를 자동 탐색
당신은 car.start()를 호출하며, 그 아래에 V8 엔진이 있는지 전기 모터가 있는지 신경 쓰지 않습니다. 이것이 추상화 (Abstraction)입니다: 안정적인 인터페이스 (Interface)와 교체 가능한 구현 (Implementation)이죠.
프롬프트 엔지니어링 (Prompt engineering)은 대개 이 과정을 건너뜁니다. 대부분의 프롬프트는 메서드 시그니처 (Method signature), 메서드 본문 (Method body), 그리고 설정 파일 (Config file)의 역할을 한 번에 수행하는 하나의 긴 문자열입니다. 모델을 교체하면 어제까지 잘 작동하던 문자열이 오늘 조용히 작동을 멈춥니다. 에러도, 경고도 없이, 그저 약간 틀린 답변을 내놓을 뿐입니다.
Stanford에서 나온 프레임워크인 DSPy는 이 문제에 OOP (객체 지향 프로그래밍) 스타일의 구조를 적용합니다. 어떻게 작동하는지 살펴보겠습니다.
계약(Contract)과 문구(Wording)를 분리하기
DSPy에서는 프롬프트를 직접 작성하지 않습니다. 대신 문구가 전혀 포함되지 않은 입출력 계약인 **시그니처 (Signature)**를 작성합니다:
class GenerateAnswer(dspy.Signature):
"""주어진 컨텍스트를 바탕으로 질문에 답합니다."""
context = dspy.InputField()
...
이것이 인터페이스입니다. 컨텍스트와 질문이 주어지면 답변을 생성합니다. 이는 answer(context, question) -> answer와 같은 메서드 시그니처처럼 읽힙니다.
그런 다음 이를 충족하는 전략 (Strategy)을 부착합니다:
class RAG(dspy.Module):
def __init__(self):
self.generate = dspy.ChainOfThought(GenerateAnswer)
...
ChainOfThought는 답변하기 전에 추론합니다. 이를 Predict로 교체하면 가시적인 추론 과정 없이 직접적인 답변을 얻게 되지만, 시그니처는 동일합니다. 호출자는 이를 전혀 눈치채지 못합니다. 이는 하나의 Repository 인터페이스 뒤에서 PostgresRepository를 MySQLRepository로 교체하는 것과 같은 움직임입니다.
프롬프트를 작성하는 대신 컴파일하기
이 지점이 DSPy가 컴파일러 (Compiler)라는 비유를 얻는 이유입니다. 레이블이 지정된 예시 (Labeled examples)와 메트릭 (Metric, 답변이 기대와 일치했는가?)을 제공하면, DSPy는 프롬프트의 표현 방식과 퓨샷 (Few-shot) 세트들을 탐색하며 각각의 점수를 매기고 가장 좋은 것을 유지합니다. 당신은 최종 프롬프트를 직접 손으로 쓰지 않습니다. DSPy가 그것을 컴파일합니다.
예를 들어, GPT-4를 대상으로 프롬프트를 튜닝하여 성능이 잘 나온다고 가정해 봅시다. 이를 Claude로 옮기면, 모델마다 서로 다른 문구에 반응하기 때문에 튜닝된 문구가 제대로 작동하지 않는 경우가 많습니다. 보통은 처음부터 다시 수동으로 재튜닝을 해야 합니다. 하지만 DSPy를 사용하면 시그니처 (Signature)는 고정된 상태로 유지됩니다. 새로운 모델에 맞춰 다시 컴파일하기만 하면, 옵티마이저 (Optimizer)가 해당 모델에 적합한 프롬프트를 다시 구축합니다. 계약 (Contract)은 교체 후에도 살아남으며, 오직 구현 (Implementation)만이 다시 구축될 뿐입니다.
실제 작동 예시
당신의 시그니처 (Signature)가 summarize(document) -> summary이고, 당신의 메트릭 (Metric)이 요약본이 원문 문서의 모든 개체명 (Named Entity)을 언급하는지 확인한다고 가정해 봅시다.
20개의 라벨링된 문서를 사용하여 옵티마이저 (Optimizer)를 실행합니다. 옵티마이저는 먼저 제로샷 (Zero-shot) 지침을 시도하고 점수를 매긴 다음, 두 개의 퓨샷 (Few-shot) 예시를 추가하여 점수를 매기고, 그 다음 요약하기 전에 개체명을 나열하는 생각의 사슬 (Chain-of-thought) 변형을 시도할 수 있습니다. 개체명 커버리지 (Entity coverage)에서 가장 높은 점수를 받은 버전이 컴파일된 프롬프트가 됩니다. 당신은 문구에 전혀 손을 대지 않았습니다. 당신이 만진 것은 메트릭 (Metric)과 예시들뿐입니다.
다음 달에 기반 모델을 GPT-4에서 Claude로 변경하더라도, 동일한 20개의 예시와 동일한 메트릭을 사용하여 동일한 컴파일 단계를 다시 실행하면 됩니다. 시그니처 summarize(document) -> summary는 절대 변하지 않습니다. 밑바닥의 레시피만 바뀔 뿐입니다.
전체 매핑
| 객체 지향 개념 (Object-oriented concept) | DSPy 대응 요소 (DSPy equivalent) |
|---|---|
| 인터페이스 / 추상 메서드 (Interface / abstract method) | 시그니처 (Signature) |
| ... |
만약 당신이 Spring Boot나 타입이 지정된 백엔드 환경에서 시간을 보냈다면, 이 표가 한눈에 익숙하게 느껴질 것입니다. 시그니처 (Signature)는 메서드 인터페이스 (Method interface)입니다. 모듈 (Module)은 그것의 한 가지 구현 (Implementation)입니다. 옵티마이저 (Optimizer)는 과거에 시행착오를 통해 프롬프트 엔지니어링을 수행하던 '사람'이 이제 자동화되고 측정 가능한 형태로 바뀐 것입니다.
다음 단계
이미 수동으로 수행하고 있는 작업 중 하나(문서에 대한 질의응답 (Question Answering), 분류 (Classification), 추출 (Extraction) 등)를 선택하여 시작해 보세요. 프롬프트 텍스트를 전혀 포함하지 않은 채 입력 (Input) 및 출력 (Output) 필드만 작성합니다. 먼저 dspy.Predict를 적용한 뒤, 그다음 dspy.ChainOfThought를 적용하여 비교해 보세요. 레이블이 지정된 10개의 예시와 간단한 지표 (Metric)를 추가한 다음, BootstrapFewShot을 실행하고 생성된 결과물을 확인하세요. 이것이 이론이 아닌 실제 환경에서 인터페이스와 구현 (Interface-and-implementation)의 분리를 확인할 수 있는 가장 빠른 방법입니다.
DSPy는 컴파일을 위한 학습 데이터셋 (Training set)과 지표 (Metric)가 필요하므로, 완전히 개방형인 생성 (Open-ended generation) 작업보다는 정답 확인이 가능한 작업에 더 적합합니다. 시작하기 전에 알아두어야 할 사항이지, 사용을 피해야 할 이유는 아닙니다.
Prasad MK는 분산 시스템 (Distributed systems), API 거버넌스 (API governance), 그리고 현대적인 AI 툴링의 기반이 되는 아키텍처에 대해 글을 씁.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
