Ir al contenido
wannacode

La ventana de contexto

En resumen: La ventana de contexto es la cantidad de tokens que un modelo ve en una solicitud; el modelo no tiene memoria, así que cada solicitud lleva el prompt de sistema y el historial, y cuando la ventana se llena, los mensajes más antiguos se caen.

Problema

El asistente olvidó el número

El asistente de soporte habla con un cliente. En el primer mensaje, el cliente da el número de pedido: 1042. Después pregunta por una oficina de mensajería, por un código de descuento, por cambiar una chaqueta. En la quinta pregunta («¿cuándo llega exactamente el paquete?») el asistente responde: «¿Cuál es tu número de pedido?».

El cliente se molesta: ya lo dijo. Y a fin de mes la factura del modelo es mayor de lo que explica el número de conversaciones.

Ambos problemas tienen una sola causa. No está en el modelo, sino en lo que el programa le envía.

La idea

El modelo no tiene memoria

Un modelo de lenguaje no recuerda nada entre dos solicitudes. Recibe texto, escribe una continuación y olvida. Así que la «memoria» de la conversación vive en tu programa: en cada turno envía de nuevo el prompt de sistema y todo el historial, todos los mensajes hasta ahora, más la pregunta nueva.

El modelo ve solo lo que está en la solicitud, y solo hasta cierta cantidad de tokens. Ese límite se llama ventana de contexto. Todo entra en ella: el prompt de sistema, el historial, la pregunta nueva y la respuesta que el modelo está por escribir.

Qué tan grande es la ventana depende del modelo. Como ejemplo: desde unas decenas de miles hasta cerca de un millón de tokens. Suena a mucho, pero los tokens se acaban más rápido que las palabras, y cada token de la solicitud se paga.

Míralo

La conversación en código

Un mensaje es un diccionario con un rol (user es el cliente, assistant es el modelo) y un texto, tal como lo piden los SDK reales:

Los tokens se cuentan de forma aproximada para que los números se vean. El conteo real lo da el tokenizador del modelo. La solicitud es el prompt de sistema más cada mensaje:

// aproximado: 4 tokens por cada 3 palabras (el tokenizador da la cuenta real)
function tokens(text) {
const words = text.trim().split(/\s+/).length;
return Math.floor((words * 4 + 2) / 3);
}
// la solicitud: el prompt de sistema y todo el historial
function requestTokens(system, history) {
let total = tokens(system);
for (const m of history) {
total += tokens(m.content);
}
return total;
}

La conversación del problema: cinco preguntas del cliente y cuatro respuestas.

const system = "Eres el asistente de soporte de la tienda. Responde breve y cortés.";
const customer = [
"Hola, el pedido 1042 aún no llegó. Lo pedí hace cinco días.",
"¿Pueden dejarlo en una oficina de mensajería en vez de mi dirección?",
"También tengo un código de descuento que no funcionó al pagar. ¿Qué hago?",
"Gracias. La chaqueta del mismo pedido me queda pequeña. ¿Puedo cambiarla por otra talla?",
"Una última cosa: ¿cuándo llega exactamente el paquete?",
];
const assistant = [
"Disculpa la demora. El pedido 1042 está con el mensajero y llegará mañana temprano.",
"Sí, el paquete espera en la oficina de mensajería, calle Mayor 12.",
"El código vale desde 50 euros. Te devolvemos la diferencia en 5 días.",
"El cambio es gratis hasta 14 días después de recibirla. Escríbenos cuando llegue.",
];

En cada turno la pregunta entra en el historial y se envía todo el historial. El precio es un ejemplo: 3 € por millón de tokens de entrada.

const history = [];
let total = 0;
console.log("turno solicitud total");
for (let i = 0; i < customer.length; i++) {
history.push({ role: "user", content: customer[i] });
const sent = requestTokens(system, history);
total += sent;
console.log(`${String(i + 1).padStart(3)} ${String(sent).padStart(7)} ${String(total).padStart(6)}`);
if (i < assistant.length) {
history.push({ role: "assistant", content: assistant[i] });
}
}
const price = 3.0; // € por millón de tokens de entrada (un precio de ejemplo)
const one = (total * price) / 1_000_000;
console.log(`una conversación: ${one.toFixed(4)} €, 20000 conversaciones: ${(one * 20000).toFixed(2)} €`);
Salida
turno solicitud total
1 32 32
2 67 99
3 101 200
4 138 338
5 167 505
una conversación: 0.0015 €, 20000 conversaciones: 30.30 €

La quinta solicitud tiene 167 tokens, aunque la pregunta nueva tiene solo 11. Toda la conversación tiene 167 tokens, pero se enviaron 505: el primer mensaje se envió cinco veces. 0,0015 € por conversación no es mucho. Pero el costo crece con cada turno, no solo con cada conversación nueva, y 20 000 conversaciones al mes ya son 30,30 €.

La idea

Cuando la ventana se llena

La conversación crece, la ventana no. Cuando la solicitud no cabe, el proveedor la rechaza con un error o la recorta él mismo. Es mejor que tu programa decida qué se cae.

