본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 05:07

프롬프트 엔지니어링은 끝났다. DSPy의 시대가 왔다: 프롬프트 대신 LLM을 프로그래밍하는 방법

요약

수동적인 프롬프트 엔지니어링의 한계를 지적하며, Stanford NLP에서 개발한 DSPy를 통한 프로그래밍 방식의 AI 시스템 구축을 제안합니다. DSPy는 프롬프트를 직접 수정하는 대신 폐쇄 루프 학습을 통해 최적화 가능한 모듈로 AI 작업을 처리합니다.

핵심 포인트

  • 수동 프롬프트 엔지니어링의 취약성, 불투명성, 비전이성 문제 해결
  • DSPy를 통한 선언적이고 최적화 가능한 언어 프로그램 구축
  • 모델 교체 시에도 성능을 유지할 수 있는 전이 가능성 확보
  • 자연어 프롬프트에서 고수준 컴파일러 방식으로의 패러다임 전환

지난 몇 년 동안 AI 기반 애플리케이션을 구축하는 것은 소프트웨어 엔지니어링이라기보다 디지털 연금술에 가깝게 느껴졌습니다. 우리 모두 그런 경험이 있습니다. 플레이그라운드(playground)나 코드 에디터 앞에 앉아 시스템 프롬프트(system prompt)를 세심하게 수정하거나, "단계별로 생각해 보세요(please think step-by-step)"라는 문구를 추가하거나, 모델에게 "심호흡을 하고(take a deep breath)" 출력 형식을 유효한 JSON으로 맞춰달라고 간청하는 일 말입니다.

우리는 이것을 "프롬프트 엔지니어링 (prompt engineering)"이라고 불렀습니다. 하지만 솔직해집시다. 이것은 엔지니어링이 아닙니다. 그것은 장인의 기술입니다. 마치 숙련된 시계 제조공이 톱니바퀴를 손으로 직접 갈아내는 것과 같습니다. 각 상호작용은 인간의 직관에 의해 다듬어지며, AI 에이전트의 최종 동작은 수 시간의 시행착오로 형성된 섬세한 조각품과 같습니다.

이러한 접근 방식은 근본적으로 결함이 있습니다. 취약하고, 불투명하며, 완전히 전이 불가능합니다.

만약 당신이 확장 가능하고, 적응하며, 스스로 개선할 수 있는 AI 시스템—예를 들어 스스로 진화하는 Hermes Agent와 같은 시스템—을 구축하고 싶다면, 수동적인 프롬프트 엔지니어링을 버려야 합니다. 이제 장인의 기술에서 체계적인 엔지니어링으로 넘어가야 할 때입니다. 바로 이 지점에서 DSPy (Stanford NLP에서 개발한 Declarative Self-improving Language Programs)가 등장합니다.

DSPy는 취약한 자연어 프롬프트를 폐쇄 루프 학습 (closed-loop learning)을 통해 자동으로 조정할 수 있는 **프로그래밍 방식의 최적화 가능한 모듈 (programmatic, optimizable modules)**로 대체합니다. 이 포스트에서는 AI 작업을 타입이 지정된 시그니처 (typed signatures)를 가진 프로그램으로 생각하는 것이 왜 패러다임의 전환인지 탐구할 것입니다. 이는 컴퓨터 과학 역사에서 수기로 작성하던 어셈블리 (assembly)에서 고수준 컴파일러 (high-level compilers)로 전환된 과정과 유사합니다.

(여기서 설명하는 개념과 코드는 저의 전자책 Hermes Agent, The Self-Evolving AI Workforce에서 발췌되었습니다.)

수동 프롬프트 방식의 세 가지 장벽

