Guia Completo do Vercel AI SDK: Construindo Apps de Chat AI de Produção com Next.js
Construir aplicações com IA nunca foi tão acessível. Com o Vercel AI SDK, você pode criar interfaces de chat com streaming sofisticadas, sistemas de completar texto e funcionalidades assistidas por IA nas suas apps Next.js com notavelmente pouco boilerplate.
Mas aqui está o desafio: o SDK evolui rapidamente, a documentação está espalhada entre versões, e a maioria dos tutoriais só arranha a superfície. Se você tentou integrar OpenAI, Anthropic ou outros provedores de LLM na sua app web e se encontrou afogando em complexidade de streaming, gestão de tokens e sincronização de estado—esse guia é pra você.
Nesse mergulho profundo e completo, vamos construir uma aplicação de chat AI pronta para produção do zero, cobrindo tudo desde hooks básicos até padrões avançados como chamadas de ferramentas, roteamento multi-modelo e rate limiting. No final, você terá uma base sólida para qualquer funcionalidade de IA que quiser construir.
📌 Nota de versão: Esse guia é pra Vercel AI SDK v6.0+ (lançado no final de 2025). Se você tá usando uma versão anterior, algumas APIs podem ser diferentes. Verifique sua versão com
npm info ai version.
Por Que Vercel AI SDK?
Antes de mergulhar no código, vamos entender por que o Vercel AI SDK se tornou a escolha preferida para integração de IA em aplicações React.
O Problema do Streaming
Quando você chama uma API de LLM diretamente, você recebe uma resposta só depois que a geração completa termina. Para uma resposta de 500 tokens, isso são 5-10 segundos de espera. Usuários odeiam isso.
// A abordagem ingênua - UX terrível const response = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Explica computação quântica' }], }); // Usuário olha pro spinner de loading por 8 segundos... console.log(response.choices[0].message.content);
O streaming resolve isso enviando tokens enquanto são gerados. Mas implementar streaming apropriado em React é surpreendentemente complexo:
- Gerenciar
ReadableStreamda API - Parsear SSE (Server-Sent Events) ou JSON delimitado por newlines
- Atualizar o estado do React sem causar cascatas de re-renders
- Lidar com sinais de abort para cancelamento
- Gerenciar estados de loading, erro e conclusão
- Sincronizar estado cliente e servidor
O Vercel AI SDK abstrai tudo isso em hooks simples e declarativos.
Arquitetura Agnóstica de Provedor
Uma das features killer do SDK é sua interface unificada entre provedores:
import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; // Mesma assinatura de função, provedores diferentes const result = await generateText({ model: openai('gpt-4-turbo'), // ou: model: anthropic('claude-3-opus'), // ou: model: google('gemini-pro'), prompt: 'Explica computação quântica', });
Troque provedores com uma mudança de uma linha. Não precisa refatorar.
Conceitos Fundamentais: Arquitetura do AI SDK
O Vercel AI SDK é dividido em três pacotes principais:
1. AI SDK Core (ai)
O pacote base que fornece:
generateText()- Gera texto com resultado completostreamText()- Streaming de geração de textogenerateObject()- Gera JSON estruturadostreamObject()- Streaming de geração de JSON estruturadoembed()- Gera embeddingsembedMany()- Embeddings em lote
2. AI SDK UI (@ai-sdk/react)
Hooks React para construir UIs:
useChat()- Gerenciamento completo de interface de chatuseCompletion()- Completar texto de um só turnouseObject()- Streaming de dados estruturadosuseAssistant()- Integração com OpenAI Assistants API
3. Pacotes de Provedores
Implementações de modelos:
@ai-sdk/openai- OpenAI, Azure OpenAI@ai-sdk/anthropic- Modelos Claude@ai-sdk/google- Modelos Gemini@ai-sdk/mistral- Mistral AI@ai-sdk/amazon-bedrock- AWS Bedrock- E muitos provedores da comunidade...
Configuração: Bootstrap do Projeto
Vamos criar uma estrutura de projeto pronta para produção:
npx create-next-app@latest ai-chat-app --typescript --tailwind --app cd ai-chat-app # Instalar pacotes do AI SDK npm install ai @ai-sdk/openai @ai-sdk/anthropic # Opcional: componentes UI npm install @radix-ui/react-scroll-area lucide-react
Configuração do Ambiente
# .env.local OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-...
Estrutura do Projeto
src/
├── app/
│ ├── api/
│ │ └── chat/
│ │ └── route.ts # Endpoint da API de chat
│ ├── page.tsx # UI principal do chat
│ └── layout.tsx
├── components/
│ ├── chat/
│ │ ├── ChatContainer.tsx
│ │ ├── MessageList.tsx
│ │ ├── MessageBubble.tsx
│ │ └── ChatInput.tsx
│ └── ui/
│ └── Button.tsx
├── lib/
│ ├── ai/
│ │ ├── models.ts # Configurações de modelos
│ │ └── prompts.ts # System prompts
│ └── utils.ts
└── types/
└── chat.ts
Construindo a API de Chat: Implementação do Servidor
A rota da API é onde a mágica acontece. Vamos construir um endpoint de chat robusto:
Rota de Chat Básica
// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export const runtime = 'edge'; // Habilita runtime edge para menor latência export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), messages, system: `Você é um assistente de IA útil. Seja conciso e claro nas suas respostas.`, }); return result.toDataStreamResponse(); }
Isso é tudo pra um chat básico! Mas apps de produção precisam de mais...
Rota de Chat Pronta para Produção
// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { streamText, convertToModelMessages } from 'ai'; import { z } from 'zod'; export const runtime = 'edge'; export const maxDuration = 30; // Tempo máximo de execução // Schema de validação de request const chatRequestSchema = z.object({ messages: z.array(z.object({ role: z.enum(['user', 'assistant', 'system']), content: z.string(), })), model: z.enum(['gpt-4-turbo', 'claude-3-opus']).default('gpt-4-turbo'), temperature: z.number().min(0).max(2).default(0.7), }); // Registro de modelos const models = { 'gpt-4-turbo': openai('gpt-4-turbo'), 'claude-3-opus': anthropic('claude-3-opus-20240229'), }; const SYSTEM_PROMPT = `Você é um assistente de IA especialista em desenvolvimento de software. Guias: - Forneça respostas precisas e bem estruturadas - Inclua exemplos de código quando relevante - Cite fontes ao fazer afirmações factuais - Admita incerteza ao invés de adivinhar - Mantenha as respostas concisas mas completas`; export async function POST(req: Request) { try { const body = await req.json(); const { messages, model, temperature } = chatRequestSchema.parse(body); const result = streamText({ model: models[model], messages: await convertToModelMessages(messages), system: SYSTEM_PROMPT, temperature, maxTokens: 4096, abortSignal: req.signal, onFinish: async ({ text, usage }) => { console.log(`Completado: ${usage.totalTokens} tokens`); }, }); return result.toDataStreamResponse(); } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify({ error: 'Request inválido', details: error.errors }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } console.error('Erro da API de Chat:', error); return new Response(JSON.stringify({ error: 'Erro interno do servidor' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }
Construindo a UI de Chat: Deep Dive no useChat
O hook useChat é o coração da implementação do lado do cliente:
Uso Básico
// src/app/page.tsx 'use client'; import { useChat } from '@ai-sdk/react'; export default function ChatPage() { const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); return ( <div className="flex flex-col h-screen max-w-2xl mx-auto p-4"> <div className="flex-1 overflow-y-auto space-y-4"> {messages.map((message) => ( <div key={message.id} className={`p-4 rounded-lg ${ message.role === 'user' ? 'bg-blue-100 ml-auto max-w-[80%]' : 'bg-gray-100 mr-auto max-w-[80%]' }`} > <p className="whitespace-pre-wrap">{message.content}</p> </div> ))} </div> <form onSubmit={handleSubmit} className="flex gap-2 pt-4"> <input value={input} onChange={handleInputChange} placeholder="Digite sua mensagem..." className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isLoading} /> <button type="submit" disabled={isLoading || !input.trim()} className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50" > {isLoading ? 'Enviando...' : 'Enviar'} </button> </form> </div> ); }
Configuração Avançada do useChat
'use client'; import { useChat, Message } from '@ai-sdk/react'; import { useState, useCallback, useRef, useEffect } from 'react'; export default function AdvancedChatPage() { const [selectedModel, setSelectedModel] = useState('gpt-4-turbo'); const scrollRef = useRef<HTMLDivElement>(null); const { messages, input, setInput, handleInputChange, handleSubmit, isLoading, error, reload, stop, append, setMessages, } = useChat({ api: '/api/chat', // Enviar dados adicionais com cada request body: { model: selectedModel, temperature: 0.7, }, // Mensagens iniciais initialMessages: [], // Gerar IDs únicos generateId: () => crypto.randomUUID(), // Chamado quando o streaming começa onResponse: (response) => { if (!response.ok) { console.error('Erro de resposta:', response.status); } }, // Chamado quando o streaming termina onFinish: (message) => { console.log('Mensagem completada:', message.id); }, // Chamado em erro onError: (error) => { console.error('Erro de chat:', error); }, }); // Auto-scroll pro fundo useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); // Adicionar mensagem programaticamente const sendQuickMessage = useCallback((content: string) => { append({ role: 'user', content }); }, [append]); // Limpar histórico do chat const clearChat = useCallback(() => { setMessages([]); }, [setMessages]); return ( <div className="flex flex-col h-screen max-w-3xl mx-auto"> <header className="flex items-center justify-between p-4 border-b"> <h1 className="text-xl font-bold">AI Chat</h1> <div className="flex items-center gap-2"> <select value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} className="p-2 border rounded" > <option value="gpt-4-turbo">GPT-4 Turbo</option> <option value="claude-3-opus">Claude 3 Opus</option> </select> <button onClick={clearChat} className="p-2 text-gray-500 hover:text-gray-700"> Limpar </button> </div> </header> <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-4"> {messages.length === 0 && ( <div className="text-center text-gray-500 mt-8"> <p className="text-lg mb-4">Como posso te ajudar hoje?</p> <div className="flex flex-wrap justify-center gap-2"> {['Explica React Server Components', 'Debugga meu código', 'Escreve uma função'].map((prompt) => ( <button key={prompt} onClick={() => sendQuickMessage(prompt)} className="px-4 py-2 bg-gray-100 rounded-full hover:bg-gray-200 text-sm" > {prompt} </button> ))} </div> </div> )} {messages.map((message) => ( <MessageBubble key={message.id} message={message} /> ))} {isLoading && ( <div className="flex items-center gap-2 text-gray-500"> <div className="animate-pulse">●</div> <span>IA pensando...</span> <button onClick={stop} className="text-red-500 text-sm"> Parar </button> </div> )} {error && ( <div className="p-4 bg-red-50 text-red-600 rounded-lg"> <p>Erro: {error.message}</p> <button onClick={() => reload()} className="text-sm underline"> Tentar de novo </button> </div> )} </div> <form onSubmit={handleSubmit} className="p-4 border-t"> <div className="flex gap-2"> <input value={input} onChange={handleInputChange} placeholder="Digite sua mensagem..." className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isLoading} /> <button type="submit" disabled={isLoading || !input.trim()} className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors" > Enviar </button> </div> </form> </div> ); } function MessageBubble({ message }: { message: Message }) { const isUser = message.role === 'user'; return ( <div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}> <div className={`max-w-[80%] p-4 rounded-2xl ${ isUser ? 'bg-blue-500 text-white rounded-br-md' : 'bg-gray-100 text-gray-900 rounded-bl-md' }`} > <p className="whitespace-pre-wrap">{message.content}</p> </div> </div> ); }
Chamadas de Ferramentas: Estendendo as Capacidades da IA
Uma das features mais poderosas dos LLMs modernos é a chamada de ferramentas (function calling). O AI SDK torna isso seamless:
Definindo Ferramentas
// src/lib/ai/tools.ts import { z } from 'zod'; import { tool } from 'ai'; export const weatherTool = tool({ description: 'Obtém o clima atual para uma localização', parameters: z.object({ location: z.string().describe('Nome da cidade, ex: "São Paulo"'), unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), }), execute: async ({ location, unit }) => { // Em produção, chama uma API de clima real const temp = Math.floor(Math.random() * 30) + 5; return { location, temperature: temp, unit, condition: 'Parcialmente nublado', }; }, }); export const searchTool = tool({ description: 'Busca na web por informações atuais', parameters: z.object({ query: z.string().describe('A consulta de busca'), }), execute: async ({ query }) => { // Em produção, usa uma API de busca return { results: [ { title: `Resultado para: ${query}`, url: 'https://example.com' }, ], }; }, }); export const calculatorTool = tool({ description: 'Realiza cálculos matemáticos', parameters: z.object({ expression: z.string().describe('Expressão matemática, ex: "2 + 2 * 3"'), }), execute: async ({ expression }) => { try { const result = Function(`"use strict"; return (${expression})`)(); return { expression, result }; } catch { return { expression, error: 'Expressão inválida' }; } }, });
Usando Ferramentas na Rota da API
// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { weatherTool, searchTool, calculatorTool } from '@/lib/ai/tools'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), messages, tools: { weather: weatherTool, search: searchTool, calculator: calculatorTool, }, // Permitir múltiplas chamadas de ferramentas em sequência maxSteps: 5, }); return result.toDataStreamResponse(); }
Lidando com Resultados de Ferramentas na UI
'use client'; import { useChat } from '@ai-sdk/react'; export default function ChatWithTools() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return ( <div> {messages.map((message) => ( <div key={message.id}> {message.role === 'user' && <UserMessage content={message.content} />} {message.role === 'assistant' && ( <div> {/* Mostrar invocações de ferramentas */} {message.toolInvocations?.map((tool) => ( <ToolCard key={tool.toolCallId} tool={tool} /> ))} {/* Mostrar conteúdo de texto */} {message.content && <AssistantMessage content={message.content} />} </div> )} </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> <button type="submit">Enviar</button> </form> </div> ); } function ToolCard({ tool }: { tool: any }) { return ( <div className="p-3 my-2 bg-purple-50 border border-purple-200 rounded-lg"> <div className="text-sm font-medium text-purple-700"> 🔧 {tool.toolName} </div> {tool.state === 'result' && ( <pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto"> {JSON.stringify(tool.result, null, 2)} </pre> )} </div> ); }
Saída Estruturada: generateObject e useObject
Às vezes você precisa que a IA retorne dados estruturados, não só texto:
Geração Estruturada do Lado do Servidor
// src/app/api/analyze/route.ts import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; const sentimentSchema = z.object({ sentiment: z.enum(['positive', 'negative', 'neutral']), confidence: z.number().min(0).max(1), keywords: z.array(z.string()), summary: z.string(), }); export async function POST(req: Request) { const { text } = await req.json(); const { object } = await generateObject({ model: openai('gpt-4-turbo'), schema: sentimentSchema, prompt: `Analise o sentimento desse texto: "${text}"`, }); return Response.json(object); }
Streaming de Objetos do Lado do Cliente
'use client'; import { useObject } from '@ai-sdk/react'; import { z } from 'zod'; const recipeSchema = z.object({ name: z.string(), ingredients: z.array(z.object({ item: z.string(), amount: z.string(), })), steps: z.array(z.string()), prepTime: z.number(), cookTime: z.number(), }); export default function RecipeGenerator() { const { object, submit, isLoading } = useObject({ api: '/api/generate-recipe', schema: recipeSchema, }); return ( <div> <button onClick={() => submit({ dish: 'bolo de chocolate' })}> Gerar Receita </button> {isLoading && <p>Gerando...</p>} {object && ( <div> <h2>{object.name}</h2> {object.ingredients && ( <ul> {object.ingredients.map((ing, i) => ( <li key={i}>{ing.amount} {ing.item}</li> ))} </ul> )} {object.steps && ( <ol> {object.steps.map((step, i) => ( <li key={i}>{step}</li> ))} </ol> )} </div> )} </div> ); }
Padrões de Produção: Rate Limiting, Cache e Tratamento de Erros
Rate Limiting com Upstash Redis
// src/lib/rate-limit.ts import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); export const rateLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests por minuto analytics: true, }); // Uso na rota da API export async function POST(req: Request) { const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'; const { success, limit, reset, remaining } = await rateLimiter.limit(ip); if (!success) { return new Response('Rate limit excedido', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }, }); } // ... continua com lógica de chat }
Cache de Respostas para Consultas Comuns
// src/lib/cache.ts import { Redis } from '@upstash/redis'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import crypto from 'crypto'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); function hashPrompt(prompt: string): string { return crypto.createHash('sha256').update(prompt).digest('hex'); } export async function getCachedOrGenerate(prompt: string): Promise<string> { const cacheKey = `ai:${hashPrompt(prompt)}`; // Verificar cache const cached = await redis.get<string>(cacheKey); if (cached) { console.log('Cache hit!'); return cached; } // Gerar nova resposta const { text } = await generateText({ model: openai('gpt-4-turbo'), prompt, }); // Cachear por 1 hora await redis.set(cacheKey, text, { ex: 3600 }); return text; }
Roteamento Multi-Modelo: Seleção Inteligente de Provedor
Para apps de produção, você pode querer rotear requests para diferentes modelos baseado na tarefa:
// src/lib/ai/router.ts import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; type TaskType = 'code' | 'creative' | 'analysis' | 'general'; function detectTaskType(message: string): TaskType { const codeKeywords = ['código', 'função', 'debug', 'implementar', 'typescript', 'python']; const creativeKeywords = ['escrever', 'história', 'criativo', 'imaginar', 'poema']; const analysisKeywords = ['analisar', 'comparar', 'avaliar', 'pesquisar', 'explicar']; const lowerMessage = message.toLowerCase(); if (codeKeywords.some(k => lowerMessage.includes(k))) return 'code'; if (creativeKeywords.some(k => lowerMessage.includes(k))) return 'creative'; if (analysisKeywords.some(k => lowerMessage.includes(k))) return 'analysis'; return 'general'; } function selectModel(taskType: TaskType) { switch (taskType) { case 'code': return anthropic('claude-3-opus-20240229'); // O melhor pra coding case 'creative': return openai('gpt-4-turbo'); // Ótimo pra escrita criativa case 'analysis': return google('gemini-1.5-pro'); // Bom pra análise de contexto longo default: return openai('gpt-4-turbo'); // Propósito geral } } export async function routedChat(messages: any[]) { const lastUserMessage = messages.filter(m => m.role === 'user').pop(); const taskType = detectTaskType(lastUserMessage?.content ?? ''); const model = selectModel(taskType); console.log(`Roteando pro modelo ${taskType}`); return streamText({ model, messages, }); }
Considerações de Deploy
Deploy na Vercel
// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { serverComponentsExternalPackages: ['@ai-sdk/openai'], }, }; module.exports = nextConfig;
Variáveis de Ambiente na Vercel
# Configurar em Vercel Dashboard > Settings > Environment Variables OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=...
Considerações do Edge Runtime
- Funções edge têm limite de 25MB
- Sem APIs de Node.js (usar Web APIs no lugar)
- Tempo de execução limitado (30s na Vercel)
- Perfeito pra respostas de IA de baixa latência
Conclusão
O Vercel AI SDK simplifica dramaticamente a construção de aplicações com IA. Cobrimos:
- Conceitos fundamentais: Streaming, arquitetura de provedores, hooks
- Deep dive no useChat: Configuração completa, gestão de estado, padrões de UI
- Chamadas de ferramentas: Estendendo capacidades de IA com funções personalizadas
- Saída estruturada: Respostas de IA type-safe com schemas
- Padrões de produção: Rate limiting, cache, tratamento de erros
- Roteamento multi-modelo: Seleção inteligente de provedor
- Testing e deploy: Melhores práticas pra produção
A chave pra uma integração de IA bem-sucedida não é só usar as ferramentas certas—é entender os padrões que fazem as aplicações confiáveis, performantes e amigáveis pro usuário.
Referência Rápida
// Chat básico const { messages, input, handleSubmit, isLoading } = useChat(); // Com opções const chat = useChat({ api: '/api/chat', body: { model: 'gpt-4' }, onFinish: (message) => saveToDb(message), onError: (error) => toast.error(error.message), }); // Controle programático chat.append({ role: 'user', content: 'Olá' }); chat.reload(); chat.stop(); chat.setMessages([]);
Comece simples, itere conforme suas necessidades, e não esqueça de monitorar uso de tokens—sua carteira vai agradecer.
Construindo algo legal com o Vercel AI SDK? O ecossistema está crescendo rápido, com novos provedores e features sendo lançados regularmente. Fique de olho na documentação oficial e nos releases do GitHub pra ficar por dentro das últimas capacidades.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit