나만의 모듈 조합하기

나만의 모듈 조합하기 (Composing your own module)

dspy.ReAct로 전환하면 단일 단계 프로그램이 다단계 에이전트로 바뀌었죠. 그런데 ReAct 자체도 결국 다른 DSPy 모듈을 조합한 것에 불과해요. 모듈은 조합 가능하기 때문에, 우리도 커스텀 모듈을 만들어 여러 단계로 작업을 분해할 수 있습니다. 이 페이지는 dspy.Module을 상속해 모듈을 직접 조합하는 법을 다룹니다.

출처: 공식문서

ReAct도 모듈의 조합이다

dspy.ReAct 내부에는 다른 DSPy 모듈들이 조합되어 있어요. 모델이 입력을 고려하고 다음 도구를 고르는 각 단계는 dspy.Predict 모듈입니다. 모델이 finish를 호출하거나 max_iters에 도달할 때까지 Predict 호출을 순환시키는 제어 흐름을 관리하는 코드 몇 줄이 붙어 있고, 그 뒤에 모델이 배운 모든 것으로 답을 종합하는 합성 단계는 ChainOfThought 모듈이에요.

모듈은 모듈식(modular)이라 조합하기만 하면 알아서 동작합니다. 이를 보여주기 위해, 하이쿠 후보를 여러 개 생성하는 모듈과 가장 좋은 작품을 고르는 모듈 — 이렇게 두 모듈로 구성된 커스텀 모듈을 만들어 볼게요.

모듈을 조합해 앙상블 만들기

LLM이 쓴 시는 주사위 던지기와 같아요. 때로는 시적이고, 때로는 예측 가능하고 밋밋합니다. 성공 가능성을 높이기 위해 주사위를 여러 번 던진 뒤 최고의 후보를 고르는 전략을 써요.

class HaikuEnsemble(dspy.Module):
    def __init__(self, n: int = 3):
        super().__init__()
        self.n = n  
        # Module 1 generates several haikus
        self.writer = dspy.ReAct(
            "location, season, mood, num_haikus: int -> haikus: list[str]", 
            tools=[wikipedia_search, get_wikipedia_page],
            max_iters=5
        )
        # Module 2 picks the most evocative
        self.judge = dspy.ChainOfThought(
            "location, season, mood, candidates: list[str] -> most_evocative_index: int"
        )

    def forward(self, location: str, season: str, mood: str) -> dspy.Prediction:
        candidates = self.writer(
            location=location, season=season, mood=mood, num_haikus=self.n,
        ).haikus
        verdict = self.judge( 
            location=location, season=season, mood=mood, candidates=candidates,
        )
        return dspy.Prediction(
            haiku=candidates[verdict.most_evocative_index],
            candidates=candidates,
            reasoning=verdict.reasoning,
        )

모듈을 만들 때는 두 함수를 작성해야 해요.

  1. __init__는 초기 상태를 세팅하고 서브모듈을 정의해요.
  2. forward는 프로그램을 호출했을 때 일어나는 일을 처리해요. 입력을 받아 서브모듈을 거쳐 조합된 출력을 반환하지요.

우리 HaikuEnsemble__init__에서 두 서브모듈을 정의해요.

  1. writer는 아까의 ReAct 프로그램과 비슷해요. 여기에 새 입력 필드 num_haikus를 추가해 모델이 몇 개의 하이쿠를 만들지 지정하고, 출력 필드는 문자열 list를 돌려주도록 바꿨어요.
  2. judge는 완전히 새로워요. location, season, mood 입력에 더해 후보 haikus를 받아 그중 가장 시적인 것을 고릅니다.

이 프로그램을 호출하면 forward 메서드가 각 모듈을 순서대로 실행한 뒤 결과를 담은 dspy.Prediction 객체 하나를 반환해요.

ensemble = HaikuEnsemble(n=5)
result = ensemble(location="Bodega Bay", season="autumn", mood="inspired")

그러면 이런 하이쿠가 선택돼요.

Mist hugs the harbor
Crimson gulls glide over tide
Leaves whisper to fog

그리고 이런 근거가 따라옵니다.

The prompt asks for the candidate that most vividly evokes Bodega Bay in autumn with an inspired mood. 

Bodega Bay is characterized by its foggy harbor, sea gulls, and a crisp coastal environment.  

The first candidate (“Mist hugs the harbor
Crimson gulls glide over tide
Leaves whisper to fog”) captures the mist‑laden harbor and the motion of gulls, tying the leaves’ whisper to the tide—a direct reference to the coastal setting and autumnal atmosphere.  

