Context Drift: por qué tu agente se vuelve tonto en la iteración 15
El agente empieza como un ingeniero senior.
Analiza el problema con precisión quirúrgica. Propone una arquitectura impecable. Modifica los primeros módulos con código limpio y bien tipado.
Pero llega la iteración 14.
De repente olvida la regla que le pusiste en el primer mensaje. Inventa funciones auxiliares que ya existen. Borra código que él mismo escribió hace cinco minutos. Y para resolver un fallo de compilación, entra en pánico y sugiere reescribir la mitad del proyecto.
No se ha vuelto tonto de golpe. Está sufriendo Context Drift: el mismo modelo, con las mismas instrucciones, deja de prestarles atención porque el historial las ha sepultado bajo miles de tokens de basura.
Y aquí va lo importante: esto no se arregla con un modelo mejor ni con una ventana más grande. Se arregla en el bucle.
Por qué el fallo aparece en la iteración 15 y no en la 3
Que un modelo reparte su atención de forma desigual —mucha al principio del prompt, mucha al final, poca en el medio— ya lo conoces. Y si no, lo tienes explicado con detalle en Context Engineering: cómo estructurar la memoria de tus agentes de IA. Este post no va de eso.
Va de lo que pasa cuando ese efecto se combina con un bucle que se ejecuta solo.
En una conversación normal tú controlas lo que entra. En un agentic loop no: cada iteración inyecta la salida de una herramienta sin que nadie la lea. Y esas salidas son enormes. Un npm test, un git diff, un grep sobre un monorepo o un schema de base de datos rondan fácil los 4.000 tokens cada uno.
Supón un agente con 2.000 tokens de system prompt y 3.000 de especificación. Eso es lo que de verdad tiene que respetar: 5.000 tokens fijos de instrucciones.
| Iteración | Salidas de herramientas acumuladas | Total en ventana | Peso de tus instrucciones |
|---|---|---|---|
| 3 | 12.000 tokens | 17.000 | 29 % |
| 8 | 32.000 tokens | 37.000 | 14 % |
| 15 | 60.000 tokens | 65.000 | 8 % |
Tus reglas no han desaparecido: siguen ahí, íntegras, en el token 1. Lo que ha cambiado es que ahora compiten contra sesenta mil tokens de ruido reciente que el modelo también considera relevante.
[System prompt + spec: 5.000 tokens] ──► Alta atención
[Log 1: salida de grep] ──┐
[Log 2: test runner completo] ├── Zona de dilución
[Log 3: schema SQL entero] ──┘
[Instrucción final] ──────────────────► Alta atención
El 92 % de lo que el modelo está leyendo en la iteración 15 es material que ya no sirve para nada. Ahí es donde empieza a alucinar.
Las 3 técnicas para eliminar el Context Drift
1. Poda de salidas de herramientas
Nunca devuelvas al contexto la salida completa de un comando.
Si un test runner ejecutó 120 tests y falló uno, el agente necesita saber tres cosas:
- El estado general (
FAILED). - El nombre del test que falló.
- Las 10 líneas relevantes del stack trace.
Los otros 3.000 tokens de tests en verde son ruido puro: encarecen la factura y compiten por la atención del modelo. Poda en el tool handler, antes de que ese texto llegue al historial. No le pidas al modelo que lo ignore, porque no puede.
Si quieres ver cuánto te está costando esto de verdad, en medir el consumo de tokens de un agente desgloso en qué se te van realmente.
2. El patrón scratchpad: memoria en disco, no en el chat
El peor sitio para guardar el estado de un proyecto es el historial del chat. Es volátil, crece sin control y no puedes consultarlo sin arrastrarlo entero.
El patrón robusto es obligar al agente a mantener un archivo en disco —scratch/state.md— como única fuente de la verdad. En cada paso clave lo actualiza:
- Tareas completadas.
- Tareas pendientes.
- Decisiones técnicas tomadas y descartadas.
Cuando el contexto conversacional se ensucia, tiras la conversación entera y arrancas otra pidiéndole que lea ese archivo. El agente recupera todo su foco por unos cientos de tokens en vez de sesenta mil.
Esta es la versión mínima y sin dependencias del asunto. Si necesitas memoria entre sesiones, perfiles de usuario o recuperación semántica, el terreno está mapeado en implementación de memoria en agentes de IA. Pero empieza por el archivo de texto: resuelve más de lo que parece.
3. Compactación rodante del historial
En lugar de acumular treinta mensajes, sustituye el tramo intermedio por un resumen sintético y conserva intactos el system prompt y los últimos turnos.
Suena trivial, y tiene dos trampas que revientan la petición en producción: cortar un mensaje de resultado de herramienta separándolo de la llamada que lo produjo, y romper la alternancia de roles que exigen algunas APIs.
export type Role = "system" | "user" | "assistant" | "tool";
export interface AgentMessage {
role: Role;
content: string;
}
/**
* Sustituye el tramo intermedio del historial por un resumen.
* Preserva el system prompt y los ultimos `keepLastTurns` mensajes.
*/
export function compactContext(
messages: AgentMessage[],
keepLastTurns = 6,
): AgentMessage[] {
if (keepLastTurns < 1) {
throw new RangeError("keepLastTurns debe ser >= 1");
}
const hasSystem = messages[0]?.role === "system";
const head = hasSystem ? messages.slice(0, 1) : [];
const body = hasSystem ? messages.slice(1) : messages;
if (body.length <= keepLastTurns) return messages;
// Un mensaje `tool` sin la llamada que lo genero es un 400 en la API.
// Retrocedemos el corte hasta que deje de apuntar a un resultado huerfano.
let cut = body.length - keepLastTurns;
while (cut > 0 && body[cut].role === "tool") cut--;
const middle = body.slice(0, cut);
if (middle.length === 0) return messages;
const summary: AgentMessage = {
role: "user",
content:
`[RESUMEN DE PASOS ANTERIORES] Se ejecutaron ${middle.length} operaciones ` +
`de inspeccion y validacion. El estado real esta en scratch/state.md; ` +
`los mensajes siguientes son la fase activa.`,
};
return [...head, summary, ...body.slice(cut)];
}
Dos notas para llevarlo a producción:
- Si tu proveedor exige alternancia estricta de roles, comprueba que
body[cut]no sea otro mensaje de usuario. Si lo es, fusiona el resumen con él en vez de insertarlo aparte. - El resumen genérico es un punto de partida, no el final. En cuanto puedas, genera ese texto con una llamada barata a un modelo pequeño que resuma los pasos reales: qué se intentó, qué falló y por qué. Un resumen que dice "se ejecutaron 12 operaciones" evita la saturación, pero no conserva el aprendizaje.
El coste oculto de compactar (y por qué no debes hacerlo cada turno)
Aquí es donde mucha gente se pega el tiro en el pie.
La caché de prompts de los proveedores funciona por prefijo: se reutiliza el principio del prompt mientras siga siendo idéntico. Cuando compactas, reescribes justo esa parte del historial, así que la petición siguiente se paga entera a precio completo, sin descuento de caché.
Si compactas cada turno, pierdes más de lo que ahorras: tendrás menos tokens, pero pagados todos a tarifa plena y con la caché reconstruyéndose sin parar.
La regla práctica que uso:
- Poda siempre, en cada llamada a herramienta. Eso no toca el prefijo cacheado, porque afecta a lo que todavía no ha entrado.
- Compacta por lotes, cuando cruzas un umbral (por ejemplo, el 60 % de la ventana) o cuando termina una fase completa de trabajo.
- Nunca compactes a mitad de una subtarea. Espera al cambio de fase: es cuando el resumen sale bien y cuando el corte de caché duele menos.
Por qué Spec-Driven Development resuelve el 80 % del problema
El Context Drift no es solo un problema de memoria; es un problema de ambigüedad inicial.
Si no le das al agente una especificación cerrada en un spec.md, tiene que deducir qué hacer sobre la marcha. Y deducir cuesta tokens: exploración, preguntas, archivos abiertos por si acaso, callejones sin salida. Todo eso acaba en el historial y satura la ventana con material que ni siquiera hacía falta.
Con Spec-Driven Development el alcance está delimitado desde el primer segundo: el agente sabe qué archivos puede tocar y qué queda fuera. Menos exploración es, literalmente, menos drift.
Es el método que enseño en profundidad en el curso Construye con IA: de la idea al producto con Claude Code y que tienes explicado paso a paso en el libro de Spec-Driven Development.
Y si además validas con esquemas de Zod todo lo que entra y sale de tus herramientas, cortas el otro vector de degradación: datos deformados que el agente arrastra durante veinte iteraciones sin que nadie los detecte. Cómo blindar esos contratos lo tienes en el curso de Zod para TypeScript.
Lo que puedes aplicar hoy
- Poda los logs. Limita la salida de cada herramienta y resume los errores antes de inyectarlos.
- Externaliza la memoria. Guarda el progreso en un archivo en disco en lugar de confiar en el historial.
- Compacta por fases, no por turnos. Y mira el impacto en la caché antes de darlo por bueno.
En Dominicode Labs diseñamos arquitecturas de agentes que ejecutan tareas de horas sin desviarse del objetivo.
Tener una ventana de contexto gigante no es una excusa para ser descuidado con lo que metes dentro. La diferencia entre un prototipo frágil y un sistema agéntico de producción no está en el modelo: está en qué le dejas leer en la iteración 15.
Preguntas frecuentes
¿Cada cuántas iteraciones conviene compactar el historial de un agente?
No lo ates a un número de iteraciones, átalo a un umbral de ocupación de la ventana y a los cambios de fase. Compactar al cruzar el 60 % de la ventana, o al terminar una subtarea completa, funciona mejor que hacerlo cada N turnos: el resumen sale más limpio y no partes el trabajo por la mitad.
¿Compactar el contexto invalida la caché de prompts y encarece la factura?
Sí. La caché funciona por prefijo idéntico, así que al reescribir el tramo intermedio del historial la siguiente petición se paga completa. Por eso conviene compactar por lotes en lugar de cada turno: podar la salida de las herramientas antes de que entren al historial reduce tokens sin tocar el prefijo ya cacheado.
¿Qué diferencia hay entre compactar el historial y reiniciar la conversación?
Compactar conserva el hilo conversacional y sustituye lo antiguo por un resumen; reiniciar tira todo y arranca de cero leyendo el archivo de estado. Compactar es más fácil de aplicar en mitad de una tarea. Reiniciar limpia mejor, pero solo es viable si el estado real vive en disco y no en el chat.
¿Cómo distingo un context drift de un fallo del modelo?
Por la reproducibilidad. Coge la petición que falló, arranca una conversación nueva con el system prompt, el estado actual y esa única instrucción, y vuelve a lanzarla. Si con el contexto limpio sale bien, no era el modelo: era el historial. Si falla igual, el problema está en tus instrucciones o en la propia tarea.
¿Sirve de algo esto si uso un modelo con ventana de un millón de tokens?
Sirve más, no menos. La ventana grande solo amplía cuánta basura cabe antes de que la petición reviente, pero la atención se sigue diluyendo y el coste por llamada crece con todo lo que arrastras. Una ventana grande es margen de maniobra, no un sustituto de la gestión del contexto.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
