¿Por qué tardó cinco segundos esa respuesta?
Un asistente de soporte planifica, llama a dos herramientas, vuelve a preguntar a un modelo y revisa su respuesta. El usuario espera cinco segundos, y la factura sale más alta de lo previsto. ¿Qué paso fue lento, y cuál se llevó el dinero?
El módulo 69 trazó una sola llamada. Esta lección va más allá: una ejecución de agente entera, con tokens, coste y fallos en cada paso.
Al final de esta lección
Nota
Puedo trazar una petición a través de una función de IA y leer la traza para hallar lo lento, lo caro o lo fallido.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Identificador de span | el identificador de un solo span | span_id |
| Identificador del padre | el identificador del span que contiene a este | parent_span_id |
| Estado | si un span terminó bien o con un error | error |
| Span de herramienta | un span para una herramienta que llamó un agente | |
| Recuento de tokens | cuántos tokens entraron o salieron | |
| Estándar abierto | un diseño común que cualquiera puede usar |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un span guarda identificadores, tiempos y datos
Cada span tiene un identificador de span y el identificador de traza de su petición. Nombra a su padre con un identificador del padre, que está vacío en la raíz.
Tiene un nombre, un inicio, un fin y un estado: ok o error. Los datos van en sus atributos. OpenTelemetry guarda los tiempos en nanosegundos desde 1970. Este laboratorio usa milisegundos desde el inicio, para que las cifras sean cortas.
Ejemplo resuelto: un span de modelo
| Campo | Valor |
|---|---|
| trace_id | 7a3f0c1d00000000000000000000aa01 |
| span_id | 0000000000000005 |
| parent_span_id | 0000000000000001 |
| name | modelo: respuesta |
| start y end | 3100 y 4900 |
| status | ok |
| attributes | model m-small-2026-08, input_tokens 2100, output_tokens 300, fallback true |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Qué registra un span de modelo
Registra el modelo y su versión, porque una versión nueva puede cambiar la velocidad y el coste. Registra los tokens de entrada y de salida y el tiempo que tardó. Anota si la llamada se reintentó o pasó a otro modelo como alternativa.
Nunca pongas datos personales en un span en texto claro. El módulo 86, en la lección «Registrar con seguridad en una aplicación de IA», enseña a dejarlos fuera.
Un agente añade un span por cada herramienta
Un agente hace más que llamar a un modelo. Planifica, llama a herramientas y luego vuelve a preguntar al modelo. Da a cada llamada a una herramienta su propio span de herramienta, hijo del span de la petición.
Un span de herramienta registra el nombre de la herramienta, su tiempo y su estado. Una herramienta que falla aparece como un error dentro de una petición que aún puede terminar bien.
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 traza del agente, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const TRACE = \"7a3f0c1d00000000000000000000aa01\";' 'const PRICES = { \"m-large-2026-09\": [3, 15], \"m-small-2026-08\": [1, 4] };' 'const spans = [];' 'function sid(n) { return String(n).padStart(16, \"0\"); }' 'function span(n, parent, name, start, end, status, attributes) {' ' spans.push({ trace_id: TRACE, span_id: sid(n), parent_span_id: parent ? sid(parent) : \"\",' ' name, start, end, status, attributes });' '}' > trace2.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 traza del agente, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'span(1, 0, \"petición del agente\", 0, 5000, \"ok\", {});' 'span(2, 1, \"modelo: plan\", 0, 800, \"ok\", { model: \"m-large-2026-09\", input_tokens: 900, output_tokens: 60 });' 'span(3, 1, \"herramienta: buscar pedido\", 800, 1100, \"ok\", { tool: \"find_order\" });' 'span(4, 1, \"herramienta: fecha de entrega\", 1100, 3100, \"error\", { tool: \"delivery_date\" });' 'span(5, 1, \"modelo: respuesta\", 3100, 4900, \"ok\", { model: \"m-small-2026-08\", input_tokens: 2100, output_tokens: 300, fallback: true });' 'span(6, 1, \"revisar respuesta\", 4900, 5000, \"ok\", {});' 'function kids(id) { return spans.filter(function (s) { return s.parent_span_id === id; }); }' 'function took(s) { return s.end - s.start; }' >> trace2.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 traza del agente, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function cost(a) {' ' const p = PRICES[a.model];' ' return p ? a.input_tokens * p[0] + a.output_tokens * p[1] : 0;' '}' 'function show(id, depth) {' ' for (const s of kids(id)) {' ' const flag = s.attributes.fallback ? \", con alternativa\" : \"\";' ' console.log(\" \".repeat(depth) + s.name + \" \" + took(s) + \" ms \" + s.status + flag);' ' show(s.span_id, depth + 1);' ' }' '}' >> trace2.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 traza del agente, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"traza \" + TRACE);' ' show(\"\", 0);' ' const leaves = spans.filter(function (s) { return kids(s.span_id).length === 0; });' ' leaves.sort(function (a, b) { return took(b) - took(a); });' ' console.log(\"paso más lento: \" + leaves[0].name + \", \" + took(leaves[0]) + \" ms\");' ' const priced = spans.filter(function (s) { return cost(s.attributes) > 0; });' ' priced.sort(function (a, b) { return cost(b.attributes) - cost(a.attributes); });' ' console.log(\"paso más caro: \" + priced[0].name + \", \" + cost(priced[0].attributes) + \" unidades\");' ' for (const s of spans.filter(function (one) { return one.status === \"error\"; })) {' ' const up = spans.find(function (one) { return one.span_id === s.parent_span_id; });' ' console.log(\"fallido: \" + s.name + \", dentro de \" + (up ? up.name : \"nada\"));' ' }' >> trace2.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 traza del agente, parte 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const live = { trace_id: \"7a3f0c1d00000000000000000000aa02\", span_id: sid(7), parent_span_id: \"\",' ' name: \"modelo: comprobación en directo\", start: 0, end: 600, status: \"ok\", attributes: {} };' ' const messages = [{ role: \"user\", content: \"En una frase corta, ¿qué registra un span?\" }];' ' const reply = await ask(messages, \"Un span registra un paso cronometrado de una petición.\", 40);' ' live.attributes = { model: \"lab\", input_tokens: reply.inTokens, output_tokens: reply.outTokens, recorded: reply.recorded };' ' console.log(\"span en directo: \" + JSON.stringify(live));' ' console.log(\"el modelo dijo: \" + reply.text);' '}' 'main();' >> trace2.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 y lee la traza
docker exec -w /home/node/project agent node trace2.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que imprime el Run
| Línea | Qué muestra |
|---|---|
| traza 7a3f0c1d00000000000000000000aa01 | El identificador de traza que comparten todos los spans |
| herramienta: fecha de entrega 2000 ms error | Un span de herramienta que falló |
| modelo: respuesta 1800 ms ok, con alternativa | Un span de modelo que usó otro modelo |
| paso más lento: herramienta: fecha de entrega, 2000 ms | El paso sin hijos que más duró |
| paso más caro: modelo: plan, 3600 unidades | Los tokens por el precio de su modelo |
| span en directo: {...} | Una llamada real como span. Sus tokens son null cuando responde una muestra grabada |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una traza se lee de tres formas
Para la velocidad, busca el paso sin hijos que más duró. Para el coste, multiplica los tokens de cada span de modelo por los precios de su modelo.
Para los fallos, lista cada span con estado error y el span que lo contiene. Un hijo fallido dentro de un span que terminó bien significa que el agente se recuperó.
Un error frecuente
Advertencia
Mucha gente cree que el paso más lento es también el más caro. Aquí la herramienta de fecha de entrega es la más lenta, pero la llamada de plan es la que más cuesta. La espera hace el tiempo y los tokens hacen el coste: revisa ambos antes de cambiar nada.
OpenTelemetry es un estándar abierto para trazas
OpenTelemetry es un estándar abierto para trazas, métricas y registros. Muchas herramientas leen su forma de span, así que un mismo registro sirve para más de un almacén.
También tiene nombres provisionales para los atributos de las llamadas de IA. Esos nombres aún están cambiando, así que toma los de la tabla como un ejemplo. Consulta la documentación actual antes de usar uno.
Nombres provisionales, consulta la documentación actual
| Nuestra clave | Un nombre provisional | Qué guarda |
|---|---|---|
| model | gen_ai.request.model | El modelo que se pidió |
| input_tokens | gen_ai.usage.input_tokens | Los tokens enviados |
| output_tokens | gen_ai.usage.output_tokens | Los tokens que salieron |
| tool | gen_ai.tool.name | La herramienta llamada |
| ninguna aquí | gen_ai.operation.name | El tipo de llamada, por ejemplo chat |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.