|
📋 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
| ||||
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}")
"baseline""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}")
search(query) BM25 keywordvector_search(query) kNN semantichybrid_search(query) BM25 + kNN (RRF)get(doc_id) full document
context needed in rlm.run()An RLM executes LLM-generated code in a Python REPL. What could go wrong?
exec() sandbox__builtins__exec(), no imports, no introspection__builtins__exec() / eval()__class__.__bases__while True: pass[0]*10**9os / sysIsolated REPL (micro-benchmark):
Full RLM loop (production):
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)
Finish action)FINAL_VAR — unbounded lengthllm_query() lives inside the REPLfor-loop / parallel_map in code50.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?