모듈: 직접 조합하기 (Modules in depth)

모듈: 직접 조합하기 (Modules: composing your own)

이 문서가 설명하는 것

dspy.Module조합의 단위예요. 서브클래스를 만들고 __init__에서 서브모듈을 정의한 뒤, 입력을 그들 사이로 흘려보내는 forward()를 작성하면 됩니다. 제어 흐름, 분기, 여러 LM 호출, 후처리 같은 커스텀 로직을 — 옵티마이저, 저장/불러오기, 설정 오버라이드, 병렬 실행이 모두 계속 동작하는 방식으로 — DSPy 생태계에 맞추는 것이 모듈의 역할입니다.

Predict / ChainOfThought / ReAct만으로는 부족해져서 이것들을 조합하고 싶을 때, 혹은 __call__이 여러분의 forward()를 뒤에서 어떻게 감싸는지 알고 싶을 때 읽으면 좋아요.

설계 결정

1. 서브클래스를 만들고 forward()를 오버라이드한다

그게 계약이에요. __init__에서 서브모듈을 정의하고, forward에서 작업을 수행하며, __call__이 그것을 감싸게 합니다. 이 분리가 존재하는 이유는 프레임워크가 인프라를 붙일 예측 가능한 지점이 필요하기 때문이에요. __init__은 서브모듈이 트리 탐색과 직렬화에 보이는 곳이고, forward는 런타임 관심사(설정 스택, 사용량 추적, 콜백)가 여러분의 코드를 감싸는 곳입니다. 둘을 섞으면 옵티마이저가 보는 것과 dump_state가 저장하는 것이 흐려져요.

2. 서브모듈은 등록이 아니라 self.__dict__를 걸어 발견한다

할당이 곧 등록입니다. register_module() 호출도, 상속 트릭도 없어요. 트리 탐색 메서드(named_predictors, named_parameters, named_sub_modules)는 호출 시점에 self.__dict__를 검사하고 리스트·딕셔너리와 직접 속성으로 재귀합니다. 발견이 lazy라서 등록을 기억할 필요가 없는 대신, 클로저 안이나 탐색되지 않는 컨테이너에 숨긴 서브모듈은 옵티마이저와 dump_state에서 보이지 않아요.

3. Parameter는 속성을 옵티마이저에 노출시키는 마커다

self.max_iters = 5를 할당하면 트리 탐색이 건너뜁니다. self.predict = dspy.Predict(...)Parameter라서 포함됩니다. 이렇게 해서 dump_state는 작게 유지되고 옵티마이저의 탐색 공간도 집중됩니다 — 배워야 할 것만 노출되거든요. PredictParameter이고 Retrieve도 마찬가지지만, 평범한 Python 속성은 아닙니다.

4. __call__forward()를 인프라로 감싼다

__call__을 통과하는 것이 settings.caller_modulesself를 밀어 넣고, settings.track_usage가 true면 사용 추적 컨텍스트를 열고, 콜백 데코레이터 체인을 실행하는 길이에요. forward()를 직접 호출하면 이 모든 게 우회됩니다 — 사용량이 집계되지 않고, 콜백도 발화하지 않으며, 호출자 스택도 갱신되지 않죠. 직접 forward 호출에 대한 지원 중단 경고가 있는 이유가 바로 이겁니다. 항상 모듈을 module(...)로 호출하세요.

5. async는 acall() / aforward()로 sync 옆에 자리한다

acall__call__의 인프라를 async로 비춥니다. aforwardforward의 async 대응물이에요. aforward를 정의하지 않은 모듈은 asyncifyforward를 워커 스레드에서 실행하는 방식으로 폴백합니다. 이렇게 하면 "sync 전용"에서 "완전 async"로 가는 길이 생겨요 — 서브모듈을 한 번에 하나씩 async로 바꾸고, 나머지은 그대로 두면 됩니다.

6. 설정은 생성자 인자가 아니라 컨텍스트로 전파된다

서브모듈은 호출 시점에 dspy.settings.lm(그리고 .adapter, .callbacks)을 읽습니다. 그 결과 with dspy.context(lm=other_lm): result = my_module(...)는 안의 모든 서브모듈의 LM을 갈아끼웁니다. 재배선이 필요 없어요. 대가는 암묵적 상태입니다 — 서브모듈의 동작은 __init__에서 할당된 것뿐 아니라 누가 호출하느냐에도 의존합니다. 이 트레이드오프를 감당할 가치가 조합에 있긴 해요.

7. _compiled=True는 옵티마이저로부터 서브모듈의 파라미터를 얼린다

named_parameters_compiled 플래그를 확인해 컴파일된 자식을 건너뜁니다. 덕분에 서브모듈을 따로 최적화하고, 결과를 저장하고, 더 큰 프로그램에 넣고, 새 옵티마이저를 바깥 프로그램에 돌려도 안쪽 것을 건드리지 않습니다. 손으로 직접 설정할 일은 드물고, 텔레프롬프트가 컴파일을 마치면 설정해요.

8. 메타클래스 ProgramMetasuper().__init__()을 강제한다

ProgramMeta는 여러분의 __init__보다 먼저 실행되는 폴백 _base_init을 설치해서, 서브클래스가 super().__init__()을 깜빡해도 정부(history, callbacks, _compiled 플래그)가 갖춰지게 합니다. 메타클래스를 서브클래스하거나 인스턴스화할 일은 없어요 — 그건 안전장치입니다. 에러 트레이스가 그 안에 떨어지면, 보통 원인은 빠진 super().__init__() 호출이에요.

9. deepcopy()는 손으로 작성됐다

모듈은 Python 기본 copy.deepcopy를 혼란시키는 클로저와 콜백을 담을 수 있어요. Module.deepcopy()는 먼저 copy.deepcopy를 시도하고 TypeError가 나면 속성별 복사로 폴백합니다. 옵티마이저는 이걸로 후보 프로그램을 포크하며, 폴백이 Module 서브클래스가 담을 수 있는 다양한 것들에 걸쳐 그 과정을 안정적으로 만들어 줍니다.

API 둘러보기

하려는 일 기준으로 묶었어요.

모듈 정의

dspy.Module(callbacks=None)
서브클래스를 만들어 쓰세요. 기본 클래스는 callbacks, history, _compiled 플래그만 저장합니다. 그 이상은 없어요. 재미있는 동작은 __call__과 트리 탐색 메서드에 있습니다.

Module.__call__(*args, **kwargs)
settings.caller_modulesself를 밀어 넣고, settings.track_usage가 true면 사용 추적 컨텍스트를 열고, forward( @with_callbacks로 데코레이팅)를 실행한 뒤 Prediction을 돌려줍니다. forwardPrediction이 아닌 것을 돌려주면 경고가 뜨는데, 사용 추적이 Prediction 반환 타입에 의존하기 때문이에요. module.forward(...) 직접 호출은 지원 중단 경고를 로그합니다.

Module.forward(**kwargs)
여러분이 오버라이드하는 메서드입니다. 시그니처 입력을 키워드 인자로 받고, 작업을 수행하고, Prediction을 돌려줍니다. __init__에 설정 읽기를 넣지 마세요 — forward 안에서 읽어서 현재 LM과 어댑터를 생성 시점이 아니라 호출 시점에 잡아야 합니다.

Module.acall(*args, **kwargs) / Module.aforward(**kwargs)
async 변형입니다. acall이 공개 진입점이고, aforward가 여러분이 오버라이드하는 것입니다. aforward를 정의하지 않으면 acallasyncify 안에서 forward를 실행하는 방식으로 폴백해요. 하나의 서브모듈만 async 전용일 때 유용합니다 — 나머지 프로그램을 async로 만들지 않고 aforward에서 await할 수 있거든요.

ProgramMeta
Module에 붙은 메타클래스입니다. 서브클래스나 인스턴스화할 일이 없어요. super().__init__()을 엄격히 요구하지 않도록 _base_init을 폴백으로 설치합니다. 에러 트레이스가 그 안에 떨어지면 원인은 보통 빠진 super().__init__()입니다.

모듈 트리 탐색

여기 메서드들은 모두 같은 원시 연산에 의존합니다. self.__dict__를 걷고 리스트·딕셔너리로 재귀하며 대상 타입과 일치하는 것을 yield하는 거죠.

Module.named_predictors() / Module.predictors()
이 모듈 아래의 모든 Predict 인스턴스를 발견합니다. 옵티마이저가 무엇을 최적화할지 찾을 때 써요. 순서는 발견 순서(__dict__를 통한 깊이 우선)입니다.

Module.named_parameters() / Module.parameters()
모든 Parameter를 발견합니다. _compiled를 존중합니다 — 컴파일로 표시된 서브모듈은 건너뛰고 옵티마이저는 그 파라미터를 건드리지 않아요.

Module.named_sub_modules(type_=BaseModule, skip_compiled=False)
다른 메서드들이 쓰는 생성기입니다. 커스텀 탐색이 필요할 때 직접 쓰세요 — 모든 ReAct 인스턴스, 특정 유저 클래스의 모든 서브모듈, type_으로 필터링할 수 있는 무엇이든 말이죠.

Module.map_named_predictors(func)
각 예측기에 func를 적용하고 제자리에서 교체합니다. 텔레프롬프트가 최적화된 예측기를 갈아끼울 때 쓰는 훅이에요. 직접 쓸 일은 드뭅니다.

