Modern Agent Engineering

Cómo hacer streaming de LangChain y LangGraph a AI SDK

Una guía práctica, respaldada por una revisión de las fuentes, para crear APIs de streaming con LangChain y LangGraph y consumirlas de forma limpia con AI SDK, desde un chat sencillo hasta agentes con ejecución durable.

22 min de lectura

Muchos equipos creen que tienen que elegir entre LangChain/LangGraph y AI SDK.

Por lo general, no es necesario.

Una configuración muy práctica para producción es:

  • LangChain o LangGraph se encarga de la orquestación, las herramientas, la memoria, los checkpoints y la lógica del agente.
  • AI SDK se encarga del contrato con el frontend, el protocolo de streaming y la facilidad de desarrollo de la UI de chat.

Esta separación es especialmente útil cuando quieres pasar de un «chat sencillo con streaming» a un «agente con estado, que usa herramientas y puede reanudarse» sin tener que reescribir la UI cada vez.

Revisé la documentación oficial relevante el March 30, 2026 antes de escribir esta guía. Los ejemplos se basan en la documentación vigente de AI SDK v6 y de LangChain/LangGraph TypeScript.

En resumen

  • Si tu backend está en TypeScript, la ruta más rápida es el adaptador oficial @ai-sdk/langchain.
  • Para un chat sencillo, el patrón es: useChat -> /api/chat -> model.stream(...) -> toUIMessageStream(...).
  • Para agentes que usan herramientas, createAgent(...).stream(...) es la opción predeterminada más clara. Usa streamEvents() cuando necesites eventos semánticos más granulares para depuración u observabilidad.
  • Para LangGraph, el concepto más importante es el modo de streaming:
    • messages para streaming de tokens
    • updates o values para el estado del grafo
    • custom para eventos de progreso
  • Para trabajo de larga duración o que debe poder reanudarse, LangGraph aporta mucho más valor cuando agregas:
    • un checkpointer
    • un thread_id
    • tasks alrededor de los efectos secundarios
    • interrupts para aprobación o revisión humana
  • AI SDK puede consumir:
    • un stream de texto simple
    • o el stream de datos UIMessage más completo a través de SSE
  • Si tu backend está en Python, AI SDK también puede consumirlo. El contrato de streaming es HTTP más SSE; no exige TypeScript.

Qué aprenderás aquí

  • Cómo encajan LangChain, LangGraph y AI SDK en una misma arquitectura.
  • La diferencia entre streaming de texto simple y streaming de UIMessage/datos.
  • Una ruta mínima de streaming con LangChain.
  • Una ruta para un agente que usa herramientas.
  • Una ruta de LangGraph con estado y persistencia de threads.
  • Cómo enviar actualizaciones de progreso personalizadas a la UI mediante streaming.
  • Patrones de diseño comunes para agentes de IA.
  • Cómo abordar el procesamiento en segundo plano y las tareas de larga duración sin quedarte sin opciones más adelante.

Revisión de fuentes: qué confirma la documentación oficial

Esta es la interpretación más clara de la documentación oficial al March 30, 2026.

1. AI SDK admite dos contratos importantes de streaming para el frontend

La documentación actual de AI SDK indica que useChat y los helpers de UI relacionados admiten ambos:

  • streams de texto
  • streams de datos que usan el protocolo UIMessage de AI SDK sobre SSE

La documentación señala explícitamente que:

  • los streams de texto son adecuados para texto simple del asistente
  • el stream de datos/UIMessage es la opción correcta cuando necesitas partes más completas, como llamadas a herramientas, resultados de herramientas, partes de razonamiento, datos personalizados, límites entre pasos y renderizado de mensajes estructurados
  • los backends personalizados pueden implementarse en otros lenguajes, como Python
  • los backends UIMessage personalizados necesitan el header x-vercel-ai-ui-message-stream: v1

Este es un punto arquitectónico importante: AI SDK es un protocolo para el frontend y una capa de UI, no solo un SDK de modelos.

2. El adaptador oficial @ai-sdk/langchain es el puente que necesitan la mayoría de los equipos

La documentación del adaptador ahora ofrece explícitamente:

  • toBaseMessages(...) para convertir UIMessage[] de AI SDK en mensajes de LangChain
  • toUIMessageStream(...) para convertir streams de LangChain o LangGraph en partes UIMessage de AI SDK
  • soporte para streamEvents()
  • soporte para eventos de datos personalizados y tipados
  • LangSmithDeploymentTransport para conectar useChat directamente a un despliegue de LangGraph

