🧠 When Context Becomes a Systems Problem: Recursive Language Models

From MIT Paper to Practical Implementation

Rethinking how LLMs handle long contexts

🤔 The Problem We All Know

Imagine your org has 50,000 GitHub issues across 300 repos...

AI Assistant
Engineer
"Tres equipos reportaron fallas no relacionadas entre sí el trimestre pasado. ¿Existe alguna conexión entre ellas?"
processing 1,000 / 50,000 issues...
LLM
✓ Analysis complete
"Estos fallos parecen ser incidentes no relacionados entre sí que ocurren en diferentes servicios."

🤔 The Problem We All Know

Imagine your org has 50,000 GitHub issues across 300 repos...

Current solutions:

❌ Truncation
Loses crucial information
❌ RAG
Misses what it can't search for
❌ Long-context
Performance still degrades with length

🤔 The Problem We All Know

Imagine your org has 50,000 GitHub issues across 300 repos...

Current solutions:

❌ Truncation
Loses crucial information
❌ RAG
Misses what it can't search for
❌ Long-context
Performance still degrades with length

🤔 The Problem We All Know

Imagine your org has 50,000 GitHub issues across 300 repos...

Current solutions:

❌ Truncation
Loses crucial information
❌ RAG
Misses what it can't search for
❌ Long-context
Performance still degrades with length

📉 Context Rot is Real

Short context
High accuracy — model handles it well
↓ same model, more context
⚠️
Medium context
Quality starts dropping — still within window
↓ keep going
💥
Long context
Collapses — before hitting the limit
Not just a window size problem — attention dilutes with length

Real example (Figure 2, MIT paper):

"In chapter 1, Alice and Bob are alive...

[hundreds of pages]

...In chapter 25, Bob died.

[hundreds of pages]

...In chapter 50: who died?"

GPT-5: "Alice" ❌
The model didn't hit a hard limit — it degraded as the context grew.
💡 The Brilliant Insight from MIT
What if we treat the context as part of the environment instead of loading it all into memory?

💡 The Brilliant Insight from MIT

What if we treat the context as part of the environment instead of loading it all into memory?

Like a programmer with a huge file:

🚫
Don't load everything
RAM would explode — so why do we do this with LLMs?
🔎
Search on demand
Open, grep, filter — inspect only what matters
🧬
Recurse & conquer
Split the problem, delegate to sub-calls, merge results

💡 The Brilliant Insight from MIT

What if we treat the context as part of the environment instead of loading it all into memory?

Like a programmer with a huge file:

🚫
Don't load everything
RAM would explode — so why do we do this with LLMs?
🔎
Search on demand
Open, grep, filter — inspect only what matters
🧬
Recurse & conquer
Split the problem, delegate to sub-calls, merge results

💡 The Brilliant Insight from MIT

What if we treat the context as part of the environment instead of loading it all into memory?

Like a programmer with a huge file:

🚫
Don't load everything
RAM would explode — so why do we do this with LLMs?
🔎
Search on demand
Open, grep, filter — inspect only what matters
🧬
Recurse & conquer
Split the problem, delegate to sub-calls, merge results

💡 The Brilliant Insight from MIT

What if we treat the context as part of the environment instead of loading it all into memory?

Like a programmer with a huge file:

🚫
Don't load everything
RAM would explode — so why do we do this with LLMs?
🔎
Search on demand
Open, grep, filter — inspect only what matters
🧬
Recurse & conquer
Split the problem, delegate to sub-calls, merge results
🧠 This is RLM: Recursive Language Models

🏗️ The 3 Defining Properties of RLM

From the paper (MIT CSAIL 2025):

📌
1 · Symbolic handle to the prompt
Context stored as variable P in memory — never inside the neural network
⚙️
2 · Persistent Turing-complete environment
Python REPL that persists across iterations — define functions, accumulate state, build logic
🔄
3 · Symbolic recursion
LLM calls itself via sub_RLM on context portions — divide and conquer at any depth

🏗️ The 3 Defining Properties of RLM

From the paper (MIT CSAIL 2025):

📌
1 · Symbolic handle to the prompt
Context stored as variable P in memory — never inside the neural network
⚙️
2 · Persistent Turing-complete environment
Python REPL that persists across iterations — define functions, accumulate state, build logic
🔄
3 · Symbolic recursion
LLM calls itself via sub_RLM on context portions — divide and conquer at any depth

🏗️ The 3 Defining Properties of RLM

From the paper (MIT CSAIL 2025):

