Cómo construir una API PHP que los modelos de IA puedan entender y usar

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) y fecha_desde (date ISO 8601). Devuelve array paginado con data, meta y links.
  • POST /tareas — requiere Idempotency-Key en el header. Cuerpo con titulo, descripcion, assignee_id y fecha_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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP