JB
_
·6 min de leitura

IA no Frontend: Streaming de LLMs com o Vercel AI SDK

Aprenda a integrar modelos de linguagem como GPT-4 e Claude diretamente em aplicações Next.js usando o Vercel AI SDK, com streaming de respostas, tool calls e gerenciamento de histórico.

AINext.jsTypeScriptLLMVercel AI SDK

O Problema de Integrar LLMs do Zero

Construir uma interface de IA do zero envolve lidar com uma série de complexidades: streaming de texto token a token, serialização de histórico de mensagens, tratamento de erros de rede, tool calls, cancelamento de requisições — tudo isso antes mesmo de pensar na UI.

O Vercel AI SDK resolve esse problema com um conjunto de abstrações que tornam a integração de LLMs em aplicações Next.js simples e tipada.

npm install ai @ai-sdk/openai
# ou para usar Claude
npm install ai @ai-sdk/anthropic

Configurando o Primeiro Endpoint de Streaming

O SDK expõe um helper streamText que funciona nativamente com Route Handlers do Next.js:

// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    system: 'Você é um assistente técnico especializado em desenvolvimento web.',
  });
 
  return result.toDataStreamResponse();
}

Três linhas de lógica real. O SDK cuida de serializar o stream no formato esperado pelo cliente.


Consumindo o Stream no Cliente

O hook useChat sincroniza automaticamente o estado da conversa com o endpoint:

'use client';
 
import { useChat } from 'ai/react';
 
export function ChatInterface() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat',
  });
 
  return (
    <div className="flex flex-col h-screen">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={message.role === 'user' ? 'text-right' : 'text-left'}
          >
            <span className="inline-block px-4 py-2 rounded-lg bg-white/5 border border-white/10">
              {message.content}
            </span>
          </div>
        ))}
 
        {isLoading && (
          <div className="text-purple-400 animate-pulse">Gerando resposta...</div>
        )}
      </div>
 
      <form onSubmit={handleSubmit} className="p-4 border-t border-white/10">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Faça uma pergunta..."
          className="w-full bg-white/5 rounded-lg px-4 py-2"
        />
      </form>
    </div>
  );
}

O hook gerencia automaticamente o histórico de mensagens, o estado de loading e o streaming token a token na interface.


Tool Calls: Dando Superpoderes ao Modelo

A feature mais poderosa do SDK é a integração de tools — funções que o modelo pode chamar para buscar dados externos, executar cálculos ou interagir com APIs:

import { streamText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    tools: {
      buscarPrecoAcao: tool({
        description: 'Busca o preço atual de uma ação na bolsa',
        parameters: z.object({
          ticker: z.string().describe('O código da ação, ex: PETR4, VALE3'),
        }),
        execute: async ({ ticker }) => {
          // Chama sua API de finanças aqui
          const preco = await fetchStockPrice(ticker);
          return { ticker, preco, timestamp: new Date().toISOString() };
        },
      }),
 
      calcularJurosCompostos: tool({
        description: 'Calcula juros compostos dado capital, taxa e período',
        parameters: z.object({
          capital: z.number(),
          taxaMensal: z.number().describe('Taxa em decimal, ex: 0.01 para 1%'),
          meses: z.number(),
        }),
        execute: async ({ capital, taxaMensal, meses }) => {
          const montante = capital * Math.pow(1 + taxaMensal, meses);
          return { montante: montante.toFixed(2), juros: (montante - capital).toFixed(2) };
        },
      }),
    },
    maxSteps: 5, // permite loops de raciocínio multi-step
  });
 
  return result.toDataStreamResponse();
}

O modelo decide autonomamente quando chamar cada tool com base no contexto da conversa — sem instrução explícita do usuário.


Streaming Estruturado com generateObject

Além de texto livre, o SDK permite gerar objetos JSON estruturados com validação via Zod, o que é ideal para geração de conteúdo:

import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
 
const PostSchema = z.object({
  titulo: z.string(),
  resumo: z.string().max(160),
  tags: z.array(z.string()).max(5),
  secoes: z.array(z.object({
    titulo: z.string(),
    conteudo: z.string(),
  })),
  nivelTecnico: z.enum(['iniciante', 'intermediario', 'avancado']),
});
 
export async function gerarRascunhoPost(tema: string) {
  const { object } = await generateObject({
    model: anthropic('claude-opus-4-6'),
    schema: PostSchema,
    prompt: `Crie a estrutura de um post técnico sobre: ${tema}`,
  });
 
  // `object` é completamente tipado via TypeScript
  return object;
}

O retorno é inferido automaticamente a partir do schema — zero type assertions.


Gerenciando Contexto Longo com trimMessages

LLMs têm limite de contexto. Para conversas longas, é necessário comprimir o histórico sem perder informação crítica:

import { streamText, trimMessages } from 'ai';
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  // Mantém as últimas N mensagens + sempre inclui a system message
  const trimmedMessages = trimMessages(messages, {
    maxTokens: 8000,
    tokenCounter: (msg) => Math.ceil(msg.content.length / 4), // estimativa simples
    strategy: 'last', // mantém as mais recentes
  });
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages: trimmedMessages,
  });
 
  return result.toDataStreamResponse();
}

Persistindo Conversas no Banco

Para aplicações reais, as mensagens precisam ser persistidas. O SDK fornece hooks de ciclo de vida para isso:

import { streamText } from 'ai';
import { db } from '@/lib/db';
 
export async function POST(req: Request) {
  const { messages, sessionId } = await req.json();
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    onFinish: async ({ text, usage }) => {
      // Chamado após o stream completar
      await db.message.createMany({
        data: [
          {
            sessionId,
            role: 'user',
            content: messages.at(-1)?.content ?? '',
          },
          {
            sessionId,
            role: 'assistant',
            content: text,
            tokensUsed: usage.totalTokens,
          },
        ],
      });
    },
  });
 
  return result.toDataStreamResponse();
}

Vale a Pena?

O Vercel AI SDK abstrai o que realmente não precisa ser reinventado: o protocolo de streaming, a serialização de mensagens, o loop de tool calls. O resultado é código focado em lógica de negócio, não em infraestrutura de IA.

Pontos de atenção:

  • Custo de tokens cresce rapidamente em conversas longas — implemente trimMessages desde o início
  • Tool calls são poderosas mas adiciona latência por round-trip ao modelo — use com moderação
  • Rate limits dos provedores exigem retry logic — o SDK não faz isso automaticamente

Para aplicações que precisam ir além do chat simples, vale explorar o novo modelo de AI Agents com generateText em loops, onde o modelo toma decisões sequenciais até atingir um objetivo. Esse é o próximo passo rumo a sistemas verdadeiramente autônomos.

A linha entre "app com IA" e "app que é IA" está ficando cada vez mais tênue. O SDK só acelera isso.