Volver a la ficha del curso Construir con LLMs: API, herramientas y MCP

Módulo 1 Cuándo una respuesta del modelo cuenta como tarea terminada

Qué vas a construir: una integración que no se fía de un HTTP 200

14 min

Hasta ahora has usado el modelo a través de Claude Code, que se encarga de hablar con él por ti. Desde este módulo lo llamas tú, desde tu propio código. Y la primera llamada parece una función cualquiera: le mandas un texto, te devuelve otro texto y la petición responde con un HTTP 200, el código con el que un servidor dice «todo bien».

Esa sensación de «ya está» es la trampa de la que va este módulo.

Así es una llamada al modelo

Con el SDK oficial de TypeScript (la librería @anthropic-ai/sdk, que envuelve la API HTTP de Claude), una llamada mínima es esto:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // lee la clave de ANTHROPIC_API_KEY

const response = await client.messages.create({
  model: "claude-sonnet-5-5",
  max_tokens: 16000,
  messages: [{ role: "user", content: "Clasifica esta solicitud de renovación: …" }],
});
  • model es el modelo que contesta.
  • max_tokens es el máximo de tokens que puede generar. Un token es un trozo de texto, a menudo una parte de palabra; por tokens se mide la longitud y por tokens se paga.
  • messages es la conversación. Aquí, un solo mensaje del usuario.

¿Por qué 16.000 tokens para clasificar un email? En Sonnet 5.5 el modelo piensa antes de responder: el thinking viene activado por defecto. Lo que piensa también cuenta dentro de max_tokens y se cobra como salida. Con un límite justo, como 1.024, hasta una clasificación sencilla puede cortarse antes de llegar a la respuesta. 16.000 deja margen en una llamada sin streaming. Si la tarea es simple, puedes pedirle que piense menos con output_config: { effort: "low" }. Con low, en las pasadas grabadas de la lección 6, cada clasificación escribió unos 300 tokens de salida y no llegó a pensar (thinking_tokens: 0). Si aun así el pensamiento se come el presupuesto, la respuesta llega con stop_reason: "max_tokens" y sin texto, y el runtime la trata como cortada.

En el proyecto, el modelo, el max_tokens y el esfuerzo se eligen en un solo sitio, live.config.json: claude-sonnet-5-5, low para clasificar, medium para el bucle con herramientas del módulo 2 (su papel loop) y, en el objeto api, 16.000 tokens.

Si vienes de otros tutoriales, quizá eches de menos temperature. El SDK actual manda la versión de API 2026-10-21, y con ella la API rechaza temperature, top_p y top_k con un error 400 en todos los modelos. No los mandes (lo tienes con su fecha de revisión en los datos con fecha).

Lo que vuelve trae bastante más que el texto:

{
  "content": [
    { "type": "thinking", "thinking": "", "signature": "…" },
    { "type": "text", "text": "…" }
  ],
  "stop_reason": "end_turn",
  "usage": { "input_tokens": 142, "output_tokens": 310 }
}
  • content es una lista de bloques. Léelos por su type y no por su posición. Si el modelo ha pensado, el primero es un bloque thinking con el texto vacío: por defecto la API no te enseña el razonamiento, solo una firma (signature) para poder reenviarlo. Después viene el texto. También puede llegar una petición para usar una herramienta (tool_use), una función tuya que el modelo quiere ejecutar.
  • stop_reason dice por qué paró el modelo. end_turn significa que terminó. max_tokens, que se quedó sin espacio, a mitad de frase o incluso antes de escribir nada si se le fue el presupuesto pensando. refusal, que se ha negado a hacerlo, y entonces el campo stop_details dice de qué tipo es la negativa. tool_use, que pide ejecutar una herramienta. Hay alguno más (stop_sequence, pause_turn, model_context_window_exceeded), y el runtime del proyecto los trata todos.
  • usage cuenta los tokens de entrada y de salida, pensamiento incluido, que es lo que te van a cobrar.
  • Además, cada respuesta lleva un identificador único (el request ID, algo como req_018E…). El SDK lo expone como response._request_id y es lo que te pedirá soporte para investigar una llamada.

Fíjate en lo importante: una respuesta cortada por max_tokens o una negativa llegan también con HTTP 200. El 200 solo dice que el servidor ha contestado. Si tu código mira el 200 y el texto, ya está contando como éxito cosas que no lo son.

Prompt de sistema y varios turnos

Dos piezas más que vas a usar en casi todas las llamadas:

const system = "Eres el asistente de los gestores de cuentas de Nexo Renewals. Escribes en español.";
const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "¿Qué pide este cliente? …" },
];
const first = await client.messages.create({ model, max_tokens: 16000, system, messages });

