dspy.Flex

dspy.Flex

이 페이지는 DSPy의 실험적 모듈 Flex를 다뤄요. 일반적인 모듈이 고정된 프롬프트 위에서 동작한다면, Flex는 그 구현 자체가 최적화 가능한 코드라는 점이 특징이에요. 즉 옵티마이저가 프롬프트 문구만 고치는 게 아니라 모듈의 소스 코드까지 다시 쓸 수 있게 해 주죠. "구조를 손으로 설계하지 말고, 옵티마이저가 찾게 하자"는 발상이에요.

출처: 문서

본문

Flex는 구현이 고정된 프롬프트가 아니라 *최적화 가능한 코드(optimizable code)*인 DSPy 모듈이에요. 시그니처로부터 구성되며, 기본적으로 그 시그니처 위의 얇은 기준선(baseline)으로 동작해요. 차이를 만드는 것은 옵티마이저가 그것으로 무엇을 할 수 있느냐예요. 지시문을 다시 쓰기만 하는 대신, dspy.GEPA는 모듈의 소스 전체를 다시 쓸 수 있어요. 작업을 여러 예측기로 쪼개고, 결정적 단계를 순수 Python으로 접고, 자체 헬퍼 함수를 작성하는 식이죠. Flex라는 것이 바로 GEPA에게 "이 모듈의 코드는 최적화 가능한 파라미터다"라고 알려 주는 표시예요.

Flex를 언제 쓸까

프로그램의 구조를 손으로 작성하는 것보다 옵티마이저가 발견하도록 맡기고 싶을 때 Flex를 꺼내면 돼요. 다음과 같은 경우가 그렇죠:

  • 최적의 분해(decomposition)가 알려져 있지 않거나 탐색할 가치가 있을 때 — 후보 구조를 평가할 메트릭과 데이터셋이 있을 때.
  • 작업의 일부가 **결정적(deterministic)**이라 LM 호출 비용을 들일 필요가 없을 때 — 산술, 파싱, 조회, 정규화 같은 것.
  • 옵티마이저가 정확성과 비용을 맞바꾸게 하고 싶을 때 — 예를 들어 명확한 경우는 코드로 답하고 정말 어려운 경우에만 LM을 쓰는 프로그램을 보상하고 싶을 때.

기본 사용법

import dspy

dspy.configure(lm=dspy.LM("openai/gpt-5"))

# Construct Flex from a signature, like any module.
solve = dspy.Flex("invoice: str -> total_cents: int")

# Runs the baseline (a single dspy.Predict).
result = solve(invoice="2 widgets @ $3.50, shipping $1.00")
print(result.total_cents)

기본 상태에서 solve는 시그니처 위의 dspy.Predict 하나를 모듈로 감싼 것에 불과해요. (tools가 있으면 대신 dspy.RLM으로 시작해요 — Tools 참고). Flex의 요점은 최적화할 때 벌어지는 일이에요 (GEPA로 최적화하기 참고). GEPA가 그 기준선을, 예를 들어 수량과 단가만 추출하는 예측기와, 그것들을 곱하고 더하는 Python 한 줄로 대체할 수 있어요.

생성된 코드는 항상 CodeInterpreter를 거쳐 실행돼요 (interpreter_factory는 기본적으로 샌드박스형 dspy.PythonInterpreter). 그래서 위 예제는 Deno가 설치되어 있어야 해요 — 인터프리터 실행 참고.

최적화가 어떻게 동작하는지

dspy.GEPA는 타입으로 Flex 서브모듈을 발견해요. GEPA가 Flex 서브모듈을 하나 이상 담은 프로그램을 컴파일하면, 각각을 **코드 컴포넌트(code component)**로 취급해요. 새 지시문 문자열을 제안하는 대신, 그 reflection 모델이 시그니처, 사용 가능한 도구, 메트릭이 실패 예시에 대해 주는 피드백에 따라 전체 모듈 소스를 새로 제안해요. GEPA는 후보 소스를 바인딩하고 평가한 다음, Pareto 최전선(frontier)을 진전시키면 유지해요. GEPA가 프롬프트에 대해 수행하는 것과 같은 탐색을 코드에 적용하는 거죠.

