Agentes con LangGraph para retail: por qué un grafo, qué primitivas hacen falta y el stack que comparten los cuatro casos

Índice

TL;DR

Un agente de LangChain construido con create_agent ya es un grafo de LangGraph. Compilado sin middleware tiene cuatro nodos: entrada, modelo, herramientas y salida. Cada middleware que se añade mete nodos nuevos en ese grafo, y el checkpointer guarda una fila por paso del grafo, no por turno de conversación. Con dos middlewares de límite, un turno con una sola llamada a herramienta pasa de 5 checkpoints a 11. Esto se mide antes de dimensionar la base de datos, no después.

El bucle de herramientas resuelve el primer caso de la serie y se queda corto en los otros tres. No sabe parar a pedir permiso y retomar desde ahí días después, no sabe abrir en abanico miles de elementos y agregar el resultado, y no sabe iterar contra un evaluador con un contador de intentos. Esas tres cosas son primitivas del grafo, no del bucle.

La serie usa un único dominio de retail, con un catálogo ficticio y unas tablas fijas, para que los cuatro agentes compartan datos, contexto de ejecución y métricas. Cada artículo enseña una primitiva sobre ese dominio: el ciclo modelo-herramientas con middleware, la interrupción con reanudación, el map-reduce con Send y reductores, y los subgrafos con bucle acotado.

El stack es el mismo en los cuatro casos y corre en un cluster propio: un modelo abierto servido en vLLM, LiteLLM delante con una clave virtual por agente, Postgres con pgvector para el checkpointer, el almacén y el RAG, y Langfuse para las trazas. Ninguna pieza exige licencia comercial ni conexión con nadie. El servidor HTTP de LangGraph se queda fuera de la serie a propósito.

Hay dos trampas de configuración que aparecen en el minuto uno y que no avisan. Un modelo servido en vLLM con nombre no reconocido por LangChain no tiene perfil, así que la ventana de contexto es desconocida para el agente. Y el middleware de resumen, construido sin disparador, no resume nunca. Ninguna de las dos lanza error.

Casi todo lo que hay publicado sobre agentes con LangGraph sigue usando create_react_agent, que está deprecado desde la 1.0. Esta serie se escribe contra langgraph 1.2.11 y langchain 1.4.1, y cada afirmación sobre el código se ha comprobado en el paquete instalado, no en la documentación.

Estás aquí: después de las licencias y del gateway, el agente

El blog ya ha cubierto lo que hay debajo. La elección del gateway de inferencia, cómo se opera LiteLLM con Langfuse, cómo se dimensiona ese gateway cuando lo que hay detrás son agentes y no personas, y, en el artículo que abre la vertical agéntica, qué es libre y qué no en deepagents y qué hay que construir para servirlo sin depender de nadie. Ese último artículo deja el motor del grafo como pieza MIT que corre en cualquier sitio y el servidor gestionado como pieza con licencia.

Esta serie baja un nivel. Ya no pregunta qué armazón elegir. Pregunta cómo se construye un agente concreto sobre ese motor, con un problema de negocio delante y con números que un responsable de tienda entienda. Son seis artículos:

  1. Este: el marco, las primitivas, el dominio compartido y el stack.
  2. Empleado de tienda: asistente de sala sobre procedimientos, stock y roturas. Ciclo modelo-herramientas, RAG con pgvector, middleware. Es el que se despliega en una semana y el que tiene el retorno más fácil de medir.
  3. Cliente: pedidos, devoluciones y sustituciones. Aquí se pone el límite de la aprobación humana, con interrupt() y reanudación sobre el checkpointer.
  4. Cadena de suministro: análisis de incidencias con proveedores y planificación de reposición. Plan-and-execute con map-reduce sobre miles de referencias.
  5. Datos de producto: enriquecimiento y control de calidad de fichas. Generador y evaluador en un bucle de reflexión acotado.
  6. Cierre operativo: los cuatro agentes servidos en el mismo cluster sin el servidor de LangGraph, con autorización por hilo, retención y sondas.

Los artículos 1 a 4 se leen sueltos. Este marco existe para no repetir en cada uno la parte de dominio, stack y método.

Después de la serie hay una tanda de dos sobre pruebas y evaluación: tests de grafo con un modelo guionizado y evaluación con datasets de Langfuse. Y una tercera sobre memoria y enrutado, que empieza por la memoria entre hilos con PostgresStore.

