Estado y memoria: lo que el bucle recuerda

Estado y memoria: lo que el bucle recuerda

30 min read

Desde la lección sobre los permisos, ninguna llamada se ejecuta sin pasar por la tabla, y ninguna observación puede saltársela. El bucle, en cambio, sigue tal como lo escribió la lección que le dio su contrato: llama al modelo con M y le añade, turno tras turno, lo que el modelo responde y lo que devuelven sus herramientas, sin quitar nada. Al modelo no le llega nada más. Es todo lo que sabe de lo que lleva hecho.

Cuando un agente contesta lo tendré en cuenta, la frase es verdad o no según dónde quede escrita. Si sólo está en su respuesta, dura lo que dure M: hasta que la conversación termina, o hasta que M deja de caber en la ventana de contexto y algo tiene que salir. Si una llamada la escribe en un fichero, dura lo que dure el fichero. Esta lección separa esos dos sitios, los dos a los que se llama «memoria», y dice qué guarda cada uno, cuánto cuesta y qué pasa cuando no dicen lo mismo.

Mira primero cómo se llena M. En el explorable, un agente va por su séptima llamada en una tarea: preparar el menú de la semana con las recetas del usuario. Cada llamada lleva el prompt de sistema, las herramientas (su nombre, su descripción y su esquema viajan con cada llamada), el pedido y los seis turnos anteriores, cada uno partido en dos: sus llamadas y sus observaciones. Los tamaños los he puesto yo, redondos, del orden que daría un tokenizador de verdad. De los 3 280 tokens que han añadido los seis turnos, 2 440 son observaciones, y casi todas son las seis recetas que leyeron los turnos 2 y 3. Las escrituras de los turnos 5 y 6 pesan en sus llamadas, que llevan dentro lo que escriben.

La séptima llamada de un agente que prepara el menú de la semana: lo que lleva cada llamada, turno a turno, y lo que entra en la ventana según quién decida qué sobra. Casi todo lo que añaden los turnos son observaciones.

La ventana empieza en 3 584 tokens, y todo suma 4 840. Con «lo trunca el modelo», lo primero que se pierde es lo que escribieron el usuario y quien hizo el harness: salen el prompt de sistema, las herramientas, el pedido y el turno 1, y de las observaciones del turno 2 entra el final, sin la llamada que las pidió. El modelo sigue trabajando sin saber para qué. Con «lo descarta el harness» salen los dos turnos más viejos, enteros, cada llamada con su observación: el modelo pierde la lista de recetas y las tres primeras que leyó. Pero esas recetas no se han ido a ninguna parte. Siguen en el entorno, y una llamada las vuelve a traer. Baja ahora la ventana a 1 536 tokens: lo que el harness no descarta ya no cabe. (El tercer modo, compactar, es el de la lección sobre la ventana, y su contrato es del bloque siguiente.)

La otra memoria se ve entre dos conversaciones. El martes, el usuario le cuenta al agente que Marta, que viene a cenar el sábado, es vegetariana, y el modelo contesta que lo tendrá en cuenta. El sábado el usuario abre una conversación nueva y pide el menú. El bucle empieza con un M de dos mensajes, y del martes no le llega nada al modelo. Si aquel día el modelo hubiera pedido además un escribir que añadiera la frase a notas.txt, el sábado un leer se lo habría dicho.

Dos memorias con un nombre

Con precisión, el agente tiene dos estados, y cada uno tiene su contrato.

M, la lista de mensajes del pseudocódigo del bucle:

  • Es lo único que lee el modelo, entero, en cada llamada.
  • Crece dos mensajes por turno, y cuánto añade cada turno lo deciden las llamadas del modelo. Una lectura entra en M con todo lo que devuelve su herramienta, y una escritura, con todo lo que escribe.
  • Cada llamada tiene que caber en la ventana de contexto, con las herramientas y la reserva para la respuesta.
  • Dura una conversación. bucle lo empieza desde inicio y lo devuelve al terminar. Si el harness sigue la conversación, el inicio del pedido siguiente es ese M con el mensaje nuevo detrás; si no, M se pierde.

El entorno, todo lo que tocan las herramientas:

  • Dura entre turnos y entre conversaciones, y otros pueden cambiarlo: el usuario, otro programa.
  • El modelo no lo ve. Sabe de él lo que le han contado las observaciones.
  • No ocupa ventana hasta que se lee, y entonces ocupa lo que se lea.

