Back

Guía Completa del Vercel AI SDK: Construyendo Apps de Chat AI de Producción con Next.js

Construir aplicaciones con IA nunca ha sido tan accesible. Con el Vercel AI SDK, puedes crear interfaces de chat con streaming sofisticadas, sistemas de completado de texto y funciones asistidas por IA en tus apps de Next.js con notablemente poco boilerplate.

Pero aquí está el desafío: el SDK evoluciona rápidamente, la documentación está dispersa entre versiones, y la mayoría de los tutoriales solo rascan la superficie. Si has intentado integrar OpenAI, Anthropic u otros proveedores de LLM en tu app web y te encontraste ahogándote en complejidad de streaming, gestión de tokens y sincronización de estado—esta guía es para ti.

En esta inmersión profunda y completa, construiremos una aplicación de chat AI lista para producción desde cero, cubriendo todo desde hooks básicos hasta patrones avanzados como llamadas a herramientas, enrutamiento multi-modelo y rate limiting. Al final, tendrás una base sólida para cualquier función de IA que quieras construir.

📌 Nota de versión: Esta guía está escrita para Vercel AI SDK v6.0+ (lanzado a finales de 2025). Si usas una versión anterior, algunas APIs pueden diferir. Verifica tu versión con npm info ai version.


¿Por Qué Vercel AI SDK?

Antes de sumergirnos en el código, entendamos por qué el Vercel AI SDK se ha convertido en la opción preferida para integración de IA en aplicaciones React.

El Problema del Streaming

Cuando llamas a una API de LLM directamente, obtienes una respuesta solo después de que la generación completa termina. Para una respuesta de 500 tokens, eso son 5-10 segundos de espera. A los usuarios les molesta esto.

// El enfoque ingenuo - terrible UX const response = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Explica la computación cuántica' }], }); // El usuario mira el spinner de carga por 8 segundos... console.log(response.choices[0].message.content);

El streaming resuelve esto enviando tokens mientras se generan. Pero implementar streaming apropiado en React es sorprendentemente complejo:

  • Gestionar ReadableStream desde la API
  • Parsear SSE (Server-Sent Events) o JSON delimitado por newlines
  • Actualizar el estado de React sin causar cascadas de re-renders
  • Manejar señales de abort para cancelación
  • Gestionar estados de carga, error y completado
  • Sincronizar estado cliente y servidor

El Vercel AI SDK abstrae todo esto en hooks simples y declarativos.

Arquitectura Agnóstica de Proveedor

Una de las características killer del SDK es su interfaz unificada entre proveedores:

import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; // Misma firma de función, diferentes proveedores const result = await generateText({ model: openai('gpt-4-turbo'), // o: model: anthropic('claude-3-opus'), // o: model: google('gemini-pro'), prompt: 'Explica la computación cuántica', });

Cambia proveedores con un cambio de una línea. No requiere refactorización.


Conceptos Fundamentales: Arquitectura del AI SDK

El Vercel AI SDK está dividido en tres paquetes principales:

1. AI SDK Core (ai)

El paquete base que proporciona:

  • generateText() - Genera texto con resultado completo
  • streamText() - Streaming de generación de texto
  • generateObject() - Genera JSON estructurado
  • streamObject() - Streaming de generación de JSON estructurado
  • embed() - Genera embeddings
  • embedMany() - Embeddings en lote

2. AI SDK UI (@ai-sdk/react)

Hooks de React para construir UIs:

  • useChat() - Gestión completa de interfaz de chat
  • useCompletion() - Completado de texto de un solo turno
  • useObject() - Streaming de datos estructurados
  • useAssistant() - Integración con OpenAI Assistants API

3. Paquetes de Proveedores

Implementaciones 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
  • Y muchos proveedores de la comunidad...

Configuración: Bootstrap del Proyecto

Creemos una estructura de proyecto lista para producción:

npx create-next-app@latest ai-chat-app --typescript --tailwind --app cd ai-chat-app # Instalar paquetes del AI SDK npm install ai @ai-sdk/openai @ai-sdk/anthropic # Opcional: componentes UI npm install @radix-ui/react-scroll-area lucide-react

Configuración del Entorno

# .env.local OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-...

Estructura del Proyecto

src/
├── app/
│   ├── api/
│   │   └── chat/
│   │       └── route.ts      # Endpoint de API de chat
│   ├── page.tsx              # UI principal de chat
│   └── layout.tsx
├── components/
│   ├── chat/
│   │   ├── ChatContainer.tsx
│   │   ├── MessageList.tsx
│   │   ├── MessageBubble.tsx
│   │   └── ChatInput.tsx
│   └── ui/
│       └── Button.tsx
├── lib/
│   ├── ai/
│   │   ├── models.ts         # Configuraciones de modelos
│   │   └── prompts.ts        # System prompts
│   └── utils.ts
└── types/
    └── chat.ts

Construyendo la API de Chat: Implementación del Servidor

La ruta de API es donde ocurre la magia. Construyamos un endpoint de chat robusto:

Ruta 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 latencia export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), messages, system: `Eres un asistente de IA útil. Sé conciso y claro en tus respuestas.`, }); return result.toDataStreamResponse(); }

¡Eso es todo para un chat básico! Pero las apps de producción necesitan más...

Ruta de Chat Lista para Producción

// 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; // Tiempo máximo de ejecución // Schema de validación 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 = `Eres un asistente de IA experto especializado en desarrollo de software. Guías: - Proporciona respuestas precisas y bien estructuradas - Incluye ejemplos de código cuando sea relevante - Cita fuentes al hacer afirmaciones factuales - Admite incertidumbre en lugar de adivinar - Mantén las respuestas concisas pero 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('Error de API de Chat:', error); return new Response(JSON.stringify({ error: 'Error interno del servidor' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }

Construyendo la UI de Chat: Deep Dive en useChat

El hook useChat es el corazón de la implementación del lado del 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="Escribe tu mensaje..." 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> ); }

Configuración Avanzada de 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 datos adicionales con cada request body: { model: selectedModel, temperature: 0.7, }, // Mensajes iniciales initialMessages: [], // Generar IDs únicos generateId: () => crypto.randomUUID(), // Llamado cuando comienza el streaming onResponse: (response) => { if (!response.ok) { console.error('Error de respuesta:', response.status); } }, // Llamado cuando el streaming termina onFinish: (message) => { console.log('Mensaje completado:', message.id); }, // Llamado en error onError: (error) => { console.error('Error de chat:', error); }, }); // Auto-scroll al fondo useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); // Añadir mensaje programáticamente const sendQuickMessage = useCallback((content: string) => { append({ role: 'user', content }); }, [append]); // Limpiar historial de 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"> Limpiar </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">¿En qué puedo ayudarte hoy?</p> <div className="flex flex-wrap justify-center gap-2"> {['Explica React Server Components', 'Debuggea mi código', 'Escribe una función'].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"> Detener </button> </div> )} {error && ( <div className="p-4 bg-red-50 text-red-600 rounded-lg"> <p>Error: {error.message}</p> <button onClick={() => reload()} className="text-sm underline"> Reintentar </button> </div> )} </div> <form onSubmit={handleSubmit} className="p-4 border-t"> <div className="flex gap-2"> <input value={input} onChange={handleInputChange} placeholder="Escribe tu mensaje..." 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> ); }

Llamadas a Herramientas: Extendiendo las Capacidades de la IA

Una de las características más poderosas de los LLMs modernos es la llamada a herramientas (function calling). El AI SDK hace esto seamless:

Definiendo Herramientas

// src/lib/ai/tools.ts import { z } from 'zod'; import { tool } from 'ai'; export const weatherTool = tool({ description: 'Obtiene el clima actual para una ubicación', parameters: z.object({ location: z.string().describe('Nombre de la ciudad, ej: "Madrid"'), unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), }), execute: async ({ location, unit }) => { // En producción, llama a una 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 en la web información actual', parameters: z.object({ query: z.string().describe('La consulta de búsqueda'), }), execute: async ({ query }) => { // En producción, usa una API de búsqueda 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('Expresión matemática, ej: "2 + 2 * 3"'), }), execute: async ({ expression }) => { try { const result = Function(`"use strict"; return (${expression})`)(); return { expression, result }; } catch { return { expression, error: 'Expresión inválida' }; } }, });

Usando Herramientas en la Ruta de 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últiples llamadas a herramientas en secuencia maxSteps: 5, }); return result.toDataStreamResponse(); }