깨진 후보가 최적화 실행을 크래시시키지는 않아요. reflection 모델이 바인딩에 실패하는 소스를 내놓으면, GEPA는 그 후보를 실패로 채점하고 최적화를 중단하지 않고 계속 진행해요.

GEPA로 최적화하기

Flex를 최적화하는 방법은 다른 DSPy 프로그램과 동일해요. 메트릭과 trainset을 dspy.GEPA에 넘기기만 하면 돼요:

import dspy

dspy.configure(lm=dspy.LM("openai/gpt-5-mini"))  # runs the program

def metric(gold, pred, trace=None, pred_name=None, pred_trace=None):
    correct = getattr(pred, "total_cents", None) == gold.total_cents
    fb = "Correct." if correct else (
        f"Wrong total: got {getattr(pred, 'total_cents', None)}, expected {gold.total_cents}. "
        "Have the LM extract line items, then sum them in Python."
    )
    return dspy.Prediction(score=1.0 if correct else 0.0, feedback=fb)

solve = dspy.Flex("invoice: str -> total_cents: int")

optimized = dspy.GEPA(
    metric=metric,
    reflection_lm=dspy.LM("openai/gpt-5", temperature=1.0, max_tokens=8000),
    max_metric_calls=60,
).compile(solve, trainset=trainset, valset=valset)

print(optimized.module_src)  # the discovered program

metric은 dspy.Prediction(score=..., feedback=...) — 스칼라 점수에 GEPA가 모듈을 수정하며 reflection하는 자연어 피드백을 더한 것을 반환해요. 효과적인 피드백 메트릭 작성법은 GEPA 가이드의 Implementing Feedback Metrics와 dspy.GEPA 튜토리얼을 참고하세요.

트레이스 인지 메트릭으로 더 가벼운 프로그램 보상하기

Flex의 일반적인 목표는 작업을 LM 밖으로 밀어내 결정적 코드로 옮기는 것이에요. 그렇게 최적화하려면 메트릭이 답이 맞는지만 보는 게 아니라 어떻게 만들어졌는지를 봐야 해요. program_trace 파라미터를 선언하면 GEPA가 채점 시점에 실행 트레이스를 메트릭에 넘겨주고, LM 호출에 패널티를 줄 수 있어요:

LLM_CALL_PENALTY = 0.15

def metric(gold, pred, trace=None, pred_name=None, pred_trace=None, program_trace=None):
    correct = getattr(pred, "total_cents", None) == gold.total_cents
    n_calls = len(program_trace) if program_trace else 0
    score = max(0.0, (1.0 if correct else 0.0) - LLM_CALL_PENALTY * n_calls)
    fb = f"{'Correct' if correct else 'Wrong'} — used {n_calls} LM call(s). Settle clear cases in Python."
    return dspy.Prediction(score=score, feedback=fb)

program_trace 파라미터는 선언에 의해 옵트인예요. 이름을 붙인 메트릭만 트레이스를 받아요. 패널티를 정확성에 비해 작게 유지하세요. 그래야 분해가 정확성을 유지해야 이길 수 있어요.

인터프리터 실행

Flex는 생성된 코드를 항상 CodeInterpreter로 실행해요. interpreter_factory는 기본적으로 dspy.PythonInterpreter(Deno/Pyodide)이며 **인자 없는 팩토리(zero-argument factory)**여서 새 인터프리터를 반환해야 해요. 단순 인스턴스(bare instance)는 받아들이지 않아요. 그래서 병렬 평가가 격리된 세션을 받을 수 있죠. dspy.configure(interpreter_factory=...)가 그 기본값을 대체해요. Flex에 넘긴 팩토리가 우선하지만, PythonInterpreter인 경우는 예외예요. 팩토리는 인터프리터 세션마다 한 번씩 호출되고, 중첩된 코드 실행 모듈이 요청한 별도 세션도 포함해요. 기본 인터프리터에서는 옵티마이저가 만든 제어 흐름, 문자열 작업, 산술, 지원되는 import가 샌드박스 안에서 실행되고, 제공된 도구 호출, 예측기 생성, 예측기 호출만 호스트로 다시 연결돼 실제 LM 호출이 돼요. 커스텀 팩토리는 자신만의 신뢰 경계를 정의해요. 예를 들어 dspy.LocalInterpreter는 프로세스 메모리와 stdout을 분리하지만 호스트 사용자의 파일시스템, 환경, 자격 증명, 네트워크, 프로세스 권한을 유지해요.

기본값이 PythonInterpreter를 만들기 때문에, 실행하려면 Deno가 설치되어 있어야 해요. 없으면 호출이 오류를 일으켜요.

solve = dspy.Flex(
    "invoice: str -> total_cents: int",
    interpreter_factory=lambda: dspy.PythonInterpreter(),  # the default; swap in your own CodeInterpreter factory here
)

한 번의 호출로 프로그램의 모든 코드 실행 모듈 뒤에 같은 인터프리터를 둘 수도 있어요 — 원격 샌드박스나, Deno를 실행할 수 없는 호스트용 하나 같은 것이요:

dspy.configure(interpreter_factory=MyInterpreter)  # every module with no factory of its own

각 호출은 자신이 만든 모든 인터프리터 세션을 소유하고 종료해요. 그래서 Flex는 호출 사이에 살아 있는 세션을 유지하지 않아요.

도구 (Tools)

tools를 넘기면 기준선이 dspy.Predict 대신 dspy.RLM으로 시작해요.

def lookup_sku(code: str) -> dict:
    """Look up a product by SKU."""
    return catalog[code]

solve = dspy.Flex("order: str -> total_cents: int", tools=[lookup_sku])

그러면 옵티마이저가 여러분의 도구를 dspy.RLM(..., tools=[...]) / dspy.ReAct(..., tools=[...])에 연결하거나 forward에서 직접 호출할 수 있어요.

저장과 불러오기

Flex는 module_src를 직렬화하므로, 프로그램을 저장/불러오면 최적화된 코드가 복원돼요:

optimized.save("solver.json")

restored = dspy.Flex("invoice: str -> total_cents: int")
restored.load("solver.json")  # rebinds the saved module_src

인터프리터는 런타임 의존성이며 직렬화되지 않아요. dspy.Flex(signature)로 재구성하면 설정된 샌드박스가 자동으로 복원돼요. 생성자에 interpreter_factory를 넘겨 최적화했다면, load를 호출하기 전에 모듈을 재구성할 때 같은 것을 넘겨야 해요.

생성자 파라미터

파라미터 타입 기본값 설명
signature str | Signature 필수 모듈의 입력과 출력을 선언 (예: "invoice -> total_cents: int").
tools list[Callable | dspy.Tool] None 생성된 코드가 호출할 수 있는 도구. 도구가 있으면 기준선은 dspy.RLM, 없으면 dspy.Predict.
interpreter_factory Callable[[], CodeInterpreter] PythonInterpreter 각 인터프리터 세션에 새 CodeInterpreter를 반환하는 인자 없는 팩토리. 기본은 dspy.PythonInterpreter(Deno 필요)이고, dspy.configure(interpreter_factory=...)가 그 기본값을 대체. 단순 인터프리터 인스턴스는 허용되지 않아요. 지원되는 Python, 라이브러리, 보안 경계는 인터프리터에 따라 달라요.
max_predictor_calls int | None 100 생성된 코드가 한 번의 forward에서 만들 수 있는 예측기 호출 상한 — 무한 루프 방지용 가드. None은 제한을 없애요.

참고 (Notes)

!!! warning "Experimental" Flex는 실험적으로 표시되어 있어요. API와 최적화 동작은 릴리스 사이에 바뀔 수 있으니, 의존한다면 버전을 고정하세요.

!!! note "Interpreter Requirements" Flex는 기본적으로 dspy.PythonInterpreter를 사용하며, Pyodide WASM 샌드박스에 Deno가 필요해요 — 설치 노트는 RLM 페이지를 참고하세요. dspy.LocalInterpreter는 추가 런타임이 필요 없지만 보안 샌드박스가 아니에요.

API 레퍼런스

::: dspy.Flex handler: python options: members: - init - call - forward - module_src - signature - deepcopy - dump_state - get_lm - inspect_history - load - load_state - named_parameters - named_predictors - named_sub_modules - parameters - predictors - reset - reset_copy - save - set_lm show_source: true show_root_heading: true heading_level: 3 docstring_style: google show_root_full_path: true show_object_full_path: false separate_signature: false inherited_members: true

더 알아보기 (Learn more)