DSPy가 왜 필요한지 이해하려면, 먼저 그것이 치료하고자 하는 질병을 진단해야 합니다. 수동 프롬프트 엔지니어링은 프로덕션급 AI 에이전트의 발목을 잡는 벽과 같은 세 가지 근본적인 한계를 가지고 있습니다:

  1. 취약성 (Fragility): 500단어로 구성된 프롬프트에서 단 한 단어만 바뀌어도 전체 에이전트 파이프라인 (Agent Pipeline)이 무너질 수 있습니다. 사소한 포맷팅 문제를 해결하기 위해 시스템 프롬프트 (System Prompt)를 업데이트했는데, 갑자기 모델이 환각 (Hallucination)을 일으키거나 전혀 관련 없는 작업을 거부하기 시작합니다.
  2. 불투명성 (Opacity): 프롬프트가 왜 성공하거나 실패하는지에 대한 추론 과정이 LLM의 블랙박스 (Black Box) 깊숙한 곳에 묻혀 있습니다. 에이전트가 실패할 때 개발자들은 근본 원인을 추측할 수밖에 없으며, 이는 데이터가 아닌 미신에 기반하여 프롬프트를 수정하는 "부두 디버깅 (Voodoo Debugging)"의 순환으로 이어집니다.
  3. 비전이성 (Non-Transferability): GPT-4에 맞춰 세심하게 최적화된 프롬프트는 Claude 3.5 Sonnet에서 성능이 저하되는 경우가 많으며, LLaMA 3와 같은 오픈 소스 모델에서는 완전히 무너져 버립니다. 모델을 교체하게 되면 기존 프롬프트를 버리고 시행착오 과정을 처음부터 다시 시작해야 합니다.

이러한 한계들은 AI 에이전트가 시간이 지남에 따라 진정으로 학습하고 진화하는 것을 방해합니다. 사용자와 함께 성장하는 에이전트를 구축하기 위해서는 프롬프트를 자동으로 컴파일 (Compile), 최적화 (Optimize), 검증 (Validate)할 수 있는 **변수 (Variables)**로 취급하는 시스템이 필요합니다.

어셈블리에서 고수준 컴파일러로: 역사적 교훈

우리가 현재 AI 역사에서 경험하고 있는 전환은 새로운 것이 아닙니다. 이는 수십 년 전 소프트웨어 엔지니어링이 겪었던 것과 정확히 동일한 전환, 즉 어셈블리 언어 (Assembly Language)에서 고수준 컴파일러 (High-level Compilers)로의 전환입니다.

컴퓨팅 초기 시절, 프로그래머들은 어셈블리 코드를 작성했습니다. 모든 명령어는 특정 CPU 아키텍처에 맞춰 수동으로 코딩되었습니다. 프로그래머는 레지스터 (Register)와 메모리 주소 (Memory Address)를 완벽하게 제어할 수 있었지만, 코드는 믿기 힘들 정도로 취약했습니다. 메모리 주소의 오타 하나가 기계 전체를 충돌시킬 수 있었습니다. 프로그램을 한 프로세서에서 다른 프로세서로 이식(Porting)한다는 것은 처음부터 다시 작성해야 함을 의미했습니다.

그 후 Fortran이나 C와 같은 고수준 언어가 등장했고, 이와 함께 **컴파일러 (Compilers)**가 나타났습니다.

[ 어셈블리 시대 (Assembly Era) ]  --> 특정 하드웨어를 위한 수동 코딩된 명령 (취약함, 비이식성)
[ 컴파일러 시대 (Compiler Era) ]  --> 고수준 코드 + 컴파일러가 하드웨어 명령으로 매핑 (견고함, 이식성)

프로그래머들은 레지스터를 직접 관리하는 대신, **변수 (variables)**와 **데이터 타입 (data types)**을 사용하여 추상적인 로직을 정의했습니다. 컴파일러가 번거로운 작업들을 처리하며, 추상적인 코드를 대상 하드웨어에 최적화된 효율적인 기계어 명령으로 자동 매핑했습니다.

AI의 세계에서 **프롬프트 (prompts)는 새로운 어셈블리 언어 (assembly language)**입니다. 여러분은 모델별로 특화된 저수준 (low-level) 명령어를 작성하고 있는 것입니다.

