Comenzamos con una cadena ficticia simple que tiene 3 componentes: 2 indicaciones y una función personalizada para unirlos. Me refiero a esto como un ejemplo ficticio porque es muy poco probable que necesite dos indicaciones separadas para interactuar entre sí, pero es un ejemplo más fácil para comenzar a comprender las devoluciones de llamadas y las canalizaciones de LangChain.
Implementar esto en código se vería así:
El código anterior es bastante material de libro de texto. La única pieza posiblemente compleja es la recuperar_texto y EjecutableLambda función que se utiliza aquí. La razón por la que esto es necesario es porque el formato de la salida de qa_prompt1 no es compatible con el formato de salida requerido por qa_prompt2.
Definición de la devolución de llamada personalizada
Para nuestra devolución de llamada personalizada, definimos una nueva subclase de BaseCallbackHandler llamada CustomCallback1 que define el on_chain_start método. La definición del método es sencilla, ya que simplemente toma los valores de entrada que se le pasan y los guarda en 2 variables específicas: entrada_cadena y entrada_serializada
Invocar la devolución de llamada personalizada
El código anterior muestra una de las formas posibles de pasar su devolución de llamada personalizada a su canalización: como una lista de objetos de devolución de llamada como el valor de una clave correspondiente de ‘devoluciones de llamada’. Esto también hace que sea fácil adivinar que puede pasar múltiples devoluciones de llamada a su canalización LangChain.
Decodificando la estructura de devolución de llamada/canalización
Ahora viene la parte interesante. Después de haber definido las devoluciones de llamada y pasarlas a nuestra canalización, ahora realizamos una inmersión profunda en los resultados de la devolución de llamada.
Primero miramos los valores almacenados en entrada_cadena
Observaciones:
- Aunque Hay 3 componentes en nuestra cadena, hay 4 valores en entrada_cadena. que corresponde a la on_chain_start El método se activa 4 veces en lugar de 3.
- Para los dos primeros entrada_cadena value/activadores on_chain_start, la entrada es la misma que la entrada proporcionada por el usuario.
A continuación veremos las salidas de entrada_serializada
Observaciones:
- El primer componente es un Secuencia ejecutable que es un componente que no fue agregado por el usuario pero que LangChain agregó automáticamente. El resto de los componentes corresponden directamente a los componentes definidos por el usuario en la tubería.
- ¡El contenido completo de serialized_input es extenso! Si bien existe una estructura definida para ese contenido, definitivamente está fuera del alcance de esta publicación y posiblemente no tenga muchas implicaciones prácticas para el usuario final.
¿Cómo interpretamos estos resultados?
En su mayor parte, los resultados observados en el entrada_cadena y entrada_serializada tener sentido. Ya sean los valores de entrada o los nombres/ID de los componentes. La única parte en gran parte desconocida es la Secuencia ejecutable componente, así que echamos un vistazo más de cerca a esto.
Como mencioné anteriormente, el contenido completo de entrada_serializada Es extenso y no es fácil de digerir. Entonces, para facilitar las cosas, solo analizamos los atributos de alto nivel descritos en entrada_serializada e intentar interpretar los resultados a través de estos atributos. Para esto, utilizamos una función de depuración personalizada llamada getChainDesglose (código en cuaderno).
Llamamos getChainDesglose en todos los valores de entrada_serializada y observe la salida. Específicamente para la primera Secuencia ejecutable elemento, nos fijamos en las claves del diccionario kwargs: nombre, medio, apellido, nombre.
Tras una inspección más cercana del argumento de los kwargs y sus valores, vemos que tienen la misma estructura que nuestros componentes de tubería anteriores. De hecho, el primer, el medio y el último componente corresponden exactamente a los componentes de la tubería definidos por el usuario.
Los detalles anteriores forman la base de la conclusión final que llegamos aquí. Que la estructura de la tubería es como se muestra a continuación:
Damos un pequeño salto aquí, ya que el diagrama de flujo anterior se confirmó después de revisar un montón de ejemplos y observar el formato en el que LangChain crea internamente estos componentes. Así que tengan paciencia mientras analizamos estos otros ejemplos que solidificarán la conclusión a la que llegamos aquí.
Con la estructura definida anteriormente, las otras piezas del rompecabezas encajan bastante bien. Centrándonos en los valores de chain_input, asignemoslos a los componentes (con su orden) definidos anteriormente.
Observaciones:
- Para RunnableSequence, dado que actúa como un contenedor para toda la canalización, la entrada del usuario también actúa como entrada para el componente RunnableSequence.
- Para el primer ChatPromptTemplate (qa_prompt1), como primer componente «verdadero» de la canalización, recibe la entrada directa del usuario.
- Para RunnableLambda (retrieve_text), recibe como entrada la salida de qa_prompt1, que es un objeto Mensaje
- Para el último ChatPromptTemplate (qa_prompt2), recibe como entrada la salida de retrieve_text, que es un dictado con ‘prompt’ como clave única.
El desglose anterior muestra cómo la estructura del oleoducto descrito anteriormente encaja perfectamente con los datos vistos en entrada_serializada y entrada_cadena