Los LLM no leen documentación en PDF ni preguntan si no entienden algo. Llaman a tus endpoints con los parámetros que infieren del nombre, la descripción y el esquema que les has dado. Si cualquiera de esas tres cosas falla, el agente falla. Por eso diseñar una API pensando en un agente de IA es distinto a diseñarla solo para un desarrollador humano.
Por qué a los LLM les cuesta usar APIs mal diseñadas
Un agente de IA usa herramientas externas a través de llamadas a función. El modelo recibe un listado de herramientas disponibles con su nombre, descripción e inputSchema, decide cuál usar, genera los parámetros y ejecuta la llamada. Si el endpoint se llama GET /usr/{id}/o?t=r, el modelo tiene que adivinar qué significa o y qué valores acepta t. A veces acierta. Muchas veces no.
Los errores tampoco llegan de la misma forma que a un humano. Un mensaje como Error 500: something went wrong no le dice al agente si el problema está en un parámetro incorrecto, en permisos o en el servidor. Sin contexto, el agente no puede corregir la llamada.
El principio es sencillo: diseña la API como si el consumidor principal fuera alguien que solo puede leer el esquema y el nombre de cada campo.
OpenAPI 3.1 no es opcional
Para que un LLM sepa qué parámetros pasar y qué esperar de vuelta, necesita el esquema. OpenAPI 3.1 es el formato estándar y la mayoría de frameworks PHP tienen soporte directo.
Lo que importa no es solo que el esquema exista, sino que cada campo tenga una description útil. Esa description es el "prompt" del LLM para esa herramienta. Si escribes description: "ID del pedido" y nada más, el modelo no sabe si es un UUID, un entero o una cadena con formato especial. Si escribes description: "UUID v4 del pedido, generado al crear el recurso. Formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", ya sabe exactamente qué poner.
Un endpoint de ejemplo bien documentado:
openapi: 3.1.0
paths:
/pedidos:
post:
summary: Crea un nuevo pedido
description: >
Crea un pedido para el cliente indicado. Usa el header
Idempotency-Key para evitar duplicados en reintentos.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [cliente_id, articulos]
properties:
cliente_id:
type: integer
description: ID numérico del cliente en el sistema
articulos:
type: array
description: Lista de artículos con su cantidad
items:
type: object
properties:
sku:
type: string
description: Código SKU del artículo (ej. "PROD-0042")
cantidad:
type: integer
minimum: 1
responses:
'201':
description: Pedido creado
content:
application/json:
example:
id: 9821
estado: pendiente
total: 49.90
'422':
description: Datos incorrectos
content:
application/json:
example:
error: "El campo 'sku' es obligatorio en cada artículo"
code: validation_error
field: articulos.0.sku
En Laravel puedes generar esto con darkaonline/l5-swagger usando atributos PHP directamente sobre los controladores. En proyectos sin framework, zircote/swagger-php hace lo mismo.
Nombres que no necesitan explicación
El nombre del endpoint y de cada parámetro es lo primero que lee el modelo. GET /usuarios/{id}/pedidos-recientes transmite el propósito sin necesidad de description. GET /usr/{id}/o?t=r requiere que alguien haya leído la documentación, y un agente no siempre la lee completa.
Para los parámetros, lo mismo: limite en lugar de l, fecha_desde en lugar de fd, estado_pedido en lugar de ep. Si el nombre ya explica el campo, la description puede limitarse a decir el formato y los valores válidos.
Los LLM también infieren el comportamiento del verbo HTTP. GET no modifica nada, POST crea, PATCH actualiza parcialmente, DELETE elimina. Respetar estos semánticos ayuda al modelo a elegir correctamente la herramienta.
Respuestas predecibles
Un agente necesita parsear la respuesta sin ambigüedad. Si a veces devuelves {"usuario": {...}} y otras veces {...} directamente, el modelo tiene que adivinar la estructura en cada llamada. Elige un formato y respétalo siempre.
Para errores, un objeto consistente con tres campos es suficiente:
// PHP: función de error estándar
function api_error(string $mensaje, string $code, ?string $field = null, int $status = 400): void {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode([
'error' => $mensaje,
'code' => $code,
'field' => $field,
]);
exit;
}
// Uso
api_error('El SKU no existe en el catálogo', 'sku_not_found', 'articulos.0.sku', 422);
Los enums merecen atención especial. Si el campo estado acepta pendiente, enviado y entregado, ponlo en el schema como enum: [pendiente, enviado, entregado]. El modelo tomará uno de esos valores sin inventarse nada.
Para paginación, un formato estándar evita que el agente tenga que deducir cómo pasar a la siguiente página:
{
"data": [...],
"meta": {
"total": 243,
"per_page": 20,
"current_page": 1
},
"links": {
"next": "/pedidos?page=2&limite=20",
"prev": null
}
}
Autenticación sin interacción humana
Los agentes no pueden resolver captchas ni hacer clic en un botón de autorización. Cualquier flujo que requiera intervención humana rompe la cadena de ejecución automática.
Las API Keys son la opción más sencilla: un Bearer token en el header Authorization que el agente incluye en cada llamada. Si necesitas algo más granular, OAuth 2.0 con el flujo client_credentials funciona bien porque no requiere que haya un usuario detrás.
// PHP: verificación de API Key en cada request
function verificar_api_key(): void {
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!str_starts_with($header, 'Bearer ')) {
api_error('Token de acceso requerido', 'unauthorized', null, 401);
}
$token = substr($header, 7);
// Verificar contra BBDD o caché
if (!api_key_valida($token)) {
api_error('Token inválido o caducado', 'invalid_token', null, 401);
}
}
Usa scopes o permisos por API Key para que cada agente solo pueda hacer lo que necesita. Un agente que consulta pedidos no debería poder crearlos.
Idempotencia: los agentes reintentan
Cuando una llamada no devuelve respuesta a tiempo, el agente suele reintentar. Si tu endpoint POST /pedidos no es idempotente, ese reintento crea un pedido duplicado.
La solución habitual es el header Idempotency-Key: el cliente envía una clave única por operación, y el servidor guarda la clave junto al resultado. Si llega la misma clave de nuevo, devuelve el resultado almacenado en lugar de ejecutar de nuevo.
// PHP: manejo de Idempotency-Key
function obtener_o_ejecutar_idempotente(string $key, callable $operacion): array {
// Buscar en caché (Redis, Memcached o tabla en BBDD)
$resultado_guardado = cache_get("idempotency:$key");
if ($resultado_guardado !== null) {
return $resultado_guardado; // Misma respuesta, sin ejecutar de nuevo
}
$resultado = $operacion();
cache_set("idempotency:$key", $resultado, ttl: 86400); // 24 horas
return $resultado;
}
// En el controlador
$key = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? null;
if ($key) {
$pedido = obtener_o_ejecutar_idempotente($key, fn() => crear_pedido($datos));
} else {
$pedido = crear_pedido($datos);
}
PUT y PATCH son idempotentes por definición: ejecutarlos varias veces con los mismos datos produce el mismo resultado. Para POST que crea recursos, el Idempotency-Key es imprescindible si el agente puede reintentar.
Exponer la API como herramienta MCP
MCP (Model Context Protocol) es el protocolo de Anthropic para exponer herramientas a LLMs de forma estandarizada. Claude, GPT-4o y Gemini son compatibles. Si expones tu API como servidor MCP, cualquier agente compatible puede descubrir y usar tus herramientas sin configuración extra.
Un servidor MCP mínimo en PHP responde a dos métodos JSON-RPC 2.0: tools/list para listar las herramientas disponibles y tools/call para ejecutar una.
<?php
// servidor_mcp.php servidor MCP mínimo en PHP
header('Content-Type: application/json');
$request = json_decode(file_get_contents('php://input'), true);
$method = $request['method'] ?? '';
$id = $request['id'] ?? null;
if ($method === 'tools/list') {
echo json_encode([
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'tools' => [
[
'name' => 'listar_pedidos',
'description' => 'Devuelve los pedidos de un cliente, opcionalmente filtrados por estado.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'cliente_id' => [
'type' => 'integer',
'description' => 'ID numérico del cliente',
],
'estado' => [
'type' => 'string',
'enum' => ['pendiente', 'enviado', 'entregado'],
'description' => 'Filtra por estado del pedido. Opcional.',
],
],
'required' => ['cliente_id'],
],
],
],
],
]);
exit;
}
if ($method === 'tools/call') {
$tool = $request['params']['name'] ?? '';
$params = $request['params']['arguments'] ?? [];
if ($tool === 'listar_pedidos') {
$pedidos = obtener_pedidos($params['cliente_id'], $params['estado'] ?? null);
echo json_encode([
'jsonrpc' => '2.0',
'id' => $id,
'result' => ['content' => [['type' => 'text', 'text' => json_encode($pedidos)]]],
]);
exit;
}
}
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'error' => ['code' => -32601, 'message' => 'Method not found']]);
Más información sobre el protocolo y cómo registrar el servidor en modelcontextprotocol.io.
Ejemplo completo: API de gestión de tareas lista para IA
Juntando todo lo anterior, estos son los tres endpoints mínimos de una API de tareas que un agente puede usar sin fricción:
- GET /tareas acepta
estado(enum),assignee_id(integer) yfecha_desde(date ISO 8601). Devuelve array paginado condata,metaylinks. - POST /tareas requiere
Idempotency-Keyen el header. Cuerpo contitulo,descripcion,assignee_idyfecha_limite. Devuelve la tarea creada con HTTP 201. - PATCH /tareas/{id} actualiza solo los campos enviados. Idempotente por definición. Devuelve la tarea actualizada.
El schema OpenAPI documenta cada campo con su tipo, formato y valores válidos. Los errores siempre tienen error, code y field. La autenticación es Bearer token con scopes separados para lectura y escritura.
Si quieres ver cómo aplicar estos principios a escala, cómo estructurar una API Laravel escalable cubre la organización de módulos, servicios y repositorios. Y si quieres aprovechar las últimas funciones del lenguaje en tu API, PHP moderno con funciones útiles para APIs te da un buen punto de partida.
Diseñar para agentes no complica la API: la obliga a ser más clara, más consistente y más predecible. Los desarrolladores humanos también lo agradecen.
Imagen: Pexels / Pixabay
