Vercel AI SDK: Construye Agentes de IA con TypeScript desde Cero
Aprende a usar Vercel AI SDK para crear agentes de IA con TypeScript y Next.js. Streaming, herramientas, generación de texto y patrones avanzados con ejemplos listos para producción.
Vercel AI SDK: Construye Agentes de IA con TypeScript desde Cero
Vercel AI SDK es la librería de TypeScript que unifica la forma de trabajar con modelos de IA en aplicaciones web. Con más de 20 millones de descargas mensuales en npm, se convirtio en el estandar para construir desde chatbots hasta agentes autonomos que ejecutan herramientas, toman decisiones y responden en streaming.
Esta guía cubre todo el camino: desde instalar el paquete hasta tener un agente funcional con tool calling, streaming y una interfaz React conectada. Todo el código funciona con Next.js App Router y TypeScript.
qué hace AI SDK y por qué te importa
AI SDK resuelve un problema concreto: cada proveedor de IA (OpenAI, Anthropic, Google) tiene su propia API, su propio formato de mensajes y su propia forma de manejar streaming. AI SDK abstrae todo eso en una interfaz unificada.
Cambiar de GPT-4o a Claude 4 o Gemini es cuestión de cambiar una línea. El resto de tu código no se toca. Eso incluye streaming, tool calling, generación de objetos estructurados y manejo de conversaciones.
Las funciones principales son:
- generateText -- genera una respuesta completa y la devuelve cuando termina
- streamText -- envia la respuesta token por token en tiempo real
- generateObject -- genera datos estructurados con un schema de Zod
- tool -- define herramientas que el modelo puede ejecutar
Si quieres entender bien el sistema de tipos que usa AI SDK por debajo, revisa la guía de tipos genéricos en TypeScript.
Instalación y configuración
Necesitas dos paquetes: el core de AI SDK y al menos un proveedor. En este tutorial usamos OpenAI, pero el código es identico para cualquier proveedor.
$ npm install ai @ai-sdk/openai zodSi prefieres usar Anthropic o Google en lugar de OpenAI:
$ npm install @ai-sdk/anthropic @ai-sdk/googleVariables de entorno
Crea tu archivo .env.local con la API key del proveedor que vayas a usar:
# Para OpenAI
OPENAI_API_KEY=tu-api-key-aquí
# Para Anthropic (opcional)
ANTHROPIC_API_KEY=tu-api-key-aquí
# Para Google (opcional)
GOOGLE_GENERATIVE_AI_API_KEY=tu-api-key-aquí.env.local a tu .gitignore. Si necesitas repasar como funcionan las variables de entorno en Next.js, tengo una guía dedicada al tema.AI SDK detecta automáticamente las variables de entorno por convención. Si tu variable se llama OPENAI_API_KEY, no necesitas configurar nada más. El proveedor la lee directamente de process.env.
generateText: generación de texto básica
generateText es la forma más directa de obtener una respuesta del modelo. Llama a la API, espera la respuesta completa y te la devuelve.
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
async function generarResumen(texto: string) {
const { text } = await generateText({
model: openai("gpt-4o"),
prompt: `Resume el siguiente texto en 3 puntos clave:\n\n${texto}`,
});
return text;
}también puedes usar el formato de mensajes para conversaciones con contexto:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
async function consultarModelo() {
const { text, usage } = await generateText({
model: openai("gpt-4o"),
system: "Eres un asistente técnico especializado en TypeScript y Next.js.",
messages: [
{
role: "user",
content: "cuál es la diferencia entre Server Components y Client Components?",
},
],
});
console.log(text);
// uso de tokens para monitorear costos
console.log(`Tokens usados: ${usage.totalTokens}`);
}El objeto usage te da promptTokens, completionTokens y totalTokens. Esto es clave para controlar costos en producción.
openai("gpt-4o") por anthropic("claude-sonnet-4-20250514") o google("gemini-2.5-flash") y el resto del código sigue igual.streamText: respuestas en tiempo real
Para interfaces de chat, esperar la respuesta completa da una experiencia mala. streamText envia la respuesta token por token, exactamente como lo ves en ChatGPT o Claude.
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
async function streamRespuesta() {
const resultado = streamText({
model: openai("gpt-4o"),
prompt: "Explica qué es un closure en JavaScript en términos simples.",
});
// Iterar sobre el stream de texto
for await (const textPart of resultado.textStream) {
process.stdout.write(textPart);
}
}La diferencia clave: generateText devuelve una Promise que resuelve cuando termina. streamText devuelve un objeto con streams que puedes consumir inmediatamente.
cuándo usar cada uno
| Función | Caso de uso |
|---|---|
generateText | Procesamiento en background, emails, resumenes, tareas sin UI |
streamText | Chat en tiempo real, interfaces donde el usuario ve la respuesta |
Tool calling: dale herramientas al modelo
aquí es donde AI SDK se pone interesante. Con tool calling, defines funciones que el modelo puede decidir ejecutar. El modelo no ejecuta código directamente, sino que indica que herramienta quiere usar y con que parámetros. AI SDK ejecuta la función por ti y le devuelve el resultado al modelo.
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const resultado = await generateText({
model: openai("gpt-4o"),
prompt: "cuál es el clima en Buenos Aires hoy?",
tools: {
obtenerClima: tool({
description: "Obtiene el clima actual de una ciudad",
parameters: z.object({
ciudad: z.string().describe("Nombre de la ciudad"),
unidad: z
.enum(["celsius", "fahrenheit"])
.optional()
.describe("Unidad de temperatura"),
}),
execute: async ({ ciudad, unidad = "celsius" }) => {
// aquí va tu llamada real a una API de clima
const respuesta = await fetch(
`https://api.weather.example/current?city=${ciudad}&unit=${unidad}`
);
return respuesta.json();
},
}),
},
});El schema de Zod no es opcional. AI SDK lo usa para generar la descripción de los parámetros que el modelo necesita para decidir como llamar a la herramienta. Cada .describe() en Zod le da contexto al modelo sobre que espera ese campo.
Herramientas múltiples
Puedes definir todas las herramientas que necesites. El modelo decide cuál usar según el contexto:
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const resultado = await generateText({
model: openai("gpt-4o"),
prompt: "Busca información sobre Next.js y guardala en la base de datos.",
tools: {
buscarWeb: tool({
description: "Busca información en la web",
parameters: z.object({
query: z.string().describe("término de búsqueda"),
}),
execute: async ({ query }) => {
// lógica de búsqueda
return { resultados: [`Resultado para: ${query}`] };
},
}),
guardarEnDB: tool({
description: "Guarda información en la base de datos",
parameters: z.object({
título: z.string(),
contenido: z.string(),
tags: z.array(z.string()).optional(),
}),
execute: async ({ título, contenido, tags }) => {
// lógica de inserción en DB
return { éxito: true, id: "abc-123" };
},
}),
},
});Construir un agente: tool calling en bucle
Un agente es un modelo que ejecuta herramientas en múltiples pasos. En lugar de una sola llamada, el modelo razona, ejecuta una herramienta, analiza el resultado, y decide si necesita ejecutar otra herramienta o si ya tiene la respuesta final.
AI SDK maneja esto con el parámetro maxSteps en streamText o generateText:
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const resultado = await generateText({
model: openai("gpt-4o"),
system: `Eres un asistente de investigación. Tienes acceso a herramientas
para buscar información y hacer calculos. Usa las herramientas que necesites
para responder la pregunta del usuario de forma completa.`,
prompt: "cuántos habitantes tiene Argentina y cuál es su PIB per capita?",
tools: {
buscarDatos: tool({
description: "Busca datos estadisticos de un pais",
parameters: z.object({
pais: z.string().describe("Nombre del pais"),
tipo: z
.enum(["población", "pib", "superficie", "general"])
.describe("Tipo de dato a buscar"),
}),
execute: async ({ pais, tipo }) => {
// Simulación -- en producción conectas a una API real
const datos: Record<string, Record<string, string>> = {
argentina: {
población: "46.6 millones (2025)",
pib: "USD 621 mil millones (2025)",
superficie: "2.78 millones de km2",
},
};
const paisKey = pais.toLowerCase();
if (datos[paisKey]) {
return tipo === "general"
? datos[paisKey]
: { [tipo]: datos[paisKey][tipo] };
}
return { error: "Pais no encontrado" };
},
}),
calcular: tool({
description: "Realiza un calculo matematico",
parameters: z.object({
expresion: z.string().describe("Expresion matematica a evaluar"),
}),
execute: async ({ expresion }) => {
// En producción usa una librería de math segura, no eval
const resultado = Function(`"use strict"; return (${expresion})`)();
return { resultado: String(resultado) };
},
}),
},
// Permite hasta 5 pasos de razonamiento
maxSteps: 5,
});
// Ver los pasos que tomo el agente
console.log(`Pasos ejecutados: ${resultado.steps.length}`);
console.log(`Respuesta final: ${resultado.text}`);Con maxSteps: 5, el agente puede hacer hasta 5 ciclos de razonamiento. En cada paso, el modelo decide si llama a una herramienta o si genera la respuesta final. AI SDK ejecuta las herramientas y alimenta los resultados de vuelta al modelo automáticamente.
El array resultado.steps te da visibilidad completa de lo que hizo el agente en cada paso: que herramienta llamo, con que parámetros, y que resultado obtuvo.
maxSteps razonable. Sin límite, un modelo podría entrar en un bucle infinito llamando herramientas sin parar. Para la mayoria de los casos, entre 3 y 10 pasos es suficiente.Route handler: API de streaming en Next.js
Ahora conectamos todo con Next.js. El route handler recibe los mensajes del frontend y devuelve un stream que el cliente consume en tiempo real.
// app/api/chat/route.ts
import { streamText, tool, UIMessage, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
// Permitir respuestas largas en Vercel
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const resultado = streamText({
model: openai("gpt-4o"),
system: `Eres un asistente técnico especializado en desarrollo web.
Responde siempre en español. Puedes usar herramientas para obtener
información cuando la necesites.`,
messages: convertToModelMessages(messages),
tools: {
buscarDocumentación: tool({
description: "Busca en la documentación oficial de una tecnologia",
parameters: z.object({
tecnologia: z.string().describe("Nombre de la tecnologia"),
tema: z.string().describe("Tema específico a buscar"),
}),
execute: async ({ tecnologia, tema }) => {
// Conecta a tu fuente de datos real
return {
resultado: `Documentación de ${tecnologia} sobre ${tema}`,
url: `https://docs.example.com/${tecnologia}/${tema}`,
};
},
}),
},
maxSteps: 3,
});
return resultado.toUIMessageStreamResponse();
}Puntos clave de este route handler:
UIMessageyconvertToModelMessages-- convierten los mensajes del formato de UI al formato que espera el modelo. Esto es necesario porque el frontend manejaparts(texto, tool calls, tool results) de forma distinta al modelo.toUIMessageStreamResponse-- convierte el stream a un formato que el hookuseChatdel frontend consume directamente.maxDuration: 30-- necesario si desplegaste en Vercel, donde las funciones serverless tienen timeout por defecto de 10 segundos.
Si planeas hacer deploy de esta aplicación, revisa la guía de deploy en Vercel con Next.js para configurar correctamente el proyecto.
Componente React: chat con useChat
El hook useChat conecta tu componente React con el route handler. Maneja el estado de los mensajes, el envio, el streaming y los errores.
"use client";
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, status, error } =
useChat();
return (
<div className="mx-auto flex w-full max-w-2xl flex-col gap-4 py-8">
{/* Lista de mensajes */}
<div className="flex flex-col gap-3">
{messages.map((mensaje) => (
<div
key={mensaje.id}
className={`rounded-lg px-4 py-3 ${
mensaje.role === "user"
? "bg-blue-600 text-white self-end"
: "bg-zinc-800 text-zinc-100 self-start"
}`}
>
{mensaje.parts.map((part, i) => {
if (part.type === "text") {
return <p key={i}>{part.text}</p>;
}
// Puedes renderizar tool calls y results aquí
if (part.type === "tool-invocation") {
return (
<div key={i} className="text-sm text-zinc-400 italic">
Ejecutando: {part.toolInvocation.toolName}...
</div>
);
}
return null;
})}
</div>
))}
</div>
{/* Indicador de estado */}
{status === "streaming" && (
<p className="text-sm text-zinc-500">Escribiendo...</p>
)}
{/* Error */}
{error && (
<p className="text-sm text-red-400">
Error: {error.message}
</p>
)}
{/* Formulario de entrada */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Escribe tu mensaje..."
className="flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-zinc-100"
/>
<button
type="submit"
disabled={status === "streaming"}
className="rounded-lg bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
Enviar
</button>
</form>
</div>
);
}El flujo completo funciona así: el usuario escribe un mensaje, handleSubmit lo envia al route handler (/api/chat por defecto), el handler procesa con streamText y devuelve un stream, y useChat actualiza messages en tiempo real mientras llegan los tokens.
"use client" porque useChat usa estado de React. Si necesitas repasar cuándo usar Server Components vs Client Components, revisa la guía de Server Components.Estructura del proyecto
así queda la estructura de archivos para una app de chat con AI SDK:
app/
api/
chat/
route.ts
page.tsx
components/
Chat.tsx
.env.local
package.json
Mejores prácticas para producción
Manejo de errores
Nunca dejes que un error del proveedor de IA rompa tu aplicación. Envuelve las llamadas en try/catch y devuelve respuestas útiles:
// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
try {
const { messages }: { messages: UIMessage[] } = await req.json();
const resultado = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
});
return resultado.toUIMessageStreamResponse();
} catch (error) {
console.error("Error en la API de chat:", error);
// Verificar si es un error de rate limit
if (error instanceof Error && error.message.includes("429")) {
return new Response("Demasiadas solicitudes. Intenta en unos segundos.", {
status: 429,
});
}
return new Response("Error interno del servidor.", {
status: 500,
});
}
}Control de costos
Cada llamada a un modelo de IA cuesta dinero. Algunas estrategias para mantener los costos bajo control:
- Elige el modelo correcto: no uses GPT-4o para tareas que GPT-4o-mini resuelve igual de bien. La diferencia de precio es 10x o más.
- Limita
maxTokens: define un tope de tokens en la respuesta para evitar respuestas excesivamente largas. - Monitorea
usage: el objetousageque devuelvengenerateTextystreamTextte da los tokens exactos de cada llamada. - Cachea respuestas: si múltiples usuarios hacen la misma pregunta, no necesitas llamar al modelo cada vez.
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const resultado = await generateText({
model: openai("gpt-4o-mini"), // Modelo más economico
prompt: "qué es TypeScript?",
maxTokens: 500, // Limitar longitud de respuesta
});
// Monitorear costos
console.log(`Tokens de entrada: ${resultado.usage.promptTokens}`);
console.log(`Tokens de salida: ${resultado.usage.completionTokens}`);Rate limiting
Protege tu endpoint para que un solo usuario no queme tu presupuesto de API:
// lib/rate-limit.ts
const solicitudes = new Map<string, { count: number; resetTime: number }>();
export function verificarRateLimit(
ip: string,
límite: number = 20,
ventanaMs: number = 60_000
): boolean {
const ahora = Date.now();
const registro = solicitudes.get(ip);
if (!registro || ahora > registro.resetTime) {
solicitudes.set(ip, { count: 1, resetTime: ahora + ventanaMs });
return true;
}
if (registro.count >= límite) {
return false;
}
registro.count++;
return true;
}// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { verificarRateLimit } from "@/lib/rate-limit";
export async function POST(req: Request) {
// Obtener IP del usuario
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
if (!verificarRateLimit(ip)) {
return new Response("límite de solicitudes alcanzado.", { status: 429 });
}
const { messages }: { messages: UIMessage[] } = await req.json();
const resultado = streamText({
model: openai("gpt-4o-mini"),
messages: convertToModelMessages(messages),
});
return resultado.toUIMessageStreamResponse();
}Cambiar de proveedor sin tocar tu código
Una de las ventajas más grandes de AI SDK es la abstracción de proveedores. Si defines tu modelo en un solo lugar, cambiar de proveedor es un cambio de una línea:
// lib/ai-model.ts
import { openai } from "@ai-sdk/openai";
// import { anthropic } from "@ai-sdk/anthropic";
// import { google } from "@ai-sdk/google";
// Cambia esta línea para cambiar de proveedor
export const modelo = openai("gpt-4o-mini");
// Alternativas:
// export const modelo = anthropic("claude-sonnet-4-20250514");
// export const modelo = google("gemini-2.5-flash");// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { modelo } from "@/lib/ai-model";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const resultado = streamText({
model: modelo, // Un solo lugar para cambiar el modelo
messages: convertToModelMessages(messages),
});
return resultado.toUIMessageStreamResponse();
}Esto te permite hacer A/B testing entre modelos, tener un modelo diferente en desarrollo y producción, o migrar de OpenAI a Anthropic sin refactorizar toda tu aplicación.
Resumen rápido
Lo que cubrimos en esta guía:
- Instalación de AI SDK con proveedores y Zod
- generateText para respuestas completas en una sola llamada
- streamText para respuestas en tiempo real token por token
- Tool calling para que el modelo ejecute funciones definidas por ti
- Agentes con
maxStepspara razonamiento en múltiples pasos - Route handler en Next.js para exponer streaming como API
- Componente React con
useChatpara la interfaz de chat - Mejores prácticas de error handling, rate limiting y control de costos
La documentación oficial de AI SDK es excelente y cubre casos que no entraron aquí, como generateObject para datos estructurados, integración con MCP (Model Context Protocol), y patrones avanzados de multi-agente.
Preguntas frecuentes
¿Qué es Vercel AI SDK y para que sirve?
Vercel AI SDK es una librería open-source de TypeScript que proporciona una interfaz unificada para construir aplicaciones con IA. Soporta múltiples proveedores (OpenAI, Anthropic, Google) y maneja streaming, herramientas y agentes de forma estandarizada.
¿Necesito pagar para usar Vercel AI SDK?
No. Vercel AI SDK es completamente gratuito y open-source. Lo que pagas son las llamadas a los proveedores de IA (OpenAI, Anthropic, etc.) según su pricing individual. Puedes probar con los tiers gratuitos de cada proveedor mientras aprendes.
¿Puedo usar AI SDK sin Vercel como hosting?
Si. AI SDK funciona con cualquier hosting que soporte Node.js: Railway, Render, AWS, tu propio servidor. No esta atado a Vercel, aunque se integra muy bien con su plataforma por el soporte nativo de edge functions y streaming.
¿Cuál es la diferencia entre generateText y streamText?
generateText espera la respuesta completa antes de devolverla. Es ideal para procesamiento en background, generación de emails, resumenes, o cualquier tarea donde no necesitas mostrar la respuesta en tiempo real. streamText envia la respuesta token por token, ideal para interfaces de chat donde el usuario ve la respuesta escribiendose progresivamente.
¿AI SDK soporta múltiples proveedores de IA?
Si. AI SDK soporta OpenAI, Anthropic (Claude), Google (Gemini), Mistral, Cohere y más. Puedes cambiar de proveedor cambiando una línea de código sin modificar el resto de tu aplicación. Los schemas de herramientas, el manejo de streaming y la estructura de mensajes son identicos entre proveedores.
Preguntas frecuentes
¿Qué es Vercel AI SDK y para que sirve?
Vercel AI SDK es una librería open-source de TypeScript que proporciona una interfaz unificada para construir aplicaciones con IA. Soporta múltiples proveedores (OpenAI, Anthropic, Google) y maneja streaming, herramientas y agentes de forma estandarizada.
¿Necesito pagar para usar Vercel AI SDK?
No, Vercel AI SDK es completamente gratuito y open-source. Lo que pagas son las llamadas a los proveedores de IA (OpenAI, Anthropic, etc.) según su pricing individual.
¿Puedo usar AI SDK sin Vercel como hosting?
Si, AI SDK funciona con cualquier hosting que soporte Node.js. No esta atado a Vercel, aunque se integra muy bien con su plataforma.
¿Cuál es la diferencia entre generateText y streamText?
generateText espera la respuesta completa antes de devolverla, ideal para procesamiento en background. streamText envia la respuesta token por token en tiempo real, ideal para interfaces de chat donde el usuario ve la respuesta escribiendose.
¿AI SDK soporta múltiples proveedores de IA?
Si, AI SDK soporta OpenAI, Anthropic (Claude), Google (Gemini), Mistral, Cohere y más. Puedes cambiar de proveedor cambiando una línea de código sin modificar el resto de tu aplicación.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.