No pagues dos veces por una respuesta
Guardar en caché una respuesta de un modelo es almacenarla y devolverla cuando llega la misma petición. Importa porque una llamada cuesta dinero y segundos, y muchas peticiones se repiten.
Una página de ayuda pide al modelo que resuma la misma política de devoluciones para cada visitante. Sin caché, mil llamadas pagan mil veces la misma respuesta. Con caché basta una llamada, y las otras novecientas noventa y nueve son aciertos.
Al final de esta lección
Nota
Puedo guardar en caché respuestas de un modelo con seguridad y decir qué hace que haya un acierto.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Caché | un almacén de respuestas que ya pagaste |
| Clave de caché | el valor que decide si dos peticiones coinciden |
| Acierto | una petición respondida desde la caché |
| Fallo | una petición que tiene que llamar al modelo |
| Caducidad | el momento en que una entrada se descarta |
| Dato personal | todo lo que identifica a una persona |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
La clave se construye con toda la petición
Dos peticiones solo pueden compartir respuesta si el modelo ve lo mismo. Por eso la clave de caché cubre todo lo que cambia la respuesta. Eso es el modelo, cada mensaje con su rol y los parámetros, como la temperatura y el límite de respuesta.
Si dejas uno fuera, sirves la respuesta equivocada. Un hash, un resumen criptográfico de ese texto, mantiene la clave corta.
Ejemplo resuelto: qué cambia la clave
| Cambio | ¿Misma clave? | Por qué |
|---|---|---|
| Mismo modelo, mensajes y parámetros | Sí | El modelo ve la misma petición |
| Una palabra distinta en un mensaje | No | Los mensajes forman parte de la clave |
| Temperatura 0 y luego 1 | No | Las respuestas pueden diferir |
| Otro modelo | No | Otro modelo responde distinto |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Las entradas caducan y lo personal queda fuera
Una respuesta envejece. Dale a cada entrada una caducidad: un tiempo de vida en segundos. Pasado ese tiempo se descarta y la siguiente petición es un fallo.
No guardes nunca lo personal. Una respuesta hecha con el correo o el pedido de un usuario podría servirse al siguiente que tenga la misma clave. Revisa la petición antes de buscar y salta la caché para ella.
Un error frecuente
Advertencia
Mucha gente cree que la caché siempre es segura, porque la respuesta es solo texto. Una respuesta guardada con los datos de una persona puede mostrarse a otra. Decide qué se puede guardar antes de guardar la primera entrada.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la caché, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const crypto = require(\"crypto\");' 'const { ask } = require(\"./model\");' '' 'let now = 0; // segundos, en un reloj que movemos nosotros' 'const TTL = 60; // una entrada vive 60 segundos' 'const cache = new Map();' > cache2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la caché, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// La clave se construye con toda la petición: modelo, mensajes y parámetros.' 'function keyOf(request) {' ' const whole = JSON.stringify([request.model, request.messages, request.temperature, request.max_tokens]);' ' return crypto.createHash(\"sha256\").update(whole).digest(\"hex\").slice(0, 12);' '}' '// Los datos personales no se guardan nunca. Aquí: un correo o un número largo.' 'function personal(request) {' ' return /@|[0-9]{7,}/.test(JSON.stringify(request.messages));' '}' >> cache2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la caché, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function answer(request, sample) {' ' if (personal(request)) return \"omitida, es personal\";' ' const key = keyOf(request);' ' const found = cache.get(key);' ' if (found && found.expires > now) return \"acierto\";' ' const reply = await ask(request.messages, sample, request.max_tokens);' ' cache.set(key, { text: reply.text, expires: now + TTL });' ' return found ? \"caducado, fallo\" : \"fallo\";' '}' >> cache2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la caché, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const tip = [{ role: \"user\", content: \"Da un consejo sobre cachés, en una frase.\" }];' ' const base = { model: \"lab\", temperature: 0, max_tokens: 40, messages: tip };' ' const other = { ...base, temperature: 1 };' ' const secret = { ...base, messages: [{ role: \"user\", content: \"Escribe a [email protected] un consejo.\" }] };' ' const sample = \"Guarda solo lo que es igual para todos.\";' ' for (const at of [0, 30, 90]) {' ' now = at;' ' console.log(\"t=\" + at + \" s: \" + (await answer(base, sample)));' ' }' ' console.log(\"misma clave con otra temperatura: \" + (keyOf(base) === keyOf(other)));' ' console.log(\"petición personal: \" + (await answer(secret, sample)));' '}' 'main();' >> cache2.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecuta la caché
docker exec -w /home/node/project agent node cache2.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Esto es una maqueta
El laboratorio no tiene servidor de caché. La caché es un Map en el script y el reloj es un número que fija el script. Un sistema real usa un servicio de caché real que comparten todos los servidores.
Las reglas son las mismas: una clave con toda la petición, una caducidad y una revisión de datos personales.
Una salida posible
| Línea | Qué muestra |
|---|---|
| t=0 s: fallo | La primera petición llama al modelo |
| t=30 s: acierto | La misma petición, respondida desde la caché |
| t=90 s: caducado, fallo | La entrada tenía 60 segundos, así que se vuelve a llamar |
| misma clave con otra temperatura: false | Un parámetro nuevo es una clave nueva |
| petición personal: omitida, es personal | Nunca se tocó la caché |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.