La analogía: el dependiente con iniciativa y el manual de turno

Un bucle de herramientas es un dependiente con iniciativa y un teléfono. Le llega una pregunta, decide a quién llamar, llama, escucha, vuelve a decidir. Para consultas de sala funciona bien y es lo que se quiere: rapidez y criterio dentro de un margen estrecho.

Un grafo es el manual de turno. Dice en qué orden se hacen las cosas, qué pasos se pueden hacer a la vez, dónde hay que parar a esperar la firma del encargado, y qué se hace si el proveedor no contesta. El dependiente sigue dentro, decidiendo en cada casilla, pero las casillas y las flechas las ha fijado alguien antes y no cambian por mucha iniciativa que tenga.

LangGraph es el manual, y create_agent es un manual de una sola casilla con el dependiente dentro. Los tres casos que vienen después del primero necesitan casillas nuevas.

Por qué un grafo y no un bucle

Antes de decidirlo hay que ver qué es exactamente el bucle en la versión actual. En langchain 1.4.1, create_agent devuelve un CompiledStateGraph de LangGraph. Compilado con un modelo y tres herramientas, sin middleware, tiene esta estructura:

__start__ -> model -> (condicional) -> tools -> model
                                    -> __end__

Cuatro nodos. El estado por defecto es AgentState, con un canal messages que acumula mensajes con el reductor add_messages, un canal privado jump_to para que el middleware desvíe el flujo, y structured_response para la salida tipada. Los middlewares se enganchan como nodos o como envoltorios. ModelCallLimitMiddleware añade un nodo antes del modelo y otro después; ToolCallLimitMiddleware añade uno después. Con esos dos, el grafo pasa a siete nodos, y el mismo turno con una llamada a herramienta deja 11 checkpoints en la base de datos en vez de 5. El checkpointer escribe una fila por superstep, y cada nodo de middleware es un superstep más. Es un coste pequeño por turno y grande por millón de turnos; se vuelve sobre él en el artículo 5.

Lo que el bucle da, y da bien:

  • Un ciclo modelo-herramientas con parada por defecto. El modelo llama a herramientas hasta que devuelve una respuesta sin llamadas. Con ToolCallLimitMiddleware y ModelCallLimitMiddleware se acota el número de vueltas por ejecución y por hilo, y exit_behavior="end" corta sin lanzar excepción.
  • Un prompt que depende del contexto de ejecución. Con @dynamic_prompt el sistema se construye en cada llamada al modelo a partir de runtime.context, que es un objeto tipado que se pasa en invoke. Tienda, empleado y rol viajan ahí, no en el prompt ni en el estado.
  • Herramientas con acceso al contexto. Una herramienta declarada con un parámetro runtime: ToolRuntime[Contexto] recibe el mismo objeto. El modelo no ve ese parámetro y no puede inventárselo. Es la forma de que la consulta de stock sepa en qué tienda está sin que el modelo lo diga.
  • Persistencia por hilo. Cualquier checkpointer que se le pase guarda el estado completo después de cada superstep, y el hilo se identifica por thread_id en la configuración.

Lo que el bucle no da:

  • Parar y retomar con intervención humana. Existe interrupt(), y existe HumanInTheLoopMiddleware que la usa por debajo para pedir aprobación antes de una herramienta. Pero el diseño del punto de aprobación, qué se muestra al humano, qué decisiones se le permiten, qué pasa si edita los argumentos y cómo se reanuda con Command(resume=...), es diseño de grafo. En el bucle es una casilla más entre el modelo y la herramienta. Cuando la aprobación depende del importe, del historial del cliente y de si el pedido ya salió del almacén, hace falta un nodo que decida si se pregunta o no, y eso ya no es el bucle.
  • Abrir en abanico y agregar. Analizar dos mil referencias con un modelo no es dos mil llamadas en secuencia dentro de un bucle. Es un nodo de planificación, un Send por elemento hacia un nodo trabajador, un reductor en el estado que acumule resultados sin pisarse, y un nodo diferido que espere a que todos acaben. add_node(..., defer=True) y cache_policy son parámetros de StateGraph.add_node, no del agente.
  • Iterar contra un evaluador con contador. Generar una ficha de producto, evaluarla contra unos criterios, corregir, volver a evaluar, y parar a la tercera. Es un ciclo con dos nodos y una arista condicional que lee un contador del estado. En el bucle se puede simular con una herramienta que evalúa, pero el modelo decide cuándo llamarla y cuándo dejar de llamarla, y esa decisión es justo la que no se le quiere dar.

La regla de la serie: se usa create_agent mientras el problema quepa en un ciclo modelo-herramientas con middleware, y se baja a StateGraph cuando hace falta una casilla que el modelo no debe controlar. El primer caso se queda en el agente. Los otros tres bajan al grafo, y en los tres el agente del caso 1 reaparece como subgrafo.

Las cuatro primitivas, una por caso

Cada artículo de la serie enseña una primitiva del grafo sobre un problema de negocio. Aquí va la lista con la forma exacta que tiene en la 1.2.11, para que los artículos posteriores no tengan que presentarla desde cero.

Caso 1: ciclo modelo-herramientas y middleware

from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware, dynamic_prompt, ModelRequest

@dynamic_prompt
def prompt(request: ModelRequest) -> str:
    ctx = request.runtime.context
    return f"Eres el asistente de la tienda {ctx.tienda}. Hablas con {ctx.empleado}."

agente = create_agent(
    modelo,
    tools=[stock_sku, procedimiento],
    middleware=[prompt, ToolCallLimitMiddleware(run_limit=6, exit_behavior="end")],
    context_schema=Contexto,
    checkpointer=checkpointer,
)

La firma completa de create_agent admite además state_schema, store, response_format, interrupt_before e interrupt_after, cache y transformers. Los que importan para esta serie son middleware, context_schema, checkpointer y store.

Caso 2: interrupción y reanudación

from langgraph.types import interrupt, Command

def aprobar_devolucion(state):
    decision = interrupt({
        "pedido": state["pedido_id"],
        "importe": state["importe"],
        "motivo": state["motivo"],
    })
    if decision["aprobada"]:
        return {"estado": "aprobada"}
    return {"estado": "rechazada", "comentario": decision.get("comentario")}

# Días después, desde otro proceso:
grafo.invoke(Command(resume={"aprobada": True}), config={"configurable": {"thread_id": hilo}})

interrupt(value) es una función de un solo argumento. Cuando se ejecuta, el grafo guarda el estado en el checkpointer y devuelve el control con el valor en __interrupt__. La reanudación entra con Command(resume=...) y el nodo se ejecuta entero otra vez desde el principio; la llamada a interrupt devuelve entonces el valor reanudado. Dos consecuencias que condicionan el diseño del caso 2: la interrupción va lo antes posible dentro del nodo, y nada con efecto externo se pone antes de ella en ese mismo nodo. Sin checkpointer, interrupt lanza ValueError.

Caso 3: abanico con Send y reductores

import operator
from typing import Annotated, TypedDict
from langgraph.types import Send
from langgraph.graph import StateGraph, START, END

class Estado(TypedDict):
    skus: list[str]
    analisis: Annotated[list[dict], operator.add]

def planificar(state):
    return [Send("analizar_sku", {"sku": s}) for s in state["skus"]]

grafo = StateGraph(Estado)
grafo.add_node("cargar", cargar)
grafo.add_node("analizar_sku", analizar_sku)
grafo.add_node("consolidar", consolidar, defer=True)
grafo.add_edge(START, "cargar")
grafo.add_conditional_edges("cargar", planificar, ["analizar_sku"])
grafo.add_edge("analizar_sku", "consolidar")
grafo.add_edge("consolidar", END)

Send(nodo, estado) lanza una instancia del nodo trabajador con un estado propio, y todas las instancias corren en el mismo superstep. El reductor operator.add sobre analisis es lo que permite que dos mil trabajadores escriban en la misma lista sin condiciones de carrera: LangGraph aplica las escrituras al final del superstep, en orden determinista. defer=True en consolidar lo retrasa hasta que no quede ningún camino pendiente hacia él. El caso 3 añade cache_policy por nodo para no reanalizar la misma referencia dos veces en la misma jornada.

Caso 4: subgrafos y bucle acotado

def decidir(state) -> str:
    if state["veredicto"] == "aprobada" or state["intentos"] >= 3:
        return "publicar"
    return "generar"

grafo.add_node("generar", generar)
grafo.add_node("evaluar", evaluar)
grafo.add_node("publicar", publicar)
grafo.add_edge("generar", "evaluar")
grafo.add_conditional_edges("evaluar", decidir, {"generar": "generar", "publicar": "publicar"})