Other candidates highlight fog or forest imagery, sea‑scapes, or harvest motifs but do not simultaneously convey the harbor, gulls, and mist as strongly. Therefore, the first candidate best aligns with the location, season, and mood.

더 큰 모델을 판사로 쓰기

이 모듈을 진짜 앙상블로 만들려면, 하이쿠 작성기의 결과를 다른 모델이 평가하도록 해 볼게요. 한 줄만 더하면 됩니다.

class HaikuEnsemble(dspy.Module):
    def __init__(self, n: int = 3):
        super().__init__()
        self.n = n  
        # Module 1 generates several haikus
        self.writer = dspy.ReAct(
            "location, season, mood, num_haikus: int -> haikus: list[str]", 
            tools=[wikipedia_search, get_wikipedia_page],
            max_iters=5
        )
        # Module 2 picks the most evocative
        self.judge = dspy.ChainOfThought(
            "location, season, mood, candidates: list[str] -> most_evocative_index: int"
        )

    def forward(self, location: str, season: str, mood: str):
        candidates = self.writer(
            location=location, season=season, mood=mood, num_haikus=self.n,
        ).haikus
        # Call a much larger model to evaluate our haikus
        with dspy.context(lm=dspy.LM("openai/gpt-5.4")):
            verdict = self.judge( 
                location=location, season=season, mood=mood, candidates=candidates,
            )
        return dspy.Prediction(
            haiku=candidates[verdict.most_evocative_index],
            candidates=candidates,
            reasoning=verdict.reasoning,
        )

with dspy.context() 문은 판사 호출에 새 모델을 설정하는 새 컨텍스트를 정의하게 해줍니다.

분해하면 고립·재사용·관리·최적화가 쉬워진다

하이쿠 작업은 작은 예시지만, HaikuEnsemble을 만드는 과정은 필요한 순간 우리 프로그램을 얼마나 쉽게 분해할 수 있는지 보여줘요. 난해한 체이닝 API가 있는 게 아니라, 모듈은 그냥 Python과 DSPy의 원시 요소인 Signature, Module, LM일 뿐입니다.

분해해야 할 이유는 AI 프로그램이 복잡해지고 실패 모드를 학습하면서 나타나요. 예를 들어 커스텀 모듈을 쓰면 이렇게 할 수 있습니다.

  • 컨텍스트 고립(Isolate context): 헤매는 조사 단계는 많은 후보 주제를 다룰 수 있지만, 최종 하이쿠 작성 호출은 선택된 것만 봐야 해요. 분리하면 각 모듈이 자기 일에 집중할 수 있지요.
  • 프로그램 간 재사용(Reusable parts): 잘 튜닝된 Wikipedia 조사 서브모듈은 하이쿠 전용이 아니에요. 신뢰할 수 있는 근거가 필요한 어떤 프로그램이든 도와줄 수 있습니다. 분해하면 여러 프로그램에서 재사용할 수 있어요.
  • 쉬운 작업은 저렴한 모델로(Route easy work): 작은 모델 호출 여러 번으로 근거 수집을 빠르고 싸게 하고, 강한 모델 한 번으로 최종적이고 뉘앙스 있는 작문을 처리할 수 있어요.
  • 커스텀 제어 흐름 설계(Design custom control flow): 최종 하이쿠를 NLP 라이브러리로 음절 수를 검사하고, 운율이 맞지 않으면 작성기를 다시 호출할 수도 있어요.
  • 단계별 독립 점검(Govern independently): 각 서브모듈은 독립된 객체이고 각 호출은 inspect_history에 따로 기록돼요. 에이전트가 의외의 동작을 하면 서브모듈 하나만 따로 호출해 정확히 무엇을 돌려줬는지 볼 수 있습니다. 하이쿠 작성에선 덜 중요하지만, 고위험 작업에선 이런 감사 능력이 결정적이에요.
  • 평가·최적화가 더 쉬워짐(Easily evaluate and optimize): 하이쿠 하나를 고립해서 점수 매기는 건 어려워요. "좋다"는 판단이 너무 많은 차원에 걸쳐 있어 깔끔하게 채점하기 어렵지요. 셋 중 최고를 고르는 건 훨씬 쉽습니다. 분해하면 실제로 점수를 매길 수 있는 하위 작업이 고립되어 평가와 최적화가 가능해져요.

분기·재시도 루프·병렬 호출 같은 제어 흐름 패턴과 이 예시를 넘어선 조합은 Modules: composing your own에서 다룹니다.

평가와 최적화 이야기가 나왔으니, 이제 다음 섹션으로 넘어갈 차례예요.

더 알아보기 (Learn more)