Voltar ao blog

Como Integrar LLMs em Aplicações Laravel: 6 Padrões Que Uso em Produção

Padrões reais de integração de Claude, GPT e Gemini em apps Laravel. Cache, fallback, custo por requisição, prompt versioning e os erros mais caros de iniciante.

RS
Richard Sakaguchi Solution Architect

Cada vez que um time decide “colocar IA no sistema” sem padrão definido, o que aparece em produção é o mesmo: chamadas síncronas no controller, prompt no meio do código, custo mensal explodindo e nenhum jeito de testar. Quatro semanas depois alguém pergunta por que o LLM “ficou ruim” — e ninguém sabe responder porque ninguém versionou o prompt.

Esse artigo é o que faço pra evitar isso. Seis padrões que aplico em toda integração Laravel + LLM que entrego, com código real, custo aproximado e os erros que já tropecei pessoalmente.

1. Service Layer dedicada (e nunca chamada direto do Controller)

A regra é simples: LLM nunca toca controller. Sempre passa por uma Service própria.

// app/Services/AI/CompletionService.php
class CompletionService
{
    public function __construct(
        private LLMProvider $provider,
        private PromptRegistry $prompts,
        private CompletionCache $cache,
        private CompletionLogger $logger,
    ) {}

    public function generate(string $promptKey, array $vars): CompletionResult
    {
        $prompt = $this->prompts->render($promptKey, $vars);

        if ($cached = $this->cache->get($prompt)) {
            return $cached;
        }

        $result = $this->provider->call($prompt);
        $this->cache->put($prompt, $result, ttl: 3600);
        $this->logger->record($promptKey, $prompt, $result);

        return $result;
    }
}

Por que isso importa: troca de provider (Anthropic ↔ OpenAI ↔ Gemini) vira uma linha. Sem isso, troca vira refactor de semanas.

2. Prompt Registry: trate prompt como código

Prompts em string literal espalhada pelo codebase é dívida garantida. Eu mantenho um PromptRegistry que carrega prompts de arquivos versionados, com versão semântica.

// resources/prompts/classificacao-os.v2.md
Você é um classificador de ordens de serviço contábil.
Categorias disponíveis: {{categorias}}
Entrada: {{descricao}}
Responda APENAS o nome da categoria, sem explicação.
$result = $completion->generate('classificacao-os@v2', [
    'categorias' => $cats->pluck('nome')->implode(', '),
    'descricao' => $os->descricao,
]);

Versão muda só quando o prompt muda. Posso comparar resultados v1 vs v2 em paralelo antes de migrar.

3. Cache agressivo (mas seguro)

LLM custa dinheiro real. Em um ERP que mantenho em produção, 41% das chamadas eram idênticas entre si — mesma entrada, mesma resposta esperada.

Cache key: hash do prompt renderizado (já com vars substituídas). TTL varia por tipo de tarefa:

Tipo de tarefaTTL recomendadoCache hit médio
Classificação determinística7 dias65%
Resumo de documento30 dias80%
Geração criativa (e-mail, copy)1 hora15%
Q&A com contexto dinâmicoSem cache

Só cuidado: nunca cacheie resultado de prompt que tem dado pessoal não-hashado na key. Você acaba retornando dado de cliente A pro cliente B.

4. Fallback entre providers (não fique refém)

Anthropic caiu? GPT está com latência alta? Gemini está bloqueado na sua região? Quem tem 1 provider tem 0 providers.

class MultiProviderLLM implements LLMProvider
{
    public function __construct(
        private array $providers,
        private array $maxLatency = ['anthropic' => 8000, 'openai' => 6000],
    ) {}

    public function call(string $prompt): CompletionResult
    {
        foreach ($this->providers as $provider) {
            try {
                return $provider->callWithTimeout($prompt, $this->maxLatency[$provider->name()] ?? 10000);
            } catch (ProviderUnavailable | TimeoutException $e) {
                Log::warning("Provider {$provider->name()} falhou", ['err' => $e->getMessage()]);
                continue;
            }
        }

        throw new AllProvidersFailed();
    }
}

A ordem dos providers vira política de negócio. Para tarefas de qualidade alta: Claude primeiro, GPT como fallback. Para tarefas de custo baixo: Gemini primeiro, Claude como fallback.

5. Queue Jobs para chamadas longas

Nunca chame LLM dentro de request HTTP que o usuário está esperando. Latência de LLM varia entre 800ms e 30 segundos dependendo do modelo, prompt e provider. Coloca isso na frente de um usuário e o sistema parece quebrado.

Eu sempre uso fila:

// app/Jobs/ProcessAICompletion.php
class ProcessAICompletion implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 60;
    public array $backoff = [10, 30, 60];

    public function handle(CompletionService $completion): void
    {
        $result = $completion->generate($this->promptKey, $this->vars);

        event(new AICompletionFinished($this->correlationId, $result));
    }
}

No frontend, Inertia.js + router.reload ou broadcast event via Pusher/Reverb avisa quando terminou. O usuário continua usando o sistema enquanto a IA trabalha.

6. Telemetria de custo POR usuário, POR feature

A pior surpresa é a fatura no fim do mês. Eu loguei tokens de entrada, tokens de saída e custo estimado por requisição, agrupado por:

  • Usuário (quem chamou)
  • Feature (qual módulo do sistema)
  • Modelo usado
  • Prompt key

Tabela:

CREATE TABLE ai_usage_log (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    feature VARCHAR(64),
    prompt_key VARCHAR(64),
    model VARCHAR(64),
    input_tokens INT,
    output_tokens INT,
    cost_usd DECIMAL(10,6),
    latency_ms INT,
    cache_hit BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP,
    INDEX idx_user_feature (user_id, feature, created_at)
);

Em um sistema que monitoro hoje, esse log me mostrou que um único feature de geração de relatório consumia 73% do custo total de IA do mês. Bastou colocar cache de 30 dias nesse fluxo específico para o custo cair de US$ 412 para US$ 89.

Erros mais caros que já cometi

  • Streaming de resposta direto pro browser sem timeout. Cliente fica com conexão aberta, server espera por minutos.
  • Prompt com dado sensível sem mascarar. CPF, e-mail, número de cartão vazaram em logs de provider.
  • Retry sem idempotência. Sistema cobrou o cliente 3 vezes porque o webhook do gateway falhou e o job retry chamou o LLM 3x pra “decidir” se cobrava.
  • Confiar 100% na resposta do LLM. Sempre valido o output (regex, schema JSON, lista finita de categorias) antes de gravar.
  • Esquecer rate limit do provider. Anthropic tem RPM por tier. Em pico, eu tomei 429 e o usuário viu erro.

Onde a Sakaguchi IA entra

Esses 6 padrões viraram biblioteca interna que reuso em todo projeto Laravel + IA que entrego. Se você está começando uma integração de LLM em produção ou se já tem uma que está doendo (custo, latência, erro), a gente pode te ajudar a estruturar isso direito. Fale com nosso time.

Sakaguchi IA

Precisa colocar isso em produção?

Engenharia de software, IA aplicada e cibersegurança para empresas que operam de verdade. Fale com nosso time.

Falar com a equipe