tutoriales·15 min de lectura

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.

Terminal
$ npm install ai @ai-sdk/openai zod

Si prefieres usar Anthropic o Google en lugar de OpenAI:

Terminal
$ npm install @ai-sdk/anthropic @ai-sdk/google

Variables de entorno

Crea tu archivo .env.local con la API key del proveedor que vayas a usar:

bash
# 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í
Nunca subas tus API keys al repositorio. Agrega .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.

typescript
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:

typescript
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.

Cambiar de proveedor es literal una línea. Reemplaza 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.

typescript
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ónCaso de uso
generateTextProcesamiento en background, emails, resumenes, tareas sin UI
streamTextChat 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.

typescript
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:

typescript
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" };
      },
    }),
  },
});
Las descripciones de las herramientas y los parámetros son tan importantes como el prompt. El modelo las lee para decidir que herramienta usar y como llenar cada campo. Dedica tiempo a escribirlas bien.

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:

typescript
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.

Define siempre un 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.

typescript
// 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:

  • UIMessage y convertToModelMessages -- convierten los mensajes del formato de UI al formato que espera el modelo. Esto es necesario porque el frontend maneja parts (texto, tool calls, tool results) de forma distinta al modelo.
  • toUIMessageStreamResponse -- convierte el stream a un formato que el hook useChat del 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.

tsx
"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.

El componente necesita la directiva "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:

Estructura de archivos

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:

typescript
// 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 objeto usage que devuelven generateText y streamText te da los tokens exactos de cada llamada.
  • Cachea respuestas: si múltiples usuarios hacen la misma pregunta, no necesitas llamar al modelo cada vez.
typescript
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}`);
Revisa el pricing de OpenAI y el pricing de Anthropic para comparar costos entre modelos. La diferencia entre usar el modelo correcto y el incorrecto puede ser de cientos de dólares al mes.

Rate limiting

Protege tu endpoint para que un solo usuario no queme tu presupuesto de API:

typescript
// 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;
}
typescript
// 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:

typescript
// 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");
typescript
// 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:

  1. Instalación de AI SDK con proveedores y Zod
  2. generateText para respuestas completas en una sola llamada
  3. streamText para respuestas en tiempo real token por token
  4. Tool calling para que el modelo ejecute funciones definidas por ti
  5. Agentes con maxSteps para razonamiento en múltiples pasos
  6. Route handler en Next.js para exponer streaming como API
  7. Componente React con useChat para la interfaz de chat
  8. 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.

#ai-sdk#typescript#nextjs#ia#agentes#vercel

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.