dspy.RLM
dspy.RLM
RLM(Recursive Language Model)은 LLM이 샌드박스 처리된 파이썬 REPL을 통해 방대한 컨텍스트를 프로그래밍 방식으로 탐색할 수 있게 해 주는 DSPy 모듈입니다. 거대한 컨텍스트를 프롬프트에 직접 넣는 대신, RLM은 컨텍스트를 외부 데이터로 취급하고 LLM이 코드 실행과 재귀적 하위 LLM 호출을 통해 그 데이터를 조사하게 합니다.
출처: 문서
본문
이 방식은 “Recursive Language Models” (Zhang, Kraska, Khattab, 2025) 논문에 설명된 접근법을 구현한 것입니다.
RLM을 언제 쓸까
컨텍스트가 커질수록 LLM 성능은 떨어지는데, 이를 context rot 현상이라고 합니다. RLM은 변수 공간(REPL에 저장된 정보)과 토큰 공간(LLM이 실제로 처리하는 것)을 분리해서 이 문제를 해결합니다. LLM은 필요한 컨텍스트만, 필요할 때 동적으로 불러옵니다.
다음과 같은 경우 RLM을 사용하세요:
- 컨텍스트가 너무 커서 LLM의 컨텍스트 창에 효과적으로 담을 수 없을 때
- 작업이 프로그램적 탐색(검색, 필터링, 집계, 청크 분할)에 이점이 있을 때
- 문제를 어떻게 분해할지 LLM이 스스로 결정해야 할 때 (당신이 아니라)
기본 사용법
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-5"))
# Create an RLM module
rlm = dspy.RLM("context, query -> answer")
# Call it like any other module
result = rlm(
context="...very long document or data...",
query="What is the total revenue mentioned?"
)
print(result.answer)
Deno 설치
RLM은 안전한 로컬 파이썬 실행을 위한 WASM 샌드박스를 만들기 위해 Deno와 Pyodide에 의존합니다.
DSPy의 관리형 Deno 런타임은 pip install "dspy[deno]"로 설치할 수 있습니다. 지원 플랫폼, 시스템 Deno 대체 방법, 의존성 격리 세부사항은 PythonInterpreter 설치 가이드를 참고하세요.
그다음 dspy.RLM을 실행하면 됩니다. 외부 샌드박스 프로바이더도 사용할 수 있습니다. 외부 샌드박스 프로바이더 사용 예시는 아직 만드는 중입니다.
동작 방식
RLM은 반복적인 REPL 루프로 동작합니다:
- LLM은 컨텍스트 전체가 아니라 컨텍스트에 대한 메타데이터(타입, 길이, 미리보기)를 받습니다.
- LLM은 데이터를 탐색하기 위해 파이썬 코드를 작성합니다(샘플 출력, 검색, 필터링).
- 코드는 샌드박스 처리된 interpreter에서 실행되고, LLM은 그 출력을 봅니다.
- LLM은
llm_query(prompt)를 호출해 스니펫에 대한 하위 LLM 호출(sub-LLM call)로 의미 분석을 수행할 수 있습니다. - 끝나면 LLM은
SUBMIT(output)을 호출해 최종 답변을 반환합니다.
LLM이 보는 것 (단계별 trace)
LLM은 컨텍스트 전체에 직접 접근하지 못하고, 먼저 데이터를 한 번 살펴봅니다(메타데이터와 미리보기). 이후에는 작성한 코드와 그 출력만을 보고 반복적으로 탐색합니다.
설정 표
| 포인트 | 타입 | 기본값 | 설명 |
|---|---|---|---|
verbose |
bool |
False |
상세한 실행 정보를 로깅할지 여부. |
tools |
list[Union[Callable, dspy.Tool]] |
None |
interpreter 코드에서 호출할 수 있는 추가 도구 함수. |
sub_lm |
dspy.LM |
None |
하위 쿼리에 사용할 LM. 기본값은 dspy.settings.lm. 여기서는 더 저렴한 모델을 쓰세요. |
interpreter_factory |
Callable[[], CodeInterpreter] |
PythonInterpreter |
호출마다 interpreter를 하나 만듭니다. RLM은 반환된 각 interpreter를 종료합니다. dspy.configure(interpreter_factory=...)가 기본값을 대체합니다. 생성자에 전달된 factory는 PythonInterpreter가 아닌 한 우선합니다. 작업 프롬프트를 위한 선택적 execution_instructions 문자열을 노출할 수 있습니다. |
내장 도구 (Built-in Tools)
REPL 안에서 LLM은 다음에 접근할 수 있습니다:
| Tool | 설명 |
|---|---|
llm_query(prompt) |
의미 분석을 위해 하위 LLM에 쿼리(~500K 문자 용량). |
llm_query_batched(prompts) |
여러 프롬프트를 동시에 쿼리(배치 작업에 더 빠름). |
print() |
출력을 출력(결과를 보려면 필수). |
SUBMIT(...) |
최종 출력을 제출하고 실행을 종료. |
| 표준 라이브러리 | re, json, collections, math 등. |
Examples
긴 문서 Q&A
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-5"))
rlm = dspy.RLM("document, question -> answer", max_iters=10)
with open("large_report.txt") as f:
document = f.read() # 500K+ characters
result = rlm(
document=document,
question="What were the key findings from Q3?"
)
print(result.answer)
더 저렴한 하위 LM 사용하기
import dspy
main_lm = dspy.LM("openai/gpt-5")
cheap_lm = dspy.LM("openai/gpt-5-nano")
dspy.configure(lm=main_lm)
# Root LM (gpt-5) decides strategy; sub-LM (gpt-5-nano) handles extraction
rlm = dspy.RLM("data, query -> summary", sub_lm=cheap_lm)
여러 타입 지정 출력
rlm = dspy.RLM("logs -> error_count: int, critical_errors: list[str]")
result = rlm(logs=server_logs)
print(f"Found {result.error_count} errors")
print(f"Critical: {result.critical_errors}")
커스텀 도구
def fetch_metadata(doc_id: str) -> str:
"""Fetch metadata for a document ID."""
return database.get_metadata(doc_id)
rlm = dspy.RLM(
"documents, query -> answer",
tools=[fetch_metadata]
)
Interpreter 설정하기
interpreter 클래스가 인자를 필요로 하지 않을 때는 직접 전달합니다. 설정된 생성이 필요하면 functools.partial 같은 zero-argument 콜러블을 사용하세요:
from functools import partial
rlm = dspy.RLM(
"context, query -> answer",
interpreter_factory=partial(
dspy.PythonInterpreter,
enable_network_access=["example.com"],
),
)
한 번의 호출로 프로그램의 모든 코드 실행 모듈 뒤에 같은 interpreter가 놓입니다. 각 호출이 설정을 읽으므로, dspy.context(interpreter_factory=...)는 그 선택을 범위 안으로 한정하고 dspy.configure(...)는 호출 전에 만들어진 모듈에도 적용됩니다. 생성자에 전달된 factory는 PythonInterpreter가 아닌 한 여전히 우선합니다:
dspy.configure(interpreter_factory=MyInterpreter)
RLM은 호출마다 이 factory에서 interpreter를 하나 만들고 종료합니다. 반환된 interpreter의 가변 tools 딕셔너리에 호출 범위의 도구를 추가하므로, 원격 샌드박스도 그 프로토콜을 지원하는 CodeInterpreter 어댑터가 필요합니다.
단일 호출에 대해 factory를 재정의하려면 interpreter_factory 키워드로 zero-argument 콜러블을 전달하세요:
result = rlm(context=data, query=query, interpreter_factory=MyInterpreter)
result = await rlm.acall(context=data, query=query, interpreter_factory=MyInterpreter)
호출 시점의 factory는 생성자 및 전역/컨텍스트 factory보다 우선하며, 명시적으로 PythonInterpreter를 전달해도 마찬가지입니다. RLM은 이를 한 번 호출하고 성공 여부와 무관하게 반환된 interpreter를 종료합니다. factory는 매번 새 interpreter를 반환해야 합니다. 호출 시점에 살아 있는 interpreter를 전달하는 방식은 더 이상 지원되지 않으니, rlm(interpreter, ...)은 rlm(..., interpreter_factory=factory)로 마이그레이션하세요. 이 옵션은 키워드 전용이며, interpreter_factory는 시그니처 입력이 아니라 런타임 설정용으로 예약되어 있습니다.
factory가 execution_instructions 문자열을 노출하면 RLM은 그것을 작업 predictor의 지시사항에 추가합니다. DSPy 어댑터는 이를 시스템 프롬프트에 배치합니다. 따라서 GEPA 같은 옵티마이저가 나머지 작업 정책과 함께 실행 지침도 적응(adapt)시킬 수 있습니다. 이 메타데이터를 노출할 수 있는 것은 factory 클래스와 설정된 콜러블 프로바이더 객체이며, 없는 익명 factory는 일반 작업 프롬프트를 계속 사용합니다. RLM은 각 작업 호출마다 이 지침을 새로 고치므로, dspy.context(interpreter_factory=...) override는 생성된 코드를 실행하는 런타임에 프롬프트를 맞춰 유지합니다.
커스텀 샌드박스 직렬화 입력
일반 파이썬 값과 다르게 샌드박스로 불러와야 하는 입력은 dspy.SandboxSerializable을 서브클래싱하세요. RLM은 이런 입력을 감지해 직렬화된 payload를 interpreter로 보내고, 설정 코드를 실행하고, 재구성된 값을 원래 입력 이름으로 노출합니다.
class DataFrame(dspy.SandboxSerializable):
def sandbox_setup(self) -> str:
return "import pandas as pd\nimport base64\nimport io"
def to_sandbox(self) -> bytes:
return base64.b64encode(self.data.to_parquet(index=False))
def sandbox_assignment(self, var_name: str, data_expr: str) -> str:
return f"{var_name} = pd.read_parquet(io.BytesIO(base64.b64decode({data_expr})))"
def rlm_preview(self, max_chars: int = 500) -> str:
return f"DataFrame: {self.data.shape[0]} rows x {self.data.shape[1]} columns"
SandboxSerializable은 또한 Pydantic 스키마 훅을 정의하므로 서브클래스를 시그니처에 직접 사용할 수 있습니다. 예를 들어 data: DataFrame = dspy.InputField()처럼 말이죠. 이 훅은 의도적으로 통과형(pass-through)입니다. Pydantic은 객체를 그대로 받아들이고 스키마/메타데이터 용도로 str(value)로 직렬화합니다. RLM의 실제 샌드박스 전송은 여전히 to_sandbox()와 sandbox_assignment()에서 나옵니다.
비동기 실행
import asyncio
rlm = dspy.RLM("context, query -> answer")
async def process():
result = await rlm.acall(context=data, query="Summarize this")
return result.answer
answer = asyncio.run(process())
Trajectory 살펴보기
result = rlm(context=data, query="Find the magic number")
# See what code the LLM executed
for step in result.trajectory:
print(f"Code:\n{step['code']}")
print(f"Output:\n{step['output']}\n")
Output
RLM은 다음을 가진 Prediction을 반환합니다:
- 시그니처의 출력 필드 (예:
result.answer) trajectory: 각 단계의reasoning,code,output을 담은 dict 리스트final_reasoning: LLM이 마지막 단계에서 한 추론
Notes
- Experimental: RLM은 experimental로 표시되어 있으며, API가 향후 릴리스에서 바뀔 수 있습니다.
- Thread Safety: 생성자와 호출 시점 factory는 동시에 호출될 수 있으며 매번 새 interpreter를 반환해야 합니다. RLM은 한 호출 동안 그 interpreter를 유지하고 이후 종료합니다.
PythonInterpreter는 처음 사용된 스레드에 유지되어야 합니다. - Interpreter Requirements: RLM은 기본적으로
PythonInterpreter를 사용하는데, Pyodide WASM 샌드박스를 위해 Deno 설치가 필요합니다.LocalInterpreter는 추가 런타임이 필요 없고 별도의 CPython 프로세스를 제공하지만 보안 샌드박스가 아닙니다. 생성된 코드가 호스트 사용자의 파일, 환경, 자격 증명, 네트워크, 프로세스 권한을 그대로 갖습니다.
API Reference
dspy.RLM(
signature: type[Signature] | str,
max_iters: int = 20,
max_llm_calls: int = 50,
max_output_chars: int = 10000,
verbose: bool = False,
tools: list[Callable] | None = None,
sub_lm: dspy.LM | None = None,
interpreter_factory: Callable[[], CodeInterpreter] = PythonInterpreter,
)
- Bases:
Module(callbacks=None)
Recursive Language Model 모듈입니다. 샌드박스 처리된 REPL을 사용해 LLM이 코드 실행을 통해 대규모 컨텍스트를 프로그래밍 방식으로 탐색하게 합니다. LLM은 데이터를 조사하고, 하위 LLM을 호출해 의미 분석을 하며, 답변을 반복적으로 구성하기 위한 파이썬 코드를 작성합니다.
interpreter_factory의 기본값은 PythonInterpreter(Deno/Pyodide/WASM)이며, dspy.configure(interpreter_factory=...)가 그 기본값을 대체합니다. 어느 경로든 원격 샌드박스용 어댑터를 받아들입니다. RLM은 실행 전에 interpreter의 가변 tools 딕셔너리를 호출 범위의 도구로 갱신합니다. 호출 시점에 interpreter_factory=로 zero-argument factory를 전달하면 단일 호출의 런타임을 재정의할 수 있습니다. RLM은 자신이 만든 모든 interpreter를 종료합니다.
Examples
# Basic usage
rlm = dspy.RLM("context, query -> output", max_iters=10)
result = rlm(context="...very long text...", query="What is the magic number?")
print(result.output)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signature |
type[Signature] | str |
입력과 출력을 정의합니다. "context, query -> answer" 같은 문자열 또는 Signature 클래스. | required |
max_iters |
int |
최대 REPL 상호작용 반복 횟수. | 20 |
max_llm_calls |
int |
실행당 최대 하위 LLM 호출(llm_query/llm_query_batched) 수. | 50 |
max_output_chars |
int |
REPL 출력에 포함할 최대 문자 수. | 10000 |
verbose |
bool |
상세한 실행 정보를 로깅할지 여부. | False |
tools |
list[Callable] | None |
interpreter 코드에서 호출할 수 있는 도구 함수 또는 dspy.Tool 객체 리스트. 내장 도구: llm_query(prompt), llm_query_batched(prompts). |
None |
sub_lm |
LM |
하위 쿼리에 사용할 LM. 기본값은 dspy.settings.lm. 여기서는 더 저렴한 모델을 쓰세요. |
None |
interpreter_factory |
Callable[[], CodeInterpreter] |
각 forward pass에 interpreter를 만드는 zero-argument 콜러블. 이 콜러블은 동시에 호출될 수 있으며, DSPy는 반환된 각 interpreter를 종료합니다. RLM은 실행 전에 반환된 interpreter의 가변 tools 딕셔너리를 갱신합니다. 이 콜러블은 작업 프롬프트용 런타임을 설명하는 execution_instructions 문자열을 노출할 수 있습니다. RLM은 활성 factory의 지침을 각 작업 호출에 적용하므로, dspy.context가 공유 predictor 시그니처를 바꾸지 않고도 런타임을 전환할 수 있습니다. 기본값은 dspy.PythonInterpreter이고, dspy.configure(interpreter_factory=...)가 기본값을 대체합니다. |
PythonInterpreter |
소스 코드는 dspy/predict/rlm.py에 있습니다.
def __init__(
self,
signature: type[Signature] | str,
max_iters: int = 20,
max_llm_calls: int = 50,
max_output_chars: int = 10_000,
verbose: bool = False,
tools: list[Callable] | None = None,
sub_lm: dspy.LM | None = None,
interpreter_factory: Callable[[], CodeInterpreter] = PythonInterpreter,
):
...
if any(
kind.extract_custom_type_from_annotation(field.rebuild_annotation())
for field in self.signature.output_fields.values()
for kind in (Noul, Choice, Score)
):
warnings.warn(
"RLM support for Noul, Choice, and Score outputs is not implemented consistently: "
"decision evidence decoding is not guaranteed, including for nested output types. "
"Use Predict with top-level decision outputs instead.",
UserWarning,
stacklevel=2,
)
self.max_iters = max_iters
self.max_llm_calls = max_llm_calls
self.max_output_chars = max_output_chars
self.verbose = verbose
self.sub_lm = sub_lm
self._interpreter_factory = interpreter_factory
self._user_tools = self._normalize_tools(tools)
self._validate_namespace(self._user_tools)
# Build the action and extract signatures
action_sig, extract_sig = self._build_signatures()
self._action_signature = action_sig
self.generate_action = dspy.Predict(action_sig)
self.extract = dspy.Predict(extract_sig)
Attributes
tools: dict[str, Tool] (property)
사용자가 제공한 도구입니다(내부 llm_query/llm_query_batched 제외).
Methods
forward(*, interpreter_factory=None, **input_args) -> Prediction
RLM을 실행해 주어진 입력에서 출력을 만듭니다.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interpreter_factory |
Callable[[], CodeInterpreter] | None |
키워드로 전달되는 선택적 zero-argument factory. 이 호출에 대해 생성자 및 설정 factory를 재정의합니다. 매번 새 interpreter를 반환해야 하며, RLM은 도구와 출력 메타데이터를 주입하고 실패를 포함해 종료 시 종료합니다. | None |
**input_args |
시그니처의 입력 필드에 맞는 입력 값들. | {} |
Returns: 시그니처의 출력 필드와 디버깅용 'trajectory'를 가진 Prediction.
Raises: ValueError — 필수 입력 필드가 없는 경우. CodeInterpreterError — interpreter 설정·프로세스·프로토콜이 실패한 경우.
소스 코드는 dspy/predict/rlm.py에 있습니다.
def forward(self, *, interpreter_factory: Callable[[], CodeInterpreter] | None = None, **input_args) -> Prediction:
"""Execute RLM to produce outputs from the given inputs.
Args:
interpreter_factory: Optional zero-argument factory, passed by keyword. Overrides the constructor
and configured factories for this invocation. Must return a fresh interpreter; RLM injects tools
and output metadata and shuts it down on exit, including failures.
**input_args: Input values matching the signature's input fields.
Returns:
Prediction with output field(s) from the signature and 'trajectory' for debugging
Raises:
ValueError: If required input fields are missing
CodeInterpreterError: If interpreter setup, process, or protocol fails
"""
self._validate_inputs(input_args)
if interpreter_factory is None:
interpreter_factory = resolve_interpreter_factory(self._interpreter_factory)
output_field_names = list(self.signature.output_fields.keys())
execution_tools = self._prepare_execution_tools()
variables = self._build_variables(**input_args)
with self._interpreter_context(execution_tools, interpreter_factory) as repl:
regular_args = self._prepare_serializable_vars(input_args, repl)
history: REPLHistory = REPLHistory(max_output_chars=self.max_output_chars)
for iteration in range(self.max_iters):
result: Prediction | REPLHistory = self._execute_iteration(
repl, variables, history, iteration, regular_args, output_field_names, interpreter_factory
)
if isinstance(result, Prediction):
return result
history = result
# Max iterations reached - use extract fallback
return self._extract_fallback(variables, history, output_field_names)
aforward(*, interpreter_factory=None, **input_args) -> Prediction (async)
forward()의 비동기 버전입니다.
그 외 상속받은 메서드
RLM은 Module/BaseModule/Parameter에서 공통 메서드를 상속받습니다. 자세한 설명은 Predict 페이지를 참고하세요.
__call__(*args, **kwargs) -> Prediction— 모듈 호출.batch(...)—dspy.Example리스트를Parallel로 병렬 처리.deepcopy()/dump_state(json_mode=True)— 복사·상태 내보내기.get_lm()/set_lm(lm)— 언어 모델 조회·설정.load(...)/load_state(...)— 상태 불러오기.named_parameters()/named_predictors()/parameters()/predictors()— 구조 탐색.reset_copy()— 복사 후 초기화.save(...)— 모듈 저장.