Ir al contenido principal
Vercel AI SDK: Por Qué Tu Agente Se Cuelga en Paso 2

Vercel AI SDK: Por Qué Tu Agente Se Cuelga en Paso 2

AI Integration
••5 min read•Por Daily Miranda Pardo

El agente arranca bien. Recibe la consulta, llama a su primera herramienta, obtiene el dato… y para. No hay segunda llamada. No hay respuesta final. El log dice finish_reason: "stop" justo después del primer tool call, como si el agente hubiera decidido que ya había terminado.

No es un bug del modelo ni de Vercel. Es que nadie le dio permiso para seguir.

El problema: generateText solo ejecuta un ciclo

La mayoría de tutoriales del Vercel AI SDK empiezan con generateText. Funciona perfecto para generación de texto simple. El problema aparece cuando añades herramientas y esperas que el agente las use de forma secuencial.

Sin maxSteps explícito, generateText hace exactamente un ciclo completo: llama al LLM, el LLM decide usar una herramienta, ejecuta la herramienta, recibe el resultado, y para. No le pide al LLM que use ese resultado para continuar razonando. El agente se queda a mitad de la tarea.

// Este agente SIEMPRE para después del primer tool call
const result = await generateText({
  model: anthropic('claude-sonnet-5-5'),
  tools: {
    buscarPedido: tool({
      description: 'Busca un pedido por ID',
      parameters: z.object({ pedidoId: z.string() }),
      execute: async ({ pedidoId }) => fetchOrder(pedidoId),
    }),
    actualizarEstado: tool({
      description: 'Actualiza el estado de un pedido',
      parameters: z.object({ pedidoId: z.string(), estado: z.string() }),
      execute: async ({ pedidoId, estado }) => updateOrder(pedidoId, estado),
    }),
  },
  messages,
});
// actualizarEstado nunca se ejecuta aunque el modelo quiera hacerlo.

El agente busca el pedido, recibe la información, y el ciclo termina ahí. actualizarEstado nunca se llama aunque esa era exactamente la intención.

La solución: streamText con maxSteps

Para agentes que necesitan razonar en múltiples pasos —buscar datos, procesarlos, actuar sobre ellos— necesitas streamText con maxSteps configurado:

// app/api/agent/route.ts
import { streamText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: anthropic('claude-sonnet-5-5'),
    maxSteps: 5, // hasta 5 ciclos LLM ↔ herramienta
    tools: {
      buscarPedido: tool({
        description: 'Busca un pedido por ID',
        parameters: z.object({ pedidoId: z.string() }),
        execute: async ({ pedidoId }) => {
          const order = await fetchOrder(pedidoId);
          if (!order) return { error: 'Pedido no encontrado', pedidoId };
          return { data: order };
        },
      }),
      actualizarEstado: tool({
        description: 'Actualiza el estado de un pedido',
        parameters: z.object({ pedidoId: z.string(), estado: z.string() }),
        execute: async ({ pedidoId, estado }) => {
          await updateOrder(pedidoId, estado);
          return { success: true, pedidoId, nuevoEstado: estado };
        },
      }),
    },
    messages,
  });

  return result.toDataStreamResponse();
}

maxSteps: 5 permite hasta 5 ciclos completos: LLM decide → herramienta ejecuta → resultado vuelve al LLM → LLM decide de nuevo. Exactamente lo que necesitas para tareas que requieren combinar varias fuentes de información antes de dar una respuesta.

Tres errores adicionales que rompen el bucle

1. Tool execute que lanza excepciones

Cuando una herramienta lanza un error, el stream se rompe y el bucle agentico termina de forma abrupta. El LLM no recibe ningún contexto sobre lo que falló y no puede tomar una decisión alternativa.

// MAL: una excepción mata el stream completo
execute: async ({ pedidoId }) => {
  const order = await fetchOrder(pedidoId);
  if (!order) throw new Error('No encontrado'); // stream muerto
  return order;
}

// BIEN: un objeto de error que el LLM puede procesar
execute: async ({ pedidoId }) => {
  const order = await fetchOrder(pedidoId);
  if (!order) return { error: 'Pedido no encontrado', pedidoId };
  return order;
}

Devolver un objeto de error en vez de lanzar una excepción le da al modelo la oportunidad de adaptar su siguiente paso: intentar con otro identificador, pedir más información al usuario, o informar del problema de forma controlada.

2. maxSteps desincronizado entre servidor y cliente

Si usas useChat en el cliente, el parámetro maxSteps debe coincidir con el del servidor. El cliente necesita saber cuántas respuestas intermedias puede esperar antes de recibir el texto final.

// app/components/AgentChat.tsx
'use client';
import { useChat } from 'ai/react';

export function AgentChat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api: '/api/agent',
      maxSteps: 5, // debe coincidir con el servidor
    });

  return (
    <form onSubmit={handleSubmit}>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
        </div>
      ))}
      <input value={input} onChange={handleInputChange} disabled={isLoading} />
      <button type="submit" disabled={isLoading}>Enviar</button>
    </form>
  );
}

3. Leer result.text antes de que el stream termine

Si usas streamText en un contexto donde necesitas el texto completo (un webhook, un cron job, una tarea de fondo), debes esperar a que el stream termine:

const result = await streamText({ ... });

// Esperar al stream antes de acceder al resultado
const text = await result.text;
const toolResults = await result.toolResults;

Sin el await, accedes al stream antes de que se complete y obtienes undefined o un valor parcial.

El patrón completo en producción

Para un agente de soporte que busca información y actualiza registros, el flujo completo quedaría así:

Servidor (app/api/agent/route.ts): streamText con maxSteps: 5, herramientas con execute que devuelven objetos —nunca lanzan excepciones—, y result.toDataStreamResponse() para el streaming al cliente.

Cliente (useChat con api: '/api/agent' y maxSteps: 5): el hook gestiona los mensajes de tipo tool_result automáticamente y espera al texto final antes de actualizar la UI.

Este patrón funciona para agentes de soporte, gestión de pedidos, análisis de datos, o cualquier flujo que combine varias herramientas antes de dar una respuesta.

Si estás construyendo este tipo de integración desde cero, en DAILYMP implementamos agentes multi-step en producción sobre Next.js — con manejo de errores, observabilidad y arquitectura que aguanta carga real. Puedes ver cómo trabajamos en Integración IA o en el enfoque AI Driven Development para equipos que quieren integrar agentes en sus flujos de trabajo existentes.


¿Tienes un agente que se queda a medias? Cuéntame cómo está montado y lo miramos juntos.

Escríbeme por WhatsApp →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Responsable: DailyMP (Daily Miranda Pardo). Usamos tu email solo para enviarte el PDF, salvo que marques la casilla de abajo. Más info en la política de privacidad.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.