LM 설정과 조회

Module.set_lm(lm)
트리의 모든 예측기를 걸으며 lm을 할당합니다. 한 LM으로 프로그램을 만들고, 전역 설정을 바꾸지 않고 다른 LM으로 재평가하고 싶을 때 유용해요.

Module.get_lm()LM
모든 예측기가 한 LM에 동의하면 그 LM을 돌려줍니다. 섞여 있으면 ValueError를 던져요. 파인튜닝이나 저장 전에 전체 프로그램이 하나의 모델인지 확인하는 안전 검사입니다.

상태와 복사

저장/불러오기 표면입니다. 전체 생명주기(JSON vs PKL, 전체 프로그램 vs 상태만, save_program=True)는 저장과 불러오기에 있고, 여기 메서드들이 그 페이지가 다루는 내용이에요.

Module.dump_state(json_mode=True) / Module.load_state(state, allow_unsafe_lm_state=False)
옵티마이저가 보는 상태를 왕복시킵니다. 예측기별 데모, 트레이스, 시그니처 지시사항, LM 설정이 여기 포함돼요. Python 클래스 자체는 아닙니다.

Module.save(path, save_program=False, modules_to_serialize=None) / Module.load(path, allow_pickle=False, allow_unsafe_lm_state=False)
사용자 지향 쌍입니다. save_program=Truecloudpickle로 모듈 전체를 디렉토리에 저장하고, 기본은 상태만 담은 JSON 또는 PKL을 씁니다.

Module.deepcopy()
copy.deepcopy를 시도하고 TypeError면 속성별 복사로 폴백합니다. 옵티마이저가 후보 프로그램을 포크할 때 써요.

Module.reset_copy()
deepcopy() 후 모든 파라미터에 reset()을 수행합니다. 새 프로그램 — 같은 구조, 옵티마이저 상태 없음 — 을 만들 때 씁니다.

배치 실행

Module.batch(examples, num_threads=None, max_errors=None, return_failed_examples=False, ...)
모듈을 많은 예시에 대해 병렬로 실행합니다. 내부적으로 dspy.Parallel을 감싸는데, dspy.settings를 스냅샷하고 각 워커 스레드 안에 그 스냅샷을 다시 적용해요(그래서 바깥의 dspy.context(lm=...)가 존중됩니다). 예측을 입력 순서대로 돌려줍니다. 평가, 데모 수집, 그리고 부끄러울 만큼 병렬이 쉬운 추론에 씁니다.

디버깅

Module.history / Module.inspect_history(n=1, file=None)
history는 이 모듈에 붙은 LM 호출 기록 목록이고, inspect_history는 최근 n개를 보기 좋게 출력합니다. 둘 다 pickle·JSON 상태에서 제외되어서, 수 메가바이트짜리 history가 저장된 프로그램을 따라다니지 않아요.

Module.callbacks
등록된 콜백 핸들러입니다. 저장 상태에서도 제외됩니다.

반환 타입

dspy.Prediction(**fields)
필드 컨테이너입니다. result.haiku로 접근하거나 result["haiku"]로 접근합니다. dspy.Example의 저장을 상속하되 입출력 구분은 버립니다.

dspy.Prediction.from_completions(list_or_dict, signature=None)
여러 LM 컴플리션에서 Prediction을 만듭니다. BestOfN, MultiChainComparison, 그리고 두 번 이상 샘플링하는 어떤 모듈이든 이걸 씁니다.

Prediction.completions
예측이 n > 1 샘플에서 왔을 때의 Completions 객체입니다. pred.completions[i]는 i번째 컴플리션을 각자의 Prediction으로 돌려줘요. 단일 샘플 호출에서는 None입니다.

Prediction.get_lm_usage() / Prediction.set_lm_usage(tokens_dict)
토큰 사용량 계산입니다. dspy.settings(track_usage=True) 아래에서 예측이 실행됐을 때 채워집니다.

dspy.Example와 비교. 같은 저장 계층을 쓰지만 의도는 달라요. Example은 학습 데이터용 — _demos_input_keys를 지니고 .with_inputs가 필터된 사본을 돌려줍니다. Prediction은 모듈 출력용 — 입출력 구분이 없고 컴플리션과 사용량이 있습니다. 들어가는 것에는 Example, 나오는 것에는 Prediction을 쓰세요.

관련 문서

  • 각 내장 모듈 서브클래스는 저마다 DD 페이지가 있어요: 예측기 zoo, ReAct, 옵티마이저별 페이지를 보세요.
  • 저장과 불러오기가 영속성 이야기를 자세히 다룹니다.
  • 설정과 context()가 LM/어댑터/콜백 오버라이드가 모듈의 서브모듈로 어떻게 전파되는지 다룹니다.