Signature 심화
Signature 심화 (Signatures in depth)
시그니처는 프로그램과 언어 모델 사이의 선언적 계약이에요. 받는 입력 필드, 만들어내는 출력 필드, 그리고 작업을 설명하는 지시로 이루어져 있죠. 이 페이지는 클래스 기반 형식이 문자열 형식에 비해 무엇을 더 주는지, 타입이 붙은 필드가 DSPy를 어떻게 통과하는지, 그리고 그 모든 것을 빚어내는 설계 결정을 다룹니다.
출처: 공식문서
의도 (Intent)
시그니처는 프로그램과 언어 모델 사이의 선언적 계약입니다. 문자열 형식 "a, b -> c" 미니언어를 벗어나, 클래스 기반 형식이 무엇을 더하는지, 타입이 붙은 필드가 어떻게 코어스되는지, 옵티마이저와 모듈이 뒤에서 시그니처를 어떻게 다루는지 알고 싶을 때 읽으면 돼요.
설계 결정 (Design decisions)
1. 바탕은 Pydantic
시그니처의 모든 필드는 pydantic FieldInfo이고, 베이스 Signature 클래스는 pydantic.BaseModel을 확장해요. DSPy 특유의 메타데이터는 각 필드의 json_schema_extra에 살아 있습니다. 타입 검증, JSON 스키마 생성, 그리고 adapter 계층이 LM 출력을 선언된 타입으로 코어스하는 데 쓰는 TypeAdapter 같은 것들을 Pydantic이 기본 제공합니다.
유용한 결과로, 어떤 pydantic 제약(gt, lt, min_length, …)도 필드에서 동작해요. 그것들은 json_schema_extra["constraints"]로 문자열화되어 adapter가 프롬프트에서 언급합니다.
2. 이중 API, 하나의 클래스
문자열 형식(dspy.Signature("a, b -> c"))과 클래스 형식(class HaikuBot(dspy.Signature))은 같은 종류의 객체 — Signature 서브클래스 — 를 만들어요. 인라인 호출은 한 줄을 원하고, 더 풍부한 명세는 독스트링·타입이 붙은 InputField/OutputField 속성·그리고 때로는 가져온 타입을 원하지요. 둘 다 단일 클래스 타입으로 수렴시키면 모듈·adapter·옵티마이저가 언제나 한 가지 형태만 다루면 됩니다.
메타클래스 SignatureMeta가 dspy.Signature("...") 호출을 가로채 make_signature로 라우팅합니다. 여러분은 Signature 인스턴스를 직접 만들지 않아요 — 클래스 자체가 주고받는 객체입니다.
3. 독스트링이 작업 지시가 된다
메타클래스는 클래스 독스트링을 cleandocs해서 __doc__로 저장하고, Signature.instructions 프로퍼티가 그것을 다시 읽어요. 독스트링이 없으면 DSPy가 "Given the fields X, produce the fields Y."를 생성해 사용합니다. adapter는 이 문자열을 필드 스키마 위 시스템 메시지로 렌더링해요.
지시는 그것이 설명하는 필드 옆에 두는 게 맞고, 옵티마이저는 다시 쓸 단일하고 잘 정의된 문자열이 필요합니다. 독스트링을 작업 의도를 산문으로 표현하는 자리로, 필드 이름과 타입은 짧고 구조적으로 유지하며, 들어갈 수 없는 것은 독스트링에 두세요.
4. 지시는 모듈이 아니라 시그니처에 산다
시그니처는 자체 지시와 필드 스키마를 지녀요. 모듈(Predict, ChainOfThought, ReAct)은 호출 시점의 전략을 제공합니다 — LM 호출을 몇 번 할지, 무엇에 대해 추론할지, 언제 멈출지요. 같은 시그니처를 이들 어느 모듈에든 넘길 수 있어서, 같은 작업에서 모듈을 비교하거나 호출 방식을 바꾸지 않고 지시만 다시 쓸 때 싸게 처리할 수 있습니다.
모듈을 바꿀 때마다 독스트링을 다시 쓰고 싶다면, 지시에 모듈에 있어야 할 제어 흐름 세부사항이 들어 있는 건 아닌지 살펴보세요.
5. deepcopy로 불변성 확보
모든 변형 메서드(with_instructions, with_updated_fields, prepend, append, insert, delete)는 기존 필드 딕셔너리를 deep-copy해서 변경을 적용하고 새 Signature 클래스를 반환해요. 원본은 그대로 남습니다.
옵티마이저는 같은 프로그램의 후보 변형을 많이 돌려요. predictor가 시그니처를 제자리에서 변형하면 후보들이 공유 상태를 통해 서로를 덮어쓰게 됩니다. Signature.fields에 직접 파고들어 필드를 변형하지 말고 반드시 메서드를 거치세요. 동등성 비교는 Signature.equals(other)를 쓰고, 평범한 ==는 클래스 동일성을 비교하므로 대개 원하는 게 아니에요.
6. 필드 순서는 의미가 있다
메타클래스는 클래스 네임스페이스에서 필드 순서를 뽑아 Signature.fields를 통해 입력을 먼저, 출력을 나중에 노출해요. adapter는 프롬프트를 렌더링할 때 같은 딕셔너리를 워킹하므로, Signature.fields.keys()에서 보는 순서가 LM이 보는 순서입니다. 입력·출력을 재배치하면 프롬프트가 바뀝니다.
prepend/append/insert로 새 필드를 넣을 때, 위치 인자는 전체 필드 리스트가 아니라 해당 필드의 섹션(입력 또는 출력) 을 대상으로 해요.
7. 타입은 여기서 선언되지만 코어스는 다른 곳에서 일어난다
시그니처는 주석만 선언해요 — season: Literal["spring", ...], haikus: list[str], 커스텀 Pydantic 모델. 파싱은 dspy/adapters/utils.py::parse_value에서 일어나는데, json_repair를 시도하고 ast.literal_eval로 폴백한 뒤 TypeAdapter(annotation)으로 검증합니다. dspy.Type 서브클래스라면 pydantic 검증이 실패할 때 adapter가 원시 문자열로 재시도해 커스텀 타입의 파서가 맡게 합니다.
이 분리는 여러 adapter(Chat, JSON, XML, TwoStep)가 같은 시그니처를 다르게 코어스해야 하기 때문이에요. JSON adapter는 모델의 JSON 모드를 활용할 수 있고, XML adapter는 태그에서 파싱하지요. 코어스를 adapter 계층에 두면 시그니처는 가볍게 유지되고 파서 로직이 중복되지 않습니다. 타입이 붙은 필드가 파싱 시점에 오동작하면 시그니처가 아니라 adapter 출력과 parse_value를 봐야 해요. Adapters: how signatures become prompts을 참고하세요.
8. 커스텀 타입은 호출자의 스택 프레임 워킹으로 해석된다
문자열 파서가 subject: MyType을 보는데 MyType이 내장이 아니거나 dspy.* 클래스가 아니면, SignatureMeta._detect_custom_types_from_caller가 호출 스택을 최대 100프레임 워킹하며 각 프레임의 locals·globals에서 이름을 찾아요. 그래서 dspy.Predict("subject: MyType -> output")을 import나 등록 없이 인라인으로 쓸 수 있습니다.
100프레임 상한은 가드레일입니다. 프레임을 쓸 수 없는 stripped·최적화된 Python에선 경고가 나고 make_signature에 custom_types={"MyType": MyType}을 명시적으로 넘겨야 해요. 프레임 인트로스펙션에 의존하고 싶지 않을 때도 같은 폴백이 언제나 동작합니다.
9. 필드 이름과 설명은 옵티마이저에 대해 불활성(inert)이다
GEPA와 다른 지시 옵티마이저는 시그니처의 독스트링을 다시 써요. 필드 이름, desc, prefix는 건드리지 않습니다. 그것들이 바뀌는 유일한 방법은 with_updated_fields입니다.
필드 이름은 프로그램의 공개 인터페이스의 일부예요 — 호출자 코드가 result.haiku를 읽고, 다운스트림 모듈이 "haiku"를 이름으로 조회하지요. 옵티마이저가 이름을 바꾸면 그 주변 프로그램이 깨집니다. 그러니 필드 이름을 신중히 지으세요 — 옵티마이저가 나중에 result를 haiku로 고쳐주지 않습니다. 필드 설명을 쓸 때도 마찬가지예요.
API 둘러보기 (API walkthrough)
목적별로 묶어서 살펴볼게요.
시그니처 선언하기
dspy.Signature
필드가 두세 개를 넘거나, 독스트링을 원하거나, 더 풍부한 타입이 필요할 때 상속하세요. dspy.Signature("a, b -> c")를 직접 호출하는 건 축약형입니다 — 메타클래스가 호출을 가로채 문자열 파서를 통해 라우팅하지요. 둘 다 진짜 Signature 서브클래스(인스턴스가 아니라)를 만들어요 — 인스턴스화하지 않습니다.
dspy.InputField(*, desc=None, prefix=None, **pydantic_kwargs)
dspy.OutputField(*, desc=None, prefix=None, **pydantic_kwargs)
pydantic.Field 위의 가벼운 팩토리입니다. 모든 pydantic 제약(gt, min_length, …)과 DSPy 특유의 desc·prefix를 받아요. prefix=는 자동 추론된 라벨을 덮어쓰고, 생략하면 infer_prefix가 속성 이름에서 하나를 유도합니다. deprecated된 format·parser 인자는 여전히 값을 받지만 더는 아무것도 하지 않습니다 — adapter 계층보다 앞서던 것들이에요.
dspy.make_signature(signature, instructions=None, signature_name="StringSignature", custom_types=None)
실제 생성자입니다. 두 가지 입력 형태가 있어요 — 문자열(_parse_signature로 파싱) 또는 {name: (type, FieldInfo)} 딕셔너리(변형 메서드가 자신의 조각으로 클래스를 재구성할 때 사용). signature_name은 결과 클래스의 __name__을 정하며 로그와 검사 도구에 표면화됩니다.
dspy.ensure_signature(signature, instructions=None)
이미 만들어진 Signature 클래스에는 no-op이고, 문자열에는 make_signature를 실행합니다. 두 형태를 모두 받아야 하는 모듈을 만들 때 써요. memoize하지 않으므로 같은 문자열을 핫 루프에서 호출하지 마세요 — 매 호출이 새 클래스를 만듭니다.
SignatureMeta
Signature 뒤의 메타클래스입니다. 상속하거나 인스턴스화하지 않아요. 하는 일은: 독스트링을 지시로 파싱하고, 모든 필드가 input 또는 output으로 태그됐는지 검증하며, 빠진 prefix= 값을 추론하고, 문자열 파싱 중 커스텀 타입 프레임 워킹을 실행합니다. 대부분의 "이상한 시그니처" 오류가 여기서 시작돼요.
시그니처 검사하기
Signature.input_fields / Signature.output_fields / Signature.fields → dict[str, FieldInfo]
필드 이름을 키로 하는 딕셔너리입니다. FieldInfo의 .annotation은 선언된 타입이고, json_schema_extra는 DSPy의 메타데이터를 지녀요 — __dspy_field_type 태그와 IS_TYPE_UNDEFINED 마커(문자열 형식 필드가 명시 타입이 없을 때 설정됨 — Predict가 None을 빈 문자열로 코어스하는 데 사용)를 포함합니다.
Signature.instructions → str
cleandoc된 독스트링입니다. 프로퍼티는 설정 가능하지만, 새 클래스를 얻으려면 with_instructions(...)를 쓰는 게 낫습니다.
Signature.signature → str
클래스를 문자열 형식으로 왕복시킵니다(예: "location, season, mood -> haiku") — 표시용이에요. 로그와 검사에 유용하고 파싱에는 쓰지 않습니다.
Signature.equals(other) → bool
지시와 각 필드의 json_schema_extra를 비교합니다. 옵티마이저와 테스트가 쓰는 동등성 술어예요. 평범한 ==는 클래스 동일성을 비교하므로 대개 원하는 게 아닙니다.
시그니처 변형하기
이 메서드들은 모두 deep-copy를 통해 새 Signature 클래스를 반환해요.
Signature.with_instructions(instructions: str)
같은 필드, 독스트링 교체된 새 클래스입니다.
Signature.append_instructions(instructions: str)
같은 필드, 독스트링이 instructions(빈 줄로 이어붙임)로 확장된 새 클래스입니다. 지시를 다시 반복하지 않고 기존 시그니처(일명 독스트링) 위에 추가 안내를 쌓고 싶을 때 써요.
Signature.with_updated_fields(name, type_=None, **json_schema_extra)
키워드 인자를 기존 json_schema_extra에 병합해 필드의 메타데이터를 교체합니다. 설명을 바꾸려면 desc=..., 라벨을 바꾸려면 prefix=..., adapter나 옵티마이저가 읽는 커스텀 키는 아무거나 넘길 수 있어요. type_을 바꾸면 새 주석이 옛것을 대체하고, IS_TYPE_UNDEFINED=False를 함께 넘기면 Predict가 주석 없는 문자열 형식 필드에 의존하는 "None을 빈 문자열로 취급" 동작을 해제합니다.
Signature.prepend(name, field, type_=None)
Signature.append(name, field, type_=None)
Signature.insert(index, name, field, type_=None)
필드의 섹션(입력 또는 출력)은 __dspy_field_type 태그로 결정됩니다 — InputField나 OutputField로 필드를 만들 때 설정되지요. 위치 인자는 모든 필드가 아니라 그 섹션 안을 대상으로 해요. insert는 음수 인덱스를 받고, 범위를 벗어나면 ValueError가 납니다.
Signature.delete(name)
필드가 없으면 조용히 no-op입니다. 큰 소리로 실패하고 싶으면 name in cls.fields를 먼저 확인하세요.
시그니처 지속하기
Signature.dump_state() → dict
Signature.load_state(state) → Signature class
최적화 실행에 걸쳐 변하는 부분 — 지시, 그리고 각 필드의 주석·prefix·desc·__dspy_field_type 태그 — 을 왕복시킵니다. Signature 클래스 자체는 직렬화되지 않아요 — 소비 프로세스가 먼저 그것을 재인스턴스화한다고 가정합니다(보통 부모 모듈의 저장 상태를 통해). Module.save()와 dspy.load()가 내부적으로 이걸 쓰므로 직접 호출할 일은 거의 없어요.
이름 유틸리티
dspy.infer_prefix(attribute_name) → str
camelCase와 snake_case 경계에서 이름을 분리하고 첫 글자를 대문자로 바꿔요. 필드가 prefix=를 생략하면 메타클래스가 호출합니다. 단독으로 쓸 일은 거의 없어요.
더 알아보기 (Learn more)
- Adapters: how signatures become prompts —
desc,prefix, 타입이 유선(wire)에서 어떻게 보이는지,parse_value가 어디서 일하는지. - Modules: composing your own —
Predict,ChainOfThought, 그리고 여러분의 서브클래스가 시그니처를 어떻게 소비하는지.