Esto significa que el problema del «código de integración» es mucho menor de lo que solía ser.

3. LangChain createAgent se ejecuta sobre el runtime de LangGraph

La documentación del runtime de LangChain es clara: createAgent se ejecuta internamente sobre el runtime de LangGraph.

Esto importa porque explica por qué las piezas del stack se combinan tan bien:

  • empieza con LangChain cuando quieras la ruta más rápida
  • pasa a LangGraph cuando necesites más control sobre el estado, las bifurcaciones, los interrupts o la ejecución durable

No se trata de dos ecosistemas independientes. Es más bien una capa de alto nivel y otra de bajo nivel del mismo modelo de ejecución.

4. El streaming de LangGraph va más allá de los tokens

La documentación de streaming de LangGraph enumera los modos actuales:

  • values: el estado completo después de cada paso
  • updates: solo los cambios de estado después de cada paso
  • messages: fragmentos de tokens más metadatos de las llamadas al LLM
  • custom: tus propios datos enviados desde nodos o herramientas mediante writer
  • tools: eventos del ciclo de vida de las herramientas
  • debug: todo

Esto es muy importante para los equipos de producto.

Si solo transmites tokens, los usuarios ven palabras.

Si transmites estado, ciclo de vida de las herramientas y progreso, los usuarios ven lo que está haciendo el sistema.

5. La ejecución durable tiene tres requisitos indispensables

La documentación de ejecución durable de LangGraph es muy explícita sobre los requisitos mínimos:

  1. habilitar la persistencia con un checkpointer
  2. ejecutar con un identificador de thread
  3. encapsular los efectos secundarios y el trabajo no determinista en tasks

La documentación también explica un punto que es fácil pasar por alto: al reanudar, la ejecución no continúa desde la misma línea exacta de código. Se reproduce desde un punto de reinicio adecuado.

Esto significa que «reanudar después» solo es seguro si la reproducción no duplica los efectos secundarios.

6. Los interrupts son la primitiva adecuada para los puntos de aprobación

La documentación de interrupts de LangGraph los describe como el mecanismo para:

  • workflows de aprobación
  • flujos de revisión y edición
  • revisión de llamadas a herramientas antes de su ejecución
  • intervención humana externa antes de continuar

Este es uno de los patrones más claros para agentes que operan en procesos reales de negocio, especialmente cuando las acciones afectan dinero, tickets, registros o clientes.

El modelo mental

La arquitectura más clara suele verse así:

Browser
  |
  v
useChat() in AI SDK
  |
  v
/api/chat
  |
  +--> convert UIMessage[] -> LangChain messages
  |
  +--> LangChain model / agent / LangGraph graph
  |
  +--> stream tokens, state updates, tool events, or custom progress
  |
  v
toUIMessageStream(...)
  |
  v
AI SDK UIMessage SSE stream
  |
  v
useChat renders message parts

Y cuando agregas durabilidad:

Browser
  |
  v
AI SDK useChat
  |
  v
API route / transport
  |
  +--> LangGraph thread_id
  +--> checkpointer
  +--> optional Redis / stream persistence
  |
  v
Durable graph execution
  |
  +--> messages
  +--> updates
  +--> custom progress
  +--> interrupts
  |
  v
Resume / reconnect / continue later

Ejemplo 1: la configuración funcional más sencilla

Empieza aquí si solo necesitas:

  • texto del asistente por streaming
  • una UI de chat limpia
  • la menor cantidad posible de componentes

Este es el patrón del adaptador oficial, ligeramente simplificado para mayor claridad.

Backend: modelo de LangChain -> stream UIMessage de AI SDK

// app/api/chat/route.ts
import { ChatOpenAI } from "@langchain/openai";
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

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

  const langchainMessages = await toBaseMessages(messages);
  const stream = await model.stream(langchainMessages);

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(stream),
  });
}

Frontend: useChat

"use client";

import { useChat } from "@ai-sdk/react";

export default function ChatPage() {
  const { messages, sendMessage, status } = useChat();

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();

        const input = event.currentTarget.elements.namedItem(
          "message",
        ) as HTMLInputElement;

        sendMessage({ text: input.value });
        input.value = "";
      }}
    >
      {messages.map((message) => (
        <div key={message.id}>
          <strong>{message.role === "user" ? "User" : "AI"}:</strong>
          {message.parts.map((part, index) =>
            part.type === "text" ? <span key={index}> {part.text}</span> : null,
          )}
        </div>
      ))}

      <input name="message" placeholder="Ask something..." />
      <button type="submit" disabled={status === "streaming"}>
        Send
      </button>
    </form>
  );
}