Así, lo que hay en M tiene dos orígenes. Una parte sólo está en M: el prompt de sistema, el pedido, cada texto del modelo (su plan, lo que ha concluido) y lo que contestó el usuario a cada pregunta de un permiso. La otra parte es una copia del entorno: la observación de una lectura dice lo que había en un fichero, y el efecto de una escritura está en el fichero que cambió. Lo primero, si sale de M, se pierde. Lo segundo lo vuelve a traer una llamada.

Eso dice qué cuesta menos sacar de M cuando no cabe. Una observación de lectura vieja es lo más barato: recuperarla cuesta un turno. El pedido es lo más caro, porque no hay herramienta que lo devuelva. Un modelo que trunca hace justo lo contrario, y pierde el pedido antes que nada. Qué puede sacar el harness, y qué no puede sacar nunca, es el contrato de la compactación, en el bloque siguiente.

Una observación es una copia con fecha

Una observación es verdad en el momento de su llamada, y M no tiene forma de decir hasta cuándo. M sólo crece: ninguna línea del bucle reescribe una observación vieja cuando el fichero cambia, ni la marca. Una observación está desactualizada cuando lo que dice ya no es lo que hay en el entorno.

Puede quedarse así por dos caminos. Uno es el propio agente: la lectura de lista.txt de un turno sigue en M después de que otro turno reescriba el fichero. Ése se resuelve leyendo M en orden, porque la escritura también está en M, detrás. El otro camino no deja rastro. Alguien cambia el entorno sin pasar por las herramientas del agente (el usuario edita la lista a mano entre dos pedidos, un programa reescribe un fichero mientras el agente trabaja) y M sigue diciendo lo de antes, con la misma seguridad.

Con una herramienta como escribir, que sustituye el fichero entero, una observación desactualizada se convierte en un cambio perdido. El modelo escribe la lista a partir de lo que M dice de ella y borra lo que añadió el usuario, y ninguna llamada falla. Lo que lo evita es leer antes de escribir, y eso es una costumbre del modelo: se le puede pedir en el prompt de sistema, pero no garantizar. La garantía tiene que estar en el contrato de la herramienta. La herramienta de edición del bloque siguiente pide el texto que espera encontrar en el fichero y falla si no lo encuentra, así que una copia vieja no puede pisar un cambio.

Lo que tiene que durar se escribe

El M de una conversación no llega a la siguiente. Lo que el agente necesite saber otro día (lo que le contó el usuario, por dónde iba una tarea larga, qué decidió y por qué) tiene que acabar en el entorno, escrito por una llamada. Para el harness es una escritura como cualquier otra: pasa por el esquema y por el permiso, y su observación dice cuántos caracteres ha escrito. Lo que cambia es para quién es: para un modelo que todavía no ha empezado, que lo leerá con un M vacío. El fichero de proyecto del bloque siguiente es esa idea con un nombre fijo, un fichero del repositorio que el harness lee al empezar.

Las dos memorias, sobre el modelo guionizado

Las dos celdas corren sobre el modelo guionizado, como todo el bloque, con el entorno de entorno.py y el ejecutar y el bucle de harness.py, sin tocar. (El permiso de la lección anterior se quedó en sus celdas, y aquí no hace falta.) La primera cuenta lo que ocupa M con el tokenizador de tu mini-GPT, como en la lección sobre la ventana. El usuario guarda en el entorno una novela, Marianela, la del corpus de tu mini-GPT, y pregunta cuántos años tiene la Nela. Dos guiones contestan lo mismo: uno lee la novela entera y el otro busca años. Las observaciones que espera cada guion salen, como en la lección anterior, de ejecutar sus llamadas sobre una copia del entorno.

import json
from pyodide.http import open_url
exec(open_url("/courses/llm-agents/guionizado.py").read()) # ModeloGuionizado
exec(open_url("/courses/llm-agents/entorno.py").read()) # el entorno y sus herramientas
exec(open_url("/courses/llm-agents/harness.py").read()) # ejecutar y el bucle, sin tocar
exec(open_url("/courses/llm-agents/bpe.py").read()) # codificar: aquí sólo cuenta tokens
F = [tuple(par) for par in json.load(open_url("/courses/llm-agents/bpe-merges.json"))]
def tokens(contenido): # un texto, o una lista de bloques escrita en JSON
if not isinstance(contenido, str):
contenido = json.dumps(contenido, ensure_ascii=False)
return len(codificar(contenido, F))

NOVELA = quitar_cabecera(open_url("/courses/llm-agents/corpus.txt").read())
E = dict(ENTORNO, **{"libros/marianela.txt": NOVELA}) # el usuario guarda una novela
INICIO = [("sistema", "Trabajas con los ficheros del usuario mediante tus herramientas."),
("usuario", "¿Cuántos años tiene la Nela? La novela está en libros/marianela.txt.")]
FINAL = texto("Dieciséis: «Dicen que tengo diez y seis años», le dice ella al doctor.")
GUIONES = {"leer": [[llamada("t1", "leer", ruta="libros/marianela.txt")], [FINAL]],
"buscar": [[llamada("t1", "buscar", patron="años")], [FINAL]]}

def vistos(respuestas, entorno): # lo que contesta el harness: ejecuta cada llamada
copia = dict(entorno)
return [[ejecutar(b, copia) for b in r if b["type"] == "tool_use"] or None for r in respuestas]

print("las herramientas, en cada llamada: %d tokens" % tokens(HERRAMIENTAS))
for nombre, respuestas in GUIONES.items():
modelo, recibe = ModeloGuionizado(guion_de(INICIO, respuestas, vistos(respuestas, E))), []
def contado(M, herramientas): # el mismo modelo, apuntando lo que ocupa M
obs = sum(tokens(c) for rol, c in M if rol == "usuario" and not isinstance(c, str))
recibe.append((len(M), sum(tokens(c) for rol, c in M), obs))
return modelo(M, herramientas)
motivo, final, M = bucle(contado, INICIO, dict(E), 10)
print("\n%s: %s · %s" % (nombre, motivo, final))
for turno, (mensajes, total, obs) in enumerate(recibe, 1):
print(" llamada %d: %d mensajes, %d tokens, %d de observaciones" % (turno, mensajes, total, obs))

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

Las herramientas ocupan 849 tokens y viajan en las dos llamadas de cada guion, antes de que el modelo haya pedido nada. La segunda llamada del guion que lee recibe un M de 142 453 tokens, y 142 312 son la observación de t1: más que la ventana de 128 000 de la lección sobre la ventana, aunque tu tokenizador corta el texto más fino que uno de verdad. Un modelo de verdad rechazaría esa llamada, modelo lanzaría y el bucle se pararía ahí. El guion que busca recibe 8 924 tokens y contesta lo mismo. El harness ha hecho lo mismo en los dos: ejecutar lo que se le pide. Y si la tarea siguiera, la observación viajaría entera en cada llamada siguiente, porque M sólo crece.

La segunda celda sigue una conversación de dos pedidos. En el primero, el agente añade a la lista lo que falta para el gazpacho. Entre los dos pedidos, el usuario abre lista.txt y añade, a mano, aceitunas. El segundo pedido va en la misma conversación: su inicio es el M del primero con el mensaje nuevo detrás, y el bucle no cambia. Lo contestan dos guiones, escritos los dos por mí: uno escribe la lista a partir de lo que ya tiene en M, y el otro la relee antes.

# Necesita la celda anterior: vistos y las piezas de los tres ficheros.
INICIO = [("sistema", "Trabajas con los ficheros del usuario mediante tus herramientas."),
("usuario", "Añade a lista.txt lo que falta para el gazpacho.")]
GAZPACHO = "pan, leche, patatas, tomates, pepino, pimiento, ajo"
PRIMERO = [[llamada("t1", "leer", ruta="recetas/gazpacho.txt"), llamada("t2", "leer", ruta="lista.txt")],
[llamada("t3", "escribir", ruta="lista.txt", contenido=GAZPACHO)],
[texto("He añadido a lista.txt tomates, pepino, pimiento y ajo.")]]
entorno = dict(ENTORNO)
_, final, M = bucle(ModeloGuionizado(guion_de(INICIO, PRIMERO, vistos(PRIMERO, entorno))),
INICIO, entorno, 10)
print("1.", final)

entorno["lista.txt"] += ", aceitunas" # el usuario, a mano, entre un pedido y el siguiente
print(" lista.txt en el entorno: ", entorno["lista.txt"])
print(" lo último que M dice de ella:", muestra(M[4][1][0]))

