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. UsastreamEvents()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:
messagespara streaming de tokensupdatesovaluespara el estado del grafocustompara 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 convertirUIMessage[]de AI SDK en mensajes de LangChaintoUIMessageStream(...)para convertir streams de LangChain o LangGraph en partes UIMessage de AI SDK- soporte para
streamEvents() - soporte para eventos de datos personalizados y tipados
LangSmithDeploymentTransportpara conectaruseChatdirectamente 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 pasoupdates: solo los cambios de estado después de cada pasomessages: fragmentos de tokens más metadatos de las llamadas al LLMcustom: tus propios datos enviados desde nodos o herramientas mediantewritertools: eventos del ciclo de vida de las herramientasdebug: 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:
- habilitar la persistencia con un checkpointer
- ejecutar con un identificador de thread
- 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.
messagesproporciona el flujo a nivel de token.valuesproporciona 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_startyon_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_iden 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ón | Ideal para | Por qué funciona |
|---|---|---|
| Cadena de prompts | Tareas deterministas de varios pasos | Es fácil de probar y entender |
| Router | Una solicitud con varias rutas especializadas | Permite controlar bien la latencia y el costo |
| Fan-out / fan-in paralelo | Subtareas independientes | Mayor capacidad de procesamiento y respuestas más rápidas |
| Orquestador-worker | Descomposición dinámica | Es flexible cuando no se conoce de antemano el número de subtareas |
| Evaluador-optimizador | Ciclos para obtener resultados de alta calidad | Permite que un paso evalúe o mejore otro |
| Agente con puntos de aprobación | Acciones de riesgo | Revisió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:
- streaming
- durabilidad
- 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_idde 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: trueno es compatible con la cancelación
Esto lleva a una separación práctica muy útil:
- Si necesitas un chat sencillo con streaming, usa
useChatde 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:
- LangChain + adaptador de AI SDK para entregar rápidamente
- Modos de streaming de LangGraph cuando el producto necesita estado y progreso
- 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
- AI SDK UI, protocolos de streaming
- AI SDK UI, persistencia de mensajes del chatbot
- AI SDK UI, streams reanudables del chatbot
- Adaptador de AI SDK, LangChain
- LangChain JS, runtime
- LangChain JS, agentes
- LangGraph JS, streaming
- LangGraph JS, memoria
- LangGraph JS, persistencia
- LangGraph JS, ejecución durable
- LangGraph JS, interrupts
- LangGraph JS, workflows y agentes
- LangSmith Deployment