Por qué este es un buen primer paso

  • Mantiene estable el frontend.
  • Te permite cambiar el backend más adelante.
  • Ya utiliza el protocolo de streaming UIMessage, más completo, por lo que no quedas limitado a «solo texto».

Ejemplo 2: agrega herramientas con un agente de LangChain

Cuando necesitas llamadas a herramientas, el siguiente paso más claro es un agente de LangChain.

La documentación oficial confirma que los agentes creados con createAgent(...) admiten métodos de ejecución al estilo de LangGraph, como stream(...) e invoke(...).

Backend: createAgent(...).stream(...)

// app/api/agent/route.ts
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { createAgent, tool } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";

const lookupOrder = tool(
  async ({ orderId }) => {
    return {
      orderId,
      status: "shipped",
      eta: "2026-03-31",
    };
  },
  {
    name: "lookup_order",
    description: "Look up order status by order id.",
    schema: z.object({
      orderId: z.string(),
    }),
  },
);

const agent = createAgent({
  model: new ChatOpenAI({
    model: "gpt-4o",
    temperature: 0,
  }),
  tools: [lookupOrder],
  systemPrompt: "You are a support operations copilot.",
});

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

  const langchainMessages = await toBaseMessages(messages);

  const stream = await agent.stream(
    { messages: langchainMessages },
    { streamMode: ["values", "messages"] },
  );

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(stream),
  });
}

¿Qué cambió?

  • Ahora transmitimos la ejecución del agente, no solo la salida del modelo.
  • messages proporciona el flujo a nivel de token.
  • values proporciona el estado después de cada paso, lo que resulta más útil a medida que el agente se vuelve más complejo.

Una perspectiva de producto

Esta es la etapa en la que los PM suelen empezar a hacer preguntas como:

  • «¿Podemos mostrar cuándo el asistente está consultando un pedido?»
  • «¿Podemos mostrar el progreso en lugar de un indicador de carga?»
  • «¿Podemos mantener estable la UI aunque cambie el backend?»

La respuesta pasa a ser «sí» cuando dejas de pensar solo en streams de tokens.

Ejemplo 3: usa streamEvents() para facilitar la depuración de los streams

La documentación del adaptador de AI SDK admite explícitamente la salida de streamEvents() de LangChain.

Esto es especialmente útil cuando:

  • quieres eventos semánticos como on_tool_start y on_tool_end
  • estás depurando o haciendo trazabilidad
  • estás migrando código existente al estilo LCEL que ya usa streams de eventos

Backend: streamEvents()

// app/api/stream-events/route.ts
import { ChatOpenAI } from "@langchain/openai";
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

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

  const langchainMessages = await toBaseMessages(messages);

  const streamEvents = model.streamEvents(langchainMessages, {
    version: "v2",
  });

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(streamEvents),
  });
}

Cuándo usar streamEvents() en lugar de stream(...)

  • Usa stream(...) cuando te interesen principalmente el renderizado de la respuesta y el estado del grafo.
  • Usa streamEvents() cuando te interesen más los eventos semánticos del ciclo de vida, la depuración o la observabilidad.

Al principio, la distinción parece pequeña, pero en producción importa mucho.

Ejemplo 4: pasa a LangGraph cuando necesites threads con estado

Aquí es donde LangGraph resulta claramente mejor que «solo un loop de agente».

Usa LangGraph cuando necesites:

  • un thread durable
  • checkpoints
  • orquestación de varios pasos
  • bifurcaciones controladas
  • capacidad de reanudación

Backend: LangGraph con un checkpointer y thread_id

// app/api/langgraph/route.ts
import { ChatOpenAI } from "@langchain/openai";
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import {
  END,
  START,
  MessagesAnnotation,
  StateGraph,
} from "@langchain/langgraph";
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";

const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

async function callModel(state: typeof MessagesAnnotation.State) {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
}

const graph = new StateGraph(MessagesAnnotation)
  .addNode("agent", callModel)
  .addEdge(START, "agent")
  .addEdge("agent", END)
  .compile({ checkpointer });

export async function POST(req: Request) {
  const { id, messages }: { id: string; messages: UIMessage[] } = await req.json();

  const langchainMessages = await toBaseMessages(messages);

  const stream = await graph.stream(
    { messages: langchainMessages },
    {
      configurable: {
        thread_id: id,
      },
      streamMode: ["messages", "updates"],
    },
  );

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(stream),
  });
}