// Segundo turno: reenvías la conversación entera, con la respuesta tal como llegó.
messages.push({ role: "assistant", content: first.content });
messages.push({ role: "user", content: "La política permite como máximo un 10 %. ¿Qué propones?" });
const second = await client.messages.create({ model, max_tokens: 16000, system, messages });
  • system es el prompt de sistema: las instrucciones de tu aplicación, separadas de lo que escribe el usuario. Ahí van el papel del modelo, el tono y los límites.
  • La API no guarda la conversación entre llamadas. En cada turno mandas todo otra vez, y por eso los tokens de entrada crecen con cada turno.
  • La respuesta del asistente se reenvía tal como llegó, con su bloque thinking incluido. En el módulo 2 verás por qué no se puede retocar.

El proyecto hace exactamente esto en la función conversationTask de src/live/api.ts, que forma parte del camino opcional con clave de API.

El caso: Nexo Renewals

Todo el módulo gira alrededor de Nexo Renewals, una empresa B2B ficticia (vende a otras empresas) que gestiona renovaciones de contratos. Le llegan emails de clientes como este:

Cliente solicita renovar el 2027-10-01 en EUR y pide un descuento del 12%.

El modelo lee el email y propone cuatro datos: la intención del cliente (renovar, negociar, cancelar o falta información), la fecha, la moneda y el descuento pedido. Eso es todo lo que puede hacer. No puede autorizar el descuento ni declarar que su propia lectura es correcta.

Lo que vas a construir

Vas a convertir una integración ingenua, que da por bueno cualquier texto, en un runtime pequeño. Llamamos runtime a la capa de tu código que llama al modelo y decide qué hacer con lo que vuelve. El tuyo tendrá cuatro propiedades:

  1. Cada respuesta acaba en un estado con nombre propio (completada, cortada, negada, pide una acción, fallida) en lugar de un ok: true.
  2. Pone límites al tiempo total, a los tokens y al tamaño de lo que entra en tu sistema.
  3. Separa un JSON bien formado de una decisión que alguien ha verificado.
  4. Mide cuántas renovaciones quedan bien resueltas, además de cuántas peticiones responde el servidor.

Prepara el proyecto

Proyecto de este módulo: llm-fundamentals. Descárgalo desde la ficha del curso, en el apartado «El código», o pídele el enlace a tu Claude conectado al curso (lo conectas una vez en Tu Claude; si no lo has hecho, la descarga de la ficha basta). Descomprímelo y entra en la carpeta llm-fundamentals/: todos los comandos del módulo se lanzan desde ahí. Necesitas Node 22.12 o superior y una terminal de macOS, Linux o Windows con WSL. Para las secciones «Con tu Claude», además, Claude Code con tu sesión iniciada, el mismo del nivel 1.

En esa carpeta, instala las dependencias:

npm ci

No necesitas clave de API. Las demos y los tests funcionan en modo fake: en vez de llamar a la API de verdad, usan respuestas preparadas que reproducen los fallos reales (cortes, negativas, JSON roto) sin red y sin coste.

Lo que haces sin clave y lo que necesita clave, para que no haya sorpresas:

Sin clave de API Con clave de API (opcional, se paga aparte del plan)
Escribir tu llamada a client.messages.create Sí: en el laboratorio escribes la decisión estructurada y npm run student:api la prueba con el SDK de verdad contra un transporte falso que responde con la forma real de la API La misma función, lanzada contra la API con npm run student:api:live
Ver un modelo de verdad responder Sí, con tu Claude: las secciones «Con tu Claude» de las lecciones 5 y 6 lanzan el mismo paso con claude -p y la sesión de tu plan npm run live:api hace el recorrido del proyecto con el SDK
Seguir la lección sin lanzar nada Sí: cada sección enseña una pasada real grabada

claude -p es Claude Code: un agente con sus propias instrucciones, que no es lo mismo que la llamada que harás en tu producto. Por eso la llamada a la API la escribes tú y la compruebas contra el transporte falso, que reproduce la API y no Claude Code.

Lo que hay dentro, para que te orientes:

  • demos/parte-1/: las demos de este módulo, de la 01 a la 04. (demos/parte-2/ es del módulo siguiente.)
  • src/: el runtime completo, como referencia.
  • starter/: el código ingenuo que vas a arreglar tú.
  • checkpoints/: fotos del código en cada etapa, para comparar.
  • tests/: los tests, que también sirven como especificación.

El curso deja en inglés los términos que vas a ver en la documentación y en los errores (runtime, streaming, tenant, payload…). Cada uno se explica donde sale por primera vez, y el glosario los reúne con la lección de cada uno.

Comprueba que todo funciona:

npm run checkpoint:00

Deberías ver:

{ checkpoint: 0, naiveTaskSuccessRate: 1, actualTaskSuccessRate: 0.25 }

Son cuatro respuestas con HTTP 200: una terminada, una cortada por max_tokens, una negativa y una petición de herramienta. Contando «200 y hay texto», la tasa de éxito es del 100 %. Contando tareas bien terminadas, del 25 %. Las dos cifras las calcula checkpoints/00-baseline.ts a partir de las cuatro respuestas: la segunda exige además stop_reason igual a end_turn. En la siguiente lección ves el mismo problema a escala, con el incidente que montó todo esto.