📌
1 · Symbolic handle to the prompt
Context stored as variable P in memory — never inside the neural network
⚙️
2 · Persistent Turing-complete environment
Python REPL that persists across iterations — define functions, accumulate state, build logic
🔄
3 · Symbolic recursion
LLM calls itself via sub_RLM on context portions — divide and conquer at any depth
🎯 Architecture: RLM High-Level View

RLM (root / depth = 0)
📋 query

S.P + P.M
🧠 Language Model
code ↓      ↑ stdout
⚙️ Environment E (Python REPL)
P = context · llm_query() · extract_after() · peek()
✅ final
response
FINAL: / FINAL_VAR:
📄 context
(1M tokens)

var P
REPL calls llm_query(sub_context) → spawns child RLMs ↓
🎯 Architecture: RLM High-Level View — Recursive Children

RLM (depth=1)
📋 sub-query

hist
🧠 LM
code stdout
⚙️ REPL (P)
✅ result
→ parent REPL
as variable
📄 sub-context

var P
can spawn depth=2 children ↓
RLM (depth=1)
📋 sub-query

hist
🧠 LM
code stdout
⚙️ REPL (P)
✅ result
→ parent REPL
as variable
📄 sub-context

var P
can spawn depth=2 children ↓
Root REPL: r1 = llm_query(chunk1)  ·  r2 = llm_query(chunk2)  ·  ...
🔄 The Iterative REPL Loop
🧠 Root LM (depth=0)
System prompt:
"Answer {query}. Interact with REPL..."
LM generates code directly:
chunk_a, chunk_b = P[:len(P)//2], P[len(P)//2:]
↓ Metadata(stdout)
hist ← hist ∥ code ∥ Metadata(stdout)
len=48203, prefix="[ context... ]"

until state[Final]
Final = res_a + res_b
code
stdout
⚙️ REPL (Python)
In[1]
# P is a string in the REPL — never sent to LM
print(P[:100])
Out[1]: "[ context starts here... ]"
In[2]
chunk_a, chunk_b = P[:len(P)//2], P[len(P)//2:]
res_a = llm_query("find relevant items", chunk_a)
res_b = llm_query("find relevant items", chunk_b)
↗️ spawns RLM (depth=1) × 2
In[N]
# Assign Final — terminates the loop
Final = res_a + "\n" + res_b
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
CodeQA — Q&A over long documents and codebases (23K–4.2M tokens). Tests retrieval and multi-hop reasoning at scale.
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
CodeQA — RLM(GPT-5) 62% vs GPT-5 24%* — 2.6× better
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
BrowseComp+ — Multi-hop questions over 1K web documents (6M–11M tokens total). GPT-5: 0% — context too large for any LLM window.
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 91.3 0 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
BrowseComp+ — RLM(GPT-5) 91.3% vs GPT-5 0%* — ∞ improvement
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 91.3 0 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
OOLONG — "One-Off Long cONtext": needle-in-a-haystack in 131K token documents. GPT-5: 44% — RLM(GPT-5): 56.5%.
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 91.3 0 56.5 44 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
OOLONG — RLM(GPT-5) 56.5% vs GPT-5 44% — 1.3× better
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 91.3 0 56.5 44 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
OOLONG-Pairs — Paired comparison: identify differences between two long documents (32K tokens each). GPT-5: 0.1% — two contexts at once is impossible.
RLM(GPT-5) vs GPT-5 Base — Accuracy (%)
RLM(GPT-5) GPT-5
0 25 50 75 100 62 24 91.3 0 56.5 44 58 0.1 CodeQA BrowseComp+ OOLONG OOLONG-Pairs
RLM wins on every benchmark — and at a lower cost per query than summary-based agents.
Observation 3 — Performance degrades with context length (OOLONG)
GPT-5 limit (272K) 0 25 50 75 100 8K 16K 32K 64K 131K 272K 524K* 1M* Input Context Length (log scale)
GPT-5 (direct) RLM(GPT-5)
Past the 272K window (red zone), GPT-5 collapses. RLM(GPT-5) stays flat at any context length — processing inputs orders of magnitude beyond the base model's limit.

🧠 RLM-Qwen3-8B

What
Qwen3-8B fine-tuned to natively operate as an RLM
First small model trained to be an RLM
Training
~1,000 trajectories from Qwen3-Coder-480B
Domain unrelated to eval benchmarks
Result
+28.3%
avg vs base Qwen3-8B as RLM

📊 Qwen3-8B: Base vs RLM vs Fine-tuned

0% 5% 15% 25% 35% 4% 26% 32% CodeQA 0% 2% 14% BrowseComp+ 0% 24% 32% OOLONG 0.1% 4.3% 5.2% OOLONG-Pairs
Base Qwen3-8B + RLM scaffold RLM fine-tuned
From Theory to Practice
Meet pyrlm-runtime
v0.3.0 — Production-ready Python implementation of the MIT RLM paper. Algorithm 1, recursive subcalls, and everything you need to run RLMs at scale.
Implements Algorithm 1 exactly as described in the paper
Adapters: OpenAI · Azure OpenAI · VertexAI · Ollama · vLLM · Generic
Parallel subcalls · SmartRouter · Elasticsearch retrieval · Live trace
Monty REPL (Rust sandbox) · Conversation history · FileCache
github.com/apenab/pyrlm-runtime
Scan to explore
the repo

🏗️ rlm-runtime Architecture

📋 User Query + Context
🔀 SmartRouter — baseline vs RLM auto-selection
🧠 RLM Orchestrator — Main loop · Conversation history · FINAL detection
⚙️ REPL
Python · Monty 🦀
peek · ask_chunks
llm_query · es_search
🔌 Adapters
OpenAI · Azure
VertexAI · Ollama
vLLM · Generic
🛡️ Policy
max_steps
max_tokens
max_subcalls
📊 Trace + Cache
Full trace
FileCache
RichTraceListener
🔍 Retriever
Elasticsearch
BM25 · kNN
Hybrid RRF
✅ Output: answer + full trace

💻 Minimal Example

from pyrlm_runtime import RLM, Context
from pyrlm_runtime.adapters import OpenAICompatAdapter

# Load your long documents
documents = [
    "Document 1: Very long content...",
    "Document 2: More content...",
    # ... 100s of documents, millions of tokens
]
context = Context.from_documents(documents)

# Initialize RLM
adapter = OpenAICompatAdapter(model="gpt-4o")
rlm = RLM(
    adapter=adapter,
    conversation_history=True,   # LLM sees its own previous attempts
    repl_backend="monty",        # Rust sandbox (secure by default)
)

# Ask questions over the entire context
answer, trace = rlm.run("What is the key term defined in these documents?", context)

print(f"Answer: {answer}")
print(f"Steps: {len(trace.steps)}  |  Tokens: {trace.total_tokens}")

🔀 SmartRouter: Baseline vs RLM

Automatically selects the cheapest execution mode — no manual switching needed.
⚡ Short context (< 8K chars)
→ Direct LLM call
→ 1 API call, minimal tokens
→ Method: "baseline"
🧠 Long context (≥ 8K chars)
→ Full RLM loop
→ REPL + subcalls + trace
→ Method: "rlm"
from pyrlm_runtime import SmartRouter, RouterConfig, ExecutionProfile

router = SmartRouter(adapter, config=RouterConfig(baseline_threshold=8000))
result = router.run(query, context, profile=ExecutionProfile.DETERMINISTIC_FIRST)

print(f"Method: {result.method}")      # "baseline" or "rlm"
print(f"Tokens: {result.tokens_used}")
DETERMINISTIC_FIRST — regex first, fallback to RLM
SEMANTIC_BATCHES — parallel subcalls
HYBRID — deterministic + semantic fallback
VERIFY — double-check with recursive subcalls

⚡ Parallel Subcalls: The Problem

Subcalls are the bottleneck — sequential LLM calls stack up linearly.
Sequential (default)
subcall 1 ──→ wait 2s
subcall 2 ──→ wait 2s
subcall 3 ──→ wait 2s
Total: ~6s · 33 chunks = 66s
Parallel (10 workers)
subcall 1,2,3... ──→ wait 2s
subcall 11,12,13... ──→ wait 2s
subcall 21,22,23... ──→ wait 2s
Total: ~7s · same 33 chunks
Wait only for the slowest in the batch, not the sum of all

🔍 External Retrieval: Architecture

Normal RLM loads all docs into memory. External Retrieval lets the LLM pull documents on demand from any index.

User
Query
loop until FINAL:
LLM
generates code
code → ← stdout
REPL
es_search · es_get
Retriever
ES · Qdrant · custom

RetrieverProtocol — 4 methods
search(query)     BM25 keyword
vector_search(query)   kNN semantic
hybrid_search(query)   BM25 + kNN (RRF)
get(doc_id)       full document
vs Normal RLM
 All docs loaded into RAM upfront
 Docs fetched on-demand from index
 No context needed in rlm.run()
 Scales to millions of documents

🔒 The Security Question

An RLM executes LLM-generated code in a Python REPL. What could go wrong?

⚠️ Current: exec() sandbox
Whitelist-based blocking
Bypasseable via __builtins__
Infinite loops hang the process
Memory bombs crash the host
✅ New: Pydantic Monty
Minimal Python interpreter in Rust
No exec(), no imports, no introspection
Timeout: 5s default (configurable)
Memory limit: 128MB default
🦀 pydantic-monty — by the Pydantic team · designed for LLM code execution

🛡️ Security: Before vs After

Threat
PythonREPL
MontyREPL 🦀
Sandbox escape via __builtins__
VULNERABLE
BLOCKED
Nested exec() / eval()
VULNERABLE
BLOCKED
Introspection __class__.__bases__
VULNERABLE
BLOCKED
Infinite loop while True: pass
HANGS
TIMEOUT 5s
Memory bomb [0]*10**9
CRASH
LIMIT 128MB
Import os / sys
BYPASSEABLE
NO IMPORTS
🔑 Secure by construction — not by blacklist

⚡ Monty: Performance Impact

Isolated REPL (micro-benchmark):

Monty is ~3-4x slower
Simple exec: 3ms vs 1ms
But all times are 1-25ms
Even worst case: 25ms

Full RLM loop (production):

Only ~1.6x slower
Overhead: +0.07ms per execution
REPL is <0.01% of total time
LLM calls dominate: 100-5000ms each
|── LLM call (500ms) ──||REPL||── LLM call (500ms) ──|
The cost of security is practically zero

🎯 Real Use Cases

1. Code Repository
Analyze entire codebases (900K+ tokens). Find implementations across files. Understand architectural decisions.
2. Deep Research
Process 100s of academic papers. Multi-hop reasoning across documents. Evidence synthesis.
3. Document Analysis
Legal contract review (100+ page contracts). Medical records analysis. Technical documentation.
4. Large Corpus Retrieval
Millions of docs in Elasticsearch. RLM directs its own retrieval strategy. No full-corpus loading needed.

When to Use RLM?

✅ Use when...
Context > 50K tokens
Info scattered across entire input
Need to examine most/all content
Accuracy over speed
Cost-sensitive vs frontier models
❌ Don't use when...
Context fits in window (<50K tokens)
Simple search would work
Info is localized (RAG is faster)
Real-time response needed
Task is trivial
Rule of thumb: if baseline truncates or fails → try RLM

🚀 Roadmap

✅ Delivered (v0.3.0)
✓ MontyREPL — Rust sandbox
✓ Parallel subcalls — llm_batch + ThreadPoolExecutor
✓ Conversation history — multi-turn self-correction
✓ Elasticsearch retrieval — BM25 + kNN + hybrid
✓ VertexAI adapter — Google Cloud support
✓ Live trace — RichTraceListener
✓ SmartRouter — auto baseline vs RLM
🔧 Next
→ SmartRouter: adaptive routing improvements
→ Agentic RAG
→ Token efficiency: prompt compression, evidence compression
→ Speed: faster subcall model, early stopping
🙋 Questions?
github.com/apenab/pyrlm-runtime
Try it yourself:
pip install pyrlm-runtime
from pyrlm_runtime import RLM, Context
from pyrlm_runtime.adapters import OpenAICompatAdapter

context = Context.from_documents([...])
rlm = RLM(adapter=OpenAICompatAdapter(model="gpt-4o"))
answer, trace = rlm.run("Your question", context)
Thank you!
Scan to explore
the repo

🤔 "But... isn't an RLM just an Agent?"

🤖 Agent (CodeAct + sub-agents)
 Prompt P loaded into LLM context (hist)
 Output generated autoregressively (Finish action)
 Sub-agent = separate tool call (JSON action)
 1M files → 1M sequential tool invocations
 Context window = hard ceiling
🔄 RLM
 P is a variable in the REPL — never enters context
 Output via FINAL_VAR — unbounded length
 Symbolic recursion: llm_query() lives inside the REPL
 1M files → for-loop / parallel_map in code
 Scales to Ω(|P|) or Ω(|P|²) semantic work
💡 The key insight (Alex Zhang, MIT — paper author):
"The REPL and sub-calling being separate is not a good thing. Sub-calling should be a feature of the language [the REPL], not a separate tool. It is strictly less expressive than the RLM design."

50.000 issues en 300 repos. Un equipo reportó fallos en checkout, otro en inventario, otro en notificaciones. El engineer sospecha que están conectados. El problema: no es que el LLM no sea suficientemente listo. Es que la información simplemente no cabe en su ventana de contexto. ¿Cuáles son las opciones que tenemos hoy?

Truncation: lo más sencillo. Si los 50.000 issues no caben, se procesan los primeros N. Si el issue clave estaba en el repo 250, mala suerte. El engineer habrá analizado una muestra sesgada sin saberlo — lo peor porque da una falsa sensación de cobertura. → ¿Y si en vez de cortar, recuperamos solo lo relevante?

Que buscamos exactamente? Cada issue parece un bug distinto, y el patron solo emerge leyendo muchos issues juntos y correlacionando patrones. RAG necesita una query. Aquí no hay query posible. → ¿Y si simplemente ampliamos la ventana de contexto?

Ventana más grande, mismo problema: el rendimiento sigue cayendo cuanto más crece el contexto. Y pagas por cada token aunque el modelo no los procese bien. → Ese fenómeno de degradación tiene nombre.

→ La solución brillante del MIT: ¿y si el contexto nunca entra en la red neuronal?

Nadie carga un archivo de 10GB en memoria. Lo abres, buscas, filtras. ¿Por qué los LLMs no hacen lo mismo? → Pero entonces, ¿cómo lo inspecciona? Buscando bajo demanda.

Escribe código: peek(), grep(), filtrar. Solo ve lo que necesita, cuando lo necesita. → Y si el problema es tan grande que ni grep alcanza: divide, delega, combina.

Tres ideas sencillas. Juntas eliminan el context rot: el modelo siempre trabaja sobre trozos pequeños, sin importar el tamaño total.

NOTAS — The 3 Defining Properties of RLM (1/3) El contexto vive como variable P en el REPL. El LLM solo ve metadatos: longitud, prefijo. Para leer el contenido, escribe código. Nunca entra en el prompt. → ¿Dónde vive ese código? En un entorno persistente.

El REPL persiste entre iteraciones: variables, funciones, estado acumulado. No es one-shot, es un loop iterativo. El modelo construye su solución paso a paso. Python - ipython — más potente, con autocompletado - jupyter — el notebook (celdas = iteraciones RLM) JavaScript / Node - node — REPL nativo de Node.js - deno — alternativa moderna con TypeScript nativo Java - jshell — REPL oficial desde Java 9 (JEP 222) → Y cuando el problema es tan grande que ni el REPL puede de una vez...

llm_query() lanza un RLM hijo con su propio REPL. Divide y vencerás a cualquier profundidad. Esto es lo que permite escalar a 10M+ tokens. → Veamos cómo encaja todo en la arquitectura.

El context en RLM no es input, es memoria accesible bajo demanda. El contexto NO se envía al LLM — se almacena como variable P en el REPL. El LLM solo ve metadata: longitud, estructura, nº de documentos. El LLM genera código → se ejecuta en el REPL → stdout vuelve al LLM. El loop termina cuando emite FINAL: o FINAL_VAR:. Cuando llama a llm_query(sub_context), se crea un RLM hijo con su propio REPL — recursión arbitraria.

Child 1

Child 2

ellipsis

El LM produce código Python crudo en cada iteración — no wrappers, no acciones explícitas. El hist acumula código + Metadata(stdout) para que el modelo se autocorrija. El loop termina cuando el LM escribe `Final = respuesta` — una asignación Python normal, no una función especial.

LongBench-v2 CodeQA — comprensión de repositorios de código. El modelo recibe un codebase completo y responde preguntas de opción múltiple sobre múltiples ficheros. Contextos de 23K a 4.2M tokens. → GPT-5 24%* → RLM(GPT-5) 62%.

RLM(GPT-5) 62% vs GPT-5 24%*. El asterisco significa que GPT-5 alcanzó su límite de ventana en muchos casos. 2.6× mejor en un benchmark donde GPT-5 ya puede intentarlo. Cuando el contexto supera la ventana, la ventaja se dispara aún más. → Ahora vamos a ver qué pasa con contextos de 6 a 11 millones de tokens.

BrowseComp-Plus: preguntas multi-salto sobre 1.000 documentos web — 6 a 11 millones de tokens en total.

RLM(GPT-5) 91.3% vs GPT-5 0%*. El resultado más espectacular del paper. De 0% a 91.3% simplemente por el cambio de arquitectura. → Ahora contextos más manejables — OOLONG con 131K tokens.

OOLONG — razonamiento sobre textos largos que requiere transformar chunks del input y agregar el resultado. Complejidad lineal. Documentos de 131K tokens. 131K cabe en la ventana de GPT-5 → GPT-5 llega a 44%. Por eso la ventaja de RLM es menor aquí.

RLM(GPT-5) 56.5% vs GPT-5 44%. +12.5 puntos. Mejora moderada — 131K es manejable para GPT-5. RLM sigue siendo mejor y más barato incluso cuando el contexto cabe en la ventana. → OOLONG-Pairs — donde la complejidad es cuadrática y GPT-5 colapsa.

OOLONG-Pairs — razonamiento sobre pares de chunks distribuidos por todo el documento. Complejidad cuadrática. ~32K tokens. Solo 32K tokens — cabe perfectamente en GPT-5. Pero la complejidad cuadrática de la tarea hace que colapse → 0.1%. RLM genera código que itera sobre pares de chunks con extract_after() y peek().

RLM gana en los 4 benchmarks. Patrón claro: cuanto más larga y compleja la tarea, mayor la ventaja. OOLONG (lineal) 1.3×, CodeQA (hasta 4.2M) 2.6×, OOLONG-Pairs (cuadrático) 580×, BrowseComp+ (6-11M) ∞.

Qué es RLM-Qwen3-8B: - Es un Qwen3-8B (modelo pequeño de 8B parámetros) que ha sido fine-tuned para operar NATIVAMENTE como un RLM. - Se entrena con ~1.000 trayectorias RLM generadas por Qwen3-Coder-480B-A35B (60× más grande) actuando como RLM. Por qué importa: - Demuestra que el comportamiento RLM se puede DESTILIAR de un modelo grande a uno pequeño. - El modelo aprende cuándo hacer subcalls, cómo chunkear, y qué inspeccionar — estrategias que un modelo vanilla ignora. - Resultado: +28.3% de media respecto a Qwen3-8B base como RLM, con menor coste de inferencia. Insight clave para el público: no es solo un scaffold alrededor del modelo. El modelo ha APRENDIDO a ser un RLM. El runtime le da la infraestructura, el fine-tuning le da la inteligencia.

El paper salió en enero 2026 y en pocas semanas ya estaba generando conversación en toda la industria. Prime Intellect, Google ADK, Yoav Goldberg, la comunidad de ML — todos reaccionando al mismo paper.

Mencionar que en poco tiempo el proyecto ha evolucionado bastante: empezó como implementación mínima del paper y ahora tiene un ecosistema completo de producción. Puntos clave: - v0.3.0 es la versión disponible el día de la charla. - Los adapters ahora cubren todos los grandes proveedores: cualquier API OpenAI-compatible, Azure, VertexAI (Google Cloud), Ollama/vLLM para local, y GenericChatAdapter para APIs custom con formato no estándar. - Las features nuevas (parallel subcalls, SmartRouter, Elasticsearch, live trace) van más allá del paper original — son las cosas que hicieron falta al usarlo en proyectos reales. - El QR lleva directamente al repo — invitar al público a explorarlo y contribuir. - Las slides siguientes van componente por componente: arquitectura, SmartRouter, subcalls paralelas, retrieval, visualización en vivo.

1. USER QUERY + CONTEXT entra al sistema. 2. SMARTROUTER: Primera decisión — ¿el contexto es pequeño (<8K chars)? Si sí, va directo al LLM como baseline (más rápido, menos tokens). Si no, activa el RLM completo. Ahorra tokens y latencia en casos simples automáticamente. 3. RLM ORCHESTRATOR (rlm.py): El corazón del sistema. Implementa el loop del paper: InitREPL → LLM genera código → REPL ejecuta → stdout vuelve al LLM → hasta FINAL. También gestiona conversation history multi-turn para autocorrección. 4. CINCO COMPONENTES: - REPL: PythonREPL (exec sandbox) o MontyREPL (Rust, secure by construction). Disponibles: peek(), ask_chunks(), llm_query() y ahora también es_search() si hay retriever. - Adapters: Abstracción para todos los proveedores. Ahora incluye VertexAI (Google Cloud) y GenericChatAdapter para APIs custom. - Policy: Límites de recursos. Thread-safe para subcalls paralelas. - Trace + Cache: Trace graba cada step. FileCache evita repetir subcalls idénticos. RichTraceListener visualiza en tiempo real. - Retriever: Conecta con Elasticsearch para corpora muy grandes sin cargar todo en memoria. 5. OUTPUT: respuesta + trace completo para debugging.

Este es el patrón más básico. Tres pasos: cargar documentos, crear el RLM, hacer la pregunta. Puntos a destacar: - `Context.from_documents()` agrupa múltiples documentos con separadores. También existe `Context.from_text()` para un solo bloque. - `conversation_history=True` es el default. Permite al LLM ver sus propias iteraciones anteriores y autocorregirse. - `repl_backend="monty"` activa el sandbox en Rust. Lo explicamos en detalle en las slides de seguridad. - El `trace` devuelve el historial completo: qué código generó el LLM en cada step, qué salió del REPL, cuántos tokens se usaron. - El modelo `gpt-4o` es solo un ejemplo. Funciona con cualquier adapter: Azure, VertexAI, Ollama...

El SmartRouter es una capa de decisión inteligente por encima del RLM. Muchas veces el contexto no es tan grande y no hace falta todo el loop RLM — una llamada directa al LLM es suficiente y mucho más barata. THRESHOLD: Por defecto 8000 chars. Si el contexto es menor, va directo al LLM. Si es mayor, activa el RLM completo. Este threshold es configurable vía RouterConfig. EXECUTION PROFILES: - DETERMINISTIC_FIRST: Intenta primero extracción determinista (regex, extract_after). Si no encuentra respuesta, cae al RLM. Cero tokens en el caso base. Ideal para extracción de campos. - SEMANTIC_BATCHES: Usa subcalls en paralelo para clasificación semántica. Bueno para tareas de clasificación/filtrado. - HYBRID: Combina determinista y semántico. El más robusto para producción. - VERIFY: Usa recursive subcalls para double-check. El más preciso pero más lento.

EL PROBLEMA: En un loop RLM típico el LLM genera código que divide el contexto en N chunks y hace una subcall por chunk. Por ejemplo, 100 documentos → ~33 chunks de 3K chars. Si cada subcall tarda 2 segundos, el tiempo total es 66 segundos — inaceptable en producción. LA SOLUCIÓN: Con ThreadPoolExecutor y 10 workers, enviamos 10 subcalls a la vez. Esperamos el tiempo del más lento del batch (~2s), no la suma. ANALOGÍA: Es como pedir 10 platos en un restaurante. Si el chef cocina de uno en uno tardas 10 veces más que si cocina todos a la vez.

PROBLEMA: El approach RLM del paper asume que todo el contexto está disponible como string en memoria. Funciona bien hasta ~500K documentos razonables. Pero ¿qué pasa con un corpus legal de 10 millones de contratos o una base de conocimiento corporativa? LA SOLUCIÓN: En lugar de cargar todo en P, el LLM puede hacer búsquedas directamente desde el REPL. Las funciones de búsqueda se inyectan en el REPL igual que peek(), llm_query() o ask_chunks(). Para el LLM, llamar a es_hybrid_search() es exactamente igual que llamar a cualquier otra función Python. Lo importante es que el Retriever queda fuera del loop — es un servicio externo que el REPL consulta. El loop real es entre el LLM y el REPL, igual que en el RLM normal, solo que en vez de hacer peek() sobre un contexto en memoria, hace búsquedas sobre un índice externo.

CONTEXTO: Este es un problema fundamental del approach RLM. Estamos dándole a un LLM la capacidad de ejecutar código arbitrario en un Python REPL. Incluso con un LLM "bueno" (GPT-5, Claude), hay riesgos: - Prompt injection: un documento malicioso podría engañar al LLM para que ejecute código peligroso. - Errores del modelo: el LLM podría generar accidentalmente un `while True: pass` o un `[0]*10**9`. - Sandbox escapes: `exec()` en CPython es notoriamente difícil de sandboxear. Hay exploits conocidos via `__builtins__.__import__('os')`, introspection con `__class__.__bases__`, etc. LA SOLUCIÓN: Pydantic Monty (https://github.com/pydantic/monty) es un intérprete Python mínimo escrito en Rust. No es CPython — es un intérprete nuevo que solo implementa un subconjunto seguro de Python. No tiene `exec()`, no tiene `eval()`, no tiene imports, no tiene acceso al MRO de Python. Es "secure by construction", no por whitelist. LÍMITES CONFIGURABLES: timeout (5s default), memoria (128MB), allocations (1M), stack depth (100). Si el código excede cualquier límite, Monty lo mata limpiamente. TRANSICIÓN: "Veamos el impacto concreto en seguridad..."

PUNTO CLAVE: "No es que Monty bloquee estos ataques — es que ni siquiera tiene los mecanismos que los hacen posibles. Es como preguntarle a una calculadora que hackee un servidor."

MENSAJE CLAVE: "La integración de Monty elimina TODAS las vulnerabilidades de seguridad conocidas del REPL, a un costo de rendimiento de +0.07ms por ejecución — menos del 0.01% del tiempo total de un ciclo RLM en producción."

1. CODE REPO: Un repo grande como CPython tiene ~900K tokens. Imposible de meter en un context window. El RLM puede explorar el repo sistemáticamente: primero mira el índice de archivos, luego va a los más relevantes, luego hace subcalls para analizar cada función. 2. DEEP RESEARCH: Imagina que tienes 200 papers sobre un tema. El RLM puede leer el abstract de cada uno, identificar los más relevantes, y luego leer esos en profundidad. Con subcalls paralelas esto es muy rápido. 3. DOCUMENT ANALYSIS: Contratos legales de 100+ páginas. El LLM no puede leer 100 páginas de una vez. Pero el RLM puede ir sección por sección, extraer las cláusulas relevantes, y construir un resumen estructurado. 4. LARGE CORPUS RETRIEVAL (NUEVO): Con la integración de Elasticsearch, el RLM puede trabajar sobre millones de documentos. El LLM decide qué buscar, cómo buscar (keyword vs semantic), y cuándo tiene suficiente información para responder. Es RAG + RLM combinados.

CUÁNDO NO: - Context fits in window: Si tienes <50K tokens, un baseline directo es más rápido y barato. El SmartRouter hace esto automáticamente. - Simple search: Para preguntas tipo "¿cuál es el valor del campo X en este JSON?" no necesitas un loop iterativo. Un regex o extract_after() es suficiente. - Info localized: Si sabes que la respuesta está en una sección concreta, RAG (BM25 + LLM) es más eficiente que un RLM completo. - Real-time: El loop RLM tarda entre 10s y 2 minutos según la complejidad. No es adecuado para respuestas en tiempo real. - Trivial: Extraer un campo de un formulario de 2 páginas no merece un RLM. RULE OF THUMB: "Si el baseline falla o trunca, prueba RLM." Es la decisión más simple posible. El SmartRouter implementa esto automáticamente con el threshold configurable.

NOTAS — RLM vs Agent ESTA ES LA PREGUNTA que todo el mundo hace: "¿pero esto no es simplemente un agente con REPL y subagentes?" La respuesta corta es NO. Parece igual por fuera, pero hay 3 diferencias arquitecturales fundamentales que el paper del MIT llama "Flaws" en el Algorithm 2 (el scaffold de agente convencional): FLAW 1 — DÓNDE VIVE EL CONTEXTO: Un agente (CodeAct, Claude Code, Codex) mete el prompt del usuario directamente en el historial del LLM (hist). Esto significa que el LLM TIENE que leer todo el contexto con atención. Si el contexto es un libro de 1M de tokens, mala suerte — lo tienes en tu context window o haces compaction (que pierde información). En un RLM, el prompt P se carga como VARIABLE en el REPL. El LLM nunca lo ve directamente. Solo recibe metadata constante: longitud, tipo, prefijo corto. Para ver partes del contexto, el LLM escribe código: print(P[:1000]), regex sobre P, etc. Esto es lo que permite escalar a contextos de 10M+ tokens. FLAW 2 — CÓMO SE GENERA EL OUTPUT: El agente genera su respuesta final de forma autoregresiva — la acción "Finish" produce texto token a token. Esto limita el output a lo que cabe en la ventana del modelo. El RLM puede devolver una variable del REPL (FINAL_VAR). Esa variable puede ser un string enorme construido programáticamente — concatenando outputs de subcalls, transformaciones, etc. El output es potencialmente ilimitado. FLAW 3 — RECURSIÓN SIMBÓLICA (la más importante): En un agente, los sub-agentes son tool calls separadas. El LLM dice "llama al sub-agente con este prompt" y el framework hace la llamada externamente. El LLM no puede poner esa llamada dentro de un for-loop o un parallel_map. En un RLM, llm_query() es una función Python dentro del REPL. El LLM puede escribir: results = [llm_query(f"Analiza {chunk}") for chunk in chunks] o incluso: with ThreadPoolExecutor() as pool: results = list(pool.map(lambda c: llm_query(f"Analiza {c}"), chunks)) Esto es RECURSIÓN SIMBÓLICA. La sub-llamada vive dentro de lógica programática. Un agente con 1M de archivos haría 1M de tool calls secuenciales en su contexto. Un RLM escribe un for-loop. Como dice Alex Zhang (el autor del paper): "Desde una perspectiva de lenguajes de programación, la forma en que Codex/Claude Code manejan sub-agentes es casi tonta. Si pensamos en el REPL como un 'lenguaje', las sub-llamadas deberían ser una feature de ese lenguaje, no algo separado." ANALOGÍA ÚTIL: Pensad en un data scientist haciendo EDA en un Jupyter notebook. No carga todo el dataset en una celda — hace print(df.head()), describe(), filtros, groupby. Cada celda es una iteración. Eso es exactamente lo que hace un RLM. El agente sería como pegar todo el CSV en el chat de ChatGPT. TRANSICIÓN: Esta distinción puede parecer sutil, pero es lo que permite que RLM escale donde los agentes colapsan. Y con eso cerramos — preguntas?