시그니처 (Signatures)

시그니처 (Signatures)

DSPy에서 LM에 작업을 맡길 때, 우리가 원하는 동작을 **시그니처(Signature)**로 명시해요. 시그니처는 DSPy 모듈의 입출력 동작을 선언적으로 정의한 명세입니다. 쉽게 말해, LM에게 어떻게 물어볼지는 정하지 않고 무엇을 해야 하는지만 알려주는 거예요.

함수 시그니처는 입력·출력 인자와 그 타입을 정한다는 점에서 익숙하실 거예요. DSPy 시그니처도 비슷하지만 두 가지가 달라요. 보통 함수 시그니처는 그저 설명하는 데 그치지만, DSPy 시그니처는 모듈의 동작을 선언하면서 초기화해요. 그리고 필드 이름이 매우 중요합니다. questionanswer는 다르고, sql_querypython_code도 다른 의미예요. 이렇게 의미적 역할을 평범한 영어 단어로 표현하는 것이랍니다.

왜 DSPy 시그니처를 써야 할까요?

모듈화되고 깔끔한 코드를 위해서예요. 그러면 LM 호출을 고품질 프롬프트로(또는 자동 파인튜닝으로) 최적화할 수 있거든요. 대부분 사람들은 길고 깨지기 쉬운 프롬프트를 손으로 얽어매거나, 파인튜닝용 데이터를 모아서 LM을 억지로 시키죠. 하지만 시그니처를 쓰면 프롬프트를 손보거나 파인튜닝하는 것보다 훨씬 모듈화되고, 적응성이 좋고, 재현 가능합니다. DSPy 컴파일러가 우리 데이터와 파이프라인에 맞춰, 우리 시그니처를 위한 고도로 최적화된 프롬프트(또는 작은 LM의 파인튜닝)를 알아서 만들어 줘요. 실제로 컴파일한 결과가 사람이 직접 쓴 프롬프트보다 나은 경우가 많았어요. DSPy 옵티마이저가 사람보다 창의적이어서가 아니라, 더 많은 것들을 시도해 보고 메트릭을 직접 튜닝할 수 있기 때문입니다.

인라인(Inline) 시그니처

시그니처는 짧은 문자열로 정의할 수 있어요. 인자 이름과 타입이 입출력의 의미적 역할을 정합니다.

  1. 질의응답: "question -> answer" — 기본 타입은 항상 str이라 "question: str -> answer: str"과 같아요.
  2. 감성 분류: "sentence -> sentiment: bool" — 예를 들어 긍정이면 True
  3. 요약: "document -> summary"

입출력 필드가 여러 개이고 타입이 붙어도 됩니다.

  1. 검색 증강 질의응답: "context: list[str], question: str -> answer: str"
  2. 추론이 있는 객관식 질의응답: "question, choices: list[str] -> reasoning: str, selection: int"

팁: 필드 이름은 유효한 변수명이면 아무거나 써도 돼요. 의미적으로 의미 있는 이름을 쓰되, 처음엔 단순하게 시작하고 키워드를 너무 일찍 최적화하지 마세요. 그런 손질은 DSPy 컴파일러의 몫이에요. 예를 들어 요약이라면 "document -> summary""text -> gist""long_context -> tldr"든 다 괜찮아요.

인라인 시그니처에 지시사항(instructions)을 추가할 수도 있어요. 이것은 런타임에 변수를 쓸 수 있습니다. instructions 키워드 인자로 시그니처에 지시사항을 붙여보죠.

toxicity = dspy.Predict(
    dspy.Signature(
        "comment -> toxic: bool",
        instructions="Mark as 'toxic' if the comment includes insults, harassment, or sarcastic derogatory remarks.",
    )
)
comment = "you are beautiful."
toxicity(comment=comment).toxic

출력:

False

예시 A: 감성 분류

sentence = "it's a charming and often affecting journey."  # example from the SST-2 dataset.

classify = dspy.Predict('sentence -> sentiment: bool')  # we'll see an example with Literal[] later
classify(sentence=sentence).sentiment