DSPy는 고수준 컴파일러 (high-level compiler) 역할을 합니다. 구체적인 프롬프트 문자열을 작성하는 대신, 데이터의 흐름을 정의하는 깔끔하고 추상적인 Python 코드를 작성합니다. 여러분은 입력과 출력을 정의하기만 하면 되며, DSPy 컴파일러가 해당 추상 프로그램을 현재 사용 중인 어떤 LLM에 대해서도 최적의 프롬프트나 파인튜닝 (fine-tuning) 명령어로 번역해 줍니다.

DSPy 이론의 핵심 기둥

DSPy가 어떻게 자기 진화형 시스템 (self-evolving systems)을 가능하게 하는지 이해하려면, 세 가지 기초 개념인 타입 지정 시그니처 (typed signatures), 최적화 가능한 모듈 (optimizable modules), 그리고 **컴파일러 (compiler)**를 분석해야 합니다.

1. 타입 지정 시그니처 (Typed Signatures): AI 프로그램의 데이터 타입 시스템

전통적인 소프트웨어 공학에서 **데이터 타입 (data type)**은 변수가 어떤 종류의 값을 보유하는지 지정하고, 해당 값에 대해 어떤 연산을 수행할 수 있는지를 결정하는 분류 체계입니다. DSPy에서 **타입 지정 시그니처 (typed signatures)**는 AI 모듈을 위한 데이터 타입 시스템 역할을 합니다.

타입 지정 시그니처는 input_fields -> output_fields 형태의 선언적 문자열 또는 Python 클래스입니다. 이는 여러분의 프로그램과 LLM 사이의 엄격한 계약 (contract)을 강제합니다.

예를 들어, 시그니처는 다음과 같을 수 있습니다:
"document: str, max_words: int -> summary: str"

이것은 단순한 문법적 설탕 (syntactic sugar)이 아닙니다. 이 시그니처는 다음과 같은 여러 가지 중요한 역할을 수행합니다:

  • 계약 준수 (Contract Enforcement): 시그니처는 모듈이 무엇을 기대하고 무엇을 생성하는지를 정확하게 선언합니다. DSPy 런타임(runtime)은 이러한 타입을 런타임에 확인하기 위한 검증 함수를 자동으로 구축할 수 있습니다.
  • 자동 데이터 생성 (Automatic Data Generation): 시그니처가 주어지면, DSPy는 입력 분포에서 샘플링하고 교사 모델 (teacher model)을 사용하여 타겟 출력을 생성함으로써 합성 학습 데이터 (synthetic training data)를 생성할 수 있습니다. 이는 새로운 기술을 배워야 하지만 실제 학습 데이터가 부족한 에이전트 (agents)에게 매우 중요합니다.
  • 조합 가능성 (Composability): 시그니처를 통해 모듈들을 레고 블록처럼 서로 연결할 수 있습니다. FileSearch 모듈 (query: str -> file_path: str)을 ReadFile 모듈 (file_path: str -> content: str)에 매끄럽게 파이프라인으로 연결하여 견고한 파이프라인을 구축할 수 있습니다.

2. 최적화 가능한 모듈: 변수로서의 프롬프트

DSPy 모듈은 dspy.Module을 상속받는 Python 클래스입니다. 이는 하나 이상의 예측기 (predictors) (예: dspy.Predict, dspy.ChainOfThought, 또는 dspy.ReAct)를 캡슐화합니다.

여기서 핵심적인 이론적 통찰은 각 예측기가 최적화될 수 있는 내부 파라미터 (parameters)를 가지고 있다는 점입니다. 이러한 파라미터에는 다음이 포함됩니다:

  • 지시문 텍스트 (LLM에 제공되는 프롬프트)
  • 퓨샷 예시 (few-shot examples, 인컨텍스트 예시 (in-context exemplars))
  • 추론 하이퍼파라미터 (inference hyper-parameters) (temperature, top-p, stop tokens)

전통적인 프롬프팅 (prompting)에서 이러한 파라미터들은 하드코딩됩니다. DSPy에서 이들은 값이 변경될 수 있는 이름이 지정된 저장 공간인 **변수 (variables)**입니다. 옵티마이저 (optimizer, DSPy 컴파일러)는 이러한 변수들을 탐색 공간 (search space)으로 취급하며, 가장 높은 성능을 내는 구성을 찾기 위해 이들을 변이 (mutating) 시킵니다.