La regla más común: conserva siempre el prompt de sistema y los mensajes más nuevos, tantos como quepan. Los más antiguos se caen. La función va del mensaje más nuevo hacia atrás y se detiene en el primero que ya no cabe:

// de lo más nuevo hacia atrás, mientras quepa
function fitWindow(history, budget) {
const kept = [];
let used = 0;
for (let i = history.length - 1; i >= 0; i--) {
const t = tokens(history[i].content);
if (used + t > budget) break;
kept.unshift(history[i]);
used += t;
}
return kept;
}

La ventana de la lección es de 150 tokens, pequeña a propósito para que quepa en una pantalla. El programa recorta la conversación del problema dos veces: una solo con la regla y otra con una nota que el programa guarda por separado, junto al prompt de sistema.

const history = [];
customer.forEach((text, i) => {
history.push({ role: "user", content: text });
if (i < assistant.length) history.push({ role: "assistant", content: assistant[i] });
});
const limit = 150;
console.log(`la solicitud: ${requestTokens(system, history)} tokens, la ventana: ${limit}`);
let window = fitWindow(history, limit - tokens(system));
let used = requestTokens(system, window);
let seen = window.some((m) => m.content.includes("1042"));
console.log(`sin nota: ${window.length} de ${history.length} mensajes, ${used} tokens`);
console.log(` 1042 en la ventana: ${seen ? "sí" : "no"}`);
const note = "Nota: pedido 1042.";
window = fitWindow(history, limit - tokens(system) - tokens(note));
used = requestTokens(system, window) + tokens(note);
seen = note.includes("1042") || window.some((m) => m.content.includes("1042"));
console.log(`con nota: ${window.length} de ${history.length} mensajes, ${used} tokens`);
console.log(` 1042 en la ventana: ${seen ? "sí" : "no"}`);
Salida
la solicitud: 167 tokens, la ventana: 150
sin nota: 7 de 9 mensajes, 132 tokens
1042 en la ventana: no
con nota: 7 de 9 mensajes, 136 tokens
1042 en la ventana: sí

Sin la nota se caen los dos mensajes más antiguos, y con ellos el número 1042. El modelo no puede usar un texto que no ve, así que vuelve a preguntar. La nota tiene 4 tokens y lo devuelve.

Míralo

La ventana se llena

// de lo más nuevo hacia atrás, mientras quepa
function fitWindow(history, budget) {
const kept = [];
let used = 0;
for (let i = history.length - 1; i >= 0; i--) {
const t = tokens(history[i].content);
if (used + t > budget) break;
kept.unshift(history[i]);
used += t;
}
return kept;
}
Paso a paso, en palabras
  1. La ventana admite 150 tokens. El prompt de sistema va siempre primero: 16 tokens.
  2. Turno 1: el cliente da el número de pedido, 1042. La solicitud tiene 32 tokens.
  3. Cada solicitud lleva todo el historial: 67, 101, 138 tokens. Enviados hasta ahora: 338.
  4. Turno 5: la solicitud tiene 167 tokens y la ventana 150. No cabe.
  5. fitWindow va desde lo más nuevo hacia atrás y se detiene en el primer mensaje que no cabe. Los dos más antiguos se caen: quedan 132 tokens.
  6. El número de pedido 1042 estaba en ellos. El modelo ya no lo ve y volverá a preguntarlo.
  7. La solución: el programa guarda una nota «pedido 1042» junto al prompt de sistema. 136 tokens, y el número se ve.
  8. La conversación tiene 167 tokens, pero se enviaron 505: el historial se paga de nuevo en cada turno.

Se caen mensajes enteros, desde el más antiguo. El prompt de sistema y la nota nunca se caen: el programa los pone por separado, antes del historial.

Pruébalo

Encuentra el error

Un colega escribe fitWindow por su cuenta. Cuando la conversación se alarga, el asistente empieza a responder preguntas de hace diez minutos y parece no oír la última. ¿Qué línea tiene el error?

Elige la línea con el error.

Pruébalo

Predice la salida

Cuatro mensajes cortos y un presupuesto de 25 tokens. El conteo es el mismo: 4 tokens por cada 3 palabras, redondeado hacia arriba. ¿Qué imprimirá el programa?

const history = [
{ role: "user", content: "Pedí dos chaquetas y un gorro el viernes pasado temprano." },
{ role: "user", content: "Solo llegó una chaqueta." },
{ role: "assistant", content: "El gorro y la segunda chaqueta salen en otro paquete." },
{ role: "user", content: "¿Y cuándo?" },
];
const window = fitWindow(history, 25);
console.log(window.length, window[0].content);
Respuesta

Pruébalo

Completa

Cada solicitud lleva el prompt de sistema. Termina el conteo.

function requestTokens(system, history) {
  let total = ;
  for (const m of history) {
    total += tokens(m.content);
  }
  return total;
}

La idea

Qué conservar y cuándo un contexto largo perjudica