출력:

True

예시 B: 요약

# Example from the XSum dataset.
document = """The 21-year-old made seven appearances for the Hammers and netted his only goal for them in a Europa League qualification round match against Andorran side FC Lustrains last season. Lee had two loan spells in League One last term, with Blackpool and then Colchester United. He scored twice for the U's but was unable to save them from relegation. The length of Lee's contract with the promoted Tykes has not been revealed. Find all the latest football transfers on our dedicated page."""

summarize = dspy.ChainOfThought('document -> summary')
response = summarize(document=document)

print(response.summary)

가능한 출력:

The 21-year-old Lee made seven appearances and scored one goal for West Ham last season. He had loan spells in League One with Blackpool and Colchester United, scoring twice for the latter. He has now signed a contract with Barnsley, but the length of the contract has not been revealed.

dspy.Predict를 제외한 대부분의 DSPy 모듈은 시그니처를 내부적으로 확장하면서 부가 정보를 돌려줘요. 예를 들어 dspy.ChainOfThought는 출력 summary를 만들기 전에 LM이 생각한 reasoning 필드를 하나 더 추가합니다.

print("Reasoning:", response.reasoning)

가능한 출력:

Reasoning: We need to highlight Lee's performance for West Ham, his loan spells in League One, and his new contract with Barnsley. We also need to mention that his contract length has not been disclosed.

클래스 기반(Class-based) 시그니처

고급 작업에서는 더 장황한 시그니처가 필요할 때가 있어요. 주로 이럴 때 씁니다.

  1. 작업의 성격을 명확히 하기 (docstring으로 표현)
  2. 입력 필드의 성격에 힌트 주기 — dspy.InputFielddesc 키워드 인자
  3. 출력 필드에 제약 걸기 — dspy.OutputFielddesc 키워드 인자

예시 C: 분류

from typing import Literal