3. DSPy 컴파일러: 메타 학습 엔진

컴파일러는 DSPy의 심장입니다. 컴파일러는 고수준 코드를 바이너리로 번역하는 것이 아니라, 주어진 작업에 대해 LLM에 프롬프팅하는 방법을 학습하는 **메타 학습 알고리즘 (meta-learning algorithm)**입니다.

컴파일 프로세스는 반복적인 루프 내에서 실행됩니다:

[ 현재 모듈 (Current Module) ] 
       │
       ▼
...
  1. 특정 지표(metric)를 사용하여 검증 데이터셋(validation dataset)에서 현재 모듈을 **평가(Evaluate)**합니다.
  2. 파라미터를 섭동(perturbing)하여 **후보군을 생성(Generate candidates)**합니다 (LLM 기반의 프롬프트 제안 사용, 서로 다른 퓨샷(few-shot) 예시 선택, 또는 하이퍼파라미터(hyper-parameters) 조정 등).
  3. 각 후보군을 지표에 따라 **점수화(Score)**합니다.
  4. 가장 성능이 좋은 후보군을 **선택(Select)**하여 새로운 베이스라인(baseline)으로 삼습니다.
  5. 최적화 예산(optimization budget)이 소진되거나 성능이 수렴(converge)할 때까지 이 과정을 **반복(Repeat)**합니다.

이 프로세스를 통해 시스템은 기반 모델(underlying model)의 가중치(weights)를 업데이트하지 않고도 작업을 해결하는 방법을 학습할 수 있습니다. 이는 LLM을 블랙박스(black box)로 취급하고 인터페이스를 최적화하며, 이로 인해 최적화 과정이 매우 비용 효율적입니다. 종종 API 호출 비용으로 단 몇 달러만이 소요됩니다.

코드 살펴보기: 취약한 프롬프트에서 DSPy 모듈로

구체적인 예시를 살펴보겠습니다. 우리가 코드 리뷰 에이전트(code review agent)를 구축한다고 가정해 봅시다.

전통적이고 취약한 방식

전통적인 파이프라인(pipeline)에서는 다음과 같이 프롬프트를 작성할 수 있습니다:

# 전통적이고 취약한 프롬프트 기반 방식
def review_code(code: str) -> str:
    system_prompt = (
...

이것은 괜찮아 보이지만, 만약 LLaMA-3-8B와 같은 오픈 소스 모델로 전환한다면 어떻게 될까요? 모델이 "도입 문구를 포함하지 말 것"이라는 지시를 완전히 무시하고, 다운스트림 파서(downstream parser)를 망가뜨리는 대화형 인사를 반환할 수도 있습니다.

DSPy 프로그래밍 방식

이제 DSPy를 사용하여 이를 다시 작성해 보겠습니다. 먼저 타입이 지정된 시그니처(typed signature)를 정의하고 이를 최적화 가능한 모듈(optimizable module) 내에 캡슐화하는 것으로 시작합니다:

import dspy

# 1단계: 시그니처(signature, 계약) 정의
...

여기서 무엇이 빠져 있는지 주목하십시오: 프롬프트 문자열(prompt strings)이 없습니다. 우리는 모델에게 어떻게 행동해야 하는지 말하지 않았습니다. 우리는 단지 입력과 출력의 구조를 선언하고, 추론 패턴(ChainOfThought)을 선택했을 뿐입니다.

모듈 컴파일하기

이 모듈을 진정으로 견고하게 만들기 위해, 우리는 이를 컴파일(compile)할 수 있습니다. 코드와 원하는 피드백의 예시를 몇 개 제공하고, 검증 지표(validation metric)를 정의한 다음, 컴파일러를 실행합니다:

from dspy.teleprompt import BootstrapFewShot

# 예시 데이터셋 (입력 및 예상 출력)
...

compile 단계 동안 DSPy는 마법 같은 일을 수행합니다. 학습 예시들을 LLM에 통과시키고, formatting_metric을 기준으로 출력값을 평가하며, 어떤 추론 경로(reasoning paths)가 성공으로 이어졌는지 식별합니다. 그런 다음 성공한 실행 결과들을 프롬프트에 주입될 **퓨샷 예시(few-shot exemplars)**로 자동 구성합니다.

기반이 되는 LLM을 GPT-4에서 Claude나 LLaMA로 교체하더라도, 단순히 컴파일러를 다시 실행하기만 하면 됩니다. 코드는 완전히 변하지 않지만, 생성된 프롬프트는 새로운 모델의 강점과 약점에 맞춰 적응합니다.

요청 훅(Request Hooks)과 지속성 메모리(Persistent Memory): 자기 진화의 인프라

Hermes Agent와 같은 고급 아키텍처에서 DSPy는 단독으로 사용되지 않습니다. DSPy는 요청 훅(request hooks) 및 **지속성 메모리(persistent memory)**와 같은 인프라 구성 요소와 통합되어, 프로덕션 환경에서 스스로 진화하는 폐쇄 루프(closed-loop) 시스템을 구축합니다.

미들웨어로서의 요청 훅(Request Hooks)

Flask와 같은 웹 프레임워크에서 요청 훅(예: @app.before_request)은 요청-응답 라이프사이클(request-response lifecycle)의 특정 시점에 코드를 자동으로 실행할 수 있게 해줍니다.

DSPy도 유사한 패턴을 사용합니다. 컴파일러는 각 모듈의 실행 전후에 훅을 주입할 수 있습니다:

  • 실행 전 훅(Pre-Execution Hooks): 입력을 로그로 남기고, 스키마 제약 조건(schema constraints)을 검증하며, 문맥적 메모리(contextual memory)를 주입합니다.
  • 실행 후 훅(Post-Execution Hooks): 성능 지표를 계산하고, 실행 추적(execution traces)을 기록하며, 실패 사례를 표시합니다.

이러한 계측(instrumentation)을 통해 최적화 엔진은 단순히 무엇이 잘못되었는지 추측하는 것이 아니라, 실패의 정확한 실행 추적을 분석할 수 있습니다.

[ 사용자 요청 ] ──> [ 실행 전 훅 ] ──> [ DSPy 모듈 ] ──> [ 실행 후 훅 ] ──> [ 추적 데이터베이스 ]

학습 기질로서의 지속성 메모리(Persistent Memory)

에이전트는 메모리 없이는 진화할 수 없습니다. 자기 개선 시스템(self-improving system)에서 지속성 메모리는 단순히 과거 대화의 캐시가 아니라, 하나의 **학습 기질(learning substrate)**입니다.

DSPy 컴파일러는 실제 세션 기록을 최적화 소스로 사용하여 이러한 기질(substrate)을 활용합니다:

  1. 실패 포착 (Failure Capturing): 에이전트가 운영 환경에서 작업을 수행하는 데 실패하면, 해당 실패(및 관련 실행 추적(execution trace))가 지속성 메모리(persistent memory)에 기록됩니다.
  2. 데이터셋 합성 (Dataset Synthesis): 최적화 엔진은 메모리 데이터베이스를 정기적으로 스캔하여 실패 사례들을 패턴별로 그룹화합니다.
  3. 표적 진화 (Targeted Evolution): 엔진은 포착된 실패 사례들을 새로운 학습 예시(training examples)로 사용하여 DSPy 컴파일 실행을 트리거합니다. 컴파일러는 해당 유형의 실패가 다시는 발생하지 않도록 모듈의 지침(instructions)을 재작성하고 새로운 예시(exemplars)를 선택합니다.

이것이 Hermes에서 사용하는 GEPA (Genetic-Pareto Prompt Evolution) 엔진의 핵심입니다. 이 엔진은 실행 추적을 읽어 사물이 실패했는지 이해하고, 표적화된 개선 사항을 제안하며, 이를 DSPy 컴파일러를 통해 실행한 뒤, 자동화된 Pull Request를 통해 최적화된 기술(skills)을 에이전트에 다시 배포합니다.

가드레일 및 제약 조건: 제약 최적화 문제 (Constrained Optimization Problem) 해결하기

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0