"Lang" es la familia de herramientas de LangChain Inc. Son piezas distintas que se apilan. Antes de escribir código conviene saber cuál resuelve qué.
Interfaz común para modelos, mensajes, prompts, tools y salida estructurada. Desde la v1 trae createAgent, un agente listo para usar.
Runtime de bajo nivel para orquestar pasos como un grafo con estado: bucles, ramas, memoria persistente, pausas para humanos.
Observabilidad y evaluación: trazas de cada llamada, datasets, experimentos. También despliega grafos (antes "LangGraph Platform").
Un paquete por proveedor. Cambiar de modelo es cambiar una línea; el resto del código no se entera.
LangGraph es la base. El createAgent de LangChain está construido encima de LangGraph: es un grafo prearmado con dos nodos (modelo y tools) en bucle. LangSmith se conecta a ambos mirando desde afuera.
| Necesitás… | Usá |
|---|---|
| Una llamada al modelo con salida tipada (p. ej. extraer filtros de una búsqueda) | LangChain: model.withStructuredOutput(zod) |
| Un asistente que decide solo qué tools llamar | LangChain: createAgent |
| Un flujo con pasos conocidos, ramas y reintentos (find → refine → disambiguate) | LangGraph: StateGraph |
| Pausar y pedirle algo al usuario a mitad del flujo | LangGraph: interrupt + checkpointer |
| Saber por qué el modelo respondió mal, medir mejoras | LangSmith |
Querés que el flujo vuelva a buscar con filtros más amplios cuando no hay resultados, como máximo dos veces. ¿Qué pieza es la natural?
Los bucles con límite son control de flujo determinista: van en el grafo, en código, no en el prompt. LangSmith observa y evalúa; no ejecuta tu lógica.
¿Qué relación hay entre createAgent y LangGraph?
El agente es un grafo prearmado. Por eso acepta checkpointer, streaming e interrupts igual que un grafo propio, y por eso podés meterlo como nodo dentro de otro grafo.
Un proyecto Node con TypeScript. Usamos Anthropic como proveedor, pero cualquier paquete @langchain/* sirve igual.
La key va en .env, nunca en el código. Cargala con node --env-file=.env o con tu loader habitual.
initChatModel("anthropic:claude-sonnet-5-5") (desde "langchain") crea el modelo a partir de un string. Útil si el proveedor sale de configuración.¿Qué devuelve model.invoke(...)?
Siempre un mensaje. El texto está en .content (o en .text), y ahí también aparecen tool_calls y usage_metadata con los tokens consumidos.
Toda conversación es una lista de mensajes con rol. LangChain los normaliza para que el mismo código funcione con cualquier proveedor.
| Clase | Rol | Quién lo escribe |
|---|---|---|
SystemMessage | system | Vos: instrucciones fijas |
HumanMessage | user | El usuario |
AIMessage | assistant | El modelo; puede traer tool_calls |
ToolMessage | tool | Tu código, con el resultado de una tool, enlazado por tool_call_id |
Todo lo que es "ejecutable" en LangChain (un modelo, un prompt, una cadena, un grafo) implementa la interfaz Runnable y tiene los mismos métodos:
El modelo pidió ejecutar una tool. Tu código la corre. ¿Cómo le devolvés el resultado?
El id enlaza pedido y respuesta. Los proveedores rechazan una conversación con un tool_call sin su ToolMessage correspondiente.
Una cadena es una tubería de Runnables: la salida de uno es la entrada del siguiente. Se arma con .pipe().
Seguí el dato por cada eslabón. Cada paso es un Runnable distinto y cambia el tipo del valor:
RunnableLambda.from(fn): cualquier función tuya como eslabón. En la práctica, pasar una función a .pipe() ya la envuelve.RunnableParallel (o un objeto literal): corre varias ramas con la misma entrada y junta los resultados en un objeto..withRetry(), .withFallbacks([otroModelo]): resiliencia sin escribir try/catch.En prompt.pipe(model).pipe(parser), ¿qué recibe model?
El template recibe variables y devuelve mensajes (un ChatPromptValue). El modelo recibe mensajes y devuelve un AIMessage. El parser recibe el AIMessage y devuelve string.
El primer nodo del buscador: convertir texto libre en filtros tipados. Es la pieza de LangChain que más vas a usar.
LangChain convierte el schema Zod en la definición de una tool (o en el modo JSON nativo del proveedor), obliga al modelo a "llamarla" y valida la respuesta contra el schema. Si no valida, lanza error: nunca recibís un objeto con forma incorrecta.
.describe() son parte del prompt. Escribilos como instrucciones..nullable() le da al modelo una salida honesta para "no lo dijo". Sin eso, inventa.{ includeRaw: true } recibís también el AIMessage crudo, útil para loguear tokens.activity normalizada, y podés pedir además un slug de categoría con z.enum([...tusSlugs]) para que el modelo solo elija entre categorías existentes.El usuario escribe "yoga". ¿Cómo evitás que el modelo invente una zona?
Si el schema exige un string, el modelo tiene que poner algo. Darle null como opción válida (y decirlo en la descripción) es lo que corta la alucinación. La temperatura no cambia eso.
Una tool es una función tuya con nombre, descripción y schema. El modelo no la ejecuta: devuelve un pedido para que vos la ejecutes.
Esa ida y vuelta (modelo → tool → modelo) repetida hasta que el modelo deja de pedir tools es exactamente un agente. La lección siguiente la automatiza.
name y description. Decí cuándo usarla y cuándo no. Una tool con descripción vaga es una tool que no se usa, o que se usa mal.Después de bindTools, el modelo devuelve un AIMessage con tool_calls. ¿Ya se consultó la base de datos?
bindTools solo le cuenta al modelo qué tools existen. Ejecutar es responsabilidad de quien orquesta: vos, createAgent o un ToolNode en un grafo.
Un agente es un bucle: el modelo decide, las tools ejecutan, el resultado vuelve al modelo. Termina cuando el modelo responde sin pedir tools.
Avanzá el bucle paso a paso. Fijate quién actúa en cada mensaje:
En la v1, el comportamiento del agente se ajusta con middleware que se engancha antes o después de cada llamada al modelo o a las tools: resumir historial largo, pedir aprobación humana antes de una tool sensible, limitar llamadas, cambiar de modelo según el caso.
¿Cuándo termina el bucle de un agente?
La condición de salida es que el modelo decida responder. Hay un límite de seguridad (recursionLimit) para evitar bucles infinitos, pero no es la forma normal de terminar.
Un grafo de LangGraph tiene tres ingredientes: un estado compartido, nodos que lo leen y devuelven actualizaciones, y aristas que deciden el siguiente nodo. Empezamos por el estado.
Un nodo devuelve solo las claves que cambia: return { matches, log: ["find → 3"] }. LangGraph combina ese parche con el estado usando el reducer de cada clave. Sin reducer, reemplaza. Con reducer, aplica tu función.
MessagesAnnotation, con un reducer que agrega mensajes y reemplaza los que tengan el mismo id. Es lo que usa createAgent por dentro. En LangGraph v1 también podés declarar el estado con un schema Zod en vez de Annotation.Dos nodos que corren en paralelo escriben matches, que no tiene reducer. ¿Qué pasa?
Con escrituras concurrentes a la misma clave, LangGraph no elige un ganador arbitrario: exige un reducer que diga cómo combinarlas. En pasos secuenciales, sin reducer, el último sí reemplaza.
Un nodo es una función (state) => parche. Las aristas fijas siempre van al mismo destino; las condicionales llaman a una función que elige.
Este es ese mismo grafo ejecutándose en tu navegador con un catálogo de prueba. El paso parse está simulado (devuelve lo que devolvería el modelo). Elegí una búsqueda y avanzá nodo a nodo; en naranja, lo que cambia en el estado.
parse usa un LLM. find, refine y el ruteo son código común. Un grafo te deja poner el modelo donde aporta y nada más, y eso abarata y estabiliza el flujo.¿Para qué sirve el tercer argumento de addConditionalEdges (la lista de destinos)?
La función de ruteo podría devolver cualquier string; la lista le permite a LangGraph validar en compile y dibujar las aristas (en Studio o con getGraph().drawMermaid()).
La búsqueda de kitesurf pasa dos veces por find. ¿Qué evita un bucle infinito si nunca hubiera resultados?
Los ciclos son legales y útiles. Tu ruteo es el que decide salir. Si te equivocás, LangGraph corta con GraphRecursionError al superar recursionLimit (25 pasos por defecto).
Con un checkpointer, LangGraph guarda el estado completo después de cada paso. Eso habilita conversaciones de varios turnos, reanudar tras un error y pausar para el usuario.
| Checkpointer | Paquete | Uso |
|---|---|---|
MemorySaver | @langchain/langgraph | Tests y desarrollo. Se pierde al reiniciar. |
PostgresSaver | @langchain/langgraph-checkpoint-postgres | Producción. Llamá await saver.setup() una vez para crear las tablas. |
SqliteSaver | @langchain/langgraph-checkpoint-sqlite | Apps locales o un solo proceso. |
El checkpointer es memoria por thread: lo que pasó en esta conversación. Para recordar cosas del usuario entre conversaciones ("prefiere actividades al aire libre") existe el Store (InMemoryStore, PostgresStore), un key-value con namespaces que los nodos leen y escriben.
checkpoint_id del historial podés re-ejecutar desde ese punto, o editar el estado con updateState y seguir. Es la mejor herramienta para depurar "¿qué hubiera pasado si el parse devolvía otra zona?".Dos usuarios buscan a la vez con el mismo grafo compilado. ¿Qué los separa?
El grafo compilado no guarda estado propio; es una definición. El estado vive en el checkpointer, indexado por thread_id.
Cuando hay varias actividades posibles, el grafo se pausa, le pregunta al usuario y sigue con su respuesta. Puede pasar una hora entre la pregunta y la respuesta: el estado está en el checkpointer.
Probalo en el simulador de la lección anterior con la búsqueda de cerámica: volver al simulador.
interrupt: vuelve a correr el nodo desde el principio, y esta vez interrupt() devuelve el valor de resume. Todo lo que esté antes del interrupt se ejecuta dos veces. Dejá las llamadas caras o con efectos (cobros, emails, inserts) en otro nodo, o hacelas idempotentes.¿Por qué interrupt exige compilar con checkpointer?
La pausa termina la ejecución actual. Sin un lugar donde guardar el estado, no hay nada que reanudar.
Cuatro herramientas que vas a necesitar cuando el buscador crezca.
Buscar en varias fuentes a la vez, cuando la cantidad se conoce recién en ejecución. Cada Send lanza una instancia del nodo con su propia entrada; los resultados se juntan con un reducer.
A veces el nodo sabe adónde ir. En vez de una arista condicional aparte, devuelve un Command. Declará los destinos con ends.
Un grafo compilado se puede usar como nodo de otro: .addNode("search", searchGraph). Así el search de somhi puede ser una pieza dentro de un asistente mayor, con su propio estado, testeable por separado.
| streamMode | Emite |
|---|---|
values | El estado completo después de cada paso |
updates | Solo el parche de cada nodo |
messages | Tokens de los LLM que se llamen dentro de los nodos |
custom | Lo que emitas vos con config.writer (p. ej. "buscando en 3 fuentes…") |
Querés consultar N fuentes en paralelo, con N conocido recién en ejecución. ¿Qué usás?
Send crea las ramas dinámicamente y LangGraph las corre en paralelo, cada una con su traza y su checkpoint. Un for con await dentro de un nodo funciona, pero es secuencial y opaco.
Con dos variables de entorno, cada ejecución del grafo aparece como un árbol: qué nodo corrió, qué prompt recibió el modelo, cuántos tokens usó y cuánto tardó.
No hace falta tocar código: los Runnables y los grafos se trazan solos. Para funciones tuyas fuera de LangChain, envolvelas con traceable.
Armá un dataset con búsquedas reales y lo que debería salir. Cada cambio de prompt o de modelo se corre contra el dataset y se compara con el anterior.
langgraph.json que apunte a tu grafo y npx @langchain/langgraph-cli dev, ves el grafo, lo ejecutás, editás el estado y reanudás interrupts desde una UI.PostgresSaver.Cambiaste el prompt de parse y "parece andar mejor". ¿Cómo lo confirmás?
Las trazas muestran qué pasó en un caso; los experimentos sobre un dataset fijo muestran si mejoró en general y qué casos se rompieron.
Todas las piezas juntas en un solo archivo. Es un punto de partida para reemplazar event_find / refine / disambiguate; adaptá runQuery a tu repositorio real.
Los nodos son funciones puras sobre el estado: testealos sueltos. Para el grafo entero, reemplazá extractor por un fake (FakeListChatModel de @langchain/core/utils/testing) o inyectá parse como dependencia.