class Emotion(dspy.Signature):
    """Classify emotion."""
    
    sentence: str = dspy.InputField()
    sentiment: Literal['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] = dspy.OutputField()

sentence = "i started feeling a little vulnerable when the giant spotlight started blinding me"  # from dair-ai/emotion

classify = dspy.Predict(Emotion)
classify(sentence=sentence)

가능한 출력:

Prediction(
    sentiment='fear'
)

팁: LM에 요청 사항을 더 명확히 지정하는 건 나쁠 게 없어요. 클래스 기반 시그니처가 그걸 도와줍니다. 다만 시그니처의 키워드를 손으로 너무 일찍 튜닝하지는 마세요. DSPy 옵티마이저가 더 잘 할 가능성이 높고, LM 사이에서도 더 잘 전이됩니다.

예시 D: 인용 충실도(faithfulness)를 평가하는 메트릭

class CheckCitationFaithfulness(dspy.Signature):
    """Verify that the text is based on the provided context."""

    context: str = dspy.InputField(desc="facts here are assumed to be true")
    text: str = dspy.InputField()
    faithfulness: bool = dspy.OutputField()
    evidence: dict[str, list[str]] = dspy.OutputField(desc="Supporting evidence for claims")

context = "The 21-year-old made seven appearances for the Hammers and netted his only goal for them in a Europa League qualification round match against Andorran side FC Lustrains last season. Lee had two loan spells in League One last term, with Blackpool and then Colchester United. He scored twice for the U's but was unable to save them from relegation. The length of Lee's contract with the promoted Tykes has not been revealed. Find all the latest football transfers on our dedicated page."

text = "Lee scored 3 goals for Colchester United."

faithfulness = dspy.ChainOfThought(CheckCitationFaithfulness)
faithfulness(context=context, text=text)

가능한 출력:

Prediction(
    reasoning="Let's check the claims against the context. The text states Lee scored 3 goals for Colchester United, but the context clearly states 'He scored twice for the U's'. This is a direct contradiction.",
    faithfulness=False,
    evidence={'goal_count': ["scored twice for the U's"]}
)

예시 E: 멀티모달 이미지 분류

class DogPictureSignature(dspy.Signature):
    """Output the dog breed of the dog in the image."""
    image_1: dspy.Image = dspy.InputField(desc="An image of a dog")
    answer: str = dspy.OutputField(desc="The dog breed of the dog in the image")

image_url = "https://picsum.photos/id/237/200/300"
classify = dspy.Predict(DogPictureSignature)
classify(image_1=dspy.Image(image_url))

가능한 출력:

Prediction(
    answer='Labrador Retriever'
)

시그니처에서의 타입 해석(Type Resolution)

DSPy 시그니처는 다양한 어노테이션 타입을 지원해요.

  1. 기본 타입str, int, bool
  2. Typing 모듈 타입list[str], dict[str, int], Optional[float]. Union[str, int]
  3. 코드에서 정의한 커스텀 타입
  4. 중첩 타입을 위한 점 표기법(MyContainer.Query 등)과 적절한 설정
  5. 특수 데이터 타입dspy.Image, dspy.History

커스텀 타입 다루기

# Simple custom type
class QueryResult(pydantic.BaseModel):
    text: str
    score: float

signature = dspy.Signature("query: str -> result: QueryResult")

class MyContainer:
    class Query(pydantic.BaseModel):
        text: str
    class Score(pydantic.BaseModel):
        score: float

signature = dspy.Signature("query: MyContainer.Query -> score: MyContainer.Score")

입력 필드 타입 검사

DSPy는 입력 필드에 넘긴 값이 시그니처에 명시된 타입과 일치하는지 자동으로 검증해요. 인라인과 클래스 기반 모두에 적용됩니다. 타입이 안 맞으면 DSPy가 경고를 로그로 남겨요.

예시: 타입 불일치 경고

# Define a signature expecting an integer as input
class MathSignature(dspy.Signature):
    """Perform a mathematical operation."""
    number: int = dspy.InputField()
    result: str = dspy.OutputField()

predictor = dspy.Predict(MathSignature)

# This will trigger a warning because we're passing a string instead of an int
predictor(number="42")  # Warning: Type mismatch for field 'number': expected int, but provided value is incompatible

타입 검사 끄기 — 타입 불일치 경고를 끄고 싶다면 이 기능을 비활성화하면 됩니다.

# Disable type mismatch warnings globally
dspy.configure(warn_on_type_mismatch=False)

predictor = dspy.Predict("number: int -> result: str")
predictor(number="42")  # No warning

선택적(Optional) 출력 필드

출력 필드는 기본적으로 필수예요. LM 응답에 선언된 출력 필드가 없으면 어댑터가 AdapterParseError를 던집니다. 출력 필드가 기본값(default value), default_factory, 또는 None을 허용하는 어노테이션을 가지면 선택적이 됩니다. LM이 선택 필드를 빠뜨리면, 파싱된 예측값은 기본값 → 팩토리 결과 → None 순서로 폴백합니다.

class Extract(dspy.Signature):
    """Extract structured information from text."""
    text: str = dspy.InputField()
    title: str = dspy.OutputField()                                # required: missing -> AdapterParseError
    note: str | None = dspy.OutputField(default="No note")         # missing -> "No note"
    tags: list[str] = dspy.OutputField(default_factory=list)      # missing -> []
    subtitle: str | None = dspy.OutputField()                      # missing -> None

입력 필드도 비슷해요. 기본값이 있는 입력이 빠지면 프롬프트에 기본값이 채워지고, None을 허용하는 어노테이션이면 프롬프트에서 생략되며, 필수 입력이 빠지면 경고가 로그로 남습니다.

시그니처로 모듈을 만들고 컴파일하기

시그니처는 구조화된 입출력으로 프로토타이핑할 때 편리하지만, 그게 전부는 아니에요. 여러 시그니처를 더 큰 DSPy 모듈로 조합하고, 이 모듈들을 최적화된 프롬프트와 파인튜닝으로 컴파일해야 합니다.