Manejando Resultados de Herramientas en la 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 invocaciones de herramientas */} {message.toolInvocations?.map((tool) => ( <ToolCard key={tool.toolCallId} tool={tool} /> ))} {/* Mostrar contenido 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> ); }

Salida Estructurada: generateObject y useObject

A veces necesitas que la IA devuelva datos estructurados, no solo texto:

Generación Estructurada del Lado del 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: `Analiza el sentimiento de este texto: "${text}"`, }); return Response.json(object); }

Streaming de Objetos del Lado del 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: 'pastel de chocolate' })}> Generar Receta </button> {isLoading && <p>Generando...</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> ); }

Patrones de Producción: Rate Limiting, Caché y Manejo de Errores

Rate Limiting con 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 en ruta de 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('Límite de rate excedido', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }, }); } // ... continúa con lógica de chat }

Caché de Respuestas para Consultas Comunes

// 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 caché const cached = await redis.get<string>(cacheKey); if (cached) { console.log('¡Caché hit!'); return cached; } // Generar nueva respuesta const { text } = await generateText({ model: openai('gpt-4-turbo'), prompt, }); // Cachear por 1 hora await redis.set(cacheKey, text, { ex: 3600 }); return text; }

Enrutamiento Multi-Modelo: Selección Inteligente de Proveedor

Para apps de producción, podrías querer enrutar requests a diferentes modelos basándote en la tarea:

// 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', 'función', 'debug', 'implementar', 'typescript', 'python']; const creativeKeywords = ['escribir', 'historia', 'creativo', 'imaginar', 'poema']; const analysisKeywords = ['analizar', 'comparar', 'evaluar', 'investigar', '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'); // El mejor para coding case 'creative': return openai('gpt-4-turbo'); // Genial para escritura creativa case 'analysis': return google('gemini-1.5-pro'); // Bueno para análisis de contexto largo default: return openai('gpt-4-turbo'); // Propósito general } } 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(`Enrutando al modelo ${taskType}`); return streamText({ model, messages, }); }

Consideraciones de Despliegue

Despliegue en Vercel

// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { serverComponentsExternalPackages: ['@ai-sdk/openai'], }, }; module.exports = nextConfig;

Variables de Entorno en Vercel

# Configurar en Vercel Dashboard > Settings > Environment Variables OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=...

Consideraciones del Edge Runtime

  • Las funciones edge tienen límite de 25MB
  • Sin APIs de Node.js (usar Web APIs en su lugar)
  • Tiempo de ejecución limitado (30s en Vercel)
  • Perfecto para respuestas de IA de baja latencia

Conclusión

El Vercel AI SDK simplifica dramáticamente la construcción de aplicaciones con IA. Cubrimos:

  • Conceptos fundamentales: Streaming, arquitectura de proveedores, hooks
  • Deep dive en useChat: Configuración completa, gestión de estado, patrones de UI
  • Llamadas a herramientas: Extendiendo capacidades de IA con funciones personalizadas
  • Salida estructurada: Respuestas de IA type-safe con schemas
  • Patrones de producción: Rate limiting, caché, manejo de errores
  • Enrutamiento multi-modelo: Selección inteligente de proveedor
  • Testing y despliegue: Mejores prácticas para producción

La clave para una integración de IA exitosa no es solo usar las herramientas correctas—es entender los patrones que hacen las aplicaciones confiables, performantes y amigables para el usuario.

Referencia Rápida

// Chat básico const { messages, input, handleSubmit, isLoading } = useChat(); // Con opciones const chat = useChat({ api: '/api/chat', body: { model: 'gpt-4' }, onFinish: (message) => saveToDb(message), onError: (error) => toast.error(error.message), }); // Control programático chat.append({ role: 'user', content: 'Hola' }); chat.reload(); chat.stop(); chat.setMessages([]);

Empieza simple, itera según tus necesidades, y no olvides monitorear el uso de tokens—tu cartera te lo agradecerá.


¿Estás construyendo algo genial con el Vercel AI SDK? El ecosistema está creciendo rápidamente, con nuevos proveedores y funciones lanzándose regularmente. Mantén un ojo en la documentación oficial y los releases de GitHub para estar al día con las últimas capacidades.

vercel-ai-sdknextjsaichatgptstreamingreacttypescript

Explora herramientas relacionadas

Prueba estas herramientas gratuitas de Pockit