dspy.RLM¶
RLM (Recursive Language Model) is a DSPy module that lets LLMs programmatically explore large contexts through a sandboxed Python REPL. Instead of feeding huge contexts directly into the prompt, RLM treats context as external data that the LLM examines via code execution and recursive sub-LLM calls.
This implements the approach described in “Recursive Language Models” (Zhang, Kraska, Khattab, 2025).
When to Use RLM¶
As contexts grow, LLM performance degrades — a phenomenon known as context rot. RLMs address this by separating the variable space (information stored in the REPL) from the token space (what the LLM actually processes). The LLM dynamically loads only the context it needs, when it needs it.
Use RLM when:
- Your context is too large to fit in the LLM’s context window effectively
- The task benefits from programmatic exploration (searching, filtering, aggregating, chunking)
- You need the LLM to decide how to decompose the problem, not you
Basic Usage¶
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 Installation¶
RLM relies on Deno and Pyodide to create a local WASM sandbox for secure Python execution.
You can install Deno with: brew install deno on MacOS or curl -fsSL https://deno.land/install.sh | sh on MacOS and Linux. See the Deno Installation Docs for more details. Make sure to accept the prompt when it asks to add it to your shell profile.
After you have installed Deno, Make sure to restart your shell.
Deno may get confused by existing package.json files it happens to find; use the environment variable DENO_NO_PACKAGE_JSON=1 to ignore package.json entirely and resolve npm:pyodide from its own cache, which is what DSPy expects.
Then you can run dspy.RLM.
Users have reported issues with the Deno cache not being found by DSPy. We are actively investigating these issues, and your feedback is greatly appreciated.
You can also work with an external sandbox provider. We are still working on creating an example of using external sandbox providers.
How It Works¶
RLM operates in an iterative REPL loop:
- The LLM receives metadata about the context (type, length, preview) but not the full context
- The LLM writes Python code to explore the data (print samples, search, filter)
- Code executes in a sandboxed interpreter, and the LLM sees the output
- The LLM can call
llm_query(prompt)to run sub-LLM calls for semantic analysis on snippets - When done, the LLM calls
SUBMIT(output)to return the final answer
What the LLM sees (step-by-step trace):¶
Step 1: Initial Metadata (no direct access to full context)¶
Output shown to the LLM:Step 2: Write Code to Explore Context¶
# Step 2: Search for relevant sections
import re
matches = re.findall(r'revenue.*?\$[\d,]+', context, re.IGNORECASE)
print(matches)
Step 3: Trigger Sub-LLM Calls¶
# Step 3: Use sub-LLM for semantic extraction
result = llm_query(f"Extract the total revenue from: {matches[1]}")
print(result)
Step 4: Submit Final Answer¶
Output shown to the user:Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
signature |
str \| Signature |
required | Defines inputs and outputs (e.g., "context, query -> answer") |
max_iters |
int |
20 |
Maximum REPL interaction loops before fallback extraction |
max_llm_calls |
int |
50 |
Maximum llm_query/llm_query_batched calls per execution |
max_output_chars |
int |
10_000 |
Maximum characters to include from REPL output |
verbose |
bool |
False |
Log detailed execution info |
tools |
list[Union[Callable, dspy.Tool]] |
None |
Additional tool functions callable from interpreter code |
sub_lm |
dspy.LM |
None |
LM for sub-queries. Defaults to dspy.settings.lm. Use a cheaper model here. |
interpreter_factory |
Callable[[], CodeInterpreter] |
PythonInterpreter |
Creates one interpreter per invocation. RLM shuts down each returned interpreter. |
Built-in Tools¶
Inside the REPL, the LLM has access to:
| Tool | Description |
|---|---|
llm_query(prompt) |
Query a sub-LLM for semantic analysis (~500K char capacity) |
llm_query_batched(prompts) |
Query multiple prompts concurrently (faster for batch operations) |
print() |
Print output (required to see results) |
SUBMIT(...) |
Submit final output and end execution |
| Standard library | re, json, collections, math, etc. |
Examples¶
Long Document 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)
Using a Cheaper Sub-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)
Multiple Typed Outputs¶
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}")
Custom Tools¶
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]
)
Configuring the Interpreter¶
Pass the interpreter class directly when it needs no arguments. For configured construction, use any zero-argument callable, such as functools.partial:
from functools import partial
rlm = dspy.RLM(
"context, query -> answer",
interpreter_factory=partial(
dspy.PythonInterpreter,
enable_network_access=["example.com"],
),
)
RLM creates and shuts down one interpreter from this factory per invocation. It adds invocation-scoped tools to the returned interpreter’s mutable tools dictionary, so remote sandboxes need a CodeInterpreter adapter that supports that protocol. To reuse a caller-owned interpreter, pass it as the first positional argument when calling the module: rlm(interpreter, context=data, query=query). RLM updates its tools and output metadata but does not shut down or restore it. Reuse is supported only for sequential calls to the same RLM instance; use the factory path for concurrency.
Custom Sandbox-Serializable Inputs¶
For inputs that should be loaded into the sandbox differently from normal Python values, subclass dspy.SandboxSerializable. RLM detects these inputs, sends their serialized payload into the interpreter, runs their setup code, and exposes the reconstructed value under the original input name.
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 also defines a Pydantic schema hook so subclasses can be used directly in DSPy signatures, for example data: DataFrame = dspy.InputField(). The hook is intentionally pass-through: Pydantic accepts the object as-is and serializes it with str(value) for schema/metadata purposes. RLM’s real sandbox transport still comes from to_sandbox() and sandbox_assignment().
Async Execution¶
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())
Inspecting the 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 returns a Prediction with:
- Output fields from your signature (e.g.,
result.answer) trajectory: List of dicts withreasoning,code,outputfor each stepfinal_reasoning: The LLM’s reasoning on the final step
Notes¶
Experimental
RLM is marked as experimental. The API may change in future releases.
Thread Safety
interpreter_factory may be called concurrently and must return a fresh interpreter each time. An interpreter passed as the first positional argument to rlm(...) or rlm.acall(...) is caller-owned and may be reused only for sequential calls to the same RLM instance. PythonInterpreter must also stay on the thread where it was first used.
Interpreter Requirements
The default PythonInterpreter requires Deno to be installed for the Pyodide WASM sandbox.
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
Recursive Language Model module.
Uses a sandboxed REPL to let the LLM programmatically explore large contexts through code execution. The LLM writes Python code to examine data, call sub-LLMs for semantic analysis, and build up answers iteratively.
The default interpreter is PythonInterpreter (Deno/Pyodide/WASM), but
interpreter_factory can create another CodeInterpreter implementation,
such as an adapter for a remote sandbox. RLM updates the interpreter’s
mutable tools dictionary with invocation-scoped tools before execution.
A caller-owned interpreter may be reused sequentially with the same RLM
instance, but must not be shared by overlapping invocations.
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
|
Defines inputs and outputs. String like “context, query -> answer” or a Signature class. |
required |
max_iters
|
int
|
Maximum REPL interaction iterations. |
20
|
max_llm_calls
|
int
|
Maximum sub-LLM calls (llm_query/llm_query_batched) per execution. |
50
|
max_output_chars
|
int
|
Maximum characters to include from REPL output. |
10000
|
verbose
|
bool
|
Whether to log detailed execution info. |
False
|
tools
|
list[Callable] | None
|
List of tool functions or dspy.Tool objects callable from interpreter code. Built-in tools: llm_query(prompt), llm_query_batched(prompts). |
None
|
sub_lm
|
LM | None
|
LM for llm_query/llm_query_batched. Defaults to dspy.settings.lm. Allows using a different (e.g., cheaper) model for sub-queries. |
None
|
interpreter_factory
|
Callable[[], CodeInterpreter]
|
Zero-argument callable that creates an interpreter for each forward pass. The
callable may be invoked concurrently, and DSPy shuts down each interpreter it returns. RLM updates
the returned interpreter’s mutable |
PythonInterpreter
|
Source code in .venv/lib/python3.14/site-packages/dspy/predict/rlm.py
Attributes¶
tools: dict[str, Tool]
property
¶
User-provided tools (excludes internal llm_query/llm_query_batched).
Methods:¶
__call__(*args, **kwargs) -> Prediction
¶
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
forward(interpreter: CodeInterpreter | None = None, /, **input_args) -> Prediction
¶
Execute RLM to produce outputs from the given inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interpreter
|
CodeInterpreter | None
|
Optional caller-owned interpreter, passed positionally. RLM injects invocation tools and output metadata into it but does not shut it down. Reuse is supported only for sequential calls to this RLM instance. |
None
|
**input_args
|
Input values matching the signature’s input fields. |
{}
|
Returns:
| Type | Description |
|---|---|
Prediction
|
Prediction with output field(s) from the signature and ‘trajectory’ for debugging |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required input fields are missing |
CodeInterpreterError
|
If interpreter setup, process, or protocol fails |
Source code in .venv/lib/python3.14/site-packages/dspy/predict/rlm.py
aforward(interpreter: CodeInterpreter | None = None, /, **input_args) -> Prediction
async
¶
Async version of forward(). Execute RLM to produce outputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interpreter
|
CodeInterpreter | None
|
Optional caller-owned interpreter, passed positionally. RLM injects invocation tools and output metadata into it but does not shut it down. Reuse is supported only for sequential calls to this RLM instance. |
None
|
**input_args
|
Input values matching the signature’s input fields. |
{}
|
Returns:
| Type | Description |
|---|---|
Prediction
|
Prediction with output field(s) from the signature and ‘trajectory’ for debugging |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required input fields are missing |
CodeInterpreterError
|
If interpreter setup, process, or protocol fails |
Source code in .venv/lib/python3.14/site-packages/dspy/predict/rlm.py
batch(examples: list[Example], num_threads: int | None = None, max_errors: int | None = None, return_failed_examples: bool = False, provide_traceback: bool | None = None, disable_progress_bar: bool = False, timeout: int = 120, straggler_limit: int = 3) -> list[Example] | tuple[list[Example], list[Example], list[Exception]]
¶
Processes a list of dspy.Example instances in parallel using the Parallel module.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
examples
|
list[Example]
|
List of dspy.Example instances to process. |
required |
num_threads
|
int | None
|
Number of threads to use for parallel processing. |
None
|
max_errors
|
int | None
|
Maximum number of errors allowed before stopping execution.
If |
None
|
return_failed_examples
|
bool
|
Whether to return failed examples and exceptions. |
False
|
provide_traceback
|
bool | None
|
Whether to include traceback information in error logs. |
None
|
disable_progress_bar
|
bool
|
Whether to display the progress bar. |
False
|
timeout
|
int
|
Seconds before a straggler task is resubmitted. Set to 0 to disable. |
120
|
straggler_limit
|
int
|
Only check for stragglers when this many or fewer tasks remain. |
3
|
Returns:
| Type | Description |
|---|---|
list[Example] | tuple[list[Example], list[Example], list[Exception]]
|
List of results, and optionally failed examples and exceptions. |
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
deepcopy()
¶
Deep copy the module.
This is a tweak to the default python deepcopy that only deep copies self.parameters(), and for other
attributes, we just do the shallow copy.
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
dump_state(json_mode=True)
¶
get_lm()
¶
Get the language model used by this module’s predictors.
Returns the language model if every module leaf uses the same LM. Raises an error if multiple different LMs are in use.
Returns:
| Type | Description |
|---|---|
|
The language model instance used by this module’s predictors. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If multiple different language models are being used by the predictors in this module. |
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
load(path, allow_pickle=False, allow_unsafe_lm_state=False)
¶
Load the saved module. You may also want to check out dspy.load, if you want to load an entire program, not just the state for an existing program.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the saved state file, which should be a .json or a .pkl file |
required |
allow_pickle
|
bool
|
If True, allow loading .pkl files, which can run arbitrary code. This is dangerous and should only be used if you are sure about the source of the file and in a trusted environment. |
False
|
allow_unsafe_lm_state
|
bool
|
If True, preserves unsafe LM endpoint keys (e.g.,
|
False
|
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
load_state(state, *, allow_unsafe_lm_state=False)
¶
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
named_parameters()
¶
Unlike PyTorch, handles (non-recursive) lists of parameters too.
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
named_predictors()
¶
Return all named Predict modules in this module.
Iterates through all parameters and returns those that are instances
of dspy.Predict, along with their names.
Returns:
| Type | Description |
|---|---|
|
list[tuple[str, Predict]]: A list of (name, predictor) tuples where name is the attribute path and predictor is the Predict instance. |
Examples:
>>> import dspy
>>> class MyProgram(dspy.Module):
... def __init__(self):
... super().__init__()
... self.qa = dspy.Predict("question -> answer")
... self.summarize = dspy.Predict("text -> summary")
...
>>> program = MyProgram()
>>> for name, p in program.named_predictors():
... print(name)
qa
summarize
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
parameters()
¶
predictors()
¶
Return all Predict modules in this module.
Returns:
| Type | Description |
|---|---|
|
list[Predict]: A list of all Predict instances in this module. |
Examples:
>>> import dspy
>>> class MyProgram(dspy.Module):
... def __init__(self):
... super().__init__()
... self.qa = dspy.Predict("question -> answer")
...
>>> program = MyProgram()
>>> len(program.predictors())
1
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
reset_copy()
¶
Deep copy the module and reset all parameters.
save(path, save_program=False, modules_to_serialize=None)
¶
Save the module.
Save the module to a directory or a file. There are two modes:
- save_program=False: Save only the state of the module to a json or pickle file, based on the value of
the file extension.
- save_program=True: Save the whole module to a directory via cloudpickle, which contains both the state and
architecture of the model.
If save_program=True and modules_to_serialize are provided, it will register those modules for serialization
with cloudpickle’s register_pickle_by_value. This causes cloudpickle to serialize the module by value rather
than by reference, ensuring the module is fully preserved along with the saved program. This is useful
when you have custom modules that need to be serialized alongside your program. If None, then no modules
will be registered for serialization.
We also save the dependency versions, so that the loaded model can check if there is a version mismatch on critical dependencies or DSPy version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the saved state file, which should be a .json or .pkl file when |
required |
save_program
|
bool
|
If True, save the whole module to a directory via cloudpickle, otherwise only save the state. |
False
|
modules_to_serialize
|
list
|
A list of modules to serialize with cloudpickle’s |
None
|
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
set_lm(lm)
¶
Set the language model for all predictors in this module.
This method recursively sets the language model on every leaf module in this module.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lm
|
The language model instance to use for all predictors. |
required |
Examples:
>>> import dspy
>>> lm = dspy.LM("openai/gpt-4o-mini")
>>> program = dspy.Predict("question -> answer")
>>> program.set_lm(lm)