SIGUE = M + [("usuario", "Añade también lo de la tortilla.")] # la misma conversación
TORTILLA = llamada("t4", "leer", ruta="recetas/tortilla.txt")
LISTA = "pan, leche, patatas, tomates, pepino, pimiento, ajo, aceitunas, huevos, cebolla"
SEGUNDO = { # dos guiones para el mismo pedido; los dos los he escrito yo
"de memoria": [[TORTILLA],
[llamada("t5", "escribir", ruta="lista.txt", contenido=GAZPACHO + ", huevos, cebolla")],
[texto("He añadido huevos y cebolla.")]],
"relee": [[TORTILLA, llamada("t5", "leer", ruta="lista.txt")],
[llamada("t6", "escribir", ruta="lista.txt", contenido=LISTA)],
[texto("He añadido huevos y cebolla.")]]}
for nombre, respuestas in SEGUNDO.items():
e = dict(entorno)
_, final, M2 = bucle(ModeloGuionizado(guion_de(SIGUE, respuestas, vistos(respuestas, e))),
SIGUE, e, 10)
print("\n2. %s: %d mensajes en M · %s" % (nombre, len(M2), final))
print(" lista.txt:", e["lista.txt"])

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

Lo último que M dice de la lista es la escritura de t3, y el entorno tiene además las aceitunas. El guion «de memoria» escribe sobre lo que dice M, y las aceitunas desaparecen con un texto final que no miente: ha añadido huevos y cebolla. El que relee gasta una llamada más y las conserva. Los dos M terminan con 13 mensajes, y en ninguno hay nada que diga cuál de las dos listas es la buena. Eso sólo lo sabe el entorno.

Comprueba tu intuición

Cuatro preguntas: qué decide lo que ocupa una observación, qué sabe el modelo en una conversación nueva, qué se puede recuperar del entorno y por qué se pierde un cambio.

En la primera celda, para el mismo pedido, la observación de leer ocupa 142 312 tokens y la de buscar, 8 788. ¿Qué decide cuánto entra en M?

El martes, el usuario le dice al agente Marta es vegetariana; tenlo en cuenta para la cena del sábado. y el modelo contesta De acuerdo, lo tendré en cuenta., sin ninguna llamada. El sábado, en una conversación nueva, el usuario pide el menú. ¿Qué sabe el modelo de Marta?

Supón que el harness tiene que quitar de M los turnos más viejos. ¿Cuáles de estas cosas podría el modelo recuperar después con una llamada?

Select every correct option. This is graded all-or-nothing: there is no partial credit.

En la segunda celda, el usuario añade aceitunas a mano entre los dos pedidos. ¿Por qué las pierde el guion «de memoria»?


El bloque tiene ya todas sus piezas: herramientas con esquema, un bucle que termina, errores que el modelo lee, permisos que ninguna observación cambia y una respuesta a qué recuerda cada parte. Pero están repartidas en cinco lecciones, en celdas que cargan ficheros congelados y guiones escritos para enseñar una cosa cada vez, y nadie ha comprobado que funcionen juntas. Tampoco gestiona nadie M cuando no cabe: esta lección dice qué sería barato sacar, y el harness todavía no saca nada, lo que queda para el bloque siguiente.

La lección siguiente, el proyecto del bloque, junta las piezas en un agente completo sobre el entorno en memoria, con su permiso y su tope, y lo prueba de punta a punta contra el modelo guionizado, con guiones que se equivocan a propósito.

Was this helpful?
Further reading3 sources · 1 paper, 2 articles

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • MemGPT: Towards LLMs as Operating Systems
    paperPacker, Wooders, Lin, Fang, Patil, Stoica y Gonzalez, 2023arXiv:2310.08560EN

    Las dos memorias de esta lección con nombres de sistema operativo: el contexto principal, que el modelo ve, y el externo, al que llega con funciones. Y un modelo que decide qué pasa de uno a otro.

  • Effective context engineering for AI agents
    articleRajasekaran, Dixon, Ryan y Hadfield (Anthropic), 2025anthropic.com/engineeringEN

    Qué hacer cuando M se llena: quitar las observaciones viejas, que el modelo puede volver a pedir; tomar notas fuera de la ventana; compactar. Lo segundo es lo que esta lección llama escribir lo que tiene que durar.

  • Effective harnesses for long-running agents
    articleJustin Young (Anthropic), 2025anthropic.com/engineeringEN

    Un agente que trabaja en muchas sesiones seguidas, cada una con un M vacío. Lo que enlaza una con la siguiente está en el entorno: un fichero de progreso y el historial del repositorio.