Capítulo 4. Técnicas avanzadas de generación de texto con LangChain
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Utilizar técnicas sencillas de ingeniería de prompts suele funcionar para la mayoría de las tareas, pero en ocasiones necesitarás utilizar un conjunto de herramientas más potente para resolver problemas complejos de IA generativa. Tales problemas y tareas incluyen:
- Longitud del contexto
-
Resumir todo un libro en una sinopsis digerible.
- Combinar entradas/salidas secuenciales LLM
-
Crear una historia para un libro, incluyendo los personajes, la trama y la construcción del mundo.
- Realizar tareas de razonamiento complejas
-
LLMs que actúan como agentes. Por ejemplo, podrías crear un agente LLM que te ayudara a conseguir tus objetivos personales de fitness.
Para afrontar con destreza estos complejos retos de IA generativa, resulta muy beneficioso familiarizarse con LangChain, un marco de trabajo de código abierto. Esta herramienta simplifica y mejora sustancialmente los flujos de trabajo de tu LLM.
Introducción a LangChain
LangChain es un marco versátil que permite la creación de aplicaciones utilizando LLMs y está disponible como paquete Python y TypeScript. Su principio central es que las aplicaciones más impactantes y distintas no se limitarán a interactuar con un modelo de lenguaje a través de una API, sino que también lo harán:
- Mejorar el conocimiento de los datos
-
El marco pretende establecer una conexión sin fisuras entre un modelo lingüístico y fuentes de datos externas.
- Mejorar la agencia
-
Se esfuerza por dotar a los modelos lingüísticos de la capacidad de comprometerse con su entorno e influir en él.
El marco LangChain ilustrado en la Figura 4-1 proporciona a una serie de abstracciones modulares esenciales para trabajar con LLM, junto con una amplia selección de implementaciones para estas abstracciones.
Cada módulo está diseñado para ser fácil de usar y se puede utilizar eficazmente de forma independiente o conjunta. Actualmente hay seis módulos comunes en LangChain:
- Modelo E/S
-
Maneja las operaciones de entrada/salida relacionadas con el modelo
- Recuperación
-
Se centra en la recuperación de textos relevantes para el LLM
- Cadenas
-
También conocidas como cadenas ejecutables LangChain, las cadenas permiten construir secuencias de operaciones LLM o llamadas a funciones
- Agentes
-
Permite a las cadenas tomar decisiones sobre qué herramientas utilizar basándose en directivas o instrucciones de alto nivel
- Memoria
-
Persiste el estado de una aplicación entre distintas ejecuciones de una cadena
- Devoluciones de llamada
-
Para ejecutar código adicional en eventos específicos, como cuando se genera cada nuevo token
Configuración del entorno
Puedes instalar LangChain en tu terminal con cualquiera de estos comandos:
-
pip install langchain langchain-openai
-
conda install -c conda-forge langchain langchain-openai
Si prefieres instalar los requisitos del paquete para todo el libro, puedes utilizar el archivo requirements.txt del repositorio de GitHub.
Se recomienda instalar los paquetes en un entorno virtual :
- Crear un entorno virtual
-
python -m venv venv
- Activar el entorno virtual
-
source venv/bin/activate
- Instala las dependencias
-
pip install -r requirements.txt
LangChain requiere integraciones con uno o más proveedores de modelos. Por ejemplo, para utilizar las API de modelos de OpenAI, tendrás que instalar su paquete Python con pip install openai
.
Como ya se explicó en el Capítulo 1, es una buena práctica establecer una variable de entorno llamada OPENAI_API_KEY
en tu terminal o cargarla desde un archivo .env utilizando python-dotenv
. Sin embargo, para la creación de prototipos, puedes omitir este paso introduciendo directamente tu clave API al cargar un modelo de chat en LangChain:
from
langchain_openai.chat_models
import
ChatOpenAI
chat
=
ChatOpenAI
(
api_key
=
"api_key"
)
Advertencia
No se recomienda codificar las claves API en los scripts por motivos de seguridad. En su lugar, utiliza variables de entorno o archivos de configuración para gestionar tus claves.
En el panorama en constante evolución de los LLM, puedes encontrarte con el reto de las disparidades entre las distintas API de los modelos. La falta de estandarización en las interfaces puede inducir capas adicionales de complejidad en la ingeniería de prompts y obstaculizar la integración sin fisuras de diversos modelos en tus proyectos.
Aquí es donde entra en juego LangChain. Como marco integral, LangChain te permite consumir fácilmente las distintas interfaces de diferentes modelos.
La funcionalidad de LangChain garantiza que no tengas que reinventar tus avisos o tu código cada vez que cambies de modelo. Su enfoque agnóstico de la plataforma promueve la experimentación rápida con una amplia gama de modelos, como Anthropic, Vertex AI, OpenAI y BedrockChat. Esto no sólo agiliza el proceso de evaluación de modelos, sino que también ahorra tiempo y recursos críticos al simplificar las complejas integraciones de modelos.
En las secciones siguientes, utilizarás el paquete OpenAI y su API en LangChain.
Modelos de chat
Los modelos de chat como el GPT-4 se han convertido en la principal forma de interactuar con la API de OpenAI. En lugar de ofrecer una respuesta directa de "texto de entrada, texto de salida", proponen un método de interacción en el que los mensajes de chat son los elementos de entrada y salida.
Generar respuestas LLM utilizando modelos de chat implica que introduzca uno o más mensajes en el modelo de chat. En el contexto de LangChain, los tipos de mensaje aceptados actualmente son AIMessage
, HumanMessage
y SystemMessage
. La salida de un modelo de chat siempre será un AIMessage
.
- MensajeSistema
-
Representa información que debería ser instrucciones para el sistema de IA. Se utilizan para guiar de algún modo el comportamiento o las acciones de la IA.
- MensajeHumano
-
Representa la información procedente de un humano que interactúa con el sistema de IA. Puede ser una pregunta, una orden o cualquier otra entrada de un usuario humano que la IA deba procesar y a la que deba responder.
- AIMensaje
-
Representa la información procedente de el propio sistema de IA. Suele ser la respuesta de la IA a una instrucción
HumanMessage
o el resultado de una instrucciónSystemMessage
.
Nota
Asegúrate de aprovechar la SystemMessage
para dar indicaciones explícitas. OpenAI ha perfeccionado el GPT-4 y los próximos modelos LLM para que presten especial atención a las directrices que se dan en este tipo de mensajes.
Vamos a crear un generador de chistes en LangChain.
Entrada:
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain.schema
import
AIMessage
,
HumanMessage
,
SystemMessage
chat
=
ChatOpenAI
(
temperature
=
0.5
)
messages
=
[
SystemMessage
(
content
=
'''Act as a senior software engineer
at a startup company.'''
),
HumanMessage
(
content
=
'''Please can you provide a funny joke
about software engineers?'''
)]
response
=
chat
.
invoke
(
input
=
messages
)
(
response
.
content
)
Salida:
Sure
,
here
's a lighthearted joke for you:
Why
did
the
software
engineer
go
broke
?
Because
he
lost
his
domain
in
a
bet
and
couldn
't afford to renew it.
En primer lugar, importa ChatOpenAI
, AIMessage
, HumanMessage
, y SystemMessage
. A continuación, crea una instancia de la clase ChatOpenAI
con un parámetro de temperatura de 0,5 (aleatoriedad).
Tras crear un modelo, se rellena una lista llamada messages
con un objeto SystemMessage
, que define el papel del LLM, y un objeto HumanMessage
, que pide una broma relacionada con la ingeniería de software.
Llamar al modelo de chat con .invoke(input=messages)
alimenta al LLM con una lista de mensajes, y luego recuperas la respuesta del LLM con response.content
.
Existe un método heredado que te permite llamar directamente al objeto chat
con chat(messages=messages)
:
response
=
chat
(
messages
=
messages
)
Modelos de chat en streaming
Es posible que hayas observado mientras utilizando ChatGPT cómo las palabras se te devuelven secuencialmente, un carácter cada vez. Este patrón distintivo de generación de respuestas se denomina streaming, y desempeña un papel crucial en la mejora del rendimiento de las aplicaciones basadas en chat:
for
chunk
in
chat
.
stream
(
messages
):
(
chunk
.
content
,
end
=
""
,
flush
=
True
)
Cuando llamas a chat.stream(messages)
, te devuelve trozos del mensaje de uno en uno. Esto significa que cada segmento del mensaje de chat se devuelve individualmente. A medida que llega cada trozo, se imprime instantáneamente en el terminal y se descarga. De esta forma, el streaming permite una latencia mínima de tus respuestas LLM.
El streaming tiene varias ventajas desde la perspectiva del usuario final. En primer lugar, reduce drásticamente el tiempo de espera de los usuarios. En cuanto el texto empieza a generarse carácter a carácter, los usuarios pueden empezar a interpretar el mensaje. No hay necesidad de construir un mensaje completo antes de verlo. Esto, a su vez, mejora significativamente la interactividad del usuario y minimiza la latencia.
Sin embargo, esta técnica conlleva sus propios retos. Un reto importante es analizar los resultados mientras se transmiten. Comprender y responder adecuadamente al mensaje mientras se está formando puede resultar intrincado, sobre todo cuando el contenido es complejo y detallado.
Crear múltiples generaciones de LLM
Puede haber situaciones en las que te resulte útil generar múltiples respuestas de los LLM. Esto es especialmente cierto cuando creas contenido dinámico, como publicaciones en redes sociales. En lugar de proporcionar una lista de mensajes, proporciona una lista de listas de mensajes.
Entrada:
# 2x lists of messages, which is the same as [messages, messages]
synchronous_llm_result
=
chat
.
batch
([
messages
]
*
2
)
(
synchronous_llm_result
)
Salida:
[
AIMessage
(
content
=
'''Sure, here's a lighthearted joke for you:
\n\n
Why did
the software engineer go broke?
\n\n
Because he kept forgetting to Ctrl+ Z
his expenses!'''
),
AIMessage
(
content
=
'''Sure, here
\'
s a lighthearted joke for you:
\n\n
Why do
software engineers prefer dark mode?
\n\n
Because it
\'
s easier on their
"byte" vision!'''
)]
La ventaja de utilizar .batch()
en lugar de .invoke()
es que puedes paralelizar el número de solicitudes de API realizadas a OpenAI.
Para cualquier ejecutable en LangChain, puedes añadir un argumento RunnableConfig
a la función batch
que contenga muchos parámetros configurables, incluyendo max_
concurrency
:
from
langchain_core.runnables.config
import
RunnableConfig
# Create a RunnableConfig with the desired concurrency limit:
config
=
RunnableConfig
(
max_concurrency
=
5
)
# Call the .batch() method with the inputs and config:
results
=
chat
.
batch
([
messages
,
messages
],
config
=
config
)
Nota
En informática, las funciones asíncronas (async) son aquellas que operan independientemente de otros procesos, lo que permite ejecutar varias solicitudes de API simultáneamente sin esperarse unas a otras. En LangChain, estas funciones asíncronas te permiten hacer muchas peticiones a la API a la vez, no una detrás de otra. Esto es especialmente útil en flujos de trabajo más complejos y disminuye la latencia general para tus usuarios.
La mayoría de las funciones asíncronas de LangChain llevan simplemente como prefijo la letra a
, como .ainvoke()
y .abatch()
. Si quieres utilizar la API asíncrona para realizar tareas más eficientes, utiliza estas funciones .
Plantillas LangChain Prompt
Hasta ahora, has codificado las cadenas en los objetos ChatOpenAI
. A medida que tus aplicaciones LLM crecen en tamaño, es cada vez más importante utilizar plantillas rápidas.
Las plantillas de avisos sirven para generar avisos reproducibles para modelos de lenguaje de IA. Constan de una plantilla, una cadena de texto que puede recibir parámetros y construir un aviso de texto para un modelo lingüístico.
Sin plantillas de avisos, probablemente utilizarías el formato Python f-string
:
language
=
"Python"
prompt
=
f
"What is the best way to learn coding in
{
language
}
?"
(
prompt
)
# What is the best way to learn coding in Python?
Pero, ¿por qué no utilizar simplemente un f-string
para las plantillas de avisos? Si utilizas las plantillas de avisos de LangChain, podrás hacerlo fácilmente:
-
Valida tus entradas rápidas
-
Combina varias preguntas con la composición
-
Define selectores personalizados que inyectarán ejemplos k-shot en tu prompt
-
Guardar y cargar avisos desde archivos .yml y .json
-
Crea plantillas de avisos personalizadas que ejecuten código o instrucciones adicionales cuando se creen
Lenguaje de Expresión LangChain (LCEL)
El operador de tubería |
es un componente clave del Lenguaje de Expresión de Cadena de Lenguaje (LCEL) que te permite encadenar distintos componentes o ejecutables en una cadena de procesamiento de datos.
En LCEL, el operador |
es similar al operador de tuberías de Unix. Toma la salida de un componente y la introduce como entrada en el siguiente componente de la cadena. Esto te permite conectar y combinar fácilmente distintos componentes para crear una compleja cadena de operaciones:
chain
=
prompt
|
model
El operador |
se utiliza para encadenar los componentes prompt y modelo. La salida del componente prompt se pasa como entrada al componente modelo. Este mecanismo de encadenamiento te permite construir cadenas complejas a partir de componentes básicos y posibilita el flujo fluido de datos entre las distintas etapas de la cadena de procesamiento.
Además, el orden importa, por lo que técnicamente podrías crear esta cadena:
bad_order_chain
=
model
|
prompt
Pero produciría un error después de utilizar la función invoke
, porque los valores devueltos por model
no son compatibles con las entradas esperadas para el indicador.
Vamos a crear un generador de nombres de empresas utilizando plantillas de avisos que nos devolverán de cinco a siete nombres de empresas relevantes:
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.prompts
import
(
SystemMessagePromptTemplate
,
ChatPromptTemplate
)
template
=
"""
You are a creative consultant brainstorming names for businesses.
You must follow the following principles:
{principles}
Please generate a numerical list of five catchy names for a start-up in the
{industry}
industry that deals with
{context}
?
Here is an example of the format:
1. Name1
2. Name2
3. Name3
4. Name4
5. Name5
"""
model
=
ChatOpenAI
()
system_prompt
=
SystemMessagePromptTemplate
.
from_template
(
template
)
chat_prompt
=
ChatPromptTemplate
.
from_messages
([
system_prompt
])
chain
=
chat_prompt
|
model
result
=
chain
.
invoke
({
"industry"
:
"medical"
,
"context"
:
'''creating AI solutions by automatically summarizing patient
records'''
,
"principles"
:
'''1. Each name should be short and easy to
remember. 2. Each name should be easy to pronounce.
3. Each name should be unique and not already taken by another company.'''
})
(
result
.
content
)
Salida:
1. SummarAI 2. MediSummar 3. AutoDocs 4. RecordAI 5. SmartSummarize
Primero importarás ChatOpenAI
, SystemMessagePromptTemplate
, y ChatPromptTemplate
. A continuación, definirás una plantilla de avisos con directrices específicas en template
, indicando al LLM que genere nombres de empresas. ChatOpenAI()
inicializa el chat, mientras que SystemMessagePromptTemplate.from_template(template)
y ChatPromptTemplate.from_messages([system_prompt])
crean tu plantilla de avisos.
Creas un LCEL chain
juntando chat_prompt
y la función model
, que luego se invoca. Esto sustituye los marcadores de posición {industries}
, {context}
, y {principles}
en el prompt por los valores del diccionario dentro de la función invoke
.
Por último, extraes la respuesta del LLM como una cadena accediendo a la propiedad .content
de la variable result
.
Dar instrucciones y especificar el formato
Unas instrucciones cuidadosamente elaboradas podrían incluir cosas como "Eres un consultor creativo que aporta ideas sobre nombres para empresas" y "Por favor, genera una lista numérica de cinco a siete nombres pegadizos para una start-up". Cuestiones como éstas guían a tu LLM para que realice la tarea exacta que le pides.
Utilizar PromptTemplate con modelos de chat
LangChain proporciona una plantilla más tradicional llamada PromptTemplate
, que requiere los argumentos input_variables
y template
.
Entrada:
from
langchain_core.prompts
import
PromptTemplate
from
langchain.prompts.chat
import
SystemMessagePromptTemplate
from
langchain_openai.chat_models
import
ChatOpenAI
prompt
=
PromptTemplate
(
template
=
'''You are a helpful assistant that translates
{input_language}
to
{output_language}
.'''
,
input_variables
=
[
"input_language"
,
"output_language"
],
)
system_message_prompt
=
SystemMessagePromptTemplate
(
prompt
=
prompt
)
chat
=
ChatOpenAI
()
chat
.
invoke
(
system_message_prompt
.
format_messages
(
input_language
=
"English"
,
output_language
=
"French"
))
Salida:
AIMessage
(
content
=
"Vous êtes un assistant utile qui traduit l'anglais en
français
.
", additional_kwargs=
{}
, example=False)
Parsers de salida
En el Capítulo 3, utilizaste expresiones regulares (regex) para extraer datos estructurados de un texto que contenía listas numéricas, pero es posible hacerlo automáticamente en LangChain con analizadores sintácticos de salida.
Los analizadores sintácticos de salida son una abstracción de nivel superior proporcionada por LangChain para analizar datos estructurados a partir de respuestas de cadena LLM. Actualmente, los analizadores sintácticos de salida disponibles son:
- Analizador de listas
- Analizador de fecha y hora
- Parser Enum
- Parser autocorregible
-
Envuelve otro analizador sintáctico de salida, y si ese analizador sintáctico de salida falla, llamará a otro LLM para corregir cualquier error.
- Analizador sintáctico Pydantic (JSON)
-
Analiza las respuestas LLM en una salida JSON que se ajuste a un esquema Pydantic.
- Reintentar analizador sintáctico
-
Permite reintentar un análisis sintáctico fallido de un análisis sintáctico de salida anterior.
- Analizador sintáctico de salida estructurada
- Analizador XML
Como descubrirás, hay dos funciones importantes para los analizadores sintácticos de salida de LangChain:
.get_format_instructions()
-
Esta función proporciona las instrucciones necesarias en tu prompt para dar salida a un formato estructurado que pueda ser analizado.
.parse(llm_output: str)
-
Esta función se encarga de analizar tus respuestas LLM en un formato predefinido.
En general, encontrarás que el analizador sintáctico Pydantic (JSON) con ChatOpenAI()
proporciona la mayor flexibilidad.
El analizador sintáctico Pydantic (JSON) aprovecha la biblioteca Pydantic de Python. Pydantic es una biblioteca de validación de datos que proporciona una forma de validar los datos entrantes utilizando anotaciones de tipo Python. Esto significa que Pydantic te permite crear esquemas para tus datos y valida y analiza automáticamente los datos de entrada de acuerdo con esos esquemas.
Entrada:
from
langchain_core.prompts.chat
import
(
ChatPromptTemplate
,
SystemMessagePromptTemplate
,
)
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain.output_parsers
import
PydanticOutputParser
from
pydantic.v1
import
BaseModel
,
Field
from
typing
import
List
temperature
=
0.0
class
BusinessName
(
BaseModel
):
name
:
str
=
Field
(
description
=
"The name of the business"
)
rating_score
:
float
=
Field
(
description
=
'''The rating score of the
business. 0 is the worst, 10 is the best.'''
)
class
BusinessNames
(
BaseModel
):
names
:
List
[
BusinessName
]
=
Field
(
description
=
'''A list
of busines names'''
)
# Set up a parser + inject instructions into the prompt template:
parser
=
PydanticOutputParser
(
pydantic_object
=
BusinessNames
)
principles
=
"""
- The name must be easy to remember.
- Use the
{industry}
industry and Company context to create an effective name.
- The name must be easy to pronounce.
- You must only return the name without any other text or characters.
- Avoid returning full stops,
\n
, or any other characters.
- The maximum length of the name must be 10 characters.
"""
# Chat Model Output Parser:
model
=
ChatOpenAI
()
template
=
"""Generate five business names for a new start-up company in the
{industry}
industry.
You must follow the following principles:
{principles}
{format_instructions}
"""
system_message_prompt
=
SystemMessagePromptTemplate
.
from_template
(
template
)
chat_prompt
=
ChatPromptTemplate
.
from_messages
([
system_message_prompt
])
# Creating the LCEL chain:
prompt_and_model
=
chat_prompt
|
model
result
=
prompt_and_model
.
invoke
(
{
"principles"
:
principles
,
"industry"
:
"Data Science"
,
"format_instructions"
:
parser
.
get_format_instructions
(),
}
)
# The output parser, parses the LLM response into a Pydantic object:
(
parser
.
parse
(
result
.
content
))
Salida:
names
=
[
BusinessName
(
name
=
'DataWiz'
,
rating_score
=
8.5
),
BusinessName
(
name
=
'InsightIQ'
,
rating_score
=
9.2
),
BusinessName
(
name
=
'AnalytiQ'
,
rating_score
=
7.8
),
BusinessName
(
name
=
'SciData'
,
rating_score
=
8.1
),
BusinessName
(
name
=
'InfoMax'
,
rating_score
=
9.5
)]
Después de cargar las bibliotecas necesarias, crea un modelo ChatOpenAI. A continuación, crea SystemMessagePromptTemplate
a partir de tu modelo y forma con él un ChatPromptTemplate
. Utilizarás los modelos Pydantic BusinessName
y BusinessNames
para estructurar la salida deseada, una lista de nombres de empresa únicos. Crearás un analizador Pydantic
para analizar estos modelos y formatearás el mensaje utilizando las variables introducidas por el usuario llamando a la función invoke
. Alimentando tu modelo con este mensaje personalizado, le permitirás producir nombres de empresas creativos y únicos utilizando la función parser
.
Es posible utilizar analizadores sintácticos de salida dentro de LCEL utilizando esta sintaxis:
chain
=
prompt
|
model
|
output_parser
Añadamos el analizador sintáctico de salida directamente a la cadena.
Entrada:
parser
=
PydanticOutputParser
(
pydantic_object
=
BusinessNames
)
chain
=
chat_prompt
|
model
|
parser
result
=
chain
.
invoke
(
{
"principles"
:
principles
,
"industry"
:
"Data Science"
,
"format_instructions"
:
parser
.
get_format_instructions
(),
}
)
(
result
)
Salida:
names=[BusinessName(name='DataTech', rating_score=9.5),...]
La cadena es ahora responsable del formato del aviso, de la llamada al LLM y de analizar la respuesta del LLM en un objeto Pydantic
.
Especifica el formato
Las peticiones anteriores utilizan modelos y analizadores sintácticos de salida Pydantic, lo que te permite indicar explícitamente a un LLM el formato de respuesta que deseas.
Merece la pena saber que pidiendo a un LLM que proporcione una salida JSON estructurada, puedes crear una API flexible y generalizable a partir de la respuesta del LLM. Esto tiene sus limitaciones, como el tamaño del JSON creado y la fiabilidad de tus peticiones, pero sigue siendo un área prometedora para las aplicaciones LLM.
Advertencia
Debes tener en cuenta los casos de perímetro, así como añadir declaraciones de gestión de errores, ya que las salidas de LLM pueden no estar siempre en el formato que desees.
Los analizadores sintácticos de salida te ahorran la complejidad e intrincación de las expresiones regulares, proporcionando funcionalidades fáciles de usar para una gran variedad de casos de uso. Ahora que los has visto en acción, puedes utilizar los analizadores sintácticos de salida para estructurar y recuperar sin esfuerzo los datos que necesitas de la salida de un LLM, aprovechando todo el potencial de la IA para tus tareas.
Además, utilizar analizadores sintácticos para estructurar los datos extraídos de los LLM te permite elegir fácilmente cómo organizar los resultados para un uso más eficiente de . Esto puede ser útil si tratas con listas extensas y necesitas ordenarlas por determinados criterios, como los nombres de las empresas.
Pruebas LangChain
Además de los analizadores sintácticos de salida para comprobar si hay errores de formato en , la mayoría de los sistemas de IA también utilizan evaluadores, o métricas de evaluación, para medir el rendimiento de cada respuesta prompt. LangChain tiene una serie de evaluadores listos para usar, que se pueden registrar directamente en su plataforma LangSmith para su posterior depuración, monitoreo y prueba. Weights and Biases es una plataforma alternativa de aprendizaje automático que ofrece una funcionalidad similar y capacidades de rastreo para los LLM.
Las métricas de evaluación son útiles para algo más que las pruebas rápidas, ya que se pueden utilizar para identificar ejemplos positivos y negativos para la recuperación, así como para construir conjuntos de datos que permitan afinar los modelos personalizados.
La mayoría de las métricas de evaluación se basan en un conjunto de casos de prueba, que son emparejamientos de entrada y salida en los que conoces la respuesta correcta. A menudo, estas respuestas de referencia son creadas o curadas manualmente por un humano, pero también es práctica común utilizar un modelo más inteligente como GPT-4 para generar las respuestas de la verdad fundamental, lo que se ha hecho para el siguiente ejemplo. Dada una lista de descripciones de transacciones financieras, utilizamos GPT-4 para clasificar cada transacción con un transaction_category
y transaction_type
. El proceso se puede encontrar en el Jupyter Notebook langchain-evals.ipynb
en el repositorio GitHub del libro.
Al tomarse la respuesta GPT-4 como correcta, ahora es posible valorar la precisión de modelos más pequeños como GPT-3,5-turbo y Mixtral 8x7b (llamados mistral-small
en la API). Si puedes conseguir una precisión suficientemente buena con un modelo más pequeño, puedes ahorrar dinero o reducir la latencia. Además, si ese modelo está disponible en código abierto como el modelo de Mistral, puedes migrar esa tarea para que se ejecute en tus propios servidores, evitando enviar datos potencialmente sensibles fuera de tu organización. Recomendamos probar primero con una API externa, antes de tomarse la molestia de autoalojar un modelo de SO.
Recuerda registrarte y suscribirte para obtener una clave API; luego expónla como variable de entorno escribiendo en tu terminal:
export MISTRAL_API_KEY=api-key
El siguiente script forma parte de un cuaderno que ha definido previamente un marco de datos df
. Para abreviar vamos a investigar sólo la sección de evaluación del script, suponiendo que ya se ha definido un marco de datos.
Entrada:
import
os
from
langchain_mistralai.chat_models
import
ChatMistralAI
from
langchain.output_parsers
import
PydanticOutputParser
from
langchain_core.prompts
import
ChatPromptTemplate
from
pydantic.v1
import
BaseModel
from
typing
import
Literal
,
Union
from
langchain_core.output_parsers
import
StrOutputParser
# 1. Define the model:
mistral_api_key
=
os
.
environ
[
"MISTRAL_API_KEY"
]
model
=
ChatMistralAI
(
model
=
"mistral-small"
,
mistral_api_key
=
mistral_api_key
)
# 2. Define the prompt:
system_prompt
=
"""You are are an expert at analyzing
bank transactions, you will be categorizing a single
transaction.
Always return a transaction type and category:
do not return None.
Format Instructions:
{format_instructions}
"""
user_prompt
=
"""Transaction Text:
{transaction}
"""
prompt
=
ChatPromptTemplate
.
from_messages
(
[
(
"system"
,
system_prompt
,
),
(
"user"
,
user_prompt
,
),
]
)
# 3. Define the pydantic model:
class
EnrichedTransactionInformation
(
BaseModel
):
transaction_type
:
Union
[
Literal
[
"Purchase"
,
"Withdrawal"
,
"Deposit"
,
"Bill Payment"
,
"Refund"
],
None
]
transaction_category
:
Union
[
Literal
[
"Food"
,
"Entertainment"
,
"Transport"
,
"Utilities"
,
"Rent"
,
"Other"
],
None
,
]
# 4. Define the output parser:
output_parser
=
PydanticOutputParser
(
pydantic_object
=
EnrichedTransactionInformation
)
# 5. Define a function to try to fix and remove the backslashes:
def
remove_back_slashes
(
string
):
# double slash to escape the slash
cleaned_string
=
string
.
replace
(
"
\\
"
,
""
)
return
cleaned_string
# 6. Create an LCEL chain that fixes the formatting:
chain
=
prompt
|
model
|
StrOutputParser
()
\|
remove_back_slashes
|
output_parser
transaction
=
df
.
iloc
[
0
][
"Transaction Description"
]
result
=
chain
.
invoke
(
{
"transaction"
:
transaction
,
"format_instructions"
:
\output_parser
.
get_format_instructions
(),
}
)
# 7. Invoke the chain for the whole dataset:
results
=
[]
for
i
,
row
in
tqdm
(
df
.
iterrows
(),
total
=
len
(
df
)):
transaction
=
row
[
"Transaction Description"
]
try
:
result
=
chain
.
invoke
(
{
"transaction"
:
transaction
,
"format_instructions"
:
\output_parser
.
get_format_instructions
(),
}
)
except
:
result
=
EnrichedTransactionInformation
(
transaction_type
=
None
,
transaction_category
=
None
)
results
.
append
(
result
)
# 8. Add the results to the dataframe, as columns transaction type and
# transaction category:
transaction_types
=
[]
transaction_categories
=
[]
for
result
in
results
:
transaction_types
.
append
(
result
.
transaction_type
)
transaction_categories
.
append
(
result
.
transaction_category
)
df
[
"mistral_transaction_type"
]
=
transaction_types
df
[
"mistral_transaction_category"
]
=
transaction_categories
df
.
head
()
Salida:
Transaction Description transaction_type transaction_category mistral_transaction_type mistral_transaction_category 0 cash deposit at local branch Deposit Other Deposit Other 1 cash deposit at local branch Deposit Other Deposit Other 2 withdrew money for rent payment Withdrawal Rent Withdrawal Rent 3 withdrew cash for weekend expenses Withdrawal Other Withdrawal Other 4 purchased books from the bookstore Purchase Other Purchase Entertainment
El código hace lo siguiente
-
from langchain_mistralai.chat_models import ChatMistralAI
: Importamos la implementación de Mistral de LangChain. -
from langchain.output_parsers import PydanticOutputParser
: Importa la clasePydanticOutputParser
, que se utiliza para analizar la salida mediante modelos Pydantic. También importamos un analizador sintáctico de la salida de cadena para gestionar un paso intermedio en el que eliminamos las barras invertidas de la clave JSON (un problema habitual en las respuestas de Mistral). -
mistral_api_key = os.environ["MISTRAL_API_KEY"]
: Recupera la clave de la API de Mistral de las variables de entorno. Es necesario configurarla antes de ejecutar el bloc de notas. -
model = ChatMistralAI(model="mistral-small", mistral_api_key=mistral_api_key)
: Inicializa una instancia deChatMistralAI
con el modelo y la clave API especificados. Mistral Small es lo que en su API llaman el modelo Mixtral 8x7b (también disponible en código abierto). -
system_prompt
yuser_prompt
: Estas líneas definen las plantillas del sistema y los avisos al usuario que se utilizan en el chat para clasificar las transacciones. -
class EnrichedTransactionInformation(BaseModel)
: Define un modelo pydánticoEnrichedTransactionInformation
con dos campos:transaction_type
ytransaction_category
, cada uno con valores específicos permitidos y la posibilidad de serNone
. Esto es lo que nos dice si la salida está en el formato correcto. -
def remove_back_slashes(string)
: Define una función para eliminar las barras invertidas de una cadena. -
chain = prompt | model | StrOutputParser() | remove_back_slashes | output_parser
: Actualiza la cadena para incluir un analizador sintáctico de salida de cadena y la funciónremove_back_slashes
antes del analizador sintáctico de salida original. -
transaction = df.iloc[0]["Transaction Description"]
: Extrae la descripción de la primera transacción de un marco de datosdf
. Este marco de datos se carga antes en el cuaderno Jupyter (omitido por brevedad). -
for i, row in tqdm(df.iterrows(), total=len(df))
: Recorre cada fila del marco de datosdf
, con una barra de progreso. -
result = chain.invoke(...)
: Dentro del bucle, la cadena se invoca para cada transacción. -
except
: En caso de excepción, se crea un objetoEnrichedTransactionInformation
por defecto con los valoresNone
. Éstos se tratarán como errores en la evaluación, pero no romperán el bucle de procesamiento. -
df["mistral_transaction_type"] = transaction_types
,df["mistral_transaction_category"] = transaction_categories
: Añade los tipos de transacción y las categorías como nuevas columnas en el marco de datos, que luego mostramos condf.head()
.
Con las respuestas de Mistral guardadas en el marco de datos, es posible compararlas con las categorías y tipos de transacción definidos anteriormente para comprobar la precisión de Mistral. La métrica de evaluación LangChain más básica consiste en hacer coincidir una cadena exacta de una predicción con una respuesta de referencia, lo que devuelve una puntuación de 1 si es correcta, y de 0 si es incorrecta. El cuaderno ofrece un ejemplo de cómo implementarlo, que muestra que la precisión de Mistral es del 77,5%. Sin embargo, si lo único que haces es comparar cadenas, probablemente no necesites implementarlo en LangChain.
Donde LangChain es valioso es en sus enfoques estandarizados y probados para implementar evaluadores más avanzados utilizando LLMs. El evaluador labeled_pairwise_string
compara dos resultados y da una razón para elegir entre ellos, utilizando GPT-4. Un caso de uso común para este tipo de evaluador es comparar los resultados de dos peticiones o modelos diferentes, sobre todo si los modelos que se están probando son menos sofisticados que GPT-4. Este evaluador que utiliza GPT-4 sigue funcionando para evaluar las respuestas de GPT-4, pero debes revisar manualmente el razonamiento y las puntuaciones para asegurarte de que está haciendo un buen trabajo: si GPT-4 es malo en una tarea, también puede ser malo para evaluar esa tarea. En el cuaderno, se volvió a ejecutar la misma clasificación de transacciones con el modelo cambiado a model = ChatOpenAI(model="gpt-3.5-turbo-1106", model_kwargs={"response_format": {"type": "json_object"}},)
. Ahora es posible hacer una comparación por pares entre las respuestas de Mistral y GPT-3.5, como se muestra en el siguiente ejemplo. Puedes ver en la salida el razonamiento que se da para justificar la puntuación.
Entrada:
# Evaluate answers using LangChain evaluators:
from
langchain.evaluation
import
load_evaluator
evaluator
=
load_evaluator
(
"labeled_pairwise_string"
)
row
=
df
.
iloc
[
0
]
transaction
=
row
[
"Transaction Description"
]
gpt3pt5_category
=
row
[
"gpt3.5_transaction_category"
]
gpt3pt5_type
=
row
[
"gpt3.5_transaction_type"
]
mistral_category
=
row
[
"mistral_transaction_category"
]
mistral_type
=
row
[
"mistral_transaction_type"
]
reference_category
=
row
[
"transaction_category"
]
reference_type
=
row
[
"transaction_type"
]
# Put the data into JSON format for the evaluator:
gpt3pt5_data
=
f
"""
{{
"transaction_category": "
{
gpt3pt5_category
}
",
"transaction_type": "
{
gpt3pt5_type
}
"
}}
"""
mistral_data
=
f
"""
{{
"transaction_category": "
{
mistral_category
}
",
"transaction_type": "
{
mistral_type
}
"
}}
"""
reference_data
=
f
"""
{{
"transaction_category": "
{
reference_category
}
",
"transaction_type": "
{
reference_type
}
"
}}
"""
# Set up the prompt input for context for the evaluator:
input_prompt
=
"""You are an expert at analyzing bank
transactions,
you will be categorizing a single transaction.
Always return a transaction type and category: do not
return None.
Format Instructions:
{format_instructions}
Transaction Text:
{transaction}
"""
transaction_types
.
append
(
transaction_type_score
)
transaction_categories
.
append
(
transaction_category_score
)
accuracy_score
=
0
for
transaction_type_score
,
transaction_category_score
\in
zip
(
transaction_types
,
transaction_categories
):
accuracy_score
+=
transaction_type_score
[
'score'
]
+
\transaction_category_score
[
'score'
]
accuracy_score
=
accuracy_score
/
(
len
(
transaction_types
)
\*
2
)
(
f
"Accuracy score:
{
accuracy_score
}
"
)
evaluator
.
evaluate_string_pairs
(
prediction
=
gpt3pt5_data
,
prediction_b
=
mistral_data
,
input
=
input_prompt
.
format
(
format_instructions
=
output_parser
.
get_format_instructions
(),
transaction
=
transaction
),
reference
=
reference_data
,
)
Salida:
{'reasoning': '''Both Assistant A and Assistant B provided the exact same response to the user\'s question. Their responses are both helpful, relevant, correct, and demonstrate depth of thought. They both correctly identified the transaction type as "Deposit" and the transaction category as "Other" based on the transaction text provided by the user. Both responses are also well-formatted according to the JSON schema provided by the user. Therefore, it\'s a tie between the two assistants. \n\nFinal Verdict: [[C]]''', 'value': None, 'score': 0.5}
Este código demuestra el sencillo evaluador de concordancia exacta de cadenas de LangChain:
-
evaluator = load_evaluator("labeled_pairwise_string")
: Se trata de una función de ayuda que puede utilizarse para cargar cualquier evaluador LangChain por su nombre. En este caso, se utiliza el evaluadorlabeled_pairwise_string
. -
row = df.iloc[0]
: Esta línea y las siete siguientes obtienen la primera fila y extraen los valores de las distintas columnas necesarias. Incluye la descripción de la transacción, así como la categoría y los tipos de transacción Mistral y GPT-3.5. Aquí se muestra una única transacción, pero este código puede ejecutarse fácilmente en un bucle a través de cada transacción, sustituyendo esta línea por una funcióniterrows
for i, row in tqdm(df.iterrows(), total=len(df)):
, como se hace más adelante en el cuaderno. -
gpt3pt5_data = f"""{{
: Para utilizar el evaluador de comparación por pares, necesitamos pasar los resultados de forma que tengan el formato correcto para el indicador. Esto se hace para Mistral y GPT-3.5, así como para los datos de referencia. -
input_prompt = """You are an expert...
: El otro formato que tenemos que corregir es el de las instrucciones. Para obtener puntuaciones de evaluación precisas, el evaluador necesita ver las instrucciones que se dieron para la tarea. -
evaluator.evaluate_string_pairs(...
: Sólo queda ejecutar el evaluador pasándole los datosprediction
yprediction_b
(GPT-3.5 y Mistral, respectivamente), así como el indicadorinput
, yreference
, que sirve de verdad de campo. -
A continuación de este código en el cuaderno, hay un ejemplo de bucle y ejecución del evaluador en cada fila del marco de datos y, a continuación, guardar los resultados y razonar de nuevo en el marco de datos.
Este ejemplo demuestra cómo utilizar un evaluador LangChain, pero hay muchos tipos diferentes de evaluadores disponibles. Los evaluadores de distancia de cadena(Levenshtein) o de distancia de incrustación se utilizan a menudo en situaciones en las que las respuestas no coinciden exactamente con la respuesta de referencia, sino que sólo necesitan aproximarse lo suficiente desde el punto de vista semántico. La distancia Levenshtein permite coincidencias difusas basadas en cuántas ediciones de un solo carácter serían necesarias para transformar el texto predicho en el texto de referencia, y la distancia de incrustación hace uso de vectores (tratados en el Capítulo 5) para calcular la similitud entre la respuesta y la referencia.
El otro tipo de evaluador que utilizamos a menudo en nuestro trabajo son las comparaciones por pares, que son útiles para comparar dos indicaciones o modelos diferentes, utilizando un modelo más inteligente como el GPT-4. Este tipo de comparación es útil porque se proporciona un razonamiento para cada comparación, que puede ser útil para depurar por qué se favoreció un enfoque sobre otro. El cuaderno de esta sección muestra un ejemplo de uso de un evaluador de comparación por pares para comprobar la precisión de GPT-3.5-turbo frente a Mixtral 8x7b.
Evaluar la calidad
Sin definir un conjunto adecuado de métricas de evaluación para definir el éxito, puede ser difícil saber si los cambios en el sistema de preguntas o en el sistema más amplio están mejorando o perjudicando la calidad de las respuestas. Si puedes automatizar las métricas de evaluación utilizando modelos inteligentes como GPT-4, podrás iterar más rápidamente para mejorar los resultados sin necesidad de una revisión humana manual costosa o que lleve mucho tiempo.
Llamada a funciones OpenAI
La llamada a funciones proporciona un método alternativo a los analizadores sintácticos de salida, aprovechando los modelos de OpenAI ajustados. Estos modelos identifican cuándo debe ejecutarse una función y generan una respuesta JSON con el nombre y los argumentos de una función predefinida. Varios casos de uso incluyen:
- Diseñar bots de chat sofisticados
-
Capaz de organizar y gestionar programaciones. Por ejemplo, puedes definir una función para programar una reunión:
schedule_meeting(date: str, time: str, attendees: List[str])
. - Convierte el lenguaje natural en llamadas procesables a la API
-
Una orden como "Enciende las luces del pasillo" puede convertirse en
control_device(device: str, action: 'on' | 'off')
para interactuar con tu API domótica. - Extraer datos estructurados
-
Esto podría hacerse definiendo una función como
extract_contextual_data(context: str, data_points: List[str])
osearch_database(query: str)
.
Cada función que utilices dentro de la llamada a la función requerirá un esquema JSON apropiado. Exploremos un ejemplo con el paquete OpenAI
:
from
openai
import
OpenAI
import
json
from
os
import
getenv
def
schedule_meeting
(
date
,
time
,
attendees
):
# Connect to calendar service:
return
{
"event_id"
:
"1234"
,
"status"
:
"Meeting scheduled successfully!"
,
"date"
:
date
,
"time"
:
time
,
"attendees"
:
attendees
}
OPENAI_FUNCTIONS
=
{
"schedule_meeting"
:
schedule_meeting
}
Después de importar OpenAI
y json
, crearás una función llamada schedule_meeting
. Esta función es una maqueta, que simula el proceso de programar una reunión, y devuelve detalles como event_id
, date
, time
, y attendees
. A continuación, crea un diccionario OPENAI_FUNCTIONS
para asignar el nombre de la función a la función real para facilitar la consulta.
A continuación, define una lista functions
que proporcione el esquema JSON de la función. Este esquema incluye su nombre, una breve descripción y los parámetros que requiere, guiando a la LLM sobre cómo interactuar con ella:
# Our predefined function JSON schema:
functions
=
[
{
"type"
:
"function"
,
"function"
:
{
"type"
:
"object"
,
"name"
:
"schedule_meeting"
,
"description"
:
'''Set a meeting at a specified date and time for
designated attendees'''
,
"parameters"
:
{
"type"
:
"object"
,
"properties"
:
{
"date"
:
{
"type"
:
"string"
,
"format"
:
"date"
},
"time"
:
{
"type"
:
"string"
,
"format"
:
"time"
},
"attendees"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"string"
}},
},
"required"
:
[
"date"
,
"time"
,
"attendees"
],
},
},
}
]
Especifica el formato
Cuando utilices la llamada a funciones con en tus modelos de OpenAI, asegúrate siempre de definir un esquema JSON detallado (que incluya el nombre y la descripción). Esto actúa como un plano para la función, guiando al modelo para que comprenda cuándo y cómo invocarla correctamente.
Después de definir las funciones, vamos a hacer una petición a la API de OpenAI. Configura una lista messages
con la consulta del usuario. Luego, utilizando un objeto OpenAI client
, enviarás este mensaje y el esquema de la función al modelo. El LLM analiza la conversación, discierne la necesidad de activar una función y proporciona el nombre de la función y los argumentos. El function
y el function_args
se analizan a partir de la respuesta del LLM. A continuación se ejecuta la función, y sus resultados se añaden de nuevo a la conversación. A continuación, se vuelve a llamar al modelo para obtener un resumen fácil de usar de todo el proceso.
Entrada:
client
=
OpenAI
(
api_key
=
getenv
(
"OPENAI_API_KEY"
))
# Start the conversation:
messages
=
[
{
"role"
:
"user"
,
"content"
:
'''Schedule a meeting on 2023-11-01 at 14:00
with Alice and Bob.'''
,
}
]
# Send the conversation and function schema to the model:
response
=
client
.
chat
.
completions
.
create
(
model
=
"gpt-3.5-turbo-1106"
,
messages
=
messages
,
tools
=
functions
,
)
response
=
response
.
choices
[
0
]
.
message
# Check if the model wants to call our function:
if
response
.
tool_calls
:
# Get the first function call:
first_tool_call
=
response
.
tool_calls
[
0
]
# Find the function name and function args to call:
function_name
=
first_tool_call
.
function
.
name
function_args
=
json
.
loads
(
first_tool_call
.
function
.
arguments
)
(
"This is the function name: "
,
function_name
)
(
"These are the function arguments: "
,
function_args
)
function
=
OPENAI_FUNCTIONS
.
get
(
function_name
)
if
not
function
:
raise
Exception
(
f
"Function
{
function_name
}
not found."
)
# Call the function:
function_response
=
function
(
**
function_args
)
# Share the function's response with the model:
messages
.
append
(
{
"role"
:
"function"
,
"name"
:
"schedule_meeting"
,
"content"
:
json
.
dumps
(
function_response
),
}
)
# Let the model generate a user-friendly response:
second_response
=
client
.
chat
.
completions
.
create
(
model
=
"gpt-3.5-turbo-0613"
,
messages
=
messages
)
(
second_response
.
choices
[
0
]
.
message
.
content
)
Salida:
These
are
the
function
arguments
:
{
'date'
:
'2023-11-01'
,
'time'
:
'14:00'
,
'attendees'
:
[
'Alice'
,
'Bob'
]}
This
is
the
function
name
:
schedule_meeting
I
have
scheduled
a
meeting
on
2023
-
11
-
01
at
14
:
00
with
Alice
and
Bob
.
The
event
ID
is
1234.
Hay que tener en cuenta varios puntos importantes al llamar a una función:
-
Es posible tener muchas funciones que la LLM pueda llamar.
-
OpenAI puede alucinar los parámetros de las funciones, así que sé más explícito en el mensaje
system
para evitarlo. -
El parámetro
function_call
se puede ajustar de varias formas:-
Para ordenar una llamada a una función concreta:
tool_choice: {"type: "function", "function": {"name": "my_function"}}}
. -
Para un mensaje de usuario sin invocación de función:
tool_choice: "none"
. -
Por defecto (
tool_choice: "auto"
), el modelo decide autónomamente si llamar a una función y a qué función.
-
Llamada a funciones paralelas
Puedes configurar tus mensajes de chat para que incluyan intents que soliciten llamadas simultáneas a varias herramientas. Esta estrategia se conoce como llamada a funciones paralelas.
Modificando el código utilizado anteriormente, la lista messages
se actualiza para obligar a programar dos reuniones:
# Start the conversation:
messages
=
[
{
"role"
:
"user"
,
"content"
:
'''Schedule a meeting on 2023-11-01 at 14:00 with Alice
and Bob. Then I want to schedule another meeting on 2023-11-02 at
15:00 with Charlie and Dave.'''
}
]
A continuación, ajusta la sección de código anterior incorporando un bucle for
.
Entrada:
# Send the conversation and function schema to the model:
response
=
client
.
chat
.
completions
.
create
(
model
=
"gpt-3.5-turbo-1106"
,
messages
=
messages
,
tools
=
functions
,
)
response
=
response
.
choices
[
0
]
.
message
# Check if the model wants to call our function:
if
response
.
tool_calls
:
for
tool_call
in
response
.
tool_calls
:
# Get the function name and arguments to call:
function_name
=
tool_call
.
function
.
name
function_args
=
json
.
loads
(
tool_call
.
function
.
arguments
)
(
"This is the function name: "
,
function_name
)
(
"These are the function arguments: "
,
function_args
)
function
=
OPENAI_FUNCTIONS
.
get
(
function_name
)
if
not
function
:
raise
Exception
(
f
"Function
{
function_name
}
not found."
)
# Call the function:
function_response
=
function
(
**
function_args
)
# Share the function's response with the model:
messages
.
append
(
{
"role"
:
"function"
,
"name"
:
function_name
,
"content"
:
json
.
dumps
(
function_response
),
}
)
# Let the model generate a user-friendly response:
second_response
=
client
.
chat
.
completions
.
create
(
model
=
"gpt-3.5-turbo-0613"
,
messages
=
messages
)
(
second_response
.
choices
[
0
]
.
message
.
content
)
Salida:
This
is
the
function
name
:
schedule_meeting
These
are
the
function
arguments
:
{
'date'
:
'2023-11-01'
,
'time'
:
'14:00'
,
'attendees'
:
[
'Alice'
,
'Bob'
]}
This
is
the
function
name
:
schedule_meeting
These
are
the
function
arguments
:
{
'date'
:
'2023-11-02'
,
'time'
:
'15:00'
,
'attendees'
:
[
'Charlie'
,
'Dave'
]}
Two
meetings
have
been
scheduled
:
1.
Meeting
with
Alice
and
Bob
on
2023
-
11
-
01
at
14
:
00.
2.
Meeting
with
Charlie
and
Dave
on
2023
-
11
-
02
at
15
:
00.
A partir de este ejemplo, queda claro cómo puedes gestionar eficazmente múltiples llamadas a funciones. Has visto cómo la función schedule_meeting
fue llamada dos veces seguidas para concertar diferentes reuniones. Esto demuestra con qué flexibilidad y sin esfuerzo puedes gestionar solicitudes variadas y complejas de utilizando herramientas basadas en IA.
Llamada a funciones en LangChain
Si prefieres evitar escribir el esquema JSON y simplemente quieres extraer datos estructurados de una respuesta LLM, entonces LangChain te permite utilizar la llamada a funciones con Pydantic.
Entrada:
from
langchain.output_parsers.openai_tools
import
PydanticToolsParser
from
langchain_core.utils.function_calling
import
convert_to_openai_tool
from
langchain_core.prompts
import
ChatPromptTemplate
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.pydantic_v1
import
BaseModel
,
Field
from
typing
import
Optional
class
Article
(
BaseModel
):
"""Identifying key points and contrarian views in an article."""
points
:
str
=
Field
(
...
,
description
=
"Key points from the article"
)
contrarian_points
:
Optional
[
str
]
=
Field
(
None
,
description
=
"Any contrarian points acknowledged in the article"
)
author
:
Optional
[
str
]
=
Field
(
None
,
description
=
"Author of the article"
)
_EXTRACTION_TEMPLATE
=
"""Extract and save the relevant entities mentioned
\
in the following passage together with their properties.
If a property is not present and is not required in the function parameters,
do not include it in the output."""
# Create a prompt telling the LLM to extract information:
prompt
=
ChatPromptTemplate
.
from_messages
(
{(
"system"
,
_EXTRACTION_TEMPLATE
),
(
"user"
,
"
{input}
"
)}
)
model
=
ChatOpenAI
()
pydantic_schemas
=
[
Article
]
# Convert Pydantic objects to the appropriate schema:
tools
=
[
convert_to_openai_tool
(
p
)
for
p
in
pydantic_schemas
]
# Give the model access to these tools:
model
=
model
.
bind_tools
(
tools
=
tools
)
# Create an end to end chain:
chain
=
prompt
|
model
|
PydanticToolsParser
(
tools
=
pydantic_schemas
)
result
=
chain
.
invoke
(
{
"input"
:
"""In the recent article titled 'AI adoption in industry,'
key points addressed include the growing interest ... However, the
author, Dr. Jane Smith, ..."""
}
)
(
result
)
Salida:
[
Article
(
points
=
'The growing interest in AI in various sectors, ...'
,
contrarian_points
=
'Without stringent regulations, ...'
,
author
=
'Dr. Jane Smith'
)]
Empezarás importando varios módulos, como PydanticToolsParser
y ChatPromptTemplate
, esenciales para analizar y crear plantillas para tus mensajes. A continuación, definirás un modelo Pydantic, Article
, para especificar la estructura de la información que quieres extraer de un texto dado. Con el uso de una plantilla personalizada y el modelo ChatOpenAI, darás instrucciones a la IA para que extraiga los puntos clave y las opiniones contrarias de un artículo. Por último, los datos extraídos se convierten ordenadamente en tu modelo Pydantic predefinido y se imprimen, permitiéndote ver la información estructurada extraída del texto.
Hay varios puntos clave, entre ellos
- Conversión del esquema Pydantic a las herramientas OpenAI
-
tools = [convert_to_openai_tool(p) for p in pydantic_schemas]
- Vincular las herramientas directamente al LLM
-
model = model.bind_tools(tools=tools)
- Crear una cadena LCEL que contenga un analizador de herramientas
-
chain = prompt | model | PydanticToolsParser(tools=pydantic_schemas)
Extraer datos con LangChain
La función create_extraction_chain_pydantic
proporciona una versión más concisa de la implementación anterior. Simplemente insertando un modelo Pydantic y un LLM que admita la llamada a funciones, puedes conseguir fácilmente la llamada a funciones en paralelo.
Entrada:
from
langchain.chains.openai_tools
import
create_extraction_chain_pydantic
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.pydantic_v1
import
BaseModel
,
Field
# Make sure to use a recent model that supports tools:
model
=
ChatOpenAI
(
model
=
"gpt-3.5-turbo-1106"
)
class
Person
(
BaseModel
):
"""A person's name and age."""
name
:
str
=
Field
(
...
,
description
=
"The person's name"
)
age
:
int
=
Field
(
...
,
description
=
"The person's age"
)
chain
=
create_extraction_chain_pydantic
(
Person
,
model
)
chain
.
invoke
({
'input'
:
'''Bob is 25 years old. He lives in New York.
He likes to play basketball. Sarah is 30 years old. She lives in San
Francisco. She likes to play tennis.'''
})
Salida:
[
Person
(
name
=
'Bob'
,
age
=
25
),
Person
(
name
=
'Sarah'
,
age
=
30
)]
El modelo pydántico Person
tiene dos propiedades, name
y age
; al llamar a la función create_extraction_chain_pydantic
con el texto de entrada, el LLM invoca dos veces la misma función y crea dos objetos People
.
Planificación de consultas
Puedes tener problemas cuando las consultas de los usuarios tienen múltiples intenciones con intrincadas dependencias. La planificación de consultas es una forma eficaz de analizar la consulta de un usuario en una serie de pasos que pueden ejecutarse como un gráfico de consulta con dependencias relevantes:
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain.output_parsers.pydantic
import
PydanticOutputParser
from
langchain_core.prompts.chat
import
(
ChatPromptTemplate
,
SystemMessagePromptTemplate
,
)
from
pydantic.v1
import
BaseModel
,
Field
from
typing
import
List
class
Query
(
BaseModel
):
id
:
int
question
:
str
dependencies
:
List
[
int
]
=
Field
(
default_factory
=
list
,
description
=
"""A list of sub-queries that must be completed before
this task can be completed.
Use a sub query when anything is unknown and we might need to ask
many queries to get an answer.
Dependencies must only be other queries."""
)
class
QueryPlan
(
BaseModel
):
query_graph
:
List
[
Query
]
Definir QueryPlan
y Query
te permite pedir primero a un LLM que analice la consulta de un usuario en varios pasos. Vamos a investigar cómo crear el plan de consulta.
Entrada:
# Set up a chat model:
model
=
ChatOpenAI
()
# Set up a parser:
parser
=
PydanticOutputParser
(
pydantic_object
=
QueryPlan
)
template
=
"""Generate a query plan. This will be used for task execution.
Answer the following query:
{query}
Return the following query graph format:
{format_instructions}
"""
system_message_prompt
=
SystemMessagePromptTemplate
.
from_template
(
template
)
chat_prompt
=
ChatPromptTemplate
.
from_messages
([
system_message_prompt
])
# Create the LCEL chain with the prompt, model, and parser:
chain
=
chat_prompt
|
model
|
parser
result
=
chain
.
invoke
({
"query"
:
'''I want to get the results from my database. Then I want to find
out what the average age of my top 10 customers is. Once I have the average
age, I want to send an email to John. Also I just generally want to send a
welcome introduction email to Sarah, regardless of the other tasks.'''
,
"format_instructions"
:
parser
.
get_format_instructions
()})
(
result
.
query_graph
)
Salida:
[
Query
(
id
=
1
,
question
=
'Get top 10 customers'
,
dependencies
=
[]),
Query
(
id
=
2
,
question
=
'Calculate average age of customers'
,
dependencies
=
[
1
]),
Query
(
id
=
3
,
question
=
'Send email to John'
,
dependencies
=
[
2
]),
Query
(
id
=
4
,
question
=
'Send welcome email to Sarah'
,
dependencies
=
[])]
Inicia una instancia ChatOpenAI
y crea un PydanticOutputParser
para la estructura QueryPlan
. A continuación, se llama a la respuesta LLM y se analiza, produciendo un query_graph
estructurado para tus tareas con sus dependencias únicas.
Crear plantillas de avisos de pocos disparos
Trabajar con las capacidades generativas de los LLM a menudo implica elegir entre el aprendizaje de disparo cero y el de pocos disparos (k-shot). Aunque el aprendizaje de disparo cero no requiere ejemplos explícitos en y se adapta a las tareas basándose únicamente en la indicación, su dependencia de la fase de preentrenamiento hace que no siempre dé resultados precisos.
Por otra parte, con el aprendizaje de pocos disparos, que consiste en proporcionar unos pocos ejemplos de la realización de la tarea deseada en la indicación, tienes la oportunidad de optimizar el comportamiento del modelo, lo que conduce a resultados más deseables.
Debido a la longitud simbólica del contexto LLM, a menudo te encontrarás compitiendo entre añadir un montón de ejemplos k-shot de alta calidad en tus prompts sin dejar de intentar generar un resultado LLM efectivo y determinista.
Nota
Aunque el límite de la ventana contextual de tokens en los LLM siga aumentando, proporcionar un número específico de ejemplos de k-shot te ayuda a minimizar los costes de la API.
Vamos a explorar dos métodos para añadir ejemplos de k-shot a tus avisos con plantillas de avisos de pocos disparos: utilizando ejemplos fijos y utilizando un selector de ejemplos.
Ejemplos de pocos disparos de longitud fija
En primer lugar, vamos a ver cómo crear una plantilla de avisos de pocos disparos utilizando un número fijo de ejemplos. La base de este método es crear un conjunto sólido de ejemplos de pocos disparos:
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.prompts
import
(
FewShotChatMessagePromptTemplate
,
ChatPromptTemplate
,
)
examples
=
[
{
"question"
:
"What is the capital of France?"
,
"answer"
:
"Paris"
,
},
{
"question"
:
"What is the capital of Spain?"
,
"answer"
:
"Madrid"
,
}
# ...more examples...
]
Cada ejemplo es un diccionario que contiene una clave question
y answer
que se utilizarán para crear pares de mensajes HumanMessage
y AIMessage
.
Formatear los ejemplos
A continuación, configurarás un ChatPromptTemplate
para formatear los ejemplos individuales, que luego se insertarán en un FewShotChatMessagePromptTemplate
.
Entrada:
example_prompt
=
ChatPromptTemplate
.
from_messages
(
[
(
"human"
,
"
{question}
"
),
(
"ai"
,
"
{answer}
"
),
]
)
few_shot_prompt
=
FewShotChatMessagePromptTemplate
(
example_prompt
=
example_prompt
,
examples
=
examples
,
)
(
few_shot_prompt
.
format
())
Salida:
Human
:
What
is
the
capital
of
France
?
AI
:
Paris
Human
:
What
is
the
capital
of
Spain
?
AI
:
Madrid
...
more
examples
...
Observa cómo example_prompt
creará los pares HumanMessage
y AIMessage
con las entradas de {question}
y {answer}
.
Después de ejecutar few_shot_prompt.format()
, los ejemplos de pocos disparos se imprimen como una cadena. Como te gustaría utilizarlos dentro de una petición LLM de ChatOpenAI()
, vamos a crear un nuevo ChatPromptTemplate
.
Entrada:
from
langchain_core.output_parsers
import
StrOutputParser
final_prompt
=
ChatPromptTemplate
.
from_messages
(
[(
"system"
,
'''You are responsible for answering
questions about countries. Only return the country
name.'''
),
few_shot_prompt
,(
"human"
,
"
{question}
"
),]
)
model
=
ChatOpenAI
()
# Creating the LCEL chain with the prompt, model, and a StrOutputParser():
chain
=
final_prompt
|
model
|
StrOutputParser
()
result
=
chain
.
invoke
({
"question"
:
"What is the capital of America?"
})
(
result
)
Salida:
Washington
,
D
.
C
.
Después de invocar la cadena LCEL en final_prompt
, tus ejemplos de pocos disparos se añaden después de SystemMessage
.
Observa que el LLM sólo devuelve 'Washington, D.C.'
. Esto se debe a que después de devolver la respuesta del LLM, ésta es analizada por StrOutputParser()
, un analizador de salida. Añadir StrOutputParser()
es una forma habitual de garantizar que las respuestas LLM en cadenas devuelvan valores de cadena. Explorarás esto más a fondo mientras aprendes cadenas secuenciales en LCEL.
Selección de Ejemplos de Pocos Tiros por Longitud
Antes de sumergirnos en el código, vamos a esbozar tu tarea. Imagina que estás construyendo una aplicación de narración de historias con GPT-4. Un usuario introduce una lista de nombres de personajes con historias generadas previamente. Sin embargo, la lista de personajes de cada usuario puede tener una longitud diferente. Incluir demasiados caracteres podría generar una historia que superara el límite de la ventana contextual del LLM. Ahí es donde puede utilizar LengthBasedExampleSelector
para adaptar el prompt en función de la longitud de la entrada del usuario:
from
langchain_core.prompts
import
FewShotPromptTemplate
,
PromptTemplate
from
langchain.prompts.example_selector
import
LengthBasedExampleSelector
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.messages
import
SystemMessage
import
tiktoken
examples
=
[
{
"input"
:
"Gollum"
,
"output"
:
"<Story involving Gollum>"
},
{
"input"
:
"Gandalf"
,
"output"
:
"<Story involving Gandalf>"
},
{
"input"
:
"Bilbo"
,
"output"
:
"<Story involving Bilbo>"
},
]
story_prompt
=
PromptTemplate
(
input_variables
=
[
"input"
,
"output"
],
template
=
"Character:
{input}
\n
Story:
{output}
"
,
)
def
num_tokens_from_string
(
string
:
str
)
->
int
:
"""Returns the number of tokens in a text string."""
encoding
=
tiktoken
.
get_encoding
(
"cl100k_base"
)
num_tokens
=
len
(
encoding
.
encode
(
string
))
return
num_tokens
example_selector
=
LengthBasedExampleSelector
(
examples
=
examples
,
example_prompt
=
story_prompt
,
max_length
=
1000
,
# 1000 tokens are to be included from examples
# get_text_length: Callable[[str], int] = lambda x: len(re.split("\n| ", x))
# You have modified the get_text_length function to work with the
# TikToken library based on token usage:
get_text_length
=
num_tokens_from_string
,
)
Primero, configura un PromptTemplate
que tome dos variables de entrada para cada ejemplo. A continuación, LengthBasedExampleSelector
ajusta el número de ejemplos según la longitud de los ejemplos de entrada, asegurándose de que tu LLM no genere una historia más allá de su ventana de contexto.
Además, has personalizado la función get_text_length
para que utilice la función num_tokens_from_string
que cuenta el número total de fichas utilizando tiktoken
. Esto significa que max_length=1000
representa el número de fichas en lugar de utilizar la siguiente función por defecto:
get_text_length: Callable[[str], int] = lambda x: len(re.split("\n| ", x))
Ahora, para unir todos estos elementos:
dynamic_prompt
=
FewShotPromptTemplate
(
example_selector
=
example_selector
,
example_prompt
=
story_prompt
,
prefix
=
'''Generate a story for
{character}
using the
current Character/Story pairs from all of the characters
as context.'''
,
suffix
=
"Character:
{character}
\n
Story:"
,
input_variables
=
[
"character"
],
)
# Provide a new character from Lord of the Rings:
formatted_prompt
=
dynamic_prompt
.
format
(
character
=
"Frodo"
)
# Creating the chat model:
chat
=
ChatOpenAI
()
response
=
chat
.
invoke
([
SystemMessage
(
content
=
formatted_prompt
)])
(
response
.
content
)
Salida:
Frodo was a young hobbit living a peaceful life in the Shire. However, his life...
Proporciona ejemplos y especifica el formato
Cuando se trabaja con ejemplos de pocas tomas, la longitud del contenido es importante para determinar cuántos ejemplos puede tener en cuenta el modelo de IA. Ajusta la longitud de tu contenido de entrada y proporciona ejemplos aptos para obtener resultados eficientes, a fin de evitar que el LLM genere contenido que pueda sobrepasar su límite de ventana de contexto.
Después de formatear la consulta, creas un modelo de chat con ChatOpenAI()
y cargas la consulta formateada en un SystemMessage
que crea una pequeña historia sobre Frodo, de El Señor de los Anillos.
En lugar de crear y formatear un ChatPromptTemplate
, a menudo es mucho más fácil invocar simplemente un SystemMesage
con un prompt formateado:
result
=
model
.
invoke
([
SystemMessage
(
content
=
formatted_prompt
)])
Limitaciones con Ejemplos de Pocos Tiros
El aprendizaje de pocos disparos tiene limitaciones. Aunque puede resultar beneficioso en determinados escenarios, puede que no siempre produzca los resultados de alta calidad esperados. Esto se debe principalmente a dos razones:
-
Los modelos preentrenados, como el GPT-4, a veces pueden sobreajustarse a los ejemplos de pocos disparos, lo que les hace dar prioridad a los ejemplos sobre la indicación real.
-
Los LLM tienen un límite de fichas. Como resultado, siempre habrá un equilibrio entre el número de ejemplos y la longitud de la respuesta. Proporcionar más ejemplos puede limitar la longitud de la respuesta y viceversa.
Estas limitaciones pueden abordarse de varias maneras. En primer lugar, si la incitación de pocos disparos no produce los resultados deseados, considera la posibilidad de utilizar frases con un marco diferente o de experimentar con el lenguaje de las propias incitaciones. Las variaciones en la formulación de la pregunta pueden dar lugar a respuestas diferentes, lo que pone de relieve la naturaleza de ensayo y error de la ingeniería de prompts.
En segundo lugar, piensa en incluir instrucciones explícitas al modelo para que ignore los ejemplos una vez que comprenda la tarea o para que utilice los ejemplos sólo como guía de formato. Esto podría influir en el modelo para que no se adapte en exceso a los ejemplos.
Si las tareas son complejas y el rendimiento del modelo con aprendizaje de pocos disparos no es satisfactorio, puede que tengas que plantearte afinar tu modelo. El ajuste fino proporciona al modelo una comprensión más matizada de una tarea específica, mejorando así el rendimiento de forma significativa.
Guardar y cargar avisos LLM
Para aprovechar eficazmente los modelos de IA generativa como el GPT-4, es beneficioso almacenar las instrucciones como archivos en lugar de código Python. Este enfoque mejora la compartibilidad, el almacenamiento y el control de versiones de tus instrucciones.
LangChain admite tanto guardar como cargar indicaciones desde JSON y YAML. Otra característica clave de LangChain es su compatibilidad con la especificación detallada en un archivo o distribuida en varios archivos. Esto significa que tienes la flexibilidad de almacenar distintos componentes, como plantillas, ejemplos y otros, en archivos distintos y referenciarlos cuando sea necesario.
Vamos a aprender a guardar y cargar avisos:
from
langchain_core.prompts
import
PromptTemplate
,
load_prompt
prompt
=
PromptTemplate
(
template
=
'''Translate this sentence from English to Spanish.
\n
Sentence:
{sentence}
\n
Translation:'''
,
input_variables
=
[
"sentence"
],
)
prompt
.
save
(
"translation_prompt.json"
)
# Loading the prompt template:
load_prompt
(
"translation_prompt.json"
)
# Returns PromptTemplate()
Tras importar PromptTemplate
y load_prompt
del módulo langchain.prompts
, defines un PromptTemplate
para las tareas de traducción del inglés al español y lo guardas como translation_prompt.json. Por último, carga la plantilla de aviso guardada utilizando la función load_prompt
, que devuelve una instancia de PromptTemplate
.
Advertencia
Ten en cuenta que el guardado de avisos de LangChain puede no funcionar con todos los tipos de plantillas de avisos. Para evitarlo, puede utilizar la biblioteca pickle o los archivos .txt para leer y escribir los avisos que LangChain no admita.
Has aprendido a crear plantillas de avisos de pocos ejemplos utilizando LangChain con dos técnicas: un número fijo de ejemplos y utilizando un selector de ejemplos.
El primero crea un conjunto de ejemplos de pocas tomas y utiliza un objeto ChatPromptTemplate
para darles formato de mensajes de chat. Esto constituye la base para crear un objeto FewShotChatMessagePromptTemplate
.
Este último enfoque, que utiliza un selector de ejemplos, es útil cuando la longitud de la entrada del usuario varía significativamente. En estos casos, se puede utilizar un LengthBasedExampleSelector
para ajustar el número de ejemplos en función de la longitud de la entrada del usuario. Esto garantiza que tu LLM no supere su límite de ventana de contexto.
Además, ya has visto lo fácil que es almacenar/cargar avisos como archivos, lo que permite compartir, almacenar y versionar mejor.
Conexión de datos
Aprovechar una aplicación LLM, junto con tus datos, descubre una plétora de oportunidades para aumentar la eficacia y perfeccionar al mismo tiempo tus procesos de toma de decisiones.
Los datos de tu organización pueden manifestarse de diversas formas:
- Datos no estructurados
-
Esto podría incluir Google Docs, hilos de plataformas de comunicación como Slack o Microsoft Teams, páginas web, documentación interna o repositorios de código en GitHub.
- Datos estructurados
-
Datos alojados ordenadamente en bases de datos SQL, NoSQL o Graph .
Para consultar tus datos no estructurados, es necesario un proceso de carga, transformación, incrustación y posterior almacenamiento dentro de una base de datos vectorial. Una base de datos vectorial es un tipo especializado de base de datos diseñado para almacenar y consultar eficazmente datos en forma de vectores, que representan datos complejos como texto o imágenes en un formato adecuado para el aprendizaje automático y la búsqueda de similitudes.
En cuanto a los datos estructurados, dado su estado ya indexado y almacenado, puedes utilizar un agente LangChain para realizar una consulta intermedia en tu base de datos. Esto permite extraer características específicas, que luego pueden utilizarse dentro de tus indicaciones LLM.
Hay varios paquetes de Python que pueden ayudarte con la ingestión de datos, como Unstructured, LlamaIndex y LangChain.
La Figura 4-2 ilustra un enfoque normalizado de la ingesta de datos. Comienza con las fuentes de datos, que luego se cargan en documentos. A continuación, estos documentos se dividen en trozos y se almacenan en una base de datos vectorial para su posterior recuperación.
En concreto, LangChain te equipa con componentes esenciales para cargar, modificar, almacenar y recuperar tus datos:
- Cargadores de documentos
-
Facilitan la carga de recursos informativos , o documentos, de diversas fuentes, como documentos de Word, archivos PDF, archivos de texto o incluso páginas web.
- Transformadores de documentos
-
Estas herramientas permiten segmentar los documentos, convertirlos en un formato de preguntas y respuestas, eliminar documentos superfluos, y mucho más.
- Modelos de incrustación de texto
-
Pueden transformar el texto no estructurado en una secuencia de números en coma flotante utilizados para la búsqueda de similitudes mediante almacenes vectoriales.
- Bases de datos vectoriales (almacenes vectoriales)
-
Estas bases de datos pueden guardar y ejecutar búsquedas sobre datos incrustados.
- Recuperadores
-
Estas herramientas ofrecen la posibilidad de consultar y recuperar datos.
Además, cabe mencionar que otros frameworks LLM como LlamaIndex funcionan perfectamente con LangChain. LlamaHub es otra biblioteca de código abierto dedicada a los cargadores de documentos y puede crear objetos Document
específicos de LangChain.
Cargadores de documentos
Imaginemos que te han encargado que construyas una canalización de recogida de datos LLM para NutriFusion Foods. La información que necesitas recopilar para el LLM está contenida en:
-
Un PDF de un libro titulado Principios de Marketing
-
Dos informes de marketing .docx en un bucket público de Google Cloud Storage
-
Tres archivos .csv con los datos de rendimiento de marketing para 2021, 2022 y 2023
Crea un nuevo Jupyter Notebook o un archivo Python en content/chapter_4 del repositorio compartido, y luego ejecuta pip install pdf2image docx2txt pypdf
, que instalará tres paquetes.
Todos los datos, salvo los archivos .docx, se encuentran en contenido/capítulo_4/datos. Puedes empezar importando todos tus distintos cargadores de datos y creando una lista all_documents
vacía para almacenar todos los objetos Document
de tus fuentes de datos.
Entrada:
from
langchain_community.document_loaders
import
Docx2txtLoader
from
langchain_community.document_loaders
import
PyPDFLoader
from
langchain_community.document_loaders.csv_loader
import
CSVLoader
import
glob
from
langchain.text_splitter
import
CharacterTextSplitter
# To store the documents across all data sources:
all_documents
=
[]
# Load the PDF:
loader
=
PyPDFLoader
(
"data/principles_of_marketing_book.pdf"
)
pages
=
loader
.
load_and_split
()
(
pages
[
0
])
# Add extra metadata to each page:
for
page
in
pages
:
page
.
metadata
[
"description"
]
=
"Principles of Marketing Book"
# Checking that the metadata has been added:
for
page
in
pages
[
0
:
2
]:
(
page
.
metadata
)
# Saving the marketing book pages:
all_documents
.
extend
(
pages
)
csv_files
=
glob
.
glob
(
"data/*.csv"
)
# Filter to only include the word Marketing in the file name:
csv_files
=
[
f
for
f
in
csv_files
if
"Marketing"
in
f
]
# For each .csv file:
for
csv_file
in
csv_files
:
loader
=
CSVLoader
(
file_path
=
csv_file
)
data
=
loader
.
load
()
# Saving the data to the all_documents list:
all_documents
.
extend
(
data
)
text_splitter
=
CharacterTextSplitter
.
from_tiktoken_encoder
(
chunk_size
=
200
,
chunk_overlap
=
0
)
urls
=
[
'''https://storage.googleapis.com/oreilly-content/NutriFusion%20Foods%2
0Marketing%20Plan%202022.docx'''
,
'''https://storage.googleapis.com/oreilly-content/NutriFusion%20Foods%2
0Marketing%20Plan%202023.docx'''
,
]
docs
=
[]
for
url
in
urls
:
loader
=
Docx2txtLoader
(
url
.
replace
(
'
\n
'
,
''
))
pages
=
loader
.
load
()
chunks
=
text_splitter
.
split_documents
(
pages
)
# Adding the metadata to each chunk:
for
chunk
in
chunks
:
chunk
.
metadata
[
"source"
]
=
"NutriFusion Foods Marketing Plan - 2022/2023"
docs
.
extend
(
chunks
)
# Saving the marketing book pages:
all_documents
.
extend
(
docs
)
Salida:
page_content='Principles of Mark eting' metadata={'source': 'data/principles_of_marketing_book.pdf', 'page': 0} {'source': 'data/principles_of_marketing_book.pdf', 'page': 0, 'description': 'Principles of Marketing Book'} {'source': 'data/principles_of_marketing_book.pdf', 'page': 1, 'description': 'Principles of Marketing Book'}
A continuación, con PyPDFLoader
, puedes importar un archivo .pdf y dividirlo en varias páginas con la función .load_and_split()
.
Además, es posible añadir metadatos adicionales a cada página porque los metadatos son un diccionario Python en cada objeto Document
. Además, fíjate en que en la salida anterior de los objetos Document
se adjuntan los metadatos source
.
Utilizando el paquete glob
, puedes encontrar fácilmente todos los archivos .csv y cargarlos individualmente en objetos LangChain Document
con un CSVLoader
.
Por último, los dos informes de marketing se cargan desde un bucket público de Google Cloud Storage y, a continuación, se dividen en trozos de 200 tokens utilizando un text_splitter
.
Esta sección te dotó de los conocimientos necesarios para crear una cadena completa de carga de documentos para el LLM de NutriFusion Foods. Empezando con la extracción de datos de un PDF, varios archivos CSV y dos archivos.docx, cada documento se enriqueció con metadatos relevantes para un mejor contexto.
Ahora tienes la posibilidad de integrar a la perfección datos de diversas fuentes documentales en una cadena de datos cohesionada.
Divisores de texto
Equilibrar la longitud de cada documento de también es un factor crucial. Si un documento es demasiado largo, puede superar la longitud contextual del LLM (el número máximo de tokens que un LLM puede procesar en una sola petición). Pero si los documentos están excesivamente fragmentados en trozos más pequeños, se corre el riesgo de perder información contextual significativa, lo que es igualmente indeseable.
Es posible que te encuentres con problemas específicos al dividir el texto, como por ejemplo
-
Los caracteres especiales como hashtags, símbolos @ o enlaces podrían no dividirse como se esperaba, afectando a la estructura general de los documentos divididos.
-
Si tu documento contiene formatos complicados, como tablas, listas o títulos de varios niveles, al divisor de texto puede resultarle difícil conservar el formato original.
Hay formas de superar estos retos que exploraremos más adelante.
Esta sección te presenta los divisores de texto en LangChain, herramientas utilizadas para dividir grandes trozos de texto y adaptarlos mejor a la ventana contextual de tu modelo.
Nota
No existe un tamaño de documento perfecto. Empieza utilizando una buena heurística y luego construye un conjunto de entrenamiento/prueba que puedas utilizar para la evaluación del LLM.
LangChain proporciona una gama de divisores de texto tan que puedes dividir fácilmente por cualquiera de los siguientes:
-
Recuento de fichas
-
Recursivamente por varios caracteres
-
Recuento de caracteres
-
Código
-
Cabeceras Markdown
Exploremos tres divisores populares: CharacterTextSplitter
,TokenTextSplitter
, y RecursiveCharacterTextSplitter
.
División del texto por longitud y tamaño de los tokens
En el Capítulo 3, aprendiste cómo para contar el número de tokens dentro de una llamada GPT-4 con tiktoken. También puedes utilizar tiktoken para dividir cadenas en trozos y documentos del tamaño adecuado.
Recuerda instalar tiktoken y langchain-text-splitters con pip install tiktoken langchain-text-splitters
.
Para dividir por el recuento de tokens en LangChain, puedes utilizar un CharacterTextSplitter
con una función .from_tiktoken_encoder()
.
Inicialmente crearás un CharacterTextSplitter
con un tamaño de trozo de 50 caracteres y sin solapamiento. Con el método split_text
, estás troceando el texto y luego imprimiendo el número total de trozos creados.
A continuación, haz lo mismo, pero esta vez con un solapamiento de trozos de 48 caracteres. Esto muestra cómo cambia el número de trozos en función de si permites el solapamiento, ilustrando el impacto de estos ajustes en cómo se divide tu texto:
from
langchain_text_splitters
import
CharacterTextSplitter
text
=
"""
Biology is a fascinating and diverse field of science that explores the
living world and its intricacies
\n\n
. It encompasses the study of life, its
origins, diversity, structure, function, and interactions at various levels
from molecules and cells to organisms and ecosystems
\n\n
. In this 1000-word
essay, we will delve into the core concepts of biology, its history, key
areas of study, and its significance in shaping our understanding of the
natural world.
\n\n
...(truncated to save space)...
"""
# No chunk overlap:
text_splitter
=
CharacterTextSplitter
.
from_tiktoken_encoder
(
chunk_size
=
50
,
chunk_overlap
=
0
,
separator
=
"
\n
"
,
)
texts
=
text_splitter
.
split_text
(
text
)
(
f
"Number of texts with no chunk overlap:
{
len
(
texts
)
}
"
)
# Including a chunk overlap:
text_splitter
=
CharacterTextSplitter
.
from_tiktoken_encoder
(
chunk_size
=
50
,
chunk_overlap
=
48
,
separator
=
"
\n
"
,
)
texts
=
text_splitter
.
split_text
(
text
)
(
f
"Number of texts with chunk overlap:
{
len
(
texts
)
}
"
)
Salida:
Number of texts with no chunk overlap: 3 Number of texts with chunk overlap: 6
En la sección anterior, utilizaste lo siguiente para cargar y dividir el .pdf en documentos LangChain:
pages = loader.load_and_split()
Puedes tener un control más granular del tamaño de cada documento creando un TextSplitter
y adjuntándolo a tus conductos de carga de Document
:
def load_and_split(text_splitter: TextSplitter | None = None) -> List[Document]
Simplemente crea un TokenTextSplitter
con un chunk_size=500
y un chunk_overlap
de 50:
from
langchain.text_splitter
import
TokenTextSplitter
from
langchain_community.document_loaders
import
PyPDFLoader
text_splitter
=
TokenTextSplitter
(
chunk_size
=
500
,
chunk_overlap
=
50
)
loader
=
PyPDFLoader
(
"data/principles_of_marketing_book.pdf"
)
pages
=
loader
.
load_and_split
(
text_splitter
=
text_splitter
)
(
len
(
pages
))
#737
El libro Principios de marketing contiene 497 páginas, pero después de utilizar un TokenTextSplitter
con un chunk_size
de 500 tokens, has creado 776 objetos LangChain Document
más pequeños.
División de texto con división recursiva de caracteres
Tratar con bloques de texto considerables puede presentar desafíos únicos en el análisis de textos. Una estrategia útil para estas situaciones consiste en utilizar la división recursiva de caracteres. Este método facilita la división de un gran cuerpo de texto en segmentos manejables, haciendo más accesible el análisis posterior.
Este enfoque resulta increíblemente eficaz cuando se maneja texto genérico. Aprovecha una lista de caracteres como parámetros y divide secuencialmente el texto basándose en estos caracteres. Las secciones resultantes se siguen dividiendo hasta que alcanzan un tamaño aceptable. Por defecto, la lista de caracteres comprende "\n\n"
, "\n"
, " "
, y ""
. Esta disposición pretende conservar la integridad de los párrafos, frases y palabras, preservando el contexto semántico.
El proceso se basa en la lista de caracteres proporcionada y dimensiona las secciones resultantes en función del recuento de caracteres.
Antes de sumergirte en el código, es esencial que entiendas lo que hace RecursiveCharacterTextSplitter
. Toma un texto y una lista de delimitadores (caracteres que definen los límites para dividir el texto). Empezando por el primer delimitador de la lista, el divisor intenta dividir el texto. Si los trozos resultantes siguen siendo demasiado grandes, pasa al siguiente delimitador, y así sucesivamente. Este proceso continúa recursivamente hasta que los trozos sean lo suficientemente pequeños o se agoten todos los delimitadores.
Utilizando la variable text
anterior, empieza importando RecursiveCharacterTextSplitter
. Esta instancia se encargará de dividir el texto. Al inicializar el divisor, se establecen los parámetros chunk_size
, chunk_overlap
y length_function
. Aquí, chunk_size
se establece en 100, y chunk_overlap
en 20.
El length_function
se define como len
para determinar el tamaño de los trozos. También es posible modificar el argumento length_function
para utilizar un recuento tokenizador en lugar de utilizar la función por defecto len
, que contará los caracteres:
from
langchain_text_splitters
import
RecursiveCharacterTextSplitter
text_splitter
=
RecursiveCharacterTextSplitter
(
chunk_size
=
100
,
chunk_overlap
=
20
,
length_function
=
len
,
)
Una vez que la instancia text_splitter
esté lista, puedes utilizar .split_text
para dividir la variable text
en trozos más pequeños. Estos trozos se almacenan en la lista texts
de Python:
# Split the text into chunks:
texts
=
text_splitter
.
split_text
(
text
)
Además de dividir simplemente el texto solapado en una lista de cadenas, puedes crear fácilmente objetos LangChain Document
con la función .create_documents
. Crear objetos Document
es útil porque te permite:
-
Almacenar documentos en una base de datos vectorial para la búsqueda semántica
-
Añade metadatos a determinados fragmentos de texto
-
Iterar sobre varios documentos para crear un resumen de nivel superior
Para añadir metadatos, proporciona una lista de diccionarios al argumento metadatas
:
# Create documents from the chunks:
metadatas
=
{
"title"
:
"Biology"
,
"author"
:
"John Doe"
}
docs
=
text_splitter
.
create_documents
(
texts
,
metadatas
=
[
metadatas
]
*
len
(
texts
))
Pero, ¿y si tus objetos Document
existentes son demasiado largos?
Puedes solucionarlo fácilmente utilizando la función .split_documents
con un comando TextSplitter
. Ésta recibirá una lista de objetos Document
y devolverá una nueva lista de objetos Document
basada en la configuración de los argumentos de tu clase TextSplitter
:
text_splitter
=
RecursiveCharacterTextSplitter
(
chunk_size
=
300
)
splitted_docs
=
text_splitter
.
split_documents
(
docs
)
Ahora has adquirido la capacidad de elaborar una canalización eficiente de carga de datos, aprovechando fuentes como PDF, CSV y enlaces de Google Cloud Storage. Además, has aprendido a enriquecer los documentos recopilados con metadatos relevantes, proporcionando un contexto significativo para el análisis y la ingeniería de prompts.
Con la introducción de los divisores de texto, ahora puedes gestionar estratégicamente el tamaño de los documentos, optimizando tanto la ventana contextual del LLM como la conservación de la información rica en contexto. Has navegado manejando textos más grandes empleando la recursividad y la división de caracteres. Estos nuevos conocimientos te permiten trabajar sin problemas con diversas fuentes de documentos e integrarlas en una sólida canalización de datos.
Descomposición de tareas
La descomposición de tareas es el proceso estratégico de diseccionar problemas complejos en un conjunto de subproblemas manejables. Este enfoque se alinea perfectamente con las tendencias naturales de los ingenieros de software, que a menudo conceptualizan las tareas como subcomponentes interrelacionados.
En ingeniería de software, utilizando la descomposición de tareas puedes reducir la carga cognitiva y aprovechar las ventajas del aislamiento de problemas y la adhesión al principio de responsabilidad única.
Curiosamente, los LLM pueden beneficiarse considerablemente de la aplicación de la descomposición de tareas en una serie de casos de uso. Este enfoque ayuda a maximizar la utilidad y eficacia de los LLM en escenarios de resolución de problemas, permitiéndoles manejar tareas intrincadas que serían difíciles de resolver como una sola entidad, como se ilustra en la Figura 4-3.
Aquí tienes varios ejemplos de LLM que utilizan la descomposición:
- Resolución de problemas complejos
-
En los casos en que un problema es polifacético y no puede resolverse con una sola indicación, la descomposición de tareas es extremadamente útil. Por ejemplo, resolver un caso jurídico complejo podría descomponerse en comprender el contexto del caso, identificar las leyes pertinentes, determinar los precedentes jurídicos y elaborar los argumentos. Cada subtarea puede ser resuelta independientemente por un LLM, proporcionando una solución completa cuando se combinan.
- Generación de contenidos
-
Para generar contenido de formato largo, como artículos o blogs, la tarea puede descomponerse en generar un esquema, escribir secciones individuales y, a continuación, compilar y refinar el borrador final. Cada paso puede ser gestionado individualmente por GPT-4 para obtener mejores resultados.
- Resumen del documento grande
-
Resumir documentos extensos, como documentos de investigación o informes de , puede hacerse más eficazmente descomponiendo la tarea en varias tareas más pequeñas, como comprender secciones individuales, resumirlas independientemente y luego compilar un resumen final.
- Agentes conversacionales interactivos
-
Para crear chatbots avanzados, la descomposición de tareas puede ayudar a gestionar distintos aspectos de la conversación, como comprender la entrada del usuario, mantener el contexto, generar respuestas relevantes y gestionar el flujo del diálogo.
- Sistemas de aprendizaje y tutoría
-
En los sistemas de tutoría digital, descomponer la tarea de enseñar un concepto en comprender los conocimientos actuales del alumno, identificar las lagunas, sugerir materiales de aprendizaje y evaluar el progreso puede hacer que el sistema sea más eficaz. Cada subtarea puede aprovechar las capacidades generativas de GPT-4 .
Divide el trabajo
La descomposición de tareas es una estrategia crucial para que aproveches todo el potencial de los LLM. Al diseccionar los problemas complejos en tareas más sencillas y manejables, puedes aprovechar la capacidad de resolución de problemas de estos modelos de forma más eficaz y eficiente.
En las secciones siguientes, aprenderás a crear e integrar varias cadenas LLM para orquestar flujos de trabajo más complicados.
Encadenamiento de avisos
A menudo te darás cuenta de que intentar hacer una sola tarea con un prompt es imposible. Puedes utilizar una mezcla de encadenamiento de avisos, que consiste en combinar múltiples entradas/salidas de avisos con avisos LLM específicamente adaptados para construir una idea.
Imaginemos un ejemplo con una empresa cinematográfica que quisiera automatizar parcialmente la creación de sus películas. Esto podría desglosarse en varios componentes clave, como por ejemplo
-
Creación de personajes
-
Generación de tramas
-
Escenas/construcción del mundo
La Figura 4-4 muestra cómo podría ser el flujo de trabajo de la consulta.
Cadena secuencial
Descompongamos la tarea en varias cadenas y recompongámoslas en una sola cadena:
character_generation_chain
-
Una cadena responsable de la creación de varios personajes dada una
'genre'
. plot_generation_chain
-
Una cadena que creará la trama dadas las claves
'characters'
y'genre'
. scene_generation_chain
-
Esta cadena generará las escenas que falten y que no se hayan generado inicialmente a partir de
plot_generation_chain
.
Empecemos creando tres variables ChatPromptTemplate
distintas, una para cada cadena:
from
langchain_core.prompts.chat
import
ChatPromptTemplate
character_generation_prompt
=
ChatPromptTemplate
.
from_template
(
"""I want you to brainstorm three to five characters for my short story. The
genre is {genre}. Each character must have a Name and a Biography.
You must provide a name and biography for each character, this is very
important!
---
Example response:
Name: CharWiz, Biography: A wizard who is a master of magic.
Name: CharWar, Biography: A warrior who is a master of the sword.
---
Characters: """
)
plot_generation_prompt
=
ChatPromptTemplate
.
from_template
(
"""Given the following characters and the genre, create an effective
plot for a short story:
Characters:
{characters}
---
Genre: {genre}
---
Plot: """
)
scene_generation_plot_prompt
=
ChatPromptTemplate
.
from_template
(
"""Act as an effective content creator.
Given multiple characters and a plot, you are responsible for
generating the various scenes for each act.
You must decompose the plot into multiple effective scenes:
---
Characters:
{characters}
---
Genre: {genre}
---
Plot: {plot}
---
Example response:
Scenes:
Scene 1: Some text here.
Scene 2: Some text here.
Scene 3: Some text here.
----
Scenes:
"""
)
Observa que a medida que las plantillas de avisos fluyen desde la generación de personajes a la de tramas y escenas, vas añadiendo más variables marcadoras de posición de los pasos anteriores.
La pregunta sigue siendo, ¿cómo puedes garantizar que estas cadenas adicionales estén disponibles para tus variables ChatPromptTemplate
posteriores?
itemgetter y extracción de claves de diccionario
Dentro de LCEL puedes utilizar la función itemgetter
del paquete operator
para extraer claves del paso anterior, siempre que hubiera un diccionario en el paso anterior:
from
operator
import
itemgetter
from
langchain_core.runnables
import
RunnablePassthrough
chain
=
RunnablePassthrough
()
|
{
"genre"
:
itemgetter
(
"genre"
),
}
chain
.
invoke
({
"genre"
:
"fantasy"
})
# {'genre': 'fantasy'}
La función RunnablePassThrough
simplemente pasa cualquier entrada directamente al siguiente paso. A continuación, se crea un nuevo diccionario utilizando la misma clave dentro de la función invoke
; esta clave se extrae utilizando itemgetter("genre")
.
Es esencial que utilices la función itemgetter
a lo largo de partes de tus cadenas LCEL para que cualquier variable marcadora de posición posterior de ChatPromptTemplate
tenga siempre valores válidos.
Además, puedes utilizar las funciones lambda
o RunnableLambda
dentro de una cadena LCEL para manipular valores anteriores del diccionario. Una lambda es una función anónima dentro de Python:
from
langchain_core.runnables
import
RunnableLambda
chain
=
RunnablePassthrough
()
|
{
"genre"
:
itemgetter
(
"genre"
),
"upper_case_genre"
:
lambda
x
:
x
[
"genre"
]
.
upper
(),
"lower_case_genre"
:
RunnableLambda
(
lambda
x
:
x
[
"genre"
]
.
lower
()),
}
chain
.
invoke
({
"genre"
:
"fantasy"
})
# {'genre': 'fantasy', 'upper_case_genre': 'FANTASY',
# 'lower_case_genre': 'fantasy'}
Ahora que ya sabes cómo utilizar las funciones RunnablePassThrough
, itemgetter
, y lambda
, vamos a introducir una última pieza de sintaxis: RunnableParallel
:
from
langchain_core.runnables
import
RunnableParallel
master_chain
=
RunnablePassthrough
()
|
{
"genre"
:
itemgetter
(
"genre"
),
"upper_case_genre"
:
lambda
x
:
x
[
"genre"
]
.
upper
(),
"lower_case_genre"
:
RunnableLambda
(
lambda
x
:
x
[
"genre"
]
.
lower
()),
}
master_chain_two
=
RunnablePassthrough
()
|
RunnableParallel
(
genre
=
itemgetter
(
"genre"
),
upper_case_genre
=
lambda
x
:
x
[
"genre"
]
.
upper
(),
lower_case_genre
=
RunnableLambda
(
lambda
x
:
x
[
"genre"
]
.
lower
()),
)
story_result
=
master_chain
.
invoke
({
"genre"
:
"Fantasy"
})
(
story_result
)
story_result
=
master_chain_two
.
invoke
({
"genre"
:
"Fantasy"
})
(
story_result
)
# master chain: {'genre': 'Fantasy', 'upper_case_genre': 'FANTASY',
# 'lower_case_genre': 'fantasy'}
# master chain two: {'genre': 'Fantasy', 'upper_case_genre': 'FANTASY',
# 'lower_case_genre': 'fantasy'}
En primer lugar, importa RunnableParallel
y crea dos cadenas LCEL llamadas master_chain
y master_chain_two
. A continuación, se invocan con exactamente los mismos argumentos; el RunnablePassthrough
pasa entonces el diccionario a la segunda parte de la cadena.
La segunda parte de master_chain
y master_chain_two
devolverá exactamente el mismo resultado.
Así que, en lugar de utilizar directamente un diccionario, puedes optar por utilizar en su lugar una función RunnableParallel
. Estas dos salidas en cadena son intercambiables, así que elige la sintaxis que te resulte más cómoda.
Vamos a crear tres cadenas LCEL utilizando las plantillas de avisos:
from
langchain_openai.chat_models
import
ChatOpenAI
from
langchain_core.output_parsers
import
StrOutputParser
# Create the chat model:
model
=
ChatOpenAI
()
# Create the subchains:
character_generation_chain
=
(
character_generation_prompt
|
model
|
StrOutputParser
()
)
plot_generation_chain
=
(
plot_generation_prompt
|
model
|
StrOutputParser
()
)
scene_generation_plot_chain
=
(
scene_generation_plot_prompt
|
model
|
StrOutputParser
()
)
Después de crear todas las cadenas, puedes unirlas a una cadena LCEL maestra.
Entrada:
from
langchain_core.runnables
import
RunnableParallel
from
operator
import
itemgetter
from
langchain_core.runnables
import
RunnablePassthrough
master_chain
=
(
{
"characters"
:
character_generation_chain
,
"genre"
:
RunnablePassthrough
()}
|
RunnableParallel
(
characters
=
itemgetter
(
"characters"
),
genre
=
itemgetter
(
"genre"
),
plot
=
plot_generation_chain
,
)
|
RunnableParallel
(
characters
=
itemgetter
(
"characters"
),
genre
=
itemgetter
(
"genre"
),
plot
=
itemgetter
(
"plot"
),
scenes
=
scene_generation_plot_chain
,
)
)
story_result
=
master_chain
.
invoke
({
"genre"
:
"Fantasy"
})
La salida se trunca cuando ves ...
para ahorrar espacio. Sin embargo, en total se generaron cinco personajes y nueve escenas.
Salida:
{
'characters'
:
'''Name: Lyra, Biography: Lyra is a young elf who possesses
..
\n\n
Name: Orion, Biography: Orion is a ..'''
,
'genre'
:
{
'genre'
:
'Fantasy'
}
'plot'
:
'''In the enchanted forests of a mystical realm, a great
darkness looms, threatening to engulf the land and its inhabitants. Lyra,
the young elf with a deep connection to nature, ...'''
,
'scenes'
:
'''Scene
1: Lyra senses the impending danger in the forest ...
\n\n
Scene 2: Orion, on
his mission to investigate the disturbances in the forest...
\n\n
Scene 9:
After the battle, Lyra, Orion, Seraphina, Finnegan...'''
}
Las escenas se dividen en elementos separados dentro de una lista Python. A continuación, se crean dos nuevos avisos para generar tanto un guión de caracteres como un aviso de resumen:
# Extracting the scenes using .split('\n') and removing empty strings:
scenes
=
[
scene
for
scene
in
story_result
[
"scenes"
]
.
split
(
"
\n
"
)
if
scene
]
generated_scenes
=
[]
previous_scene_summary
=
""
character_script_prompt
=
ChatPromptTemplate
.
from_template
(
template
=
"""Given the following characters:
{characters}
and the genre:
{genre}
, create an effective character script for a scene.
You must follow the following principles:
- Use the Previous Scene Summary:
{previous_scene_summary}
to avoid
repeating yourself.
- Use the Plot:
{plot}
to create an effective scene character script.
- Currently you are generating the character dialogue script for the
following scene:
{scene}
---
Here is an example response:
SCENE 1: ANNA'S APARTMENT
(ANNA is sorting through old books when there is a knock at the door.
She opens it to reveal JOHN.)
ANNA: Can I help you, sir?
JOHN: Perhaps, I think it's me who can help you. I heard you're
researching time travel.
(Anna looks intrigued but also cautious.)
ANNA: That's right, but how do you know?
JOHN: You could say... I'm a primary source.
---
SCENE NUMBER:
{index}
"""
,
)
summarize_prompt
=
ChatPromptTemplate
.
from_template
(
template
=
"""Given a character script, create a summary of the scene.
Character script:
{character_script}
"""
,
)
Técnicamente, podrías generar todas las escenas de forma asíncrona. Sin embargo, es beneficioso saber qué ha hecho cada personaje en la escena anterior para evitar repetir puntos.
Por tanto, puedes crear dos cadenas LCEL, una para generar los guiones de los personajes por escena y otra para los resúmenes de escenas anteriores:
# Loading a chat model:
model
=
ChatOpenAI
(
model
=
'gpt-3.5-turbo-16k'
)
# Create the LCEL chains:
character_script_generation_chain
=
(
{
"characters"
:
RunnablePassthrough
(),
"genre"
:
RunnablePassthrough
(),
"previous_scene_summary"
:
RunnablePassthrough
(),
"plot"
:
RunnablePassthrough
(),
"scene"
:
RunnablePassthrough
(),
"index"
:
RunnablePassthrough
(),
}
|
character_script_prompt
|
model
|
StrOutputParser
()
)
summarize_chain
=
summarize_prompt
|
model
|
StrOutputParser
()
# You might want to use tqdm here to track the progress,
# or use all of the scenes:
for
index
,
scene
in
enumerate
(
scenes
[
0
:
3
]):
# # Create a scene generation:
scene_result
=
character_script_generation_chain
.
invoke
(
{
"characters"
:
story_result
[
"characters"
],
"genre"
:
"fantasy"
,
"previous_scene_summary"
:
previous_scene_summary
,
"index"
:
index
,
}
)
# Store the generated scenes:
generated_scenes
.
append
(
{
"character_script"
:
scene_result
,
"scene"
:
scenes
[
index
]}
)
# If this is the first scene then we don't have a
# previous scene summary:
if
index
==
0
:
previous_scene_summary
=
scene_result
else
:
# If this is the second scene or greater then
# we can use and generate a summary:
summary_result
=
summarize_chain
.
invoke
(
{
"character_script"
:
scene_result
}
)
previous_scene_summary
=
summary_result
En primer lugar, establecerás un character_script_generation_chain
en tu script, utilizando varios ejecutables como RunnablePassthrough
para un flujo de datos fluido. Crucialmente, esta cadena integra model = ChatOpenAI(model='gpt-3.5-turbo-16k')
, un potente modelo con una generosa ventana de contexto de 16k, ideal para tareas extensas de generación de contenidos. Cuando se invoca, esta cadena genera hábilmente guiones de personajes, basándose en entradas como perfiles de personajes, género y detalles de la escena.
Enriqueces dinámicamente cada escena añadiendo el resumen de la escena anterior, creando una memoria intermedia sencilla pero eficaz. Esta técnica garantiza la continuidad y el contexto de la narración, mejorando la capacidad del LLM para generar guiones de personajes coherentes.
Además, verás cómo StrOutputParser
convierte elegantemente las salidas del modelo en cadenas estructuradas, haciendo que el contenido generado sea fácilmente utilizable.
Divide el trabajo
Recuerda que diseñar tus tareas en una cadena secuencial se beneficia enormemente del principio Divide el trabajo. Dividir las tareas en cadenas más pequeñas y manejables puede aumentar la calidad general de tu producción. Cada cadena de la cadena secuencial contribuye con su esfuerzo individual a la consecución del objetivo general de la tarea.
El uso de cadenas te da la posibilidad de utilizar diferentes modelos. Por ejemplo, utilizar un modelo inteligente para la ideación y un modelo barato para la generación suele dar resultados óptimos. Esto también significa que puedes tener modelos afinados en cada paso.
Estructuración de las cadenas LCEL
En LCEL debes asegurarte de que la primera parte de tu cadena LCEL es un tipo ejecutable. El código siguiente arrojará un error:
from
langchain_core.prompts.chat
import
ChatPromptTemplate
from
operator
import
itemgetter
from
langchain_core.runnables
import
RunnablePassthrough
,
RunnableLambda
bad_first_input
=
{
"film_required_age"
:
18
,
}
prompt
=
ChatPromptTemplate
.
from_template
(
"Generate a film title, the age is
{film_required_age}
"
)
# This will error:
bad_chain
=
bad_first_input
|
prompt
Un diccionario Python con el valor 18 no creará una cadena LCEL ejecutable. Sin embargo, todas las implementaciones siguientes funcionarán:
# All of these chains enforce the runnable interface:
first_good_input
=
{
"film_required_age"
:
itemgetter
(
"film_required_age"
)}
# Creating a dictionary within a RunnableLambda:
second_good_input
=
RunnableLambda
(
lambda
x
:
{
"film_required_age"
:
x
[
"film_required_age"
]
}
)
third_good_input
=
RunnablePassthrough
()
fourth_good_input
=
{
"film_required_age"
:
RunnablePassthrough
()}
# You can also create a chain starting with RunnableParallel(...)
first_good_chain
=
first_good_input
|
prompt
second_good_chain
=
second_good_input
|
prompt
third_good_chain
=
third_good_input
|
prompt
fourth_good_chain
=
fourth_good_input
|
prompt
first_good_chain
.
invoke
({
"film_required_age"
:
18
})
# ...
Las cadenas secuenciales son estupendas para construir incrementalmente el conocimiento generado que utilizarán las cadenas futuras, pero a menudo producen tiempos de respuesta más lentos debido a su naturaleza secuencial. Como tales, las cadenas de datos SequentialChain
son más adecuadas para tareas del lado del servidor, donde las respuestas inmediatas no son una prioridad y los usuarios no están esperando una respuesta en tiempo real.
Cadenas de documentos
Imaginemos que antes de aceptar tu historia generada en , la editorial local te ha pedido que proporciones un resumen basado en todos los guiones de los personajes. Este es un buen caso de uso para las cadenas de documentos, porque necesitas proporcionar un LLM con una gran cantidad de texto que no cabría en una sola solicitud LLM debido a las restricciones de longitud del contexto.
Antes de profundizar en el código, vamos a hacernos una idea general. El script que vas a ver realiza una tarea de resumen del texto sobre una colección de escenas.
Recuerda instalar Pandas con pip install pandas
.
Ahora, empecemos con el primer conjunto de código:
from
langchain_text_splitters
import
CharacterTextSplitter
from
langchain.chains.summarize
import
load_summarize_chain
import
pandas
as
pd
Estas líneas están importando todas las herramientas necesarias que necesitas. CharacterTextSplitter
y load_summarize_chain
son del paquete LangChain y te ayudarán con el procesamiento de texto, mientras que Pandas (importado como pd
) te ayudará a manipular tus datos.
A continuación, te ocuparás de tus datos:
df
=
pd
.
DataFrame
(
generated_scenes
)
Aquí, creas un Pandas DataFrame a partir de la variable generated_scenes
, convirtiendo efectivamente tus escenas sin procesar en un formato de datos tabulares que Pandas puede manipular fácilmente.
Luego tienes que consolidar tu texto:
all_character_script_text
=
"
\n
"
.
join
(
df
.
character_script
.
tolist
())
En esta línea, estás transformando la columna character_script
de tu DataFrame en una única cadena de texto. Cada entrada de la columna se convierte en un elemento de la lista, y todos los elementos se unen con nuevas líneas intermedias, dando como resultado una única cadena que contiene todos los guiones de caracteres.
Una vez tengas listo el texto, prepáralo para el proceso de resumen:
text_splitter
=
CharacterTextSplitter
.
from_tiktoken_encoder
(
chunk_size
=
1500
,
chunk_overlap
=
200
)
docs
=
text_splitter
.
create_documents
([
all_character_script_text
])
Aquí, creas una instancia de CharacterTextSplitter
utilizando su método de clase from_tiktoken_encoder
, con parámetros específicos para el tamaño del trozo y el solapamiento. A continuación, utilizas este divisor de texto para dividir el texto de tu guión consolidado en trozos adecuados para ser procesados por tu herramienta de resumen.
A continuación, configura tu herramienta de resumen:
chain
=
load_summarize_chain
(
llm
=
model
,
chain_type
=
"map_reduce"
)
Esta línea se refiere a la configuración de tu proceso de resumen. Estás llamando a una función que carga una cadena de resumen con un modelo de chat en un enfoque de estilo map-reduce
.
Luego ejecuta la integración:
summary
=
chain
.
invoke
(
docs
)
Aquí es donde realmente realizas el resumen del texto. El método invoke
ejecuta el resumen en los trozos de texto que has preparado antes y almacena el resumen en una variable.
Por último, imprime el resultado:
(
summary
[
'output_text'
])
Esta es la culminación de todo tu duro trabajo. El texto resumen resultante se imprime en la consola para que lo veas.
Este script toma una colección de escenas, consolida el texto, lo trocea, lo resume y luego imprime el resumen:
from
langchain.text_splitter
import
CharacterTextSplitter
from
langchain.chains.summarize
import
load_summarize_chain
import
pandas
as
pd
df
=
pd
.
DataFrame
(
generated_scenes
)
all_character_script_text
=
"
\n
"
.
join
(
df
.
character_script
.
tolist
())
text_splitter
=
CharacterTextSplitter
.
from_tiktoken_encoder
(
chunk_size
=
1500
,
chunk_overlap
=
200
)
docs
=
text_splitter
.
create_documents
([
all_character_script_text
])
chain
=
load_summarize_chain
(
llm
=
model
,
chain_type
=
"map_reduce"
)
summary
=
chain
.
invoke
(
docs
)
(
summary
[
'output_text'
])
Salida:
Aurora and Magnus agree to retrieve a hidden artifact, and they enter an ancient library to find a book that will guide them to the relic...'
Merece la pena señalar que incluso aunque hayas utilizado una cadena map_reduce
, hay cuatro cadenas principales para trabajar con objetos Document
dentro de LangChain.
Cosas
La cadena de inserción de documentos , también denominada cadena de relleno (derivada del concepto de relleno o llenado), es el enfoque más sencillo entre las diversas estrategias de encadenamiento de documentos. La Figura 4-5 ilustra el proceso de integración de varios documentos en una única petición LLM.
Perfecciona
La cadena refinar documentos(Figura 4-6) crea una respuesta LLM mediante un proceso cíclico que actualiza iterativamente su salida. Durante cada bucle, combina la salida actual (derivada del LLM) con el documento actual. Se realiza otra petición LLM para actualizar la salida actual. Este proceso continúa hasta que se han procesado todos los documentos.
Map Reducir
La cadena map reduce documents de la Figura 4-7 comienza con una cadena LLM a cada documento por separado (un proceso conocido como paso Map), interpretando la salida resultante como un nuevo documento generado.
Posteriormente, todos estos documentos recién creados se introducen en una cadena distinta de combinación de documentos para formular una salida singular (proceso denominado paso Reducir). Si es necesario, para que los nuevos documentos encajen perfectamente en la longitud del contexto, se utiliza un proceso opcional de compresión en los documentos mapeados. Si es necesario, esta compresión se produce recursivamente.
Mapa Re-rank
También existe el re-ranking de mapas, que opera ejecutando una tarea inicial en cada documento. Éste no sólo se esfuerza por cumplir una tarea determinada, sino que también asigna una puntuación de confianza que refleja la certeza de su respuesta. A continuación, se selecciona y devuelve la respuesta con la puntuación de confianza más alta.
La Tabla 4-1 muestra las ventajas e inconvenientes de elegir una estrategia específica de cadena documental.
Acércate a | Ventajas | Desventajas |
---|---|---|
Cosas Documentos Cadena |
Fácil de poner en práctica. Ideal para escenarios con documentos pequeños y pocas entradas. |
Puede no ser adecuado para manejar documentos grandes o entradas múltiples, debido a la limitación del tamaño de la solicitud. |
Refinar la cadena de documentos |
Permite el refinamiento iterativo de la respuesta. Más control sobre cada paso de la generación de la respuesta. Bueno para tareas de extracción progresiva. |
Puede no ser óptimo para aplicaciones en tiempo real debido al proceso de bucle. |
Cadena de documentos Map Reduce |
Permite el procesamiento independiente de cada documento. Puede manejar grandes conjuntos de datos reduciéndolos a trozos manejables. |
Requiere una gestión cuidadosa del proceso. El paso opcional de compresión puede añadir complejidad y perder el orden de los documentos. |
Mapa Re-ranking Cadena de Documentos |
Proporciona una puntuación de confianza para cada respuesta, lo que permite seleccionar mejor las respuestas. |
El algoritmo de clasificación puede ser complejo de aplicar y gestionar. Puede no proporcionar la mejor respuesta si el mecanismo de puntuación no es fiable o no está bien afinado. |
Puedes leer más sobre cómo implementar diferentes cadenas de documentos en la completa API de LangChain y aquí.
Además, es posible cambiar simplemente el tipo de cadena dentro de la función load_summarize_chain
:
chain
=
load_summarize_chain
(
llm
=
model
,
chain_type
=
'refine'
)
Existen enfoques más novedosos y personalizables para crear cadenas de resumen utilizando LCEL, pero para la mayoría de tus necesidades load_summarize_chain
proporciona resultados suficientes.
Resumen
En este capítulo, has revisado exhaustivamente el marco LangChain y sus componentes esenciales. Has aprendido la importancia de los cargadores de documentos para recopilar datos y el papel de los divisores de texto en el manejo de grandes bloques de texto.
Además, se te presentaron los conceptos de descomposición de tareas y encadenamiento de tareas. Al descomponer los problemas complejos en tareas más pequeñas, viste el poder del aislamiento de problemas. Además, ahora comprendes cómo el encadenamiento de instrucciones puede combinar múltiples entradas/salidas para una generación de ideas más rica.
En el próximo capítulo, aprenderás sobre las bases de datos vectoriales, incluido cómo integrarlas con los documentos de LangChain, y esta capacidad desempeñará un papel fundamental en la mejora de la precisión de la extracción de conocimiento de tus datos.
Get Ingeniería Prompt para la IA Generativa now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.