어댑터 (Adapters)

어댑터: 시그니처가 프롬프트가 되는 법 (Adapters)

이 문서가 설명하는 것

어댑터(Adapter)는 Signature와 LM 사이의 계층이에요. 시그니처의 지시사항, 필드, 데모를 채팅 메시지로 포맷하고, 호출을 보내고, LM의 응답을 다시 타입이 붙은 Python 값으로 파싱합니다. 어댑터마다 사용하는 프롬프트 모양이 달라요 — 채팅 마커, JSON, XML, 아니면 2단계 추출(two-stage extract)이죠. 그래서 같은 시그니처를 포맷 강점이 크게 다른 모델들에 대해 실행할 수 있습니다.

요청이 실제로 어떻게 보이는지, 왜 타입 필드가 한 방식으로 파싱되고 다른 방식은 아닌지, [[ ## field_name ## ]] 마커가 어디서 오는지, 혹은 ChatAdapter가 이상하게 동작할 때 어떤 어댑터로 바꿔야 하는지 알고 싶을 때 읽어 보세요.

설계 결정

1. 어댑터는 플러그인식(pluggable)이다

같은 시그니처, 다른 프롬프트 모양, 시그니처 변경 없음. LM마다 읽고 만들어내길 선호하는 게 크게 달라요. 마커가 있는 지시사항 따르기에 훈련된 모델은 ChatAdapter[[ ## field ## ]] 포맷을 좋아하고, 네이티브 구조화 출력 모드를 가진 모델은 JSONAdapter가 최고이며, 포맷을 불안정하게 하는 reasoning 모델은 TwoStepAdapter를 원합니다. 이 결정을 하나의 인터페이스 뒤에 두면 시그니처·모듈·옵티마이저가 어떤 LM 계열을 상대하는지 알 필요가 없어요.

2. ChatAdapter가 기본값이다

텍스트 전용, 모델에 구애받지 않으며, [[ ## field ## ]] 마커를 씁니다. JSON 모드, 함수 호출, 네이티브 구조화 출력 같은 특별한 LM 기능이 필요 없어요. 그 폭이기 때문에 기본값입니다. 안전망도 포함돼 있는데, 정규식 파서가 실패하면 자동으로 JSONAdapter로 폴백합니다(use_json_adapter_fallback=False로 토글 가능).

3. 모든 어댑터는 고정된 생명주기를 따른다

preprocess → format → LM call → postprocess → parse. 이 다섯 단계를 따라가면 어떤 어댑터든 디버그할 수 있어요. Preprocess는 네이티브 LM 기능(함수 호출, 추론)을 위해 시그니처를 조정하고, Format은 메시지를 만들며, LM 호출은 원시 출력을 돌려주고, Postprocess는 도구 호출과 네이티브 타입 응답(Reasoning, Citations)을 꺼내며, Parse는 각 선언된 출력 필드에 parse_value를 돌립니다.

4. 타입 변환은 중앙화된다

dspy/adapters/utils.py::parse_value는 모든 어댑터가 위임하는 유일한 함수입니다. 어댑터들은 LM 출력에서 필드 값을 찾는 방식(정규식 마커, JSON 키, XML 태그)만 다르고, 일단 원시 문자열을 얻으면 모두 같은 parse_value(value, annotation)을 호출해요. 이렇게 하면 변환 규칙이 어댑터 교체에도 일관되게 유지되고, 타입 필드가 이상하게 동작할 때 살펴볼 곳이 하나로 집중됩니다.

5. 커스텀 타입은 자기 직렬화를 소유한다

Image, Audio, Code, Reasoning, Tool, ToolCalls, 그리고 dspy.Type의 모든 사용자 서브클래스는 자기만의 format()과(선택적으로) parse_lm_response()를 끼웁니다. 어댑터는 이미지나 오디오를 특별 취급하지 않아요. 필드의 어노테이션이 dspy.Type 서브클래스면, 어댑터는 type.format()을 호출해 프로바이더의 content-block 포맷으로 렌더링하고, type.parse_lm_response()로 다시 읽습니다. 새 모달리티를 추가한다는 건 새 서브클래스를 쓴다는 뜻이에요 — 어댑터 변경은 없습니다.

6. 네이티브 LM 기능은 타입을 통해 드러난다

함수 호출, 구조화 출력, 추론은 타입의 adapt_to_native_lm_feature()parse_lm_response() 훅을 타고 흐릅니다. 예를 들어 dspy.Reasoning 출력 필드는 어댑터에게 "lm_kwargsreasoning_effort를 설정하고, 정규식으로 파싱하는 대신 response['reasoning_content']에서 추론을 끌어내"라고 말해요. 모델별 기능 통합은 어댑터를 넘어 재사용 가능한 타입 계층에 살아 있습니다.

7. 파싱 에러 시 ChatAdapter가 JSONAdapter로 폴백한다

토글 가능하고 기본은 켜짐입니다. LM이 처음으로 형식이 잘못된 [[ ## ## ]] 출력을 만들면, ChatAdapter는 파싱 에러를 잡고 요청을 JSONAdapter로 다시 실행합니다. 이것은 새 코드에겐 정당한 동작보다 관대해요 — 에러를 보고 싶을 테니까요 — 하지만 기본값인 이유는 ChatAdapter를 더 넓은 모델 범위에 대해 즉시 쓸 수 있게 만들기 때문입니다. 테스트에서는 use_json_adapter_fallback=False를 설정하세요.

8. JSONAdapter는 구조화 출력 모드를 선호한다

lm.supported_params를 확인하고 단계를 내려갑니다. OpenAI 스타일 response_format: json_schema를 먼저, 그다음 json_object 모드, 그다음 일반 텍스트 JSON. 첫 번째 단계가 가장 신뢰할 수 있는데, 모델이 지시받기만 하는 게 아니라 디코딩 시점에 제약되기 때문이에요.

9. TwoStepAdapter는 생성을 추출과 분리한다

자유 형식 텍스트를 만들지만 포맷을 불안정하게 하는 reasoning 모델용입니다. 1단계: 메인 LM이 필드 마커 없는 평범한 산문으로 작업을 받고 원하는 모양을 만듭니다. 2단계: 더 작은 추출기(extractor) LM이 ChatAdapter로 출력을 읽고 선언된 필드를 끌어냅니다. o1, o3-mini처럼 포맷 신뢰성이 병목인 모델에 유용해요.

10. 필드 마커 포맷은 하드코딩된다

[[ ## name ## ]]가 패턴이며, 낮은 충돌과 깔끔한 정규식을 위해 선택됐어요. 대괄호-해시 모양은 실제 텍스트나 코드에 나타날 가능성이 낮고, 대칭 구조가 파서를 단순하게 만듭니다. 이걸 바꿀 설정 노브는 없습니다. JSONAdapter와 XMLAdapter는 자기만의 포맷을 쓰고, 다른 채팅 스타일 포맷을 원하면 Adapter를 서브클래스하세요.

11. 파인튜닝 데이터 내보내기는 어댑터별로 다르다

format_finetune_data는 ChatAdapter(OpenAI 메시지 포맷)에 구현돼 있고, JSONAdapter는 NotImplementedError를 던지며, TwoStepAdapter도 지원하지 않아요. BootstrapFinetune을 쓴다면 ChatAdapter를 유지하거나 선택한 어댑터에 format_finetune_data를 구현하세요.

API 둘러보기

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

어댑터들

dspy.ChatAdapter(callbacks=None, use_native_function_calling=False, native_response_types=None, use_json_adapter_fallback=True)
기본값입니다. 필드 마커로 채팅 스타일 프롬프트를 만들고, 같은 마커에 대한 정규식으로 응답을 파싱하며, (기본적으로) 정규식이 놓치면 JSONAdapter로 폴백합니다. 테스트에서 하드 에러를 원하면 use_json_adapter_fallback=False를 설정하세요.

dspy.JSONAdapter(callbacks=None, use_native_function_calling=True)
구조화된 JSON을 출력합니다. 내부적으로 ChatAdapter를 확장합니다 — 포맷은 비슷하지만 출력 지시가 JSON을 요구하고 파싱은 json_repair를 씁니다. 생성자의 use_native_function_calling=True 기본값은 도구 호출이 연결되면 바뀝니다.

dspy.XMLAdapter(callbacks=None)
<field_name>value</field_name> 태그. 리스트는 반복되는 <item> 태그를 씁니다.

dspy.TwoStepAdapter(extraction_model: BaseLM, **kwargs)
추론당 LM 호출 두 번. 메인 LM이 포맷에 약한 reasoning 모델일 때 쓰세요 — 추출기는 보통 ChatAdapter를 쓰는 값싼 범용 LM입니다. 아직 파인튜닝을 지원하지 않아요.

dspy.BAMLAdapter
JSON 기반이지만, 출력 스키마를 BAML 스타일로 주석 처리된 Pydantic 형태로 렌더링합니다. JSONAdapter의 원시 JSON 스키마가 복잡한 중첩 타입에 너무 장황할 때 시도해 볼 만해요.

dspy.Adapter
기본 클래스. 새 프롬프트 모양을 원할 때 서브클래스하세요 — format(), parse(), 그리고 (선택적으로) format_finetune_data()를 구현하면 됩니다.

어댑터 생명주기

커스텀 어댑터를 쓸 때만 이 메서드들을 오버라이드하겠지만, 형식이 안 좋은 프롬프트나 파싱 실패를 디버그할 때 읽으면 도움이 됩니다.

Adapter.__call__(lm, lm_kwargs, signature, demos, inputs) / Adapter.acall(...)
공개 진입점입니다. 안쪽 흐름은 preprocess → format → LM call → postprocess입니다. lm_kwargs(temperature, max_tokens, response_format 등)는 preprocess 중 어댑터가 구조화 출력이나 함수 호출을 요청할 때 손대는 곳이에요.

Adapter.format(signature, demos, inputs)list[dict]
시그니처, 데모, 입력을 채팅 메시지로 바꿉니다. 조합하는 조각들:

  • format_system_message(signature) — 시스템 메시지: 필드 설명 + 형식 템플릿 + 지시사항.
  • format_field_description(signature) — 타입과 제약이 있는 필드별 목록.
  • format_field_structure(signature) — 마커 포맷의 설명.
  • format_task_description(signature)signature.instructions.
  • format_demos(signature, demos) — 각 데모가 user/assistant 쌍이 됩니다.
  • format_user_message_content(signature, inputs) — 현재 호출의 입력.
  • format_assistant_message_content(signature, outputs) — 데모 안에서 사용.
  • format_conversation_history(signature, history) — 필드 타입이 dspy.History이면, 한 필드의 값에 우겨 넣는 대신 턴 메시지로 확장.

Adapter.parse(signature, completion)dict
LM 응답에서 타입이 붙은 필드 값을 추출합니다. ChatAdapter는 마커 패턴을 정규식으로 매치하고 필드별로 나눠 각 값을 parse_value에 위임합니다. JSONAdapter는 JSON 객체를 파싱하고 키로 값을 끌어내며, XMLAdapter는 태그를 걸어 답니다.

Adapter.format_finetune_data(signature, demos, inputs, outputs)
데모를 LM 프로바이더의 파인튜닝 포맷으로 직렬화합니다. ChatAdapter는 OpenAI 메시지 포맷을 씁니다. 다른 어댑터는 NotImplementedError를 던져요.

타입 변환

모든 어댑터가 LM이 만든 문자열을 타입 값으로 바꿀 때 호출하는 단일 함수.

dspy/adapters/utils.pyparse_value(value, annotation)
전략: 어노테이션이 str이면 그대로 통과. Enum이나 Literal이면 허용 값과 매치. 그 외에는 json_repair.loads를 시도하고, ast.literal_eval로 폴백하고, 원시 문자열로 폴백한 뒤 TypeAdapter(annotation)으로 검증. 검증이 실패하고 어노테이션이 dspy.Type 서브클래스면 원시 값으로 재시도해서 타입 자체의 파서가 한 번 해보게 합니다.

같은 파일에서 트레이스백에서 볼 수 있는 다른 헬퍼들:

  • format_field_value(field_info, value, assume_text=True) — parse의 역함수: 프롬프트를 위해 타입 값을 직렬화.
  • serialize_for_json(value) — Pydantic 인지 JSON 직렬화. JSONAdapter가 사용.
  • translate_field_type(field_info) — 프롬프트가 보여주는 제약 문자열("greater than: 0")을 생성.
  • get_field_description_string(fields) — 필드 목록 렌더링.
  • find_enum_member(enum_cls, raw) — 이름이나 값으로 enum 해석.

커스텀 타입 래퍼

Python 표준 이상으로 어댑터가 렌더링·파싱할 줄 아는 타입들. 각각 format()을 구현하고, 일부는 네이티브 LM 훅을 위해 parse_lm_response()adapt_to_native_lm_feature()를 구현합니다.

dspy.adapters.types.Type
기본 클래스. 서브클래스(이건 pydantic.BaseModel)를 만들고 format()을 구현해 새 타입을 끼우세요. 어댑터는 format()의 출력을 <<CUSTOM-TYPE-START-IDENTIFIER>>...<<END-IDENTIFIER>>로 감싸서, 멀티모달 콘텐츠가 단일 메시지 스트림에 삽입되고 나중에 분리될 수 있게 합니다.

dspy.Image(source) URL 참조, data URI, 바이트, 또는 PIL 이미지. format()은 프로바이더의 이미지 content block({"type": "image_url", "image_url": {"url": ...}})을 돌려줍니다. 일반적인 생성과 어댑터 파싱은 파일시스템이나 네트워크에 절대 접근하지 않아요. 로컬 파일을 읽으려면 Image.from_path(path), 원격 리소스를 다운로드해 base64 인코딩하려면 Image.from_url(url)을 쓰세요. 지원 중단된 직접 호출 Image(url, download=True)도 3.3까지는 호환을 위해 다운로드하며, Image.from_url(url)로 마이그레이션하세요.

dspy.Audio(source) data URI, 인메모리 바이트, 또는 배열 데이터. 원시 base64는 Audio(data=..., audio_format=...)로 넘겨야 합니다. 프로바이더의 오디오 content block으로 렌더링돼요. 리소스 로딩은 Audio.from_path(path)Audio.from_url(url)을 쓰세요.

dspy.File(file_data=None, file_id=None, filename=None) 인메모리 바이트, data URI, 또는 파일 ID(일부 프로바이더는 파일을 미리 업로드하고 ID로 참조). 로컬 파일을 읽으려면 File.from_path(path)를 쓰세요.

dspy.Code(code, language="python") 클래스 레벨 language 파라미터가 있는 코드. dspy.Code["java"]는 Java용으로 타입된 Code 서브클래스를 만듭니다. format()은 원시 문자열을 돌려줍니다 — 래퍼도 펜싱도 없어요.

dspy.History(messages) 대화 턴. 어댑터가 필드에서 이 타입을 보면, 한 필드의 값에 우겨 넣는 대신 메시지를 실제 user/assistant 메시지로 확장합니다. LM이 이전 턴을 메시지로 보길 원할 때 쓰세요.

dspy.Reasoning(content) 문자열 같은 래퍼. LM이 reasoning 모드(o1, o3-mini, GPT-5 thinking 변형)를 지원하면 Reasoning.adapt_to_native_lm_feature()lm_kwargsreasoning_effort를 설정하고 시그니처에서 필드를 제거하며, parse_lm_response()는 네이티브 응답 필드에서 추론을 끌어냅니다. LM이 네이티브 추론을 지원하지 않으면 일반 텍스트 필드로 폴백하고 ChainOfThought가 추가하는 reasoning 필드처럼 동작해요.

dspy.Tool(func, name=None, desc=None, args=None, arg_types=None, arg_desc=None) Python 콜러블을 감쌉니다. args/arg_types/arg_desc를 넘기지 않으면 함수 시그니처를 자동으로 인트로스펙트합니다. ReAct와 모듈이 도구를 받는 모든 곳에서 사용됩니다. 전체 도구 이야기는 Tools / ReAct / MCP DD 페이지에 있어요.

dspy.adapters.types.tool.ToolCalls.from_dict_list(...) LM이 만든 도구 호출 목록을 네이티브 함수 호출 응답에서 파싱한 것.

dspy.adapters.types.Citations 기본 네이티브 응답 타입으로 선언됐습니다. 프로바이더가 인용을 네이티브로 돌려주면(예: Anthropic), 어댑터는 타입의 parse_lm_response로 그것을 추출합니다.

3.3의 리소스 로딩 마이그레이션

DSPy 3.3에서는 Image, Audio, File 값을 생성·검증할 때 locator 모양의 문자열을 로컬 파일을 읽거나 원격 URL을 가져오라는 지시로 해석하지 않습니다. 이렇게 하면 LM 출력 파싱과 다른 검증 경로가 호스트 접근을 암묵적으로 허용하지 않습니다. 리소스 로딩은 이제 명시적 팩토리가 필요해요.

3.3 이전 3.3 교체 동작
Image(path) Image.from_path(path) 로컬 이미지 읽고 포함
Image(url, download=True) Image.from_url(url) 원격 이미지 다운로드·포함
Image.from_url(url) 또는 Image.from_url(url, download=False) Image(url) 다운로드 안 하는 URL 참조 유지
Audio(path) Audio.from_path(path) 로컬 오디오 파일 읽고 포함
Audio(url) Audio.from_url(url) 원격 오디오 다운로드·포함
File(path) File.from_path(path) 로컬 파일 읽고 포함
Image.from_file(path) Image.from_path(path) 지원 중단 별칭 교체
Audio.from_file(path) Audio.from_path(path) 지원 중단 별칭 교체

data URI, 바이트, PIL 이미지, 오디오 배열, 구조화 딕셔너리 같은 안전한 인메모리 입력은 계속 지원됩니다. 지원 중단된 직접 호출 Image(url, download=True)는 경고와 함께 3.3까지 동작하고, Image.from_file(), Image.from_PIL(), Audio.from_file()도 3.4에서 제거 예정입니다.

Image.from_url()Audio.from_url()은 호출자가 시작한 동기 HTTP 요청을 수행하고 리디렉션을 따릅니다. SSRF 허용 목록으로 대상을 검증하지 않으므로, 애플리케이션은 신뢰할 수 없는 입력에서 파생된 URL을 호출하기 전에 검증하거나 허용 목록에 넣어야 합니다.

어떤 어댑터를 쓸지 설정하기

  • dspy.configure(adapter=dspy.JSONAdapter()) — 프로세스 전체 기본값.
  • with dspy.context(adapter=dspy.XMLAdapter()): ... — 범위 제한 오버라이드.
  • LM 기반 자동 선택은 없어요. ChatAdapter가 기본값이고 다른 걸 설정하기 전까지 기본값으로 남습니다. 일부 텔레프롬프트(예: BootstrapFinetune)는 LM을 키로 한 adapter 딕셔너리를 받아서, 파인튜닝 루프의 다른 LM이 다른 어댑터를 쓸 수 있어요.

관련 문서

  • 시그니처 심화 — 어댑터가 소비하는 것.
  • 설정과 context()configurecontext가 어댑터 선택을 어떻게 전파하는지.
  • 도구와 MCPToolToolCalls는 어댑터가 포맷하지만 모듈이 구동합니다.
  • ReAct와 ReActV2 — 두 에이전트 루프가 어댑터에 history와 도구 호출을 어떻게 제시하는지.