Voltar para o blog
post.md

Como integrar IA em uma aplicação Full Stack sem criar um chatbot improvisado

Veja como estruturar uma funcionalidade com IA considerando backend, frontend, fila, worker, validação, segurança, custos e experiência do usuário.

IA Full StackLLMBackendFrontendArquitetura

Chamar uma API de IA é simples. O desafio real começa quando a funcionalidade precisa lidar com custo, latência, erro, validação, segurança, persistência e experiência do usuário dentro de uma aplicação Full Stack.

Contexto

Muita integração com IA começa do jeito mais direto possível: uma chamada para uma API de modelo, um prompt, uma resposta e uma tela exibindo o resultado. Para um protótipo, isso pode ser suficiente.

Mas quando a ideia precisa virar parte de uma aplicação real, a conversa muda. Uma feature com IA pode demorar, falhar, custar mais do que o esperado, retornar algo fora do formato, lidar com dados sensíveis e precisar de revisão humana.

Arquitetura Full Stack para integrar IA com frontend, API, domínio, fila, worker, camada de IA, provedor, validação e métricas.
Uma funcionalidade com IA precisa conversar com produto, backend, frontend, dados, erro, custo e validação.

O problema da chamada direta

O problema de colocar a chamada ao modelo direto no controller ou no componente é que responsabilidades começam a se misturar. O frontend sabe demais sobre o provedor, o backend monta prompt no lugar errado, a resposta é exibida sem validação e trocar de modelo vira uma mudança espalhada.

Funciona no começo. Depois cobra juros.

Uma separação mais saudável

Eu gosto de pensar nesse fluxo em camadas: frontend, API, serviço de domínio, camada de IA, provedor de modelo e registros de logs, métricas, custos e avaliações.

  • O frontend mostra status, erro, resultado e revisão.
  • A API valida entrada e registra a solicitação.
  • O serviço de domínio entende a regra do produto.
  • A camada de IA monta contexto, seleciona modelo, chama provedor e valida resposta.
  • Logs e métricas registram modelo, custo, latência, status e falhas.

Quando usar fila e worker

Se a tarefa é lenta, cara, instável ou precisa de reprocessamento, uma fila começa a fazer sentido. O usuário inicia a solicitação, a API registra status pendente, um worker processa a chamada de IA, a resposta é validada e o frontend acompanha o resultado.

  • Solicitação registrada com status.
  • Job enviado para fila.
  • Worker chama a camada de IA.
  • Resposta validada antes de salvar.
  • Frontend acompanha status, erro e resultado.
ai-job-status.tsts
1type AIJobStatus =2  | 'pending'3  | 'processing'4  | 'completed'5  | 'failed'6  | 'cancelled';78type AIJob = {9  id: string;10  userId: string;11  feature: 'report_summary';12  status: AIJobStatus;13  provider?: 'openai' | 'anthropic' | 'google';14  model?: string;15  promptVersion: string;16  inputTokens?: number;17  outputTokens?: number;18  costEstimateCents?: number;19  latencyMs?: number;20  errorCode?: string;21  createdAt: string;22  updatedAt: string;23};

O que vale registrar

Para uma funcionalidade com IA, eu registraria pelo menos o tipo da tarefa, versão do prompt, provedor, modelo, tokens quando disponíveis, custo estimado, tempo de processamento, status, erro, resposta validada e avaliação humana ou automática quando existir.

Isso ajuda a entender a feature depois que ela sai da demo. Sem esse registro, fica difícil saber se o problema é prompt, modelo, dado de entrada, latência, custo ou experiência de uso.

Validar a resposta antes de salvar

Se a funcionalidade espera JSON, eu prefiro tratar a resposta do modelo como dado externo não confiável. Ela precisa passar por schema, assim como qualquer entrada importante do sistema.

report-summary.schema.tsts
1import { z } from 'zod';23export const ReportSummarySchema = z.object({4  summary: z.string().min(80).max(1200),5  riskLevel: z.enum(['low', 'medium', 'high']),6  mainPoints: z.array(z.string().min(8)).min(3).max(7),7  confidence: z.number().min(0).max(1),8});910const parsed = ReportSummarySchema.safeParse(modelOutput);1112if (!parsed.success) {13  throw new InvalidAIResponseError(parsed.error);14}

Timeout, retry e fallback

Outro ponto que separa demo de sistema real é decidir como a aplicação reage quando o provedor demora, recusa, fica indisponível ou devolve um formato inválido.

worker-flow.tsts
1for (const attempt of [1, 2, 3]) {2  try {3    const output = await ai.generate(request, {4      timeoutMs: 20_000,5      idempotencyKey: job.id,6    });78    return validateAndPersist(output);9  } catch (error) {10    if (!isRetryable(error) || attempt === 3) break;11    await wait(exponentialBackoff(attempt));12  }13}1415const fallbackOutput = await ai.generate(request, {16  provider: 'secondary',17  timeoutMs: 20_000,18});1920return validateAndPersist(fallbackOutput);

UX também faz parte da arquitetura

Quando uma funcionalidade com IA demora ou falha, a interface precisa ajudar. Status claro, tentativa novamente, histórico, edição do resultado e indicação de revisão humana podem ser tão importantes quanto a chamada ao modelo.

Conclusão

Integrar IA em uma aplicação Full Stack não é encaixar um chatbot em algum canto da interface. É desenhar um fluxo que conversa com frontend, backend, dados, erros, custos, validação e experiência do usuário.