Encuentra el paso lento
Trazar una llamada a un modelo es registrar qué hizo cada paso de una petición y cuánto tardó. Importa porque una función con modelo falla en mitad de una cadena. Una línea de registro al principio y otra al final no dicen dónde.
Un usuario avisa de que las respuestas tardan diez segundos. ¿Fue el prompt, el modelo, un reintento o la revisión posterior? Una traza lo responde de un vistazo.
Al final de esta lección
Nota
Puedo registrar lo que hace cada llamada para encontrar de dónde viene un fallo o un retraso.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Traza | el registro de una petición, de principio a fin |
| Span | un paso cronometrado dentro de una traza |
| Padre | el span que contiene a otro span |
| Atributo | un dato con nombre guardado en un span |
| Id de traza | el id que comparten todos los spans de una petición |
| Censura | quitar los secretos antes de guardar un registro |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una traza es un árbol de spans
Una traza es una petición. Se compone de spans. Cada span tiene un nombre, un inicio, un final y un padre: el span que lo contiene. Solo un span no tiene padre: la raíz.
Todos los spans de una petición llevan el mismo id de traza. Así los reúne un almacén. Un span guarda también atributos, como la ruta o el resultado.
Ejemplo resuelto: una petición
| Span | Padre | Inicio | Final | Duró |
|---|---|---|---|---|
| petición | ninguno | 0 | 856 | 856 ms |
| construir el prompt | petición | 0 | 4 | 4 ms |
| llamada al modelo | petición | 4 | 854 | 850 ms |
| revisar la respuesta | petición | 854 | 856 | 2 ms |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Lo que una traza nunca debe guardar
Un almacén de trazas lo lee mucha gente y se conserva semanas. Por eso un span nunca guarda un secreto: una clave de API, un token o una contraseña. Nunca guarda datos personales: un correo, un nombre o un teléfono.
Normalmente tampoco debe guardar el prompt ni la respuesta. Guarda tamaños, cuentas y resultados. Quita las claves peligrosas en un solo sitio, antes de guardar el span.
Un error frecuente
Advertencia
Mucha gente cree que guardar el prompt entero en el registro es inofensivo, porque el registro es interno. Pero los prompts contienen lo que escribieron los usuarios, y escriben nombres y secretos. Guarda la longitud y el resultado, no el texto.
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 el trazador, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'let now = 0; // un reloj que movemos nosotros: todos imprimen los mismos tiempos' 'const spans = [];' 'function tick(ms) { now = now + ms; }' 'function start(name, parent) {' ' const span = { id: spans.length + 1, name, parent, start: now, end: now, attrs: {} };' ' spans.push(span);' ' return span;' '}' 'function finish(span) { span.end = now; }' > trace1.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 trazador, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Claves que nunca llegan a una traza: secretos y datos personales.' 'const NEVER = [\"email\", \"api_key\", \"prompt\", \"name\"];' 'function note(span, key, value) {' ' span.attrs[key] = NEVER.includes(key) ? \"[quitado]\" : value;' '}' >> trace1.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 trazador, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function show(parent, depth) {' ' for (const s of spans) {' ' if (s.parent === parent) {' ' console.log(\" \".repeat(depth) + s.name + \" \" + (s.end - s.start) + \" ms \" + JSON.stringify(s.attrs));' ' show(s.id, depth + 1);' ' }' ' }' '}' >> trace1.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 trazador, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function modelStep(root) {' ' const call = start(\"llamada al modelo\", root.id);' ' const messages = [{ role: \"user\", content: \"Di un uso de una traza, en una frase.\" }];' ' const reply = await ask(messages, \"Una traza muestra qué paso de una petición fue lento.\", 40);' ' tick(850); // 850 ms fijos en nuestro reloj, tarde lo que tarde la llamada real' ' note(call, \"resultado\", reply.text.length > 0 ? \"ok\" : \"vacío\");' ' note(call, \"prompt\", messages[0].content);' ' finish(call);' '}' >> trace1.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 trazador, parte 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const root = start(\"petición\", 0);' ' note(root, \"ruta\", \"/pregunta\");' ' note(root, \"email\", \"[email protected]\");' ' const build = start(\"construir el prompt\", root.id);' ' tick(4); finish(build);' ' await modelStep(root);' ' const check = start(\"revisar la respuesta\", root.id);' ' tick(2); finish(check); finish(root);' ' console.log(\"traza t-0001\");' ' show(0, 0);' '}' 'main();' >> trace1.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 e imprime la traza
docker exec -w /home/node/project agent node trace1.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una maqueta y cómo leerla
El laboratorio no tiene servicio de trazas. Por eso los spans están en una lista y el reloj es un número que mueve el script. Un sistema real envía los spans a un servicio de trazas real. La forma es la misma.
Para hallar el paso lento usa el tiempo propio: la duración de un span menos las duraciones de sus hijos. Un padre siempre parece lento, porque contiene a sus hijos.
Una salida posible
| Línea | Qué muestra |
|---|---|
| traza t-0001 | El id de traza |
| petición 856 ms | La raíz, con la ruta y el correo quitado |
| construir el prompt 4 ms | Un hijo, con dos espacios de sangría |
| llamada al modelo 850 ms | El paso lento, con el prompt quitado |
| revisar la respuesta 2 ms | El último hijo |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.