Laravel News publicó el 12 de mayo de 2026 una novedad pequeña en superficie pero importante para producto: Laravel AI SDK ahora permite usar sub-agents. La idea es que un agente principal pueda delegar una tarea concreta a otro agente especializado, con sus propias instrucciones, herramientas, proveedor, modelo y configuración.
Para equipos PHP que ya tienen Laravel en producción, esto cambia cómo se integran flujos de IA: de un único monolito prompt a una arquitectura de agentes desacoplados.
Por qué los sub-agents cambian las reglas
Una aplicación Laravel normal separa responsabilidades en controladores, servicios, jobs, policies, listeners y comandos. No tendría sentido que la parte de IA volviera a mezclarlo todo en una función gigante.
Los sub-agents aplican esa misma disciplina a flujos con modelos:
- Aislamiento de contexto: cada sub-agente recibe solo los datos que necesita, no el historial completo del padre.
- Herramientas mínimas: un agente de reembolsos no necesita acceso a herramientas de facturación.
- Coste granular: sabes exactamente qué tokens consume cada sub-agente.
- Testeable: cada agente se prueba de forma independiente.
Arquitectura recomendada: Agentes como servicios
app/AI/
├── Agents/
│ ├── SupportAgent.php # Agente principal (orquestador)
│ ├── RefundsAgent.php # Sub-agente especializado
│ ├── BillingAgent.php
│ └── DocumentationAgent.php
├── Contracts/
│ └── AgentContract.php # Interface común
├── DTOs/
│ ├── AgentTask.php
│ └── AgentResult.php
├── Logging/
│ └── AgentLogger.php # Observabilidad centralizada
└── Exceptions/
└── AgentFailedException.php
1. Interfaz común para todos los agentes
// app/AI/Contracts/AgentContract.php
namespace App\AI\Contracts;
use App\AI\DTOs\{AgentTask, AgentResult};
interface AgentContract
{
public function identifier(): string;
public function run(AgentTask $task): AgentResult;
public function tools(): array;
public function maxTokens(): int;
}
2. Agente principal: orquesta sin microgestionar
// app/AI/Agents/SupportAgent.php
namespace App\AI\Agents;
use App\AI\Contracts\AgentContract;
use App\AI\DTOs\{AgentTask, AgentResult};
use App\AI\Logging\AgentLogger;
class SupportAgent implements AgentContract
{
private array $subAgents = [];
public function __construct(
private AgentLogger $logger,
) {
$this->subAgents = [
'refunds' => app(RefundsAgent::class),
'billing' => app(BillingAgent::class),
'documentation' => app(DocumentationAgent::class),
];
}
public function identifier(): string
{
return 'support_agent';
}
public function tools(): array
{
return [
'classify_ticket',
'escalate_to_human',
'search_knowledge_base',
];
}
public function maxTokens(): int
{
return 2048;
}
public function run(AgentTask $task): AgentResult
{
$start = hrtime(true);
$this->logger->start($this->identifier(), $task);
// 1. Clasificar el ticket
$classification = $this->classify($task);
// 2. Delegar al sub-agente adecuado si aplica
if ($classification['delegates_to'] ?? null) {
$subAgent = $this->subAgents[$classification['delegates_to']] ?? null;
if (!$subAgent) {
return AgentResult::error("No sub-agent found for: {$classification['delegates_to']}");
}
$subTask = new AgentTask(
id: $task->id,
payload: $classification['context'] ?? $task->payload,
metadata: [
'parent_agent' => $this->identifier(),
'delegation_reason' => $classification['reason'],
],
);
$subResult = $subAgent->run($subTask);
// 3. Consolidar respuesta
$result = AgentResult::success([
'classification' => $classification,
'resolution' => $subResult->data,
]);
} else {
$result = AgentResult::success([
'classification' => $classification,
'resolution' => $classification['direct_response'],
]);
}
$latencyMs = (hrtime(true) - $start) / 1e6;
$this->logger->end($this->identifier(), $task, $result, $latencyMs);
return $result;
}
private function classify(AgentTask $task): array
{
// Aquí iría la llamada al modelo para clasificar
// y decidir si delegar o responder directamente
return [
'delegates_to' => 'refunds',
'reason' => 'ticket_contains_refund_request',
'context' => $task->payload,
];
}
}
3. Sub-agente: acotado y revisable
// app/AI/Agents/RefundsAgent.php
namespace App\AI\Agents;
use App\AI\Contracts\AgentContract;
use App\AI\DTOs\{AgentTask, AgentResult};
class RefundsAgent implements AgentContract
{
public function identifier(): string
{
return 'refunds_agent';
}
public function tools(): array
{
return [
'get_order_by_id',
'get_refund_policy',
'calculate_refund_amount',
'draft_refund_response',
];
}
public function maxTokens(): int
{
return 1024; // Sub-agente más ligero
}
public function run(AgentTask $task): AgentResult
{
$orderId = $task->payload['order_id'] ?? null;
$reason = $task->payload['reason'] ?? 'not_specified';
if (!$orderId) {
return AgentResult::error('Missing order_id in refund request');
}
// Llamada al modelo con instrucciones muy específicas
$response = $this->callModel(
prompt: $this->buildPrompt($orderId, $reason),
maxTokens: $this->maxTokens(),
temperature: 0.2, // Baja temperatura para outputs predecibles
);
// Validar que la respuesta tenga el formato esperado
$parsed = $this->validateResponse($response);
return AgentResult::success([
'recommendation' => $parsed['recommendation'],
'reason' => $parsed['reason'],
'amount_cents' => $parsed['amount_cents'],
'customer_message_draft' => $parsed['customer_message_draft'],
'requires_human_approval' => $parsed['requires_human_approval'],
]);
}
private function buildPrompt(int $orderId, string $reason): string
{
$order = \App\Models\Order::find($orderId);
return <<<PROMPT
You are a refund processing assistant.
Review the following order and reason, then determine the appropriate refund action.
ORDER:
- ID: {$order->id}
- Total: {$order->total_cents} cents
- Status: {$order->status}
- Created: {$order->created_at->format('Y-m-d')}
- Items: {$order->items->count()}
REFUND REASON: {$reason}
POLICY: Full refund within 30 days, prorated after.
Respond in strict JSON:
{
"recommendation": "approve|partial|manual_review|reject",
"reason": "short explanation",
"amount_cents": 0,
"customer_message_draft": "...",
"requires_human_approval": true|false
}
PROMPT;
}
private function callModel(string $prompt, int $maxTokens, float $temperature): string
{
// Llamada real al proveedor configurado
return json_encode([
'recommendation' => 'manual_review',
'reason' => 'order_has_partial_delivery',
'amount_cents' => 4999,
'customer_message_draft' => 'We need to verify the shipment status before proceeding.',
'requires_human_approval' => true,
]);
}
private function validateResponse(string $response): array
{
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON from refunds agent: ' . json_last_error_msg());
}
return $data;
}
}
4. DTOs tipados
// app/AI/DTOs/AgentTask.php
namespace App\AI\DTOs;
class AgentTask
{
public function __construct(
public readonly string $id,
public readonly array $payload,
public readonly array $metadata = [],
) {}
}
// app/AI/DTOs/AgentResult.php
namespace App\AI\DTOs;
class AgentResult
{
private function __construct(
public readonly bool $success,
public readonly ?array $data = null,
public readonly ?string $error = null,
) {}
public static function success(array $data): self
{
return new self(success: true, data: $data);
}
public static function error(string $message): self
{
return new self(success: false, error: $message);
}
public function requiresHumanApproval(): bool
{
return $this->data['requires_human_approval'] ?? false;
}
}
Testing: cada agente como unidad independiente
// tests/Feature/AI/Agents/RefundsAgentTest.php
use App\AI\Agents\RefundsAgent;
use App\AI\DTOs\{AgentTask, AgentResult};
test('refunds agent returns valid result for known order', function () {
$order = \App\Models\Order::factory()->create([
'total_cents' => 9999,
'status' => 'delivered',
]);
$agent = new RefundsAgent();
$task = new AgentTask(
id: 'test-1',
payload: ['order_id' => $order->id, 'reason' => 'defective_product'],
);
$result = $agent->run($task);
expect($result)->toBeInstanceOf(AgentResult::class)
->and($result->success)->toBeTrue()
->and($result->data)->toHaveKey('recommendation')
->and($result->data)->toHaveKey('amount_cents')
->and($result->data)->toHaveKey('requires_human_approval');
});
test('refunds agent fails gracefully when order is missing', function () {
$agent = new RefundsAgent();
$task = new AgentTask(
id: 'test-2',
payload: ['reason' => 'no_order_id_provided'],
);
$result = $agent->run($task);
expect($result->success)->toBeFalse()
->and($result->error)->toContain('Missing order_id');
});
test('support agent delegates correctly to refunds sub-agent', function () {
// Test de integración ligero que verifica la orquestación
$support = app(\App\AI\Agents\SupportAgent::class);
$task = new AgentTask(
id: 'test-3',
payload: ['ticket' => 'I want a refund for order #123', 'user_id' => 1],
);
$result = $support->run($task);
expect($result->success)->toBeTrue()
->and($result->data)->toHaveKey('classification')
->and($result->data)->toHaveKey('resolution');
});
Observabilidad: saber qué pasó dentro de cada agente
// app/AI/Logging/AgentLogger.php
namespace App\AI\Logging;
use App\AI\DTOs\{AgentTask, AgentResult};
use Illuminate\Support\Facades\Log;
class AgentLogger
{
public function start(string $agentId, AgentTask $task): void
{
Log::channel('ai')->info('agent.start', [
'agent' => $agentId,
'task_id' => $task->id,
'payload_size' => strlen(json_encode($task->payload)),
'timestamp' => now()->toIso8601String(),
]);
}
public function end(string $agentId, AgentTask $task, AgentResult $result, float $latencyMs): void
{
$logData = [
'agent' => $agentId,
'task_id' => $task->id,
'success' => $result->success,
'latency_ms' => round($latencyMs, 2),
'timestamp' => now()->toIso8601String(),
];
if ($result->requiresHumanApproval()) {
$logData['requires_human_approval'] = true;
Log::channel('ai')->warning('agent.requires_human_approval', $logData);
} else {
Log::channel('ai')->info('agent.end', $logData);
}
}
public function delegation(string $parentAgent, string $subAgent, string $reason): void
{
Log::channel('ai')->info('agent.delegation', [
'parent' => $parentAgent,
'sub_agent' => $subAgent,
'reason' => $reason,
]);
}
}
Control de costes por agente
// app/AI/Agents/Traits/HasCostTracking.php
namespace App\AI\Agents\Traits;
use Illuminate\Support\Facades\Cache;
trait HasCostTracking
{
public function trackCost(string $agentId, int $inputTokens, int $outputTokens, string $model = 'gpt-4o-mini'): void
{
$pricing = [
'gpt-4o-mini' => ['input' => 0.15, 'output' => 0.60],
'gpt-4o' => ['input' => 2.50, 'output' => 10.00],
'claude-sonnet-4' => ['input' => 3.00, 'output' => 15.00],
][$model] ?? ['input' => 0.15, 'output' => 0.60];
$costUsd = ($inputTokens * $pricing['input'] / 1_000_000)
+ ($outputTokens * $pricing['output'] / 1_000_000);
$dailyKey = "ai:cost:daily:" . date('Y-m-d') . ":{$agentId}";
$monthlyKey = "ai:cost:monthly:" . date('Y-m') . ":{$agentId}";
Cache::increment($dailyKey, (int) ($costUsd * 1_000_000));
Cache::increment($monthlyKey, (int) ($costUsd * 1_000_000));
}
}
Dónde aplicarlo primero en producto
Los mejores casos iniciales son flujos de bajo riesgo y alto volumen:
| Flujo | Agente principal | Sub-agentes | Riesgo |
|---|---|---|---|
| Clasificar tickets de soporte | SupportAgent | RefundsAgent, BillingAgent, DocsAgent | Bajo |
| Resumir bugs reproducibles | BugAgent | PriorityAgent, AssignAgent | Bajo |
| Preparar respuestas de facturación | BillingAgent | InvoiceAgent, PaymentAgent | Medio |
| Generar borradores de documentación | DocsAgent | TechWriterAgent, ReviewerAgent | Bajo |
| Revisar contenido antes de publicar | ReviewAgent | SEOAgent, GrammarAgent | Medio |
No empezaría por autorizaciones, pagos directos o acciones irreversibles. Ahí la IA puede asistir, pero la decisión final debe quedar protegida por reglas de negocio tradicionales.
Conclusión: Agentes como ciudadanos de primera clase
Laravel AI SDK con sub-agents permite tratar los agentes como ciudadanos de primera clase en tu arquitectura: con contracts, tests, logging, costes asignables y un ciclo de vida gobernado.
El salto cualitativo no es técnico, es de diseño: pasar de “un prompt que lo hace todo” a agentes pequeños, auditables y conectados a casos de uso reales. Ese es el tipo de integración que merece producción.
¿Llevas esto a tu equipo?
Disponible para consultoría técnica o contratación como Senior que ya trabaja con flujos multi-agente en producción:
- Arquitectura de agentes (2-3 días): diseñar el árbol de agentes, herramientas y políticas para tu producto.
- Implementación hands-on (1-2 semanas): sub-agentes, testing, observabilidad, control de costes.
- Auditoría de integraciones IA existentes (2 días): encontrar mejoras rápidas y reducir riesgo.
Modalidades: EOR (Deel, Remote, Oyster), freelance B2B (Autónomo, factura intracomunitaria 0% IVA), o contrato directo si tienes entidad en España.
Ver mi perfil, stack y condiciones →
Artículos relacionados
- Laravel AI SDK: integración limpia de IA en PHP (2026) — Actions, Prompts versionados, DTOs tipados, Policies, testing por contrato
- Cómo uso Claude Code en producción con Laravel y Vue — Flujo matinal, refactor, MCP, writing
- Stack técnico para startups en 2026 — Laravel, Vue, Inertia, testing, despliegue
