Flex: 최적화 가능한 모듈 코드
Flex: 최적화 가능한 모듈 코드 (Flex: Optimizable module code)
dspy.Flex 는 솔루션의 올바른 형태(shape) 를 미리 모를 때를 위한 것입니다. 다른 모든 모듈은 구성 시점에 구조를 고정합니다 — Predict 는 한 번 호출, ChainOfThought 는 추론이 있는 한 번 호출, ReAct 는 도구 루프 — 그리고 최적화는 그 고정된 구조 주변의 프롬프트만 튜닝해요. Flex 는 구조 자체를 탐색 공간으로 옮깁니다. 시그니처를 주면 dspy.Predict 나 dspy.RLM 기준선으로 시작하고, dspy.GEPA 가 그 전체 구현 — predictor가 몇 개인지, 어떤 기본 요소를 쓰는지, 무엇이 LM 대신 Python에서 도는지 — 을 당신의 메트릭에 맞춰 다시 씁니다. 가장 좋은 분해를 발견하고 최적화하는 쪽을 선호할 때 집어 들으세요.
출처: 문서
본문
설계 결정
1. Flex는 소스 코드가 최적화 가능한 파라미터인 모듈이다
보통 모듈의 튜닝 가능한 표면은 predictor들의 지시문입니다. Flex 의 튜닝 가능한 표면은 dspy.Module 서브클래스 전체인데, 소스 문자열로 보관되고 module_src 로 노출됩니다. 그 클래스는 평소의 두 메서드를 가져요. __init__ 은 필요한 predictor를 구성하고, forward 는 그것들을 호출해 dspy.Prediction 을 반환합니다. 모듈을 최적화한다는 것은 그 소스를 더 나은 버전으로 교체한다는 뜻입니다 — 다른 predictor, 다른 제어 흐름, 더 많거나 적은 Python요. 최적화의 단위는 더 이상 프롬프트가 아니라 프로그램 코드입니다.
2. 그것은 어떤 시그니처에도 끼어들고 단순한 기준선에서 시작한다
어떤 dspy.Signature 로든 Flex 를 구성하면 즉시 실행 가능해요. 도구가 없으면 기준선 소스는 시그니처 전체에 대한 단일 dspy.Predict 이고, 도구가 있으면 기준선이 그것들을 호출할 수 있도록 dspy.RLM 입니다. 기준선은 탐색의 출발점으로 작동하는 가장 단순한 것입니다.
3. GEPA는 Flex 를 타입으로 발견하고 텍스트 대신 코드를 최적화한다
GEPA가 프로그램을 컴파일할 때 Flex 하위 모듈들을 열거해 작업을 나눕니다. 각 Flex 는 코드 구성요소(code component) 가 되고, 그 밖의 모든 predictor는 지시문 구성요소(instruction component) 로 남습니다. 코드 구성요소는 현재 module_src 로 시드되어 전용 코드 제안자(code proposer) 가 진화시키고, 지시문 구성요소는 현재 지시문으로 시드되어 GEPA의 평소 지시문 제안자가 진화시킵니다. 각 쪽에는 자체 오버라이드가 있어요. 커스텀 instruction_proposer 는 지시문 제안자를, 커스텀 code_proposer 는 코드 제안자를 교체하며, 미설정으로 둔 쪽은 기본값을 유지합니다.
4. 코드 제안자는 프로그램 전체 동작에 대해 반성한다
GEPA의 지시문 최적화는 predictor별입니다. 한 predictor의 입력, 출력, 피드백을 봅니다. 코드 최적화는 그렇게 할 수 없어요. 왜냐하면 predictor들이 바로 다시 쓰여지는 것의 일부이기 때문입니다 — 다음 후보에는 존재하지 않을 수 있어요. 그래서 코드 구성요소는 대신 프로그램 전체의 I/O를 반성합니다 — 모듈의 입력, 최종 예측, 예시별 메트릭 피드백요. 제안자는 시그니처, 가능한 도구, 허용된 기본 요소 목록, 현재 소스, 실패 예시 배치를 받고 완전히 수정된 모듈 클래스를 반환하도록 요청받습니다.
5. Flex 내부의 predictor는 그 코드가 소유하며 병렬로 튜닝되지 않는다
한 프로그램에서 Flex 를 보통 모듈과 섞으면, GEPA는 Flex 의 코드와 다른 predictor들의 지시문을 최적화합니다 — 하지만 Flex 안에 사는 predictor들의 지시문은 절대 최적화하지 않아요. 그 predictor들은 현재 module_src 가 구성하며 다음 코드 후보에 의해 통째로 교체될 것이므로, 그 지시문을 튜닝하는 것은 덮어써질 수 있는 것을 최적화하는 일입니다.
Flex 는 (dspy.Predict 처럼) Parameter 와 Module 의 서브클래스라, 부모 프로그램의 named_parameters() 는 그것을 하나의 잎으로 산출하고 그 안으로 재귀하지 않습니다. 부모의 named_predictors(), predictors(), set_lm(), 그리고 BootstrapFewShot 같은 데모/지시문 최적화기들은 Flex 의 내부를 보지 못해요. Flex 는 자신에 대해 같은 것을 보고합니다. flex.named_predictors() 는 비어 있습니다 — 그 갱신 단위는 코드이지, 코드가 구성하는 predictor들이 아니에요. Flex 는 자기 LM을 지닐 수 있고(flex.set_lm(...)), forward 는 그것을 호출 전체의 주변 LM으로 적용합니다. 그렇지 않으면 브리지된 predictor 호출이 호출 시점의 주변 LM을 해석합니다(dspy.configure(lm=...) 또는 호출자의 dspy.context(lm=...)). reset_copy() 는 Predict 를 초기화하는 방식으로 Flex 를 초기화합니다. LM은 지워지고 module_src 는 유지되는데, 튜닝된 지시문과 마찬가지예요.
6. 깨진 후보는 충돌이 아니라 실패로 점수 매겨진다
반성 모델이 코드를 저술하고, 코드는 틀릴 수 있어요. 파싱되지 않는 후보는 GEPA가 평가용 프로그램을 만들기 위해 결합할 때 예외를 던지고, Flex 최적화는 그것을 잡아 기록하고 전체 배치를 실패 점수로 매깁니다. 탐색은 계속되고 깨진 후보는 단순히 선택되지 않아요. 파싱되지만 실행될 때 깨지는 코드 — 해석되지 않는 임포트, 어떤 입력에서 예외를 던지는 엣지 케이스 — 는 각각 forward 에서 예시별로 실패합니다. 각 크래시된 예시는 자기 슬롯에서(예시 인덱스로) 실패 점수로 매겨져, 살아남은 점수가 배치와 정렬을 유지하고 GEPA의 인스턴스별 장부가 온전하게 남습니다. 어느 쪽이든, 최적화는 구성상 반성 모델의 실수에 견고합니다.
7. 생성된 코드는 항상 인터프리터를 통해 실행된다
Flex 는 interpreter_factory 에서, 아니면 dspy.settings.interpreter_factory 에서, 아니면 dspy.PythonInterpreter(Deno/Pyodide)에서 인터프리터를 가져옵니다. 그것들은 각각 무인자(0-argument) 팩토리여야 하며, 맨 인스턴스나 None 은 거부됩니다. 기본값으로는 제공된 도구 호출, predictor 구성, predictor 호출이 호스트로 다시 브리지되는 것을 제외하면 모든 것이 샌드박스에 머뭅니다. 커스텀 팩토리는 자기 신뢰 경계를 정의합니다. LocalInterpreter 는 프로세스 메모리와 stdout을 분리하지만 호스트 사용자의 파일시스템·환경·자격증명·네트워크·프로세스 권한을 유지합니다. 팩토리는 각 세션에 새 인터프리터를 만들고, forward는 바깥 세션을 소유하며 중첩된 코드 실행 모듈은 별도 세션을 요청할 수 있어요. 커스텀 인터프리터 간 소스 이식성은 보장되지 않습니다. max_predictor_calls 는 생성된 코드가 하나의 forward 에서 만들 수 있는 predictor 호출 수를 제한합니다.
8. 선언된 출력 타입은 샌드박스 경계에서 강제된다
생성된 코드가 반환하는 모든 것은 JSON으로 건너가므로, int, list[str], 또는 pydantic 모델로 선언된 필드는 맨 문자열이나 dict로 도착할 수 있어요. Flex 는 나가는 길에 각 선언된 출력 필드를 그 주석에 대해 파싱하므로, Flex 는 같은 시그니처의 dspy.Predict 와 같은 타입을 반환하고 그것을 대체 가능한 것으로 유지됩니다. 두 가지가 뒤따릅니다. 타입 이름은 양방향 왕복을 견뎌야 합니다. 기준선은 커스텀 타입을 시그니처 문자열로 렌더링하고(text: str -> person: Person) 호스트는 Flex 자신의 시그니처에서 그 이름들을 해석합니다. 시그니처는 경계를 텍스트로 건너고 dspy의 평소 호출자-프레임 타입 조회는 dspy 안쪽에서 실행되는데, 거기서는 당신의 모듈이 범위 밖이기 때문이에요. 그리고 출력이 일치하지 않는 후보 — 잘못된 모양, 또는 선언된 필드 자체가 누락 — 는 그 필드를 가리키는 CodeInterpreterError 를 던지며, GEPA는 다른 곳에서 깨지는 Prediction 을 메트릭에 넘기기보다 그 예시를 실패 점수로 매깁니다. 시그니처 문자열로 전혀 왕복할 수 없는 주석(Callable 같은 것)은 결합되지 않을 소스로 렌더링되는 대신 타입 없이 올려집니다.
9. 코드는 상태이고, 인터프리터는 런타임 의존성이다
저장된 Flex 는 {"module_src": ..., "lm": ...} 입니다 — 코드에 더해 모듈에 직접 설정된 LM요. 내부 predictor는 저장되지 않아요. 그것들은 코드에서 파생되고, 각 forward 가 결합된 소스로부터 재구성합니다. 인터프리터는 살아 있는 런타임 자원이고 직렬화되지 않습니다. save(path, save_program=True) 는 브리지를 제외한 채 프로그램을 cloudpickle하고, 로딩이 그것을 재구축합니다. dspy.Flex(signature) 로 재구성하면 구성된 샌드박스를 복원하므로, 생성자에 전달한 커스텀 interpreter_factory 로 최적화한 경우에만 load 전에 다시 공급하면 됩니다.
10. Flex는 실험적이고 인터페이스가 변동 중이다
클래스는 @experimental 데코레이터를 답니다. API와 직렬화 형식을 마이너 릴리스 사이에 바뀔 수 있는 것으로 취급하고, 의존한다면 버전을 고정하세요.
API 살펴보기
Flex 정의하고 실행하기
dspy.Flex(signature, *, tools=None, interpreter_factory=PythonInterpreter, max_predictor_calls=100)
시그니처를 파싱하고 기준선 소스 — 시그니처에 대한 단일 dspy.Predict, 또는 tools 가 주어지면 dspy.RLM — 을 결합합니다. interpreter_factory 를 무인자 팩토리로 검증하고 샌드박스 브리지를 설정합니다. dspy.configure(interpreter_factory=...) 가 기본값을 교체합니다. Flex 에 전달된 팩토리가 이기는데, 단 PythonInterpreter 일 때는 제외합니다.
__call__(**inputs) / forward(**inputs)
현재 결합된 소스를 인터프리터 안에서 실행하며 predictor 호출을 호스트로 브리지합니다. 시그니처의 출력 필드에 대한 dspy.Prediction 을 반환하고, 키워드 입력만 받습니다.
module_src
현재 구현을 소스로 담는 읽기 전용 속성 — dspy.Module 서브클래스 하나요. 이것이 GEPA가 시드로 읽고 수용된 각 후보로 덮어쓰는 값입니다.
GEPA로 최적화하기
dspy.GEPA(metric=..., reflection_lm=..., ...).compile(flex_program, trainset=..., valset=...)
하나 이상의 Flex 하위 모듈을 포함하는 프로그램을 컴파일하면 각 Flex 의 코드와 모든 비-flex predictor의 지시문이 하나의 예산 아래 함께 최적화됩니다. module_src(flex 하위 모듈별)가 발견된 최고 코드인 새 프로그램을 반환합니다. 자동 예산은 지시문 predictor들과 나란히 각 flex 하위 모듈을 하나의 구성요소로 셉니다.
메트릭의 feedback
다른 GEPA 메트릭처럼, 피드백 문자열은 제안자에게 전달되는 프롬프트로 갑니다 — 여기서는 코드 제안자에게요. 왜 출력이 틀렸는지를 진단하고 구조를 암시하는 피드백이 맨 점수보다 다시 쓰기를 훨씬 잘 조종합니다.
트레이스 인식 메트릭 (program_trace)
메트릭의 여섯 번째 파라미터로 program_trace=None 을 추가하면 GEPA가 채점 시점에 실행 트레이스를 전달하므로, 답이 어떻게 만들어졌는지에 대해 점수를 매길 수 있어요. 예를 들어 len(program_trace) 을 LM 호출 수로 써서 점수에 작은 호출당 패널티를 접을 수 있습니다.
도구와 샌드박싱
tools=[...]
평범한 함수나 dspy.Tool 인스턴스로, 생성된 코드에서 이름으로 참조되므로 각 이름은 유효한 Python 식별자여야 합니다. 도구를 제공하면 기준선이 dspy.RLM 이 되고 코드 제안자에게 그것들이 범위 안에 있다고 알립니다 — dspy.RLM/dspy.ReAct 에 연결하거나, 직접 호출하거나, 자기 인라인 헬퍼로 보완할 수 있어요.
interpreter_factory=...
기본값은 dspy.PythonInterpreter(샌드박스, Deno 필요)입니다. dspy.configure(interpreter_factory=...) 는 호출보다 앞서 만들어진 Flex 를 포함해 각 인터프리터 세션에서 그 기본값을 교체합니다. 각 인터프리터 세션에 새 CodeInterpreter 를 반환하는 무인자 callable이어야 하므로, 병렬 평가와 중첩된 코드 실행 모듈은 격리된 세션을 받을 수 있어요. dspy.RLM 에서처럼 맨 인터프리터 인스턴스는 받지 않습니다. 이 저수준 훅은 서로 다른 인터프리터 간의 소스·표준 라이브러리 이식성이나 특정 보안 경계를 보장하지 않아요.
max_predictor_calls
생성된 코드가 하나의 forward 에서 만들 수 있는 predictor 호출의 최대 수입니다. 통제 불능 루프를 막습니다. None 은 제한을 제거합니다.
생성된 코드가 무엇을 쓸 수 있는지
최적화기가 저술한 코드는 실제 dspy 패키지에 대해 돌지 않아요. 샌드박스 안에서 dspy 는 작은 shim이고, 그 임무는 predictor 구성과 predictor 호출을 호스트로 다시 넘기는 것입니다. 그래서 그것이 노출하는 것은 라이브러리보다 좁고, 코드 제안자가 알게 되는 표면과 같습니다.
사용 가능
dspy.Module— 생성된 소스가 서브클래싱하는 기본 클래스.__init__이 predictor를 할당하고,forward는 샌드박스에서 실행됩니다.dspy.Predict,dspy.ChainOfThought,dspy.ReAct,dspy.ReActV2,dspy.RLM— 샌드박스에서 구성되지만 호스트 위에 구축되어 실제 LM 호출이 일어납니다. 생성자 kwargs는 JSON으로 건너갑니다.dspy.Signature("inputs -> outputs", "instructions")— 문자열 형식만, 내부 predictor에 지시문을 주는 데 쓰입니다. 호스트가 진짜Signature로 되돌리는 마커이지 그 클래스가 아니에요. 메서드도,with_instructions()도,dspy.InputField/dspy.OutputField클래스 형식도 없습니다.dspy.Prediction(**fields)—forward의 반환값. 필드를 담지만 호스트Prediction타입은 아닙니다.dspy.Tool(func)— 통과(pass-through) 래퍼입니다.dspy.Flex(tools=...)에 넘긴 도구만 브리지된 서브-predictor에 넘겨질 수 있고, 이미 이름으로 범위 안에 있으므로 감싸기는 선택적입니다.- Python 표준 라이브러리 —
forward안에서 임포트됩니다(인터프리터의 자체 샌드박스가 여전히 적용됩니다.dspy.PythonInterpreter는 기본적으로 파일시스템·네트워크 접근이 없음).
사용 불가
- 어댑터(
dspy.ChatAdapter,dspy.JSONAdapter, …). 호스트가 모든 브리지 호출을 포맷하고 파싱합니다. 생성된 코드는 프롬프트나 원시 완성을 결코 보지 못합니다. dspy.settings,dspy.context,dspy.configure,dspy.LM. LM은 호스트가 구성한 것이 무엇이든 그대로입니다. 생성된 코드는 하나를 선택하거나 재구성할 수 없습니다.dspy.Example,dspy.Evaluate, 최적화기, 검색기, 그리고dspy.Flex자체 — 중첩 없음.- 클래스 기반 시그니처와 타입 필드 선언.
- 일반적으로 호스트 객체. 값은 JSON으로 건너가므로,
forward안에서 정의한 도구는 브리지된 서브-predictor에 넘겨질 수 없고, JSON 직렬화 가능하지 않은 predictor 필드는 조용히 저하되는 대신 예외를 던집니다.
이 표면 밖의 어떤 것도 후보가 실행될 때 실패하고, GEPA는 실패 점수로 매깁니다 — 누락된 이름은 실행을 크래시시키는 대신 탐색 한 단계의 비용이 됩니다. shim은 dspy/predict/flex/_sandbox_shim.py 에 있고, 제안자-대면 버전의 이 목록은 dspy/predict/flex/primitives_doc.py 입니다.
저장하고 불러오기
save(path) / load(path) / dump_state() / load_state(state)
module_src 는 직렬화된 상태에서 이동합니다(모듈에 설정된 LM과 함께), 그래서 로딩은 최적화된 코드를 복원하고, predictor들은 매 forward 마다 그것에서 재구성됩니다. 인터프리터는 직렬화되지 않아요. dspy.Flex(signature) 로 재구성하면 구성된 샌드박스를 복원하므로, 커스텀 interpreter_factory 를 거기에 전달했을 때만 load 전에 생성자에서 다시 공급하세요.
크로스링크
dspy.FlexAPI 레퍼런스 — 생성자 표, 예시, 전체 메서드 목록.- 내장 모듈 변형들 —
Flex가 다른 비-Predict모듈들 사이에서 어디에 앉는지. - GEPA 심층 탐구 —
Flex의 코드 탐색을 구동하는 반성적 최적화기. - RLM: 코드로 큰 컨텍스트 탐험하기 —
Flex가 도구-활성 기준선으로 쓰는 모듈.