La unidad del SDK es el agente: una clase que reúne las instrucciones, el esquema y las herramientas de un trabajo.
php artisan make:agent SiteCopywriter --structured
El nombre de la clase es la primera decisión, y merece más cuidado del que suele recibir. Ponle el nombre de lo que tu aplicación necesita, no del modelo ni del proveedor. SiteCopywriter, FieldRewriter, PageTranslator, EditorPlanner. Nunca OpenAiAgent ni GptHelper.
namespace App\Ai\Agents;
use App\Ai\Lane;
use App\Ai\TenantProviders;
use App\Models\Workspace;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Laravel\Ai\Providers\Provider;
#[Timeout(55)]
#[MaxTokens(6000)]
final class SiteCopywriter
implements Agent, HasStructuredOutput
{
use Promptable;
public function __construct(
private readonly Workspace $workspace,
) {}
public function provider(): Provider
{
return app(TenantProviders::class)
->for($this->workspace, Lane::Copy);
}
public function instructions(): string
{
return SiteCopyPrompt::SYSTEM;
}
public function schema(JsonSchema $schema): array
{
return SiteCopySchema::for($schema);
}
}
Aquí dentro no hay ningún array messages, ni response_format, ni temperature. Ese es el vocabulario de un proveedor, y se queda dentro del SDK. Lo que la clase sí dice es todo lo que tu aplicación ha decidido sobre este trabajo: qué credenciales lo pagan, cuánto puede tardar, cuánto puede costar, qué se le dice y qué forma debe devolver.
Esa última lista es lo importante. Una clase de agente no es un envoltorio de un modelo. Es el lugar donde quedan escritas las decisiones sobre un uso de un modelo, en un archivo, en un commit, donde un revisor puede verlas.
La prueba de si la frontera está en el lugar correcto es la de siempre: ¿podrías llevar este trabajo a un proveedor completamente distinto sin cambiar a ninguno de quienes lo llaman? Con el proveedor construido en un solo método y el vocabulario en manos del SDK, la respuesta es un cambio de una línea en TenantProviders.
Las credenciales pertenecen a un inquilino, no a la aplicación
Esta es la decisión que la mayoría de las primeras implementaciones toma mal, y es muy difícil corregirla a posteriori.
El archivo de configuración del SDK guarda una clave por proveedor. Es el valor por defecto correcto para una demo y el equivocado para un producto. Una sola clave para toda la aplicación significa que el uso de todos los inquilinos cae en el mismo saco. No puedes atribuir el gasto. No puedes poner tope a un inquilino ruidoso sin ponérselo a todos. No puedes dejar que un cliente traiga su propia clave. Y si esa clave se rota o se topa con un límite de peticiones, todos los inquilinos fallan a la vez.
Así que el proveedor se construye en cada llamada, a partir del espacio de trabajo que la paga:
namespace App\Ai;
use App\Exceptions\MissingAiCredentials;
use App\Models\Workspace;
use Laravel\Ai\Ai;
use Laravel\Ai\Providers\Provider;
final class TenantProviders
{
public function for(
Workspace $workspace,
Lane $lane,
): Provider
{
$credential = $workspace->aiCredential;
if (
$credential === null
|| $credential->key === ''
) {
throw MissingAiCredentials::for($workspace);
}
return Ai::build([
'driver' => $credential->driver,
'key' => $credential->key,
'models' => [
'text' => [
'default' => $lane->model(
$credential->driver,
),
],
],
]);
}
}
Ai::build() convierte un array de configuración en un proveedor en tiempo de ejecución, que es justo el gancho que necesita una aplicación multiinquilino. La fila de credenciales guarda el driver y una clave cifrada; el carril, que el capítulo 4 presenta como es debido, elige el modelo para ese driver. Un comerciante que cambia de un proveedor a otro cambia una fila, y todos los agentes lo siguen.
No hay valor de reserva con una clave compartida en el entorno, y esa ausencia es deliberada. Nuestra primera versión lo tenía, para los inquilinos que no habían traído la suya. Eso significaba que un inquilino con la credencial guardada rota nos facturaba a nosotros en silencio en lugar de fallar, y que un entorno de pruebas con una clave real en su .env podía gastar dinero que nadie atribuía. Quitamos el valor de reserva en octubre. Un espacio de trabajo sin credencial ahora falla, siempre, en la frontera.
Haz esto el primer día. Adaptar credenciales por inquilino a un sistema que daba por hecha una sola clave global significa tocar cada punto de llamada, cada test y cada clave de caché, casi siempre bajo presión, casi siempre porque llegó una factura.