El contador intentos vive en el estado con un reductor que suma, y la arista condicional lo lee. El modelo no ve ese contador ni decide cuándo parar. generar y evaluar pueden ser cada uno un agente de create_agent compilado y añadido como nodo; un grafo compilado es un nodo válido y comparte con el padre las claves de estado que tengan el mismo nombre.

El dominio compartido

Cuatro agentes sobre cuatro catálogos distintos no serían una serie. Se fija uno, ficticio, con el tamaño justo para que el caso 3 escale.

Tres tiendas, T001 a T003, un almacén central A001 y un catálogo generado de 2.000 referencias en el repositorio de ejemplo, con un generador para subir a 30.000 cuando el caso 3 lo necesite. Las tablas, en Postgres:

TablaClavesQuién la usa
productossku, nombre, familia, proveedor_id, pvpTodos
fichassku, titulo, descripcion, atributos (jsonb), estadoCaso 4
stock(tienda, sku), unidades, reservadas, actualizadoCasos 1, 2, 3
movimientosid, tienda, sku, tipo (venta, rotura, merma, recepción), unidades, empleado, tsCasos 1, 3
pedidos / lineas_pedidopedido_id, cliente_id, estado, canal; sku, unidades, precioCaso 2
devolucionesid, pedido_id, motivo, importe, estado, aprobada_porCaso 2
proveedores / incidenciasproveedor_id, plazo_dias, sla; id, proveedor_id, sku, tipo, abierta_ts, cerrada_tsCaso 3
procedimientosid, seccion, texto, rol_minimo, embedding (vector)Caso 1

El contexto de ejecución también es común. Es una dataclass que se pasa en cada invoke y que las herramientas y el prompt leen desde runtime.context:

@dataclass
class Contexto:
    tienda: str        # "T001"
    empleado: str      # identificador, no nombre
    rol: str           # "dependiente" | "encargado" | "compras" | "catalogo"

Tres decisiones sobre este contexto que se mantienen en toda la serie. La primera: el rol se comprueba dentro de la herramienta, no en el prompt. Una herramienta que solo puede usar un encargado devuelve un error estructurado si el rol no es el suyo, y el prompt le dice al modelo que no insista. Un modelo no es una capa de autorización. La segunda: el contexto no se guarda en el estado ni en el checkpoint. Viaja en cada llamada, así que un hilo reanudado por otra persona lleva el contexto de esa persona. La tercera: el thread_id lo construye la aplicación a partir del empleado y de un identificador de sesión, nunca lo elige el cliente. El checkpointer no comprueba propietarios; esa comprobación es de la capa HTTP que se escribe en el artículo 5.

El stack común

Todo corre en un cluster de Kubernetes genérico. No hay ninguna pieza que exija salir a Internet ni clave de licencia.

PiezaPapelNota
vLLMSirve el modelo abiertoUn modelo de clase 30B en FP8 con ventana de 32.768 tokens por defecto para los casos 1 y 2; 128K para el 3
LiteLLMGateway delante de vLLMUna clave virtual por agente, con presupuesto y límite de peticiones. Es lo que separa el coste de cada caso
Postgres 16 + pgvectorCheckpointer, almacén y RAGBases separadas en el mismo cluster: agentes para checkpoints y store, retail para las tablas de negocio y los embeddings
LangfuseTrazas y costePor el callback de LangChain en los casos 1 a 4; el artículo 5 discute la vía OTel
FastAPIServir los grafosSe escribe en el artículo 5; hasta entonces los ejemplos se invocan desde Python

Dos detalles del modelo que hay que fijar antes de escribir una línea del agente.

Pasar la instancia del modelo, no la cadena. create_agent acepta "openai:nombre" como atajo, pero con un modelo servido en vLLM el nombre no está en la tabla de perfiles de LangChain, y la instancia construida a mano es la única forma de controlar base_url, api_key y el perfil:

from langchain_openai import ChatOpenAI

modelo = ChatOpenAI(
    model="qwen3-30b-a3b",
    base_url="http://litellm.agentes.svc:4000/v1",
    api_key=os.environ["LITELLM_KEY_TIENDA"],
    temperature=0,
    profile={"max_input_tokens": 32768},
)

Sin profile, modelo.profile es None para cualquier nombre que no sea un modelo de OpenAI. Verificado con langchain-openai 1.6.2: ChatOpenAI(model="qwen3-30b", base_url=...) devuelve perfil None sin avisar. Con perfil, devuelve el diccionario que se le pasó. El valor tiene que quedar por debajo del --max-model-len de vLLM, porque el agente cuenta los tokens de forma aproximada y el servidor los cuenta con su tokenizador.

