Compress I: trimming
implementarás gestión de historial por ventana deslizante (trim_messages, RemoveMessage) en el grafo de Magallanes, sabrás exactamente qué se pierde y cuándo basta, y demostrarás la mejora re-ejecutando el sweep de N0 contra el baseline. Es el arreglo más barato del catálogo: la primera vez que bates tu propia curva.
2.1El problema
El presupuesto de N0 ya te dijo dónde duele. En Magallanes, la partida que se dispara son los tool results: cada leer() mete un documento entero en el historial, y el loop append-only no lo descarta. La pendiente que mediste no era ruido — era arrastre que se acumula turno tras turno.
Tienes el plan de intervención sobre la mesa. La palanca para un historial que crece sin límite es compress, y el principio del nivel dice que la apliques primero: es la más barata. Aquí "barata" es literal. El arreglo que vas a escribir son unas diez líneas dentro de un nodo del grafo. La idea: tirar lo viejo.
Pero "tirar lo viejo" esconde dos preguntas que no puedes saltarte. La primera: ¿cuánto recuperas de fiabilidad al hacerlo? La segunda, la incómoda: ¿qué rompes al tirar? Un documento ya leído puede ser basura recuperable. O puede ser la decisión temprana de la que depende la sección 3 del informe. La ventana deslizante no distingue.
En esta lección montas el trimming y eliges su presupuesto con los datos de N0. Después corres otra vez el sweep para ver las dos curvas superpuestas: dónde mejora y dónde el fallo reaparece. Esa honestidad —saber qué tiras— es lo que separa esta intervención de "borrar mensajes y rezar".
2.2Qué vas a poder hacer
Al terminar serás capaz de:
- Implementar un nodo de trimming con
trim_messagesen el grafo de Magallanes, blindando el system prompt y los pares tool-call/tool-result. - Elegir el presupuesto
max_tokensdesde la pendiente que mediste en N0, no a ojo. - Re-ejecutar el sweep de N0 y leer la curva baseline contra la de trimming: dónde mejora, dónde recae.
- Diagnosticar un fallo post-trimming y nombrar qué parámetro lo habría evitado.
Necesitas saber antes:
- De N0·L3 (El presupuesto de tu agente): qué partida del historial crece por turno y por qué. Lo recuperamos en 2.3.
- De N0·L4 (El context sweep): cómo se construye la curva calidad-vs-longitud con
sweep.py. La vuelves a usar en 2.5. - El esqueleto congelado de Magallanes: el grafo de LangGraph con
MessagesState, sus 3 tools (buscar,leer,escribir_seccion) y el flag de longitud del lab. - Que
MessagesStatetrae el reduceradd_messages— la pieza que hace posible el borrado por id. Lo vemos en 2.4.
2.3Recupera
Antes de seguir, responde de memoria. Esto reactiva lo anterior y engancha lo nuevo.
- De N0·L3: en Magallanes, ¿qué partida del presupuesto domina el crecimiento por turno y cuál es su causa estructural?
- Del plan de palancas del nivel: ¿qué palanca corresponde a un historial que crece sin límite, y por qué se aplica antes que las otras?
- De N0·L4: cuando re-ejecutas el sweep tras un cambio, ¿qué tienes que mantener idéntico para que la comparación valga?
Comprueba tu respuesta
- Los tool results dentro del historial. Cada
leer()devuelve un documento completo que se añade y nunca se descarta — el loop ReAct es append-only. Esa acumulación sin límite es la pendiente. · fuente: corpus A.6 (ACON, "unbounded context growth… reasoning degradation") + esqueleto de Magallanes (N0·L3). - Compress — "retaining only the tokens required" (Lance Martin, Context Engineering for Agents, 23 jun 2025). Se aplica primero porque compress sobre el historial es la palanca más barata; el principio operativo es "do the simplest thing that works" (Anthropic, 29 sep 2025). · fuente: corpus B.2, B.3.
- El mismo encargo literal, las mismas ≥4 longitudes y el mismo
seed. Solo cambia la intervención que mides. Si cambias el encargo o el seed, mides otra cosa, no la mejora. · fuente: N0·L4 (protocolo del sweep).
2.4El concepto
La causa: el loop append-only
Aquí está la raíz, y conviene nombrarla antes de tocar código. El loop ReAct de Magallanes acumula cada interacción en un contexto append-only —solo se agrega, nunca se quita—. El corpus lo describe como patrón conocido: "unbounded context growth… prohibitive inference memory costs and reasoning degradation" (ACON, arXiv:2510.00615, Microsoft Research, oct 2025). · fuente: corpus A.6.
Compress es la respuesta a esa causa. La definición de Martin es escueta: compress es "retaining only the tokens required" (23 jun 2025). · fuente: corpus B.2. Retener solo los tokens que la tarea necesita. El trimming es la forma más directa de hacerlo.
Qué es el trimming
La herramienta es trim_messages de langchain_core. Su firma tiene un detalle que importa: salvo messages, todos los argumentos son keyword-only (corpus F.1) — los pasas por nombre o el código no corre.
1from langchain_core.messages import trim_messages
2
3recortados = trim_messages(
4 state["messages"],
5 max_tokens=4000, # el presupuesto del historial (lo eliges en 2.5)
6 token_counter="approximate", # contador rápido sin LLM; o pasa el modelo
7 strategy="last", # conserva los mensajes MÁS RECIENTES
8 include_system=True, # el system prompt NO se trimea
9 start_on="human", # empieza la ventana en un HumanMessage
10 allow_partial=False, # no parte un mensaje por la mitad
11)Este bloque es el trimming: una ventana deslizante que conserva los mensajes más recientes que caben en max_tokens y descarta el resto.
La analogía: es la memoria de trabajo de alguien que toma notas en una reunión larga con un cuaderno de una sola página. Cuando la página se llena, arranca lo de arriba para escribir abajo. Dónde falla la analogía: una persona recuerda lo importante de lo que arrancó; el trimming plano no — descarta por antigüedad, no por relevancia. Eso es justo lo que lo hace barato y lo que lo hace peligroso.
Cada parámetro hace un trabajo concreto:
strategy="last"conserva la cola: los mensajes recientes. La alternativa,"first", conservaría el principio y tiraría lo nuevo — lo contrario de lo que quieres en un agente que avanza. · fuente: corpus F.1.max_tokenses el presupuesto del historial en tokens. Es la decisión de ingeniería de la lección, y la tomas con los datos de N0, no a ojo.token_countercuenta los tokens."approximate"es un shortcut válido (usacount_tokens_approximatelypor dentro); también aceptas el propio modelo, que cuenta exacto pero más lento. · fuente: corpus F.1.include_system=Trueblinda elSystemMessageen el índice 0: queda fuera del recorte. Sin esto, la ventana se traga el system prompt en cuanto el historial crece. · fuente: corpus F.1.start_on="human"hace que la ventana empiece en unHumanMessage. Evita arrancar la ventana en mitad de un intercambio.allow_partial=Falseimpide partir un mensaje. · fuente: corpus F.1.
El borrado dentro del estado: RemoveMessage
trim_messages te da la lista recortada. Pero en un grafo de LangGraph el historial vive en el estado, y quieres borrar selectivamente dentro de ese estado. Para eso está RemoveMessage.
1from langchain_core.messages import RemoveMessage
2
3# Dentro de un nodo: marca para borrado todos menos los últimos 3 mensajes.
4def podar(state):
5 mensajes = state["messages"]
6 if len(mensajes) > 3:
7 return {"messages": [RemoveMessage(id=m.id) for m in mensajes[:-3]]}RemoveMessage(id=...) no borra por sí mismo. Marca un mensaje por su id, y el reducer add_messages es quien ejecuta la eliminación al ver ese objeto en la actualización del estado (corpus F.1). Aquí hay una dependencia que rompe a la gente: RemoveMessage exige ese reducer. Si tu estado usa una lista genérica sin él, no pasa nada — el borrado se ignora en silencio. MessagesState, el estado base de Magallanes, ya trae add_messages, así que funciona. · fuente: corpus F.1.
Qué se pierde
Aquí está la parte honesta. El trimming descarta todo lo anterior a la ventana. Y "todo" incluye cosas que no son ruido:
- Las decisiones tempranas del agente (qué enfoque eligió para el informe).
- El encargo original, si no lo proteges — y por eso
include_system=Trueno es opcional. - Los doc_ids ya leídos: el agente pierde el registro de qué documentos consultó y puede volver a buscarlos.
El trimming plano descarta por antigüedad, no por valor. No tiene forma de saber que ese mensaje del turno 2 era la decisión que sostiene toda la sección 3.
Cuándo basta — y cuándo no
El trimming es suficiente cuando lo relevante es lo reciente y no hay dependencias de largo alcance. Un asistente de soporte donde cada pregunta se resuelve con el contexto de los últimos turnos: la ventana deslizante sobra.
No basta en encargos long-horizon como el de Magallanes. Su informe se construye a lo largo de docenas de turnos; la sección 4 depende de una decisión del turno 3. Cuando la ventana tira ese turno 3, el agente pierde el hilo. Esa es exactamente la grieta que motiva la compaction de L3: lo que no puedes tirar, hay que destilarlo.
Normaliza el error
Dos fallos son tan comunes que conviene nombrarlos antes de que los cometas.
Fallo nº1: trimear el system prompt o el encargo. Si olvidas include_system=True, la ventana acaba comiéndose el system prompt. El síntoma es desconcertante: el agente "olvida" qué informe estaba escribiendo a mitad de run. No es un bug del modelo — le quitaste sus instrucciones.
Fallo nº2: cortar un tool-call dejando su tool-result huérfano. Un leer() produce dos mensajes ligados: la llamada (AIMessage con tool_call) y su resultado (ToolMessage). Si la ventana corta entre ambos, dejas un tool-result sin su llamada — o al revés. Muchos modelos rechazan ese historial mal formado. start_on="human" y allow_partial=False existen para que la ventana no corte en mitad de un par.
Lo que NO es trimming
El trimming no es resumir. No destila ni preserva nada: descarta por completo lo que cae fuera de la ventana. Esa es la diferencia con la compaction de L3, que sí intenta conservar el contenido en forma comprimida. Confundir "trimear" con "resumir" lleva a esperar que el trimming recuerde lo importante. No lo hace — y por eso es barato.
2.5Míralo funcionar
Vamos a añadir el paso de trimming al grafo de Magallanes, elegir su max_tokens con los datos de N0, y re-ejecutar el sweep para superponer las dos curvas. Es el esqueleto congelado del nivel con un nodo nuevo.
Antes de leerlo línea a línea, lee el bloque entero de corrido. Lo difícil aquí no es ninguna línea suelta. Es ver dónde encaja el trimming en el flujo: se recorta el historial justo antes de invocar el modelo, no después. Primero hazte una idea de la forma.
El nodo de trimming en el grafo
1# gestion_historial.py — palanca compress sobre Magallanes (N1·L2)
2from langchain_core.messages import trim_messages
3from langgraph.graph import StateGraph, MessagesState, START, END
4
5# 'modelo' y las 3 tools (buscar/leer/escribir_seccion) vienen del esqueleto congelado.
6
7def llamar_modelo(state: MessagesState):
8 # Recorta el historial ANTES de invocar el modelo: la ventana deslizante.
9 recortados = trim_messages(
10 state["messages"],
11 max_tokens=4000, # presupuesto elegido con los datos de N0·L3 (ver abajo)
12 token_counter="approximate", # rápido; sin coste de LLM
13 strategy="last", # conserva lo reciente
14 include_system=True, # blinda el system prompt — NO opcional
15 start_on="human", # ventana empieza en HumanMessage — pares tool-call/result no quedan huérfanos
16 allow_partial=False,
17 )
18 respuesta = modelo.invoke(recortados)
19 return {"messages": [respuesta]}
20
21builder = StateGraph(MessagesState)
22builder.add_node("llamar_modelo", llamar_modelo)
23# ... el resto del grafo de Magallanes (nodo de tools, edges condicionales) sin cambios ...
24builder.add_edge(START, "llamar_modelo")
25graph = builder.compile()Tres cosas de este nodo:
- El trimming pasa antes de
modelo.invoke. El estado guarda el historial completo; lo que recortas es solo la vista que ve el modelo en este turno. El presupuesto se aplica en cada invocación. MessagesStatees el estado base de Magallanes. Traeadd_messages, así que si más adelante usasRemoveMessagepara borrado persistente, funcionará.- El patrón —trimear dentro del nodo, justo antes de invocar— es una composición natural del diseño de LangGraph, no un snippet copiado de un how-to oficial. La doc de LangChain no publica un ejemplo verbatim de
trim_messagescomo nodo de StateGraph (corpus F.1, ausencia confirmada); el patrón se deriva de la firma.
Elegir max_tokens con los datos de N0
No pongas max_tokens a ojo. Ya tienes la pendiente del historial de N0·L3: la tabla de tokens por turno donde cada leer() era un escalón de miles de tokens. Usa esos números.
El criterio: el presupuesto debe dejar caber varios turnos recientes con sus tool results. Pero debe quedar muy por debajo del punto de inflexión que localizaste en el sweep de N0·L4. Pon un ejemplo: si el codo de tu curva caía donde el contexto llega a ~40K tokens, parte de un presupuesto de historial holgadamente por debajo. No es un valor mágico; es una hipótesis que el sweep confirma o tumba.
Re-ejecutar el sweep
Ahora viene el paso que hace válida la intervención: re-ejecutar el sweep.py de N0·L4 sin tocar nada más. Mismo encargo, mismas longitudes, mismo seed. Lo único que cambió en el grafo es el nodo de trimming. · fuente: corpus F.8 (protocolo del harness; el script sweep.py viene de N0·L4).
1python sweep.py # ahora con el grafo trimeado; mismo encargo, longitudes y seedPara la dim 5 de C1 necesitas el coste comparado. El propio trim_messages con token_counter="approximate" te da la cuenta antes de invocar. Compara sum(count_tokens_approximately([m]) for m in state["messages"]) con sum(count_tokens_approximately([m]) for m in recortados) en cada turno — esa diferencia es la factura del trimming, que reportarás en L5.
El sweep emite la curva del grafo trimeado. La superpones a la baseline de N0. Estos números son ilustrativos —tu run dará los suyos—; lo que importa es la forma de las dos curvas juntas:
longitud (docs irrelevantes) | calidad
--------------------------------------------------
0 docs | baseline 0.92 | trimming 0.92
10 docs | baseline 0.85 | trimming 0.88
25 docs | baseline 0.61 | trimming 0.79
50 docs | baseline 0.34 | trimming 0.48
Lee las dos curvas juntas, no cada número suelto. En las longitudes medias (10–25 documentos) el trimming gana. La ventana tira el relleno irrelevante que el loop append-only habría arrastrado, y la calidad aguanta más arriba. Ahí la palanca hace lo que prometía.
Pero mira las longitudes altas (50 documentos). La curva de trimming sigue por encima de la baseline, sí — pero también cae. ¿Por qué? Porque a esa altura la ventana ya no tira solo relleno: empieza a tirar decisiones tempranas del propio Magallanes. Recuperó algo de calidad descartando ruido, y perdió algo descartando contenido que importaba. Esa caída no es un fallo de tu código — es el límite estructural del trimming plano, y es el problema que L3 ataca.
Self-explanation
Antes de leer la respuesta, intenta tú dos preguntas:
- ¿Por qué
strategy="last"y no"first"? - ¿Por qué
include_system=Trueno es opcional aquí?
Razónalo y comprueba
-
Porque Magallanes avanza: lo más útil para el próximo paso es lo más reciente —el subtema que está investigando ahora, los documentos que acaba de leer—.
strategy="first"conservaría el arranque del run y tiraría justo eso. En un agente que progresa, conservar la cola es la elección correcta. · fuente: corpus F.1. -
Porque el system prompt de Magallanes contiene el encargo y sus instrucciones de comportamiento. Si la ventana se lo come, el agente pierde qué informe escribe y cómo.
include_system=Truelo deja fuera del recorte (lo blinda en el índice 0). Sin esa línea, el run largo se rompe por el fallo nº1. · fuente: corpus F.1.
Si pensaste que include_system es "una buena práctica opcional", revisa: en este grafo es la línea que evita que el agente 'olvide' su propio encargo. No es estilo; es corrección.
2.6Hazlo tú
Andamiaje decreciente: primero comparas presupuestos con el script dado; luego, sin plantilla, construyes un borrado selectivo más fino.
Ejercicio 1 — dos presupuestos, tres curvas
Tienes el grafo trimeado con max_tokens=4000. Corre el sweep con dos presupuestos más: uno más ajustado (p. ej. 2000) y uno más holgado (p. ej. 8000). Superpón las tres curvas resultantes sobre la baseline.
Pista: solo cambia max_tokens en el nodo; el encargo, las longitudes y el seed se quedan idénticos — es la regla del protocolo. · fuente: corpus F.8 (protocolo del harness; el script sweep.py viene de N0·L4).
Elaborative interrogation — antes de correrlo, predice: ¿qué le pasa a la curva si el presupuesto es demasiado ajustado?
Comprueba tu predicción
Un presupuesto demasiado ajustado empeora la curva, no la mejora. Con la ventana muy pequeña, el trimming tira no solo el relleno sino también tool results y decisiones que el agente todavía necesitaba para el turno siguiente. El síntoma se parece al de N0: el agente repite búsquedas o pierde el hilo del subtema en curso.
Hay un punto dulce. Demasiado holgado y el contexto vuelve a saturar (la palanca apenas actúa); demasiado ajustado y tiras contenido vivo. El sweep es lo que te dice dónde está ese punto para Magallanes — por eso eliges max_tokens con datos, no con intuición.
Feedback: si predijiste que "más ajustado siempre comprime más y por tanto mejora", revisa. Comprimir de más es tan dañino como no comprimir: descartas información que la tarea aún usaba. La curva castiga ambos extremos.
Ejercicio 2 — borrado selectivo con RemoveMessage
Sin plantilla. El trimming plano descarta por antigüedad. Construye una variante más fina. En lugar de trimear todo lo viejo, usa RemoveMessage para borrar solo los tool results de documentos que Magallanes ya incorporó al informe, conservando lo demás.
La idea: si escribir_seccion ya volcó el contenido de un documento al informe, ese tool result es recuperable —vive en el informe— y puede irse. Pero las decisiones y el encargo se quedan.
1from langchain_core.messages import RemoveMessage
2
3def podar_docs_incorporados(state):
4 # Marca para borrado los tool results de 'leer' cuyos doc_id ya están en el informe.
5 a_borrar = [
6 m.id
7 for m in state["messages"]
8 if es_tool_result_de_leer(m) and doc_ya_incorporado(m, state)
9 ]
10 return {"messages": [RemoveMessage(id=mid) for mid in a_borrar]}Las funciones es_tool_result_de_leer y doc_ya_incorporado las defines tú según cómo Magallanes marca lo incorporado. Mide esta variante contra el trimming plano en el sweep. No hay solución cerrada — reutiliza el patrón de 2.5.
Pregunta para guiar la medición: ¿en qué tramo de la curva esperas que el borrado selectivo gane al trimming plano, y por qué? (Pista: piensa dónde el trimming plano tiraba decisiones que el selectivo conserva.)
2.7Comprueba
Sin pistas. Gate de maestría: diagnosticar un fallo post-trimming.
Re-ejecutaste el sweep con trimming. En el run de la longitud alta, lees la traza y ves esto: el informe cubre los subtemas 2 y 3 con detalle, pero ignora por completo el subtema 1. Como si nunca lo hubieran pedido.
- Diagnostica: ¿qué se trimeó para que ocurra esto?
- Corrige: ¿qué parámetro de
trim_messageslo habría evitado, o qué otro arreglo aplicarías? - Predice: si subes la longitud del sweep aún más, ¿qué esperas que pase con este síntoma?
Criterio de corrección + feedback
-
Diagnóstico: la ventana deslizante tiró el principio del run, donde estaba el encargo del subtema 1 o la decisión temprana de cómo abordarlo. Con
strategy="last", los primeros mensajes son los primeros en caer. Si el encargo de los tres subtemas vivía en el primerHumanMessagey no en el system prompt, el trimming lo descartó — y el agente, al no verlo, no investigó el subtema 1. -
Corrección: la causa raíz es que el encargo no estaba blindado. Dos arreglos válidos: (a) mover el encargo completo al
SystemMessagey mantenerinclude_system=True, para que quede fuera del recorte; (b) si el encargo debe ser unHumanMessage, subir las dependencias de largo alcance no se resuelve con trimming — se resuelve destilándolas, que es la compaction de L3. El parámetroinclude_systemsolo protege el system prompt; el encargo en un human turn temprano no lo protege. -
Predicción: al subir la longitud, el síntoma empeora. Cuanto más largo el run, más mensajes recientes empujan al encargo fuera de la ventana, y antes ocurre el olvido. El subtema 1 será el primero en desaparecer en runs cada vez más cortos. Es la señal de que el trimming plano tocó techo en este encargo.
Feedback formativo: si diagnosticaste que se trimeó el encargo temprano, dominas lo esencial — ese es el fallo nº1, el que invalida un run long-horizon de raíz. Si propusiste solo subir max_tokens como arreglo, está incompleto: un presupuesto mayor retrasa el olvido pero no lo evita en un run suficientemente largo. La diferencia entre "retrasar" y "evitar" es justo lo que separa trimming de compaction. Gate: necesitas el diagnóstico (qué se trimeó) y un arreglo que blinde el encargo para superar este punto.
2.8Conecta
Acabas de batir el baseline por primera vez. No es retórica: la curva de trimming queda por encima de la de N0 en las longitudes medias, con el mismo encargo, longitudes y seed. Eso es lo que la rúbrica de C1 te pedirá demostrar — mejora medida (dim 4) y su coste (dim 5).
Y lo hiciste con la palanca más barata, la primera del plan: unas diez líneas en un nodo. "Do the simplest thing that works" (Anthropic, 29 sep 2025) no era un eslogan — era el orden correcto de ataque. · fuente: corpus B.3.
Pero la curva de trimming se cae donde las dependencias tempranas importan. Lo viste en 2.5 y lo diagnosticaste en 2.7: a longitud alta, la ventana ya no tira relleno, tira decisiones. Y una decisión no se puede recuperar descartándola — solo destilándola.
Esa es la frontera de esta lección. Lo que no se puede tirar hay que comprimirlo sin perderlo: resumir el historial viejo preservando decisiones, estado y pendientes. Eso es la compaction, y es N1·L3. El run muerto de Magallanes a 50 documentos —el que reaparece al final de tu curva de trimming— es justo el problema con el que abre la próxima lección.
2.9Reflexiona
Tómate un minuto. Responder esto por escrito consolida lo aprendido mejor que releer.
- ¿Qué aprendiste? Resume en una frase por qué el trimming es barato y por qué eso mismo lo hace peligroso en runs long-horizon.
- ¿Qué sigue sin estar claro? ¿Tienes claro la diferencia entre
include_system(qué protege) y el problema del encargo en un human turn temprano (qué NO protege)? Si no, vuelve a 2.7. - ¿Qué harías distinto? La próxima vez que alguien proponga "borrar el historial viejo" como arreglo, ¿qué dos preguntas le harías antes de aceptarlo?
Esto requiere práctica. La intuición de "cuánto presupuesto deja vivo lo que importa" llega corriendo sweeps con distintos max_tokens, no leyendo. En L3 destilas lo que aquí tuviste que tirar.