Formas de hacer caber una conversación en la ventana:

  • Los últimos n mensajes (una ventana deslizante). La más simple y la más común. Pierde el inicio.
  • Una nota con los datos que el programa guarda por su cuenta: un número de pedido, una dirección, lo que ya se prometió. Barata y exacta, pero tienes que saber qué datos importan.
  • Un resumen de los turnos viejos, escrito por el modelo. Conserva más, pero cuesta otra solicitud y puede omitir un detalle.
  • Buscar en lugar de enviar todo. Los documentos de la tienda no entran completos en la solicitud: el programa encuentra los fragmentos que responden la pregunta. Eso es la búsqueda semántica, la base de RAG, y tiene su propio módulo más adelante en el curso.

Cuándo un contexto largo perjudica:

  • Costo y lentitud. Cada token de la solicitud se paga y se lee en cada turno. Un historial el doble de largo es una solicitud el doble de cara y una respuesta más lenta.
  • Perdido en el medio. Los modelos aprovechan mejor el inicio y el final de un contexto largo que el medio. Un dato enterrado en medio de 50 páginas puede pasar inadvertido.
  • La ventana no es una base de datos. Lo que debe recordarse mañana (pedidos, preferencias) se guarda en la base de datos de la tienda. A la solicitud solo entra lo que necesita esta respuesta.
  • Texto ajeno en el contexto. Todo lo que está en la ventana influye en la respuesta, incluso el texto que tú no escribiste. Por qué eso es peligroso se explica en la lección sobre inyección de prompts, en el módulo de agentes.
Más precisamente, para curiosos

Por qué lo que envías crece tan rápido. Sea S los tokens del prompt de sistema y sea m lo que cada turno suma al historial. La solicitud del turno k es de unos S + k·m. La suma de n turnos es n·S + m·(1 + 2 + … + n) = n·S + m·n·(n + 1) / 2. En palabras: una conversación el doble de larga cuesta cerca de cuatro veces más tokens enviados. Por eso los chats largos se recortan o se resumen.

La respuesta también está en la ventana. Si la ventana es de 200 000 tokens (un ejemplo) y la solicitud es de 199 000, quedan 1000 para la respuesta. Por eso la solicitud tiene un límite para la respuesta (max_tokens, en la lección sobre la llamada a la API), y tiene que caber junto con el historial.

El modelo de bigramas tiene una ventana de un token. El modelo de la lección Predecir el siguiente token mira solo la última palabra. El número 1042 del inicio de la frase nunca le llega. Los modelos grandes miran miles de tokens atrás, pero siempre hasta un límite.

Por qué la ventana es limitada. La atención, la idea de la lección Embeddings: el significado como vectores, compara cada token con cada token anterior. Con el doble de tokens hay cerca de cuatro veces más comparaciones. Por eso un contexto largo es caro para el proveedor y lento para ti.

Caché de prompts. Los proveedores pueden guardar el inicio repetido de una solicitud (el prompt de sistema, los documentos) y cobrar menos por él. Cómo usarlo está en la lección sobre costo y latencia.

Contadores reales. Aquí los tokens son palabras × 4 / 3. Un tokenizador real da cerca de 4 caracteres por token en inglés, y para otros idiomas suele dar bastantes más tokens por el mismo significado. La respuesta de la API te dice el número exacto (usage.input_tokens).

Ejercicio

Una conversación larga

Resuelve la tarea en tu editor.

El asistente mantiene una conversación de 20 turnos. El prompt de sistema tiene 200 tokens, cada pregunta del cliente tiene 32 y cada respuesta tiene 45. Escribe un programa que calcule la solicitud de cada turno (el prompt de sistema, todas las preguntas y respuestas pasadas y la pregunta nueva) y el total enviado. Imprime cada quinto turno y, al final, el costo a 3 € por millón de tokens de entrada (un ejemplo) para una conversación y para 10 000.

Salida
turno 5: solicitud 540, total 1930
turno 10: solicitud 925, total 5785
turno 15: solicitud 1310, total 11565
turno 20: solicitud 1695, total 19270
una conversación: 0.0578 €, 10000 conversaciones: 578.10 €

La solicitud veinte tiene 1695 tokens, y en total se envían 19 270, más de once veces la última.

Más tareas

  • Una ventana de 1000 tokens. Cuando la solicitud pase de 1000, descarta el turno más antiguo (pregunta y respuesta). ¿Cuánto se envía ahora en total?
  • Una nota en lugar del historial. En lugar de todo el historial, envía el prompt de sistema, una nota de 60 tokens y los últimos 2 turnos. Compara el costo.

Test

Ponte a prueba

1. El cliente escribe una segunda pregunta al asistente. ¿Qué envía el programa al modelo?
2. ¿Qué es la ventana de contexto?
3. La conversación tiene 5 turnos y la última solicitud tiene 167 tokens. ¿Por qué se envían 505 en total?
4. fitWindow recorta el historial. ¿Por qué va desde el mensaje más nuevo hacia atrás?
5. ¿Cuál NO ayuda cuando un dato importante del inicio de la conversación se cae de la ventana?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·