En una aplicación real, llama a await checkpointer.setup() una sola vez, cuando inicialices por primera vez el checkpointer respaldado por Postgres.

Por qué importa

Ahora el mismo id puede convertirse en:

  • tu ID de chat en AI SDK
  • tu thread_id en LangGraph
  • tu clave de ejecución durable en el checkpointer

Este tipo de alineación simplifica los sistemas.

updates frente a values

Usa updates cuando quieras señales de estado más ligeras y económicas.

Usa values cuando quieras el estado completo del grafo después de cada paso, por ejemplo, para:

  • UIs de depuración más completas
  • paneles de inspección
  • flujos de producto en los que cada paso actualiza un plan o una lista de verificación visible

Ejemplo 5: envía eventos de progreso personalizados a la UI mediante streaming

Este es uno de los patrones más útiles de todo el stack.

La documentación del runtime de LangChain indica que este incluye un stream writer, y la documentación de streaming de LangGraph explica que el modo custom emite datos definidos por el usuario a través de writer.

El adaptador de AI SDK luego convierte estos eventos en partes tipadas data-{type}.

Backend: emite progreso desde una herramienta

// app/api/custom-data/route.ts
import { createUIMessageStreamResponse, type UIMessage } from "ai";
import { createAgent, tool } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";

const analyzeDataset = tool(
  async ({ dataset }, config) => {
    const steps = ["loading", "cleaning", "summarizing"];

    for (const [index, step] of steps.entries()) {
      config.writer?.({
        type: "progress",
        id: `dataset-${dataset}`,
        step,
        message: `${step} ${dataset}`,
        progress: Math.round(((index + 1) / steps.length) * 100),
      });

      await new Promise((resolve) => setTimeout(resolve, 500));
    }

    return {
      dataset,
      summary: "No major issues found",
    };
  },
  {
    name: "analyze_dataset",
    description: "Analyze a dataset and report progress.",
    schema: z.object({
      dataset: z.string(),
    }),
  },
);

const agent = createAgent({
  model: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }),
  tools: [analyzeDataset],
});

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

  const langchainMessages = await toBaseMessages(messages);

  const stream = await agent.stream(
    { messages: langchainMessages },
    { streamMode: ["values", "messages", "custom"] },
  );

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(stream),
  });
}

Frontend: renderiza data-progress

"use client";

import { useChat } from "@ai-sdk/react";

export default function AnalysisPage() {
  const { messages, sendMessage } = useChat({
    onData: (dataPart) => {
      if (dataPart.type === "data-progress") {
        console.log("progress update", dataPart.data);
      }
    },
  });

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) => {
            if (part.type === "text") {
              return <div key={index}>{part.text}</div>;
            }

            if (part.type === "data-progress") {
              return (
                <div key={index}>
                  {part.data.progress}% - {part.data.message}
                </div>
              );
            }

            return null;
          })}
        </div>
      ))}

      <button
        onClick={() =>
          sendMessage({
            text: "Analyze the sales dataset",
          })
        }
      >
        Start
      </button>
    </div>
  );
}

Por qué este patrón es tan útil

Reemplaza estados de carga imprecisos:

"Thinking..."

por información clara sobre el progreso:

loading sales
cleaning sales
summarizing sales

Esto mejora:

  • la UX
  • las demos para las partes interesadas
  • la confianza
  • la depuración

Ejemplo 6: conecta useChat directamente a un runtime de LangGraph desplegado

Esta es la opción avanzada más clara si ya tienes un despliegue de LangGraph.

El adaptador de AI SDK ahora incluye LangSmithDeploymentTransport, lo que significa que el frontend puede comunicarse directamente con un despliegue de LangGraph sin una ruta de backend personalizada de por medio.

"use client";

import { useMemo } from "react";
import { useChat } from "@ai-sdk/react";
import { LangSmithDeploymentTransport } from "@ai-sdk/langchain";

export default function LangGraphChat() {
  const transport = useMemo(
    () =>
      new LangSmithDeploymentTransport({
        url: "https://your-deployment.us.langgraph.app",
        apiKey: process.env.NEXT_PUBLIC_LANGGRAPH_API_KEY,
      }),
    [],
  );

  const { messages, sendMessage } = useChat({ transport });

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) =>
            part.type === "text" ? <div key={index}>{part.text}</div> : null,
          )}
        </div>
      ))}

      <button onClick={() => sendMessage({ text: "Research AI agent architecture" })}>
        Ask
      </button>
    </div>
  );
}

Este patrón resulta muy atractivo cuando:

  • tu runtime de LangGraph ya está desplegado
  • quieres mantener sencillo el código del frontend
  • no quieres implementar la lógica de transporte por tu cuenta

Si tu backend está en Python

Esto importa porque muchos equipos que usan LangGraph trabajan principalmente con Python.

La documentación del protocolo de streaming de AI SDK indica explícitamente que los endpoints compatibles personalizados pueden implementarse en otros lenguajes, como Python.

Regla general

  • Si solo necesitas texto simple del asistente, emite un stream de texto.
  • Si necesitas llamadas a herramientas, eventos de progreso o un renderizado más completo, emite el protocolo UIMessage SSE.

Estructura mínima de UIMessage SSE compatible con Python

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json

app = FastAPI()


@app.post("/api/chat")
async def chat():
    async def event_stream():
        yield 'data: {"type":"start","messageId":"msg-1"}\n\n'
        yield 'data: {"type":"text-start","id":"text-1"}\n\n'
        yield f'data: {json.dumps({"type":"text-delta","id":"text-1","delta":"Hello from Python"})}\n\n'
        yield 'data: {"type":"text-end","id":"text-1"}\n\n'
        yield 'data: {"type":"finish"}\n\n'
        yield 'data: [DONE]\n\n'

    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
        headers={
            "x-vercel-ai-ui-message-stream": "v1",
        },
    )

En el frontend, la misma llamada a useChat() sigue funcionando.

Esa es la idea clave:

A AI SDK le importa el contrato de streaming, no si tu backend usa TypeScript o Python.

Patrones de diseño comunes para agentes de IA

La guía de workflows de LangGraph es especialmente útil en este punto. Distingue entre workflows y agentes, y luego documenta varios patrones recurrentes.

Esta es la versión resumida que usaría en proyectos reales.

PatrónIdeal paraPor qué funciona
Cadena de promptsTareas deterministas de varios pasosEs fácil de probar y entender
RouterUna solicitud con varias rutas especializadasPermite controlar bien la latencia y el costo
Fan-out / fan-in paraleloSubtareas independientesMayor capacidad de procesamiento y respuestas más rápidas
Orquestador-workerDescomposición dinámicaEs flexible cuando no se conoce de antemano el número de subtareas
Evaluador-optimizadorCiclos para obtener resultados de alta calidadPermite que un paso evalúe o mejore otro
Agente con puntos de aprobaciónAcciones de riesgoRevisión humana antes de los efectos secundarios

Una regla práctica

Usa el patrón más sencillo que se ajuste al trabajo:

  • Si la ruta se conoce en gran medida, usa un workflow.
  • Si la ruta debe adaptarse durante la ejecución, usa un agente.
  • Si las acciones son riesgosas, agrega puntos de aprobación basados en interrupts.

Este consejo puede sonar aburrido, pero evita mucha complejidad innecesaria.

Procesamiento en segundo plano para tareas de larga duración

Aquí es donde los equipos suelen confundir tres problemas distintos:

  1. streaming
  2. durabilidad
  3. reanudación/reconexión

Están relacionados, pero no son lo mismo.

El patrón en el que más confío

User sends message
    |
    v
API / transport receives chat id
    |
    +--> LangGraph runs with thread_id
    +--> checkpointer saves progress
    +--> worker emits messages / updates / custom events
    |
    v
AI SDK renders partial progress
    |
    +--> refresh / disconnect / later revisit
    |
    v
resume same thread / stream / approval state

Qué hacer cuando una tarea puede tardar un tiempo

1. Mantén la capa web sin estado

No guardes el estado activo de un thread en la memoria del proceso de una sola instancia de Node.

Persiste:

  • el ID de chat
  • el thread_id de LangGraph
  • el historial de mensajes o los mensajes de la UI
  • cualquier identificador de stream activo

2. Usa checkpoints de LangGraph para el estado de ejecución

Si el trabajo puede pausarse, reintentarse o reanudarse, los checkpoints suelen ser más importantes que una UI sofisticada.

Sin ellos, «reanudar después» suele significar «empezar de nuevo y esperar que nada falle».

3. Trata los efectos secundarios como sensibles a la reproducción

Esta es una de las reglas más importantes de ejecución durable en la documentación de LangGraph.

Si al reanudar se puede reproducir la ejecución, entonces:

  • los correos electrónicos
  • los pagos
  • las escrituras en tickets
  • las actualizaciones del CRM
  • las mutaciones mediante APIs externas

