모듈: 나만의 모듈 조합하기

모듈: 나만의 모듈 조합하기 (Modules: composing your own)

dspy.Module 은 조합(composition)의 단위예요. 서브클래싱하고, __init__ 에서 하위 모듈을 몇 개 정의하고, 그들을 통해 입력을 흘려보내는 forward() 를 쓰면 됩니다. 모듈은 커스텀 로직 — 제어 흐름, 분기, 여러 번의 LM 호출, 후처리 — 을 최적화기·저장/불러오기·설정 덮어쓰기·병렬 실행이 모두 계속 동작하는 방식으로 DSPy 생태계에 맞추는 방법이에요.

출처: 문서

본문

Predict/ChainOfThought/ReAct 만으로는 부족해서 그것들을 조합하고 싶을 때, 또는 __call__ 이 뒤에서 당신의 forward() 에 무엇을 하는지 이해하고 싶을 때 이 문서를 읽으세요.

설계 결정

1. 서브클래싱하고 forward() 를 오버라이드한다

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

2. 하위 모듈은 등록이 아니라 self.__dict__ 를 걸어가며 발견된다

할당(assignment)이 등록입니다. register_module() 호출도, 상속 트릭도 없어요. 트리 탐색 메서드들(named_predictors, named_parameters, named_sub_modules)은 호출 시점에 self.__dict__ 를 검사하고, 직접 속성뿐 아니라 리스트와 dict 안으로도 재귀합니다. 발견이 지연(lazy)된다는 것은 등록을 기억할 필요가 없다는 뜻이지만, 동시에 클로저 안이나 탐색되지 않는 컨테이너에 숨겨진 하위 모듈은 최적화기와 dump_state 에 보이지 않는다는 뜻이기도 해요.

3. Parameter 는 속성을 최적화기-가시로 표시하는 마커다

self.max_iters = 5 를 할당하면 트리 탐색이 그것을 건너뜁니다. self.predict = dspy.Predict(...) 를 할당하면 그것은 Parameter 이고 포함됩니다. 이것은 dump_state 를 작게 유지하고 최적화기의 탐색 공간을 집중시킵니다. 배워야 할 것들만 노출되는 거죠. Predict 는 Parameter 이고, Retrieve 도 그렇습니다. 평범한 Python 속성은 아닙니다.

4. __call__ 이 forward() 를 인프라로 감싼다

__call__ 을 통과하는 것이 self 를 settings.caller_modules 에 밀어 넣고, settings.track_usage 가 참이면 사용 추적 컨텍스트를 열며, 콜백 데코레이터 체인을 실행합니다. forward() 를 직접 호출하면 그 모든 것을 우회해요 — 사용량이 집계되지 않고, 콜백이 발화하지 않으며, 호출자 스택이 갱신되지 않습니다. 직접 forward 호출의 deprecation 경고가 바로 그 이유로 존재합니다. 항상 모듈을 module(...) 으로 호출하세요.

5. Async는 acall() / aforward() 로 sync 옆에 나란히 존재한다

acall 은 async를 위해 __call__ 의 인프라를 미러링하고, aforward 는 forward 의 async 대응입니다. aforward 를 정의하지 않은 모듈은 asyncify 를 통해 워커 스레드에서 sync forward를 실행하는 것으로 폴백합니다. 이것은 "sync만"에서 "완전 async"로 세상을 다시 쓰지 않고도 가는 길을 줍니다. 하위 모듈을 한 번에 하나씩 async로 바꾸고, 나머지는 그대로 두면 돼요.

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

하위 모듈은 호출 시점에 dspy.settings.lm (그리고 .adapter, .callbacks) 을 읽습니다. 그 결과: with dspy.context(lm=other_lm): result = my_module(...) 이 내부의 모든 하위 모듈에 대해 LM을 교체합니다. 재배선이 필요 없어요. 대가는 암묵적 상태입니다 — 하위 모듈의 동작은 __init__ 에서 할당된 것뿐 아니라 누가 그것을 호출하는지에 달려 있어요. 그 트레이드오프를 견딜 만한 가치가 조합 이야기에는 있습니다.

7. _compiled=True 는 하위 모듈의 파라미터를 최적화기로부터 얼려(freeze) 둔다

named_parameters 는 _compiled 플래그를 확인하고 컴파일된 자식을 건너뜁니다. 이것은 하위 모듈을 단독으로 최적화하고, 결과를 저장하며, 더 큰 프로그램에 넣은 뒤, 바깥 프로그램에 새 최적화기를 실행해도 안쪽 것을 건드리지 않게 해 줍니다. 손으로 설정할 일은 드물어요. 텔레프롬프트가 컴파일을 끝낼 때 설정합니다.

8. 메타클래스 ProgramMeta 가 super().__init__() 를 강제한다

ProgramMeta 는 당신의 __init__ 전에 실행되는 폴백 _base_init 을 설치해서, 서브클래스가 super().__init__() 을 잊어도 장부(히스토리, 콜백, _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) self 를 settings.caller_modules 에 밀어 넣고, settings.track_usage 이면 사용 추적 컨텍스트를 열며, (@with_callbacks 로 데코레이트된) forward 를 실행하고 Prediction 을 반환합니다. forward 가 Prediction 이 아닌 것을 반환하면 경고를 받습니다. 사용 추적이 Prediction 반환 타입에 의존하기 때문이에요. module.forward(...) 직접 호출은 deprecation 경고를 기록합니다.

Module.forward(**kwargs) 당신이 오버라이드하는 것. 시그니처 입력을 키워드 인자로 받고, 작업을 수행하며, Prediction 을 반환합니다. __init__ 에 설정 읽기를 넣지 마세요 — 현재 LM과 어댑터가 구성 시점이 아니라 호출 시점에 잡히도록 forward 안에서 읽으세요.

Module.acall(*args, **kwargs) / Module.aforward(**kwargs) Async 변형입니다. acall 은 공개 진입점이고, aforward 가 오버라이드 대상이에요. aforward 를 정의하지 않으면 acall 은 asyncify 안에서 forward 를 실행하는 것으로 폴백합니다. 하위 모듈 하나가 async 전용일 때 유용합니다 — 프로그램의 나머지를 async로 만들지 않고도 aforward 안에서 그것을 await 할 수 있어요.

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

모듈 트리 걸어가기

이것들은 모두 같은 기본 연산에 의존합니다: self.__dict__ 를 걸으며 리스트와 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) 각 predictor에 func 를 적용하고 제자리에서 교체합니다. 텔레프롬프트가 최적화된 predictor를 바꿔 끼울 때 쓰는 훅입니다. 직접 쓸 일은 드물어요.

LM 설정하고 가져오기

Module.set_lm(lm) 트리의 모든 predictor를 걸으며 lm 을 할당합니다. 하나의 LM으로 프로그램을 만든 뒤, 설정을 전역으로 바꾸지 않고 다른 모델에서 다시 평가하고 싶을 때 유용합니다.

Module.get_lm() → LM 모든 predictor가 하나에 동의하면 그 LM을 반환합니다. 섞여 있으면 ValueError 를 던집니다. 파인튜닝이나 저장 전에 프로그램 전체가 하나의 모델에 있는지 확인하고 싶을 때의 안전 검사입니다.

상태와 복사본

저장/불러오기 표면입니다. 전체 수명주기 — JSON vs PKL, 전체 프로그램 vs 상태 전용, save_program=True — 는 저장과 불러오기에 있고, 여기의 메서드가 바로 그 페이지의 주제입니다.

Module.dump_state(json_mode=True) / Module.load_state(state, allow_unsafe_lm_state=False) 최적화기-가시 상태(predictor별 데모, 트레이스, 시그니처 지시문, 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=True 는 모듈 전체를 디렉터리에 cloudpickle하고, 기본값은 상태 전용 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 개를 보기 좋게 출력합니다. 둘 다 피클·JSON 상태에서 제외되므로, 수 메가바이트짜리 히스토리가 저장된 프로그램에 딸려 가지 않아요.

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 은 모듈 출력용입니다 — 입력/출력 분할이 없고, completions와 사용량이 있습니다. 들어가는 것에는 Example, 나오는 것에는 Prediction 을 쓰세요.

크로스링크

  • 각 내장 모듈 서브클래스에는 자체 DD 페이지가 있습니다. predictor 동물원(zoo), ReAct, 최적화기별 페이지를 보세요.
  • 저장과 불러오기가 영속성 이야기를 자세히 다룹니다.
  • 설정과 context()가 LM/어댑터/콜백 오버라이드가 모듈의 하위 모듈로 어떻게 전파되는지 다룹니다.

더 알아보기 (Learn more)