Por qué ya no utilizamos LangChain para crear nuestros agentes de IA

En Octomind, utilizamos agentes de inteligencia artificial con múltiples LLM para crear y corregir automáticamente pruebas de un extremo a otro en Playwright. Hasta hace apenas unos meses lo hacíamos con el Marco LangChain.

En esta publicación, compartiré nuestras luchas con LangChain y por qué reemplazar sus rígidas abstracciones de alto nivel con bloques de construcción modulares simplificó nuestra base de código e hizo que nuestro equipo fuera más feliz y productivo.

La historia de fondo

Usamos LangChain en producción durante más de 12 meses, comenzando a principios de 2023 y luego eliminándolo en 2024.

LangChain parecía ser la mejor opción para nosotros en 2023. Tenía una lista impresionante de componentes y herramientas, y su popularidad se disparó. Prometió “Permitir a los desarrolladores pasar de una idea a un código funcional en una tarde.”Pero los problemas comenzaron a surgir a medida que nuestros requisitos se volvieron más sofisticados, convirtiendo a LangChain en una fuente de fricción, no de productividad.

A medida que su inflexibilidad comenzó a mostrarse, pronto nos encontramos sumergiéndonos en los aspectos internos de LangChain para mejorar el comportamiento de nivel inferior de nuestro sistema. Pero debido a que LangChain abstrae intencionalmente tantos detalles de usted, a menudo no era fácil ni posible escribir el código de nivel inferior que necesitábamos.

Los peligros de ser un marco temprano

La IA y los LLM están cambiando rápidamente y cada semana surgen nuevos conceptos e ideas. Entonces, cuando se crea un marco como LangChain en torno a múltiples tecnologías emergentes, diseñar abstracciones que resistan la prueba del tiempo es increíblemente difícil.

Estoy seguro de que si hubiera intentado construir un marco como LangChain cuando lo hicieron, no lo habría hecho mejor. Los errores son fáciles de señalar en retrospectiva, y la intención de esta publicación no es criticar injustamente a los desarrolladores principales de LangChain ni a sus colaboradores. Todos están haciendo lo mejor que pueden.

Elaborar abstracciones bien diseñadas es difícil – incluso si se comprenden bien los requisitos. Pero cuando se modelan componentes en tal estado de flujo (por ejemplo, agentes), es más seguro usar abstracciones solo para bloques de construcción de niveles inferiores.

El problema con las abstracciones de LangChain

LangChain fue útil al principio cuando nuestros requisitos simples se alinearon con sus presunciones de uso. Pero sus abstracciones de alto nivel pronto hicieron que nuestro código fuera más difícil de entender y frustrante de mantener. Cuando nuestro equipo comenzó a dedicar tanto tiempo a comprender y depurar LangChain como a crear funciones, no fue una buena señal.

Los problemas con el enfoque de LangChain hacia las abstracciones se pueden demostrar con este ejemplo trivial de traducción de una palabra del inglés al italiano.

Aquí hay un ejemplo de Python usando solo el paquete OpenAI:

from openai import OpenAI

client = OpenAI(api_key="")
text = "hello!"
language = "Italian"

messages = [
    {"role": "system", "content": "You are an expert translator"},
    {"role": "user", "content": f"Translate the following from English into {language}"},
    {"role": "user", "content": f"{text}"},
]

response = client.chat.completions.create(model="gpt-4o", messages=messages)
result = response.choices[0].message.content

Este es un código simple que es fácil de entender y que contiene una sola clase y una llamada de función. El resto es Python estándar.

Comparemos esto con la versión de LangChain:

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

os.environ["OPENAI_API_KEY"] = ""
text = "hello!"
language = "Italian"


prompt_template = ChatPromptTemplate.from_messages(
    [("system", "You are an expert translator"),
     ("user", "Translate the following from English into {language}"),
     ("user", "{text}")]
)

parser = StrOutputParser()
chain = prompt_template | model | parser
result = chain.invoke({"language": language, "text": text})

El código hace más o menos lo mismo, pero ahí es donde terminan las similitudes.

Ahora tenemos tres clases y cuatro llamadas a funciones. Pero lo más preocupante es la introducción de tres nuevas abstracciones:

  • Plantillas de mensajes: proporcionar un mensaje al LLM
  • Analizadores de salida: procesamiento de la salida del LLM
  • Cadenas: la “sintaxis LCEL” de LangChain anula el operador `|` de Python

Todo lo que LangChain ha logrado es aumentar la complejidad del código sin beneficios perceptibles.

Este código podría estar bien para prototipos en etapa inicial. Pero Para uso en producción, cada componente debe entenderse razonablemente. para que no explote inesperadamente en condiciones de uso del mundo real. Debe cumplir con las estructuras de datos proporcionadas y diseñar su aplicación en torno a esas abstracciones.

Veamos otra comparación de abstracciones en Python, esta vez para obtener JSON de una API.

Usando el paquete http incorporado:

import http.client
import json

conn = http.client.HTTPSConnection("api.example.com")
conn.request("GET", "/data")
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()

Usando el paquete de solicitudes:

import requests

response = requests.get("https://api.example.com/data")
data = response.json()

El ganador es obvio. Así es como se siente una buena abstracción.

Por supuesto, estos son ejemplos triviales. Pero lo que quiero decir es que las buenas abstracciones simplifican el código y reducen la carga cognitiva necesaria para comprenderlo.

LangChain intenta hacerle la vida más fácil haciendo más con menos código ocultando detalles lejos de ti. Pero cuando esto se logra a costa de la simplicidad y la flexibilidad, las abstracciones pierden su valor.

LangChain también tiene la costumbre de utilizar abstracciones encima de otras abstracciones, por lo que a menudo te ves obligado a pensar en términos de abstracciones anidadas para entender cómo usar una API correctamente. Esto conduce inevitablemente a comprender enormes rastros de pila y a depurar el código del marco interno que no escribió en lugar de implementar nuevas funciones.

El impacto de LangChain en nuestro equipo de desarrollo

Nuestra aplicación hace un uso intensivo de agentes de IA para realizar diferentes tipos de tareas, como el descubrimiento de casos de prueba, la generación de pruebas de Playwright y la reparación automática.

Cuando quisimos pasar de una arquitectura con un único agente secuencial a algo más complejo, LangChain fue el factor limitante. Por ejemplo, generar subagentes y permitirles interactuar con el agente original. O varios agentes especializados interactuando entre sí.

En otro caso, necesitábamos cambiar dinámicamente la disponibilidad de las herramientas a las que podían acceder nuestros agentes, según la lógica empresarial y los resultados del LLM. Pero LangChain no proporciona un método para observar externamente el estado de un agente, lo que nos lleva a reducir el alcance de nuestra implementación para adaptarla a la funcionalidad limitada disponible para los agentes de LangChain.

Una vez que lo eliminamos, ya no tuvimos que traducir nuestros requisitos en soluciones apropiadas de LangChain. Podríamos simplemente codificar.

Entonces, si no es LangChain, ¿qué marco debería utilizar? Quizás no necesite ningún marco.

¿Necesita un marco para crear aplicaciones de IA?

LangChain nos ayudó desde el principio brindándonos funciones LLM para que pudiéramos concentrarnos en desarrollar nuestra aplicación. Pero en retrospectiva, habríamos estado mejor a largo plazo sin un marco.

La larga lista de componentes de LangChain da la impresión de que crear una aplicación basada en LLM es complicado. Pero los componentes principales que necesitarán la mayoría de las aplicaciones suelen ser:

  • Un cliente para la comunicación LLM
  • Funciones/Herramientas para llamar a funciones
  • Una base de datos vectorial para RAG
  • Una plataforma de observabilidad para seguimiento, evaluación, etc.

El resto son ayudas en torno a esos componentes (por ejemplo, fragmentación e incrustación de bases de datos vectoriales) o tareas habituales de la aplicación, como la gestión de archivos y el estado de la aplicación mediante la persistencia de datos y el almacenamiento en caché.

Si comienza su viaje de desarrollo de IA sin un marco, sí, le llevará más tiempo crear su propia caja de herramientas y requerirá más aprendizaje e investigación iniciales. Pero este es un tiempo bien empleado y una inversión que vale la pena para usted y el futuro de su aplicación, ya que está aprendiendo los fundamentos del campo en el que va a operar.

En la mayoría de los casos, su uso de LLM será simple y directo. Principalmente escribirá código secuencial, iterará según las indicaciones y mejorará la calidad y previsibilidad de su salida. La mayoría de las tareas se pueden realizar con código simple y una colección relativamente pequeña de paquetes externos.

Incluso si utiliza agentes, es poco probable que haga mucho más que una simple comunicación de agente a agente en un flujo secuencial predeterminado con lógica empresarial para manejar el estado de los agentes y sus respuestas. No necesita un marco para implementar esto.

Si bien el espacio de los agentes está evolucionando rápidamente con posibilidades interesantes y casos de uso interesantes, recomendamos mantener las cosas simples por ahora mientras se solidifican los patrones de uso de los agentes.

Mantenerse rápido y ágil con bloques de construcción

Suponiendo que no estás enviando código basura a producción, la velocidad a la que un equipo puede innovar e iterar es la métrica más importante para el éxito. Gran parte del desarrollo en el espacio de la IA está impulsado por la experimentación y la creación de prototipos.

Pero los marcos suelen estar diseñados para Hacer cumplir una estructura basada en patrones de uso bien establecidos. – algo que las aplicaciones basadas en LLM aún no tienen. Tener que traducir nuevas ideas en código específico del marco limita la velocidad de iteración.

Un enfoque de bloques de construcción prefiere código simple de bajo nivel con paquetes externos cuidadosamente seleccionados, manteniendo su arquitectura ágil para que los desarrolladores puedan dedicar su atención al problema que están tratando de resolver.

Un componente básico es algo simple que usted cree que se comprende de manera integral y es poco probable que cambie. Por ejemplo, una base de datos vectorial. Es un tipo conocido de componente modular con un conjunto básico de características para que pueda intercambiarse fácilmente fuera y reemplazado. Su código base debe ser ágil y adaptable para maximizar su velocidad de aprendizaje y el valor que obtiene de cada ciclo de iteración.

. . .

Espero haber descrito de manera cuidadosa y justa nuestros desafíos con LangChain y por qué alejarnos por completo de los marcos ha sido enormemente beneficioso para nuestro equipo.

Nuestra estrategia actual de utilizar bloques de construcción modulares con abstracciones mínimas nos permite desarrollarnos más rápidamente y con menos fricción.

fabián ambos
Ingeniero de aprendizaje profundo en Octomind

You may also like

Leave a Comment