deben aislarse cuidadosamente, por lo general mediante tasks o nodos claramente separados.

4. Usa interrupts para las aprobaciones, no booleanos improvisados

Si una persona debe aprobar una llamada a una herramienta o una acción de negocio, modela esa pausa explícitamente.

Esto es más claro que:

  • poner en espera una solicitud
  • consultar continuamente flags dispersos
  • mantener el estado de una aprobación pendiente dentro de una pestaña del navegador

Consejos específicos de AI SDK para trabajo de larga duración

La documentación actual de AI SDK indica que useChat admite streams reanudables para generaciones de larga duración, pero también señala que:

  • la reanudación requiere mensajes y streams activos persistidos
  • necesitas almacenamiento, Redis y endpoints POST/GET
  • resume: true no es compatible con la cancelación

Esto lleva a una separación práctica muy útil:

  • Si necesitas un chat sencillo con streaming, usa useChat de la forma habitual.
  • Si necesitas una UX de larga duración que permita reconectarse, agrega streams reanudables de AI SDK.
  • Si necesitas orquestación durable y estado reanudable, combina eso con checkpoints e identificadores de thread de LangGraph.

En otras palabras:

La reanudación de AI SDK se encarga de reconectar el stream. La ejecución durable de LangGraph se encarga de reanudar el trabajo.

Son responsabilidades complementarias, no competidoras.

Un ejemplo sencillo de interrupt

Esta es la idea básica de un punto de aprobación humana en LangGraph:

import { interrupt, Command } from "@langchain/langgraph";

async function approveRefund(state: {
  orderId: string;
  amount: number;
}) {
  const decision = interrupt({
    kind: "approval",
    action: "refund_order",
    orderId: state.orderId,
    amount: state.amount,
  });

  if (!decision.approved) {
    return {
      status: "cancelled",
    };
  }

  return {
    status: "approved",
  };
}

// later, resume the same thread:
await graph.invoke(
  new Command({
    resume: {
      approved: true,
    },
  }),
  {
    configurable: {
      thread_id: "chat_123",
    },
  },
);

La UI concreta para este proceso puede variar.

El punto arquitectónico es más importante que el botón específico que se presione:

los interrupts proporcionan un límite explícito de pausa y reanudación que se adapta a procesos reales de negocio.

Errores comunes que debes evitar

1. Usar streams de texto cuando realmente necesitas streams de datos

Si necesitas:

  • renderizado de herramientas
  • eventos de progreso
  • partes de razonamiento
  • mensajes que combinan varios pasos

usa el stream UIMessage/de datos, no texto simple.

2. Olvidar thread_id

La persistencia de LangGraph se basa en threads.

Si no pasas un identificador de thread, en realidad no tienes un estado de conversación reanudable.

3. Mezclar el estado volátil de la UI con el estado de ejecución durable

Que la ventana de chat esté abierta no representa el estado de ejecución.

El estado durable debe residir en:

  • un checkpointer
  • una base de datos
  • o un runtime diseñado para threads reanudables

4. Ocultar todo el progreso detrás de un indicador de carga

Transmitir solo el texto final suele ser técnicamente correcto, pero poco satisfactorio desde la perspectiva del producto.

Los eventos de progreso suelen ser económicos de agregar y marcan una gran diferencia en la confianza.

5. Reproducir efectos secundarios de forma insegura

Si un grafo reanudado puede enviar accidentalmente el mismo reembolso o correo electrónico, o ejecutar la misma mutación dos veces, no tienes un problema de streaming.

Tienes un problema de diseño de durabilidad.

Conclusión

La forma más clara de pensar en este stack es:

  • LangChain es tu ruta rápida hacia el comportamiento de un agente.
  • LangGraph es tu capa de control para threads, checkpoints, interrupts y orquestación durable.
  • AI SDK es tu capa de streaming e interacción en el frontend.

Esa combinación escala bien desde:

  • «solo transmitir una respuesta»

hasta:

  • «transmitir la ejecución de un agente con estado, que usa herramientas y puede reanudarse, con actualizaciones de progreso y puntos de aprobación»

Si hoy diseñara desde cero, normalmente empezaría con esta progresión:

  1. LangChain + adaptador de AI SDK para entregar rápidamente
  2. Modos de streaming de LangGraph cuando el producto necesita estado y progreso
  3. checkpointer + thread_id + interrupts cuando el trabajo requiere durabilidad o se vuelve crítico para el negocio

Esa ruta mantiene estable la UI mientras el backend evoluciona.

Fuentes