Inicio / Artículos

De texto sin procesar a pipelines: analizadores, LCEL, elementos ejecutables y memoria en LangChain

Cómo LangChain convierte el texto bruto del modelo en datos estructurados, compone pasos con LCEL e la interfaz Runnable, y gestiona la memoria de conversación en la actualidad.

Editor: REACTAPP.TOP10 min de lectura2276 palabras

Fuentes: LangChain Explained: Output Parsers, Chains, Runnables, and Memory

Llamar a un modelo de lenguaje con una plantilla de prompt bien estructurada es solo el primer paso en una aplicación real. El modelo sigue devolviendo una cadena no estructurada, cada llamada olvida todo lo que ocurrió antes, y conectar manualmente el prompt, el modelo y el procesamiento posterior se convierte rápidamente en código genérico. Esta guía aborda los cuatro conceptos de LangChain que resuelven esos problemas: analizadores de salida, cadenas construidas con LCEL, la interfaz Runnable que permite la composición y la memoria. Después de esto, deberías poder leer cualquier pipeline LCEL, elegir el analizador o Runnable adecuado para una tarea y saber dónde se almacena el estado de la conversación en LangChain actual.

Se asume que ya sabes qué son un envoltorio de LLM y una plantilla de prompt en LangChain, ya que todo lo que sigue se basa directamente en ellos.

Analizadores de salida: convertir texto en datos

El problema que resuelven los analizadores

Cualquier cosa que le pidas a un modelo, la respuesta llega como texto en formato libre. Para un chatbot sencillo que muestra la respuesta en pantalla, eso es suficiente. Pero en cuanto tu código necesita actuar sobre esa respuesta, por ejemplo al guardarla en una base de datos, pasarla a otra función o devolverla desde una API en formato JSON, una simple cadena de texto se convierte en un problema.

Un analizador de salida se sitúa entre el modelo y el resto de tu programa. Toma el texto generado y lo convierte en algo estructurado: una lista, un diccionario, un objeto JSON o una instancia de tu propia clase en Python. Una forma útil de imaginarlo es como un intérprete: el modelo comunica mediante prosa desestructurada, y el analizador transforma esa prosa en una forma en la que tu código pueda confiar.

Por qué no debes analizar manualmente

  • La salida del modelo no tiene un formato garantizado; la misma solicitud puede generar estructuras ligeramente diferentes en ejecuciones distintas.
  • Las APIs, las bases de datos y las interfaces de usuario esperan datos que sean consistentes y predecibles.
  • La división ad hoc de cadenas o las expresiones regulares son frágiles y fallan ante la primera variación inesperada.
  • Los analizadores también pueden validar, por lo que un campo faltante o un valor mal formado se detecta en la etapa inicial en lugar de generar un error confuso más adelante en el proceso.
  • La familia de analizadores integrados

    LangChain incluye varios analizadores, cada uno diseñado para un formato de salida diferente:

    • Analizador de salida de cadena (StrOutputParser): la opción más sencilla, que extrae el texto plano del mensaje de respuesta del modelo. Úsalo cuando quieras una cadena limpia para mostrarla o para pasarla a otra etapa.
    • Analizador de lista: divide la respuesta en una lista de Python, lo cual es útil cuando se solicitan varios elementos como palabras clave o sugerencias.
    • Analizador de salida JSON: convierte la respuesta en un objeto JSON y es uno de los analizadores más utilizados cuando se necesitan datos estructurados.
    • Analizador de salida Pydantic: asigna directamente la respuesta a un modelo Pydantic, lo que permite la validación de tipos además de la estructura. Es la opción adecuada cuando los datos deben seguir un esquema estricto.
    • Analizador de fechas y horas: genera un objeto real de Python datetime en lugar de una fecha escrita como texto.
    • Analizador Enum: restringe la salida a un valor de un conjunto predefinido y devuelve ese valor.
    • Analizador XML: lee la salida escrita en formato de etiquetas XML y la convierte en un diccionario.

    Los analizadores estructurados ofrecen un método get_format_instructions(). Este devuelve un bloque de texto que se inserta en la instrucción, indicando al modelo con precisión cómo organizar su respuesta para que el analizador pueda leerla. El analizador de cadenas simples no tiene nada que imponer, por lo que es el único miembro de la familia para el cual las instrucciones de formato no son aplicables.

    Analizadores versus salida estructurada nativa

    Muchos modelos actuales pueden devolver datos estructurados de forma nativa, y LangChain expone esta funcionalidad a través de with_structured_output(). Cuando está disponible, suele reemplazar a un analizador, ya que el proveedor del modelo aplica el esquema en lugar de que la instrucción lo solicite amablemente. Los analizadores siguen siendo útiles cuando un modelo carece de salida estructurada nativa, y cuando se desea una validación o transformación adicional más allá de lo que garantiza el proveedor.

    Cadenas y LCEL: conectando las etapas

    Qué es una cadena

    Una cadena es una serie de pasos en los que la salida de cada paso se convierte en la entrada del siguiente. En lugar de escribir tres fragmentos de código separados para formatear una solicitud, llamar al modelo y analizar el resultado, los conectas en un único pipeline reutilizable.

    El ejemplo canónico consta de tres etapas: un modelo de plantilla para la solicitud, luego el modelo en sí y finalmente un analizador de resultados. Proporcionas una entrada bruta como un tema o una pregunta, y la cadena formatea la solicitud, llama al modelo y analiza lo que devuelve todo en un solo flujo.

    Qué te ofrecen las cadenas

    • Menos código genérico. La conexión entre los pasos se escribe una sola vez en lugar de repetirse en cada punto de llamada.
    • Modularidad. La plantilla de solicitud, el modelo y el analizador son componentes independientes, por lo que puedes reemplazar cualquiera de ellos sin afectar a los demás.
  • Legibilidad. Una cadena muestra de un vistazo la dirección del flujo de datos, incluso cuando el flujo de trabajo es complejo.
  • Reutilización. Una vez definida, una cadena puede ser llamada una y otra vez con diferentes entradas.
  • Composición con el operador pipe

    El Lenguaje de Expresiones de LangChain, LCEL, es lo que hace que la creación de cadenas parezca sencilla. Utiliza el operador | para unir componentes, de manera similar a como se concatenan comandos en una shell de Unix, en lugar de anidar llamadas a funciones entre sí.

    Las dos líneas siguientes construyen la cadena de tres etapas descrita anteriormente y la ejecutan una vez con una entrada en forma de diccionario. Tenga en cuenta que la clave del diccionario, topic, debe coincidir con una variable en el plantilla de prompt.

    chain = prompt | model | output_parser
    result = chain.invoke({"topic": "black holes"})
    

    La entrada llega a prompt, lo que genera un mensaje de solicitud formateado; este se envía a model, y la respuesta del modelo va a output_parser. Cada componente transfiere su resultado directamente al siguiente, y result almacena lo que devuelva el analizador.

    Cada cadena LCEL también incluye un conjunto estándar de métodos de ejecución:

    • .invoke() ejecuta la cadena una vez para una sola entrada.
    • .batch() la ejecuta sobre una lista de entradas en una sola llamada.
    • .stream() genera la salida de forma incremental a medida que se produce, en lugar de hacer esperar al usuario hasta obtener la respuesta completa. El flujo por lotes solo te entrega los tokens uno por uno si cada paso de la cadena puede procesar un flujo; un paso que necesita toda la entrada primero la almacenará en búfer.

    Este estilo declarativo es en gran medida la razón por la cual LCEL parece compuesto. Se describe qué debe ocurrir y en qué orden, no la mecánica de cómo cada paso llama al siguiente.

    Runnables: la interfaz subyacente

    Por qué todo puede ser canalizado

    Detrás de LCEL, cada componente, ya sea una prompt, un modelo, un analizador o una función que hayas escrito tú mismo, es un Runnable. Un Runnable es una interfaz común que cualquier componente puede implementar, y ese contrato compartido es exactamente lo que hace posible el uso de |. Dado que cada componente expone los mismos métodos, se pueden conectar independientemente de lo que hagan internamente.

    La misma interfaz es la razón por la cual interactuar con una cadena resulta uniforme. Ya sea que un componente llame a un modelo, reestructure datos o ejecute varios procesos al mismo tiempo, se controla mediante los mismos métodos .invoke(), .batch() y .stream(). Una cadena compuesta es en sí misma un Runnable, por lo que se pueden anidar cadenas dentro de otras más grandes sin necesidad de ningún tratamiento especial.

    Los Runnables que usarás con más frecuencia

    • RunnableSequence ejecuta los pasos estrictamente en orden, pasando la salida de cada uno al siguiente paso. Al unir componentes con | se crea automáticamente uno de estos en segundo plano. Uso típico: primero el prompt, luego el modelo y finalmente el analizador.
  • RunnableParallel ejecuta varios objetos Runnable al mismo tiempo con la misma entrada y reúne sus resultados en un diccionario indexado por nombre. Uso típico: resumir un texto y detectar su idioma de forma concurrente en lugar de hacerlo uno tras otro.
  • RunnablePassthrough devuelve su entrada sin cambios. Suelen utilizarse dentro de un RunnableParallel cuando se necesita que la entrada original viaje junto con los valores transformados. Uso típico: devolver la pregunta original del usuario junto con la respuesta generada.
  • RunnableLambda envuelve una función normal de Python para que pueda ser un paso en una cadena. Uso típico: procesamiento previo o posterior personalizado, como limpiar texto, contar palabras o aplicar reglas de negocio entre pasos.
  • RunnableBranch agrega enrutamiento if/else, enviando la entrada a diferentes componentes ejecutables según una condición. Uso típico: dirigir preguntas técnicas a una cadena y preguntas generales a otra.
  • Cuando se comprende que los modelos, analizadores y cadenas son todos componentes ejecutables que comparten una misma interfaz, LangChain deja de parecer un conjunto de casos especiales. Se convierte en una composición ordinaria: pequeñas unidades con la misma estructura, combinadas en unidades más grandes que mantienen esa misma estructura.

    Advertencia sobre la sobrecomposición

    Dado que cualquier cosa puede convertirse en un Runnable, resulta tentador expresar toda la lógica de la aplicación como cadenas anidadas. Esto funciona bien para pasos sencillos, pero las ramas y lambdas profundamente anidadas pueden ser más difíciles de depurar que en Python puro. Una regla razonable es utilizar LCEL para el pipeline orientado al modelo y mantener los flujos de control complejos en código normal, o bien trasladarlos a un framework basado en grafos cuando involucren bucles y estado.

    Memoria: mantener el contexto entre turnos

    Qué significa la memoria aquí

    Cada llamada a un modelo es, por defecto, sin estado. No tiene conocimiento de lo que se le preguntó hace un momento a menos que la aplicación envíe nuevamente el intercambio anterior. En LangChain, la memoria es el conjunto de mecanismos para almacenar el contexto de la conversación y recuperarlo posteriormente, de modo que una aplicación pueda continuar una conversación en lugar de tratar cada mensaje como si fuera el primero.

    Por qué las aplicaciones de conversación lo necesitan

    • Sin memoria, un chatbot pierde todo en el momento en que llega un nuevo mensaje: sin continuidad, sin seguimientos y sin contexto compartido.
    • La conversación natural depende de los intercambios anteriores; por ejemplo, saber que “él” en un mensaje posterior se refiere a alguien mencionado dos mensajes antes.
    • El contexto recordado hace que un asistente parezca coherente y capaz de mantener una conversación, en lugar de responder a cada mensaje de forma aislada.
    • Es un requisito básico para los bots de soporte al cliente, los asistentes personales y cualquier otra aplicación que requiera múltiples intercambios.

    Clases clásicas de memoria

    El enfoque tradicional utilizaba clases de memoria que residían dentro del proceso en ejecución. La más sencilla guardaba un búfer con toda la conversación; otra condensaba los mensajes antiguos en un resumen continuo para ahorrar tokens. En cada nueva llamada, el contexto almacenado se volvía a incluir en la petición.

    Dónde reside ahora la memoria

    Esas clases de memoria en proceso han quedado obsoletas ya que LangChain ha trasladado el estado de las conversaciones a la capa de persistencia de LangGraph. En ese modelo, los mecanismos de checkpointing guardan el estado de la conversación por sesión (identificada por un hilo), mientras que almacenes separados conservan la memoria a largo plazo que debe perdurar entre sesiones. El concepto no ha cambiado: el contexto anterior debe guardarse en algún lugar para poder reutilizarse. Lo que sí ha cambiado es la implementación recomendada, que ahora es más duradera y más adecuada para entornos de producción, ya que el estado puede persistirse en una base de datos real en lugar de desaparecer cuando se reinicia el proceso. Si desea ver cómo un mecanismo de checkpointing almacena realmente ese estado, inside LangGraph's InMemorySaver explica en detalle su funcionamiento interno.

    Un camino de aprendizaje sensato es primero comprender la idea clásica sobre la memoria, ya que explica por qué es importante y qué se incluye en el prompt, y luego adoptar el enfoque basado en la persistencia para cualquier cosa destinada a producción. Dado que esta área ha cambiado rápidamente, consulte la documentación actual de LangChain y LangGraph para conocer las APIs exactas antes de comenzar a desarrollar.

    Conclusión

    Junto con los envoltorios para LLM y las plantillas de prompt, estos conceptos forman la herramienta básica de LangChain. Cada uno es sencillo por separado, pero su valor radica en su combinación: una plantilla alimenta al modelo, un analizador convierte el texto del modelo en datos, cada componente es un elemento ejecutable unido en una cadena con |, y una capa de persistencia guarda la conversación entre llamadas.

    • Utilice un analizador de salida o una salida estructurada nativa, siempre que el modelo la soporte, cada vez que el código consuma la respuesta del modelo.
  • Lea cualquier pipeline LCEL de izquierda a derecha como una RunnableSequence; recurra a RunnableParallel, RunnablePassthrough, RunnableLambda y RunnableBranch cuando el flujo no sea lineal.
  • Tenga en cuenta que la transmisión por flujo es tan incremental como el paso más lento en dicho proceso.
  • Considere las clases de memoria heredadas como un concepto para aprender, y utilice los mecanismos de punto de control y almacenamiento de LangGraph para aplicaciones reales.
  • Con estos componentes básicos en su lugar, los temas siguientes naturales son la generación mejorada mediante recuperación de información y los agentes, ambos construidos exactamente a partir de estos elementos.

    Lecturas relacionadas

  • LangGraph vs Pydantic AI: Por qué el modelo, y no el framework, determinó la precisión — Una prueba de rendimiento controlada con 160 ejecuciones de LangGraph y Pydantic AI muestra una precisión idéntica en las llamadas a herramientas, y revela que la elección del modelo y el diseño de la evaluación son mucho más importantes.
  • De un chatbot de un solo nodo a un agente respaldado por MCP en LangGraph — Construya una aplicación de LangGraph capa por capa: estado y reductores, bordes, bucles de herramientas, hilos con puntos de control, tres modos de transmisión en tiempo real, y herramientas proporcionadas a través de MCP.