El middleware de resumen no resume sin disparador. SummarizationMiddleware(modelo) sin trigger construye una lista de cláusulas vacía y _should_summarize devuelve False siempre. No hay error ni aviso. Con trigger=("fraction", 0.8) sí hay comprobación en el constructor: sin perfil lanza una excepción con el mensaje de que hace falta profile={"max_input_tokens": ...}. Es el único sitio de todo el stack donde la ausencia de perfil se hace notar; en el resto, un modelo sin perfil es un modelo con ventana infinita hasta que vLLM devuelve un error de longitud de contexto.

Con esto, el resumen se configura así en los cuatro casos:

SummarizationMiddleware(
    modelo,
    trigger=("fraction", 0.75),
    keep=("fraction", 0.25),
)

Y el agente no llega nunca al límite del servidor. Con 32.768 de ventana, resume al pasar de 24.576 tokens aproximados y se queda con los últimos 8.192.

Lo que se mide en cada caso

Un responsable de tienda no compra un grafo. Compra minutos de dependiente, devoluciones bien resueltas o fichas publicadas. Cada artículo cierra con una tabla de métricas medidas, y las métricas salen de dos sitios: Langfuse, para lo que el agente hace, y la base de negocio, para lo que pasa después.

CasoMétrica de agente (Langfuse)Métrica de negocio (Postgres)
1 EmpleadoLatencia p50/p95 por consulta, tokens y coste por consulta, llamadas a herramienta por turno, tasa de cortes por límiteConsultas resueltas sin escalar al encargado, tiempo hasta registro de una rotura
2 ClienteInterrupciones por cada cien pedidos, tiempo entre interrupción y reanudación, decisiones editadas frente a aprobadas tal cualDevoluciones aprobadas fuera de política (debe ser cero), importe medio aprobado con y sin humano
3 SuministroTokens por referencia analizada, aciertos de caché de nodo, duración del abanico frente al número de referenciasReferencias con rotura evitada, días de cobertura tras la reposición propuesta
4 FichasIntentos por ficha hasta veredicto, tasa de aprobación a la primera, segunda y tercera, tokens por ficha publicadaFichas publicadas por hora, devoluciones por descripción incorrecta

Las métricas de agente se sacan con dos valores en la configuración: metadata={"caso": "tienda", "tienda": ctx.tienda} y el thread_id. Langfuse agrupa por ellos. Las métricas de negocio se sacan con SQL sobre las tablas del dominio, con el empleado y el asiento que las herramientas escriben en movimientos y devoluciones. Correlar las dos es cuestión de escribir el trace_id de Langfuse en la fila de negocio; las herramientas de escritura de la serie lo hacen.

Lo que la serie no cubre

El servidor HTTP de LangGraph (langgraph-api), su cola de ejecuciones, el cron, Studio y la API de asistentes, hilos y ejecuciones. Todo eso tiene licencia Elastic 2.0, pide clave de licencia comercial para producción y llama a casa; el artículo de deepagents lo documenta con fichero y línea. La serie sirve los grafos con FastAPI en el artículo 5 y asume que se pierde esa API. langgraph dev aparece en los ejemplos como lo que es, un servidor en memoria para desarrollo.

Tampoco cubre entrenamiento ni ajuste fino del modelo. Los cuatro casos son de inferencia sobre un modelo abierto tal cual sale del repositorio, y las decisiones de dimensionado son de inferencia: ventana, batching en vLLM, aciertos de caché de prefijo.

Y no cubre la parte de interfaz. Cómo se muestra una interrupción a un encargado en una tablet es un problema de aplicación; el caso 2 entrega el contrato de datos de la interrupción y de la reanudación, no la pantalla.

Método

Cada afirmación sobre el comportamiento del código se ha verificado ejecutando el paquete instalado, con estas versiones:

PaqueteVersión
langgraph1.2.11
langgraph-checkpoint4.2.0
langgraph-checkpoint-postgres3.1.2
langgraph-prebuilt1.1.0
langchain1.4.1
langchain-core1.6.3
langchain-openai1.6.2

Cuando un artículo dice “hace X”, se ha ejecutado X. Cuando dice “el código sugiere X”, no se ha ejecutado y va marcado así. Los ejemplos de código de la serie se ejecutan contra un modelo falso de LangChain que devuelve mensajes con llamadas a herramientas fijas, para verificar la estructura del grafo, el orden de los nodos y el número de checkpoints; y contra vLLM para las cifras de latencia y tokens. Las dos cosas se distinguen en el texto.

Un aviso de fechas. La 1.0 de LangGraph y de LangChain salió en octubre de 2025 y deprecó create_react_agent a favor de create_agent. La mayoría de guías, cursos y respuestas en foros siguen mostrando el prebuilt antiguo, con langgraph.prebuilt.create_react_agent y un state_modifier en vez de middleware. Funcionan todavía, con aviso de deprecación, y no tienen el sistema de middleware. Nada de esta serie se apoya en ellos.

Checklist

Antes de escribir el primer agente sobre este stack:

  • El modelo se pasa como instancia de ChatOpenAI con base_url al gateway y profile={"max_input_tokens": N}, con N por debajo del --max-model-len de vLLM.
  • SummarizationMiddleware lleva trigger explícito; sin él no resume.
  • El contexto de ejecución es una dataclass que se pasa en cada invoke y no se guarda en el estado.
  • El rol se comprueba dentro de las herramientas con efecto, no en el prompt.
  • El thread_id lo construye la aplicación; el cliente no lo elige.
  • El checkpointer de Postgres se inicializa con setup() una vez, desde un proceso con permiso de DDL, no desde los pods del agente.
  • Cada agente tiene su clave virtual en LiteLLM con presupuesto, para que el coste salga separado por caso.
  • Se ha contado cuántos checkpoints deja un turno típico con el middleware elegido, y se ha multiplicado por los turnos por día antes de dimensionar la base de datos.

Trampas y cosas que no son lo que parecen

El middleware no es gratis en la base de datos. Cada nodo de middleware es un superstep y cada superstep es una fila de checkpoint. Con los dos límites del caso 1, 11 filas por turno con una herramienta, contra 5 sin ellos. Con SummarizationMiddleware, que también es un nodo, trece. El coste de cómputo es despreciable; el de almacenamiento y el de la limpieza que no existe, no.

ChatOpenAI sin perfil no avisa. El agente asume ventana desconocida, SummarizationMiddleware con disparador por fracción se niega a construirse, y con disparador por tokens funciona a ciegas respecto al servidor. La única defensa es el profile explícito y una aserción al arrancar.

SummarizationMiddleware(modelo) es un middleware que no hace nada. Sin trigger, la lista de cláusulas está vacía y no resume nunca. Se ve el nodo en el grafo y en las trazas, y eso da una falsa sensación de que el contexto está controlado.

interrupt() re-ejecuta el nodo entero. Lo que haya antes de la interrupción en ese nodo se ejecuta dos veces: al interrumpir y al reanudar. Si es una escritura en el TPV, se escribe dos veces. Esto condiciona todo el caso 2.

El checkpointer de Postgres solo sabe borrar hilos enteros. En langgraph-checkpoint-postgres 3.1.2, delete_thread está implementado; prune, copy_thread y delete_for_runs heredan de la clase base y lanzan NotImplementedError. Y la propia clase base avisa de que una poda parcial hecha a mano puede dejar los canales delta reconstruyéndose vacíos sin error. La retención es un trabajo de borrado por hilo que se escribe en el artículo 5.

langgraph dev no es un servidor. Es hot reload y estado en memoria. En el artículo 5 los grafos se sirven con FastAPI y un checkpointer de Postgres, y esa es la única configuración que la serie llama producción.

Cierre

Lo que se lleva de este marco es una regla y un stack. La regla: create_agent mientras el problema quepa en un ciclo modelo-herramientas con middleware, y StateGraph cuando haga falta una casilla que el modelo no debe controlar, sea una firma, un abanico o un contador. El stack: vLLM, LiteLLM, Postgres con pgvector y Langfuse en un cluster propio, con el modelo pasado como instancia y con perfil, y con el contexto de ejecución fuera del estado.

El siguiente artículo construye el primer agente sobre eso. Es el más sencillo de los cuatro y el que más se parece a lo que la mayoría de equipos ya tiene a medias: un asistente que responde a un dependiente sobre stock y procedimientos, con las herramientas comprobando el rol y con la tabla de métricas que un responsable de tienda puede leer al final de la primera semana.

Ver también

Fuentes