시그니처 심층 탐구
시그니처 심층 탐구 (Signatures in depth)
시그니처는 당신의 프로그램과 언어 모델 사이의 선언적 계약(declarative contract) 이에요. 받아들이는 입력 필드, 만들어 내는 출력 필드, 그리고 작업을 설명하는 지시문이 그것이죠. 이 페이지에서는 문자열 형식이 주는 것에 더해 클래스 형식이 무엇을 제공하는지, 타입이 지정된 필드가 DSPy 안에서 어떻게 이동하는지, 런타임에 시그니처를 어떻게 수정하는지, 그리고 그 모든 것을 관통하는 설계 결정을 다룹니다.
출처: 문서
본문
문자열 형식의 "a, b -> c" 미니 언어를 벗어난 뒤, 클래스 형식이 무엇을 더하는지, 타입 필드가 어떻게 강제 변환(coerce)되는지, 최적화기와 모듈이 뒤에서 시그니처를 어떻게 조작하는지 알고 싶은 때 이 문서를 읽으세요.
설계 결정
1. 내부는 Pydantic
Signature의 모든 필드는 pydantic FieldInfo 이고, 기본 Signature 클래스는 pydantic.BaseModel 을 확장합니다. DSPy 고유의 메타데이터는 각 필드의 json_schema_extra 에 담겨요. Pydantic은 DSPy가 손으로 만들었을 것들을 제공하는데, 타입 검증, JSON-schema 생성, 그리고 어댑터 계층이 LM 출력을 선언된 타입으로 강제 변환할 때 쓰는 TypeAdapter 가 그것입니다.
유용한 결과 하나: 어떤 pydantic 제약(gt, lt, min_length, …)이든 필드에 동작해요. 그들은 json_schema_extra["constraints"] 로 문자열화되고, 어댑터가 프롬프트에서 그것을 언급합니다.
2. 두 가지 API, 하나의 클래스
문자열 형식(dspy.Signature("a, b -> c"))과 클래스 형식(class HaikuBot(dspy.Signature))은 같은 종류의 객체, 즉 Signature 서브클래스를 만듭니다. 인라인 호출은 한 줄 짜리를 원하고, 더 풍부한 스펙은 docstring, 타입이 지정된 InputField/OutputField 속성, 때로는 임포트된 타입을 원해요. 둘 다 단일 클래스 타입으로 압축하면 모듈·어댑터·최적화기가 단 한 가지 모양만 처리하게 됩니다.
메타클래스 SignatureMeta 가 dspy.Signature("...") 호출을 가로채서 make_signature 를 통해 라우팅해요. 당신은 Signature 인스턴스를 만드는 일은 없습니다. 클래스 자체가 여기저기 전달되는 객체이기 때문이에요.
3. docstring이 작업 지시문이 된다
메타클래스는 클래스 docstring을 cleandoc 처리해 __doc__ 로 저장하고, Signature.instructions 속성이 그것을 다시 읽어요. docstring이 없으면 DSPy는 "Given the fields X, produce the fields Y." 를 생성해 사용합니다. 어댑터는 이 문자열을 필드 스키마 위의 프롬프트 시스템 메시지로 렌더링해요.
지시문은 그것이 설명하는 필드 옆에 있는 게 맞고, 최적화기는 다시 쓸 단일하고 잘 정의된 문자열을 필요로 합니다. docstring을 산문으로 작업 의도를 표현하는 자리로 취급하고, 필드 이름과 타입은 짧고 구조적으로 유지하며, 거기 안에 안 들어가는 것은 docstring에 넣으세요.
4. 지시문은 모듈이 아니라 시그니처에 있다
Signature는 자기 자신의 지시문과 필드 스키마를 지닙니다. 모듈(Predict, ChainOfThought, ReAct)은 호출 시점 전략을 제공합니다 — LM 호출을 몇 번 할지, 무엇에 대해 추론할지, 언제 멈출지 같은 것들이죠. 같은 Signature를 그들 중 어느 것에든 넘길 수 있어서, 같은 작업에서 모듈을 비교하거나 호출 방식을 바꾸지 않고 지시문을 다시 쓰는 것이 값싸집니다.
모듈을 바꿀 때마다 docstring을 다시 써야 한다는 걸 발견하면, 지시문 안에 모듈에 속해야 할 제어 흐름 세부사항이 들어 있을 가능성이 커요.
5. deepcopy에 의한 불변성(immutability)
모든 수정 메서드(with_instructions, with_updated_fields, prepend, append, insert, delete)는 기존 필드 dict를 deep-copy 하고, 변경을 적용한 뒤 새 Signature 클래스를 반환합니다. 원본은 그대로 남아요.
최적화기는 같은 프로그램의 수많은 후보 변형을 실행합니다. 만약 한 predict 기기가 제자리에서 시그니처를 수정한다면, 후보들이 공유 상태를 통해 서로를 덮어쓰게 될 거예요. Signature.fields 에 직접 접근해 필드를 수정하려 하지 말고 반드시 메서드를 통하세요. 동등성 비교에는 Signature.equals(other) 를 쓰세요. 평범한 == 는 클래스 정체성을 비교하는데, 보통 그것은 당신이 원하는 것이 아닙니다.
6. 필드 순서는 의미가 있다
메타클래스는 클래스 네임스페이스에서 필드 순서를 추출하고, Signature.fields 를 통해 입력을 먼저, 그다음 출력으로 노출합니다. 어댑터는 프롬프트를 렌더링할 때 같은 dict를 따라가므로, Signature.fields.keys() 에서 보이는 순서가 LM이 보는 순서입니다. 입력이나 출력을 재배열하면 프롬프트가 바뀌어요.
prepend/append/insert 로 새 필드를 넣을 때, 위치 인자는 전체 필드 목록이 아니라 필드가 속한 섹션(입력 또는 출력) 내를 겨냥합니다.
7. 타입은 여기서 선언되지만, 강제 변환은 다른 곳에서 일어난다
Signature는 주석만 선언합니다: season: Literal["spring", ...], haikus: list[str], 커스텀 Pydantic 모델 같은 것들이요. 실제 파싱은 dspy/adapters/utils.py::parse_value 에서 일어나는데, json_repair 를 시도하고 ast.literal_eval 로 폴백한 뒤 TypeAdapter(annotation) 에 대해 검증합니다. dspy.Type 서브클래스의 경우 pydantic 검증이 실패하면 어댑터가 원시 문자열로 재시도하여 타입의 커스텀 파서가 처리하게 합니다.
이 분리는 여러 어댑터(Chat, JSON, XML, TwoStep)가 같은 Signature를 다르게 강제 변환해야 하기 때문에 존재해요. JSON 어댑터는 모델의 JSON 모드를 활용할 수 있고, XML 어댑터는 태그에서 파싱합니다. 강제 변환을 어댑터 계층에 두면 Signature는 날씬하게 유지되고 파서 로직이 중복되지 않습니다. 타입 필드가 파싱 시점에 잘못 작동하면 Signature가 아니라 어댑터의 출력과 parse_value 를 보세요. 어댑터: 시그니처가 프롬프트가 되는 법을 참고하세요.
8. 커스텀 타입은 호출자의 스택 프레임을 걸어가며 해석된다
문자열 파서가 subject: MyType 을 보고 MyType 이 내장이거나 dspy.* 클래스가 아니면, SignatureMeta._detect_custom_types_from_caller 가 호출 스택의 최대 100개 프레임을 걸어가며 각 프레임의 locals와 globals에서 이름을 찾아요. 덕분에 아무것도 임포트하거나 등록하지 않고도 dspy.Predict("subject: MyType -> output") 를 인라인으로 쓸 수 있습니다.
100프레임 제한은 안전장치입니다. 프레임을 쓸 수 없는 stripped되거나 최적화된 Python에서는 경고가 나오고, make_signature 에 custom_types={"MyType": MyType} 을 명시적으로 전달해야 합니다. 프레임 내부 조사에 의존하고 싶지 않을 때도 같은 폴백이 동작합니다.
9. 필드 이름과 설명은 최적화기에 불활성(inert)이다
GEPA와 다른 지시문 최적화기들은 Signature의 docstring을 다시 씁니다. 그들은 필드 이름, desc, prefix 를 건드리지 않아요. 그것들이 바뀌는 유일한 경로는 with_updated_fields 입니다.
필드 이름은 프로그램의 공개 인터페이스의 일부입니다: 호출 코드는 result.haiku 를 읽고, 하류 모듈은 이름으로 "haiku" 를 찾아요. 최적화기가 이름을 바꾸면 그 주변 프로그램을 깨뜨릴 겁니다. 그러니 필드 이름을 신중히 지으세요. 최적화기가 나중에 result 를 haiku 로 고쳐 주지 못해요. 필드 설명을 쓸 때도 마찬가지입니다.
API 살펴보기
하려는 일에 따라 묶었어요.
시그니처 선언하기
dspy.Signature
필드가 두세 개를 넘어가거나, docstring을 원하거나, 더 풍부한 타입이 필요할 때 서브클래싱하세요. 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 인자는 여전히 값을 받지만 더 이상 아무것도 하지 않아요 — 어댑터 계층보다 앞선 것들이거든요.
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 를 실행합니다. 두 형태를 모두 받아야 하는 모듈을 쓸 때 사용하세요. 메모이즈하지 않으므로 같은 문자열로 핫 루프에서 호출하지 마세요 — 호출마다 새 클래스를 만들거든요.
SignatureMeta
Signature 뒤의 메타클래스입니다. 서브클래싱하거나 인스턴스화하지 않아요. 하는 일: docstring을 지시문으로 파싱하고, 모든 필드가 input 또는 output으로 태그됐는지 검증하고, 빠진 prefix= 값을 추론하며, 문자열 파싱 중에 커스텀 타입 프레임 탐색을 실행합니다. 대부분의 "이상한 시그니처" 에러는 여기서 발생해요.
시그니처 들여다보기
Signature.input_fields / Signature.output_fields / Signature.fields → dict[str, FieldInfo]
필드 이름을 키로 하는 dict들입니다. FieldInfo 의 .annotation 은 선언된 타입이고, json_schema_extra 는 DSPy의 메타데이터를 담는데, __dspy_field_type 태그와 IS_TYPE_UNDEFINED 마커가 포함됩니다(문자열 형식 필드에 명시적 타입이 없을 때 설정되며, Predict가 None 을 빈 문자열로 강제 변환하는 데 사용).
Signature.instructions → str
cleandoc 처리된 docstring입니다. 속성이므로 설정 가능하지만, 새 클래스를 얻으려면 with_instructions(...) 를 선호하세요.
Signature.signature → str
클래스를 문자열 형식(예: "location, season, mood -> haiku")으로 왕복(round-trip)시켜 표시용으로 씁니다. 로그와 검사에 유용하고, 파싱에는 쓰이지 않습니다.
Signature.equals(other) → bool
지시문과 모든 필드의 json_schema_extra 를 비교합니다. 최적화기와 테스트가 쓰는 동등성 술어예요. 평범한 == 는 클래스 정체성을 비교하는데, 보통 원하는 것이 아닙니다.
시그니처 수정하기
여기의 모든 메서드는 deep-copy를 통해 새 Signature 클래스를 반환합니다.
Signature.with_instructions(instructions: str)
같은 필드, 새 docstring을 가진 새 클래스.
Signature.append_instructions(instructions: str)
같은 필드, instructions 로(빈 줄로 이어 붙여) 확장된 docstring을 가진 새 클래스. 기존 시그니처(docstring)의 지시문을 되풀이하지 않으면서 그 위에 추가 지침을 겹겹이 쌓고 싶을 때 사용하세요.
Signature.with_updated_fields(name, type_=None, **json_schema_extra)
키워드 인자들을 기존 json_schema_extra 에 병합해 필드의 메타데이터를 교체합니다. desc=... 를 넘기면 설명이 바뀌고, prefix=... 를 넘기면 레이블이 바뀌며, 어댑터나 최적화기가 읽는 어떤 커스텀 키든 넘길 수 있어요. 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
최적화 실행마다 달라지는 부분(지시문, 각 필드의 annotation·prefix·desc·__dspy_field_type 태그)을 왕복시킵니다. Signature 클래스 자체는 직렬화되지 않아요 — 소비하는 프로세스가 먼저 재인스턴스화한다는(보통 부모 모듈의 저장된 상태를 통해) 가정이 있습니다. Module.save() 와 dspy.load() 가 내부에서 이것들을 쓰며, 직접 호출할 일은 드뭅니다.
이름 유틸리티
dspy.infer_prefix(attribute_name) → str
이름을 camelCase와 snake_case 경계에서 나누고 결과를 대문자로 만듭니다. 메타클래스가 필드에서 prefix= 를 생략했을 때 호출합니다. 단독으로 쓸 일은 드물어요.
크로스링크
- 어댑터: 시그니처가 프롬프트가 되는 법 —
desc,prefix, 타입이 와이어에서 어떻게 보이는지, 그리고parse_value가 어디서 작업을 하는지. - 모듈: 나만의 모듈 조합하기 —
Predict,ChainOfThought, 그리고 당신의 서브클래스가 시그니처를 어떻게 소비하는지. - GEPA와 최적화기 선택 가이드 — docstring이 어떻게 다시 쓰이고, 필드 이름은 왜 고정으로 남는지.