En este post te voy a explicar paso a paso cómo puedes cerar un chatbot de Q&A basado en un LLM que responda sobre una serie de documentos que ya tengas. Para ello, utilizaremos la librería langchain, el modelo Falcon7b y la base de datos vectorial Chroma.
En este post no explicaremos qué es ni cómo funciona una base de datos vectorial. Si quieres aprender más sobre ello te recomiendo que leas este post.
El post tendrá las siguientes partes:
- Extracción de los documentos que utilizará el modelo para dar respuesta.
- Inclusión de los documentos en la base de datos vectorial.
- Creación chatbot.
Así pues, empecemos con nuestro tutorial sobre cómo crear un chatbot que responda con tus propios documentos. ¡Vamos con ello!
Extracción de los documentos que utilizará el modelo para dar respuesta
El primer punto de todos es obtener los documentos que el modelo deberá utilizar para poder dar respuesta. En el caso de una empresa serán los documentos de la empresa. En mi caso, como no tengo dichos documentos, voy a extraer las páginas de Wikipedia de las distintas películas de Harry Potter. Para ello, utilizaré la librería wikipedia y extraeré los enlaces de esta página de la Wikipedia usando Pandas, para lo cual tendré que instalar también la librería lxml.
pip install pandas lxml wikipedia
import pandas as pd
import wikipedia
url = 'https://en.wikipedia.org/wiki/Harry_Potter_(film_series)'
film_data = pd.read_html(url)[1]
film_names = film_data['Film'].tolist()
film_contents = [wikipedia.page(title = film).content for film in film_names]
film_data['page_content'] = film_contents
film_data.head()
| Year | Film | Director | Screenwriter | Producer(s)[n 2] | Composer | Novel by J. K. Rowling | page_content |
|---|---|---|---|---|---|---|---|
| 0 | 2001 | Harry Potter and the Philosopher’s Stone | Chris Columbus | Steve Kloves | David Heyman | John Williams | Harry Potter and the Philosopher’s Stone (1997) |
| 1 | 2002 | Harry Potter and the Chamber of Secrets | Chris Columbus | Steve Kloves | David Heyman | John Williams | Harry Potter and the Chamber of Secrets (1998) |
| 2 | 2004 | Harry Potter and the Prisoner of Azkaban | Alfonso Cuarón | Steve Kloves | David Heyman, Chris Columbus and Mark Radcliffe | John Williams | Harry Potter and the Prisoner of Azkaban (1999) |
| 3 | 2005 | Harry Potter and the Goblet of Fire | Mike Newell | Steve Kloves | David Heyman | Patrick Doyle | Harry Potter and the Goblet of Fire (2000) |
| 4 | 2007 | Harry Potter and the Order of the Phoenix | David Yates | Michael Goldenberg | David Heyman and David Barron | Nicholas Hooper | Harry Potter and the Order of the Phoenix (2003) |
Como puedes ver, ya tengo los datos extraidos. Además, cuento con información adicional de las películas, lo cual me servirá por si quiero filtrar y que el modelo únicamente obtenga el contexto de cierto tipo de películas. Hecho esto, vayamos con el siguiente paso para crear nuestro chatbot de Q&A basado en un modelo GPT.
Inclusión de los documentos en la base de datos vectorial
Tal como he comentado anteriormente, en este post no nos vamos a detener en exceso en qué son y cómo funcionan las bases de datos vectoriales, puesto que ya escribí un post específico sobre estas (enlace).
En este caso, para que nuestro modelo de GPT pueda trabajar mejor con los datos de la base de datos vectorial no vamos a introducir todo el documento de golpe. En su lugar, vamos a «partir» cada texto en trozos de un tamaño predeterminado. Estos «trozos» de texto no serán excluyentes, sino que tendrán un pequeño solape entre ellos. De esta forma, el modelo tendrá un acceso más sencillo a la información que necesita.
Aunque pueda sonar complejo, no te preocupes, no lo es tanto. Por suerte, la librería langchain incluye todas las funcionalidades para poder realizar este proceso. Así pues, el primer paso será instalar la librería langchain con el siguiente comando:
pip install langchain
Ahora que tienes la librería instalada, vamos a usar el módulo RecursiveCharacterTextSplitter la cual permite hacer justamente lo que he comentado. Así pues, lo primero de todo, vamos a definir nuestro splitter:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1500,
chunk_overlap = 20,
length_function = len
)
Ahora que tenemos nuestro splitter, vamos a definir la metadata que vamos a inlcuir a cada documento. En nuestro caso, la metadata que incluiremos será:
- Año en el que se estrenó la película.
- Nombre de la película.
- Director.
Podríamos añadir más metadatos, pero a modo de ejemplo esto nos servirá. Recurda, los metadatos nos sirven por si queremos que nuestro modelo solo utilice cierto tipo de documentos para dar una respuesta y no todos.
Así pues, vamos a crear estos metadatos:
metadata = film_data[['Year', 'Film', 'Director']].to_dict(orient='records')
Por último, vamos a crear los documentos utilizando los textos y los metadatos que disponemos:
documents = text_splitter.create_documents(
texts= film_data['page_content'].tolist(),
metadatas= metadata
)
Con esto ya tenemos nuestros documentos creados. Ahora solo quedaría incluirlos en nuestra base de datos vectorial. Aunque esto podemos hacerlo de forma manual, tal como vimos en el post sobre bases de datos vectoriales, langchain también tiene incorporado un módulo para que sea más fácil trabajar con bases de datos vectoriales.
En cualquier caso, vamos a tener que instalar la librería chromadb, lo cual podemos hacer con el siguiente comando:
pip install chromadb
Además, en este paso también tenemos que definir la función de embeddings que vayamos a usar. En mi caso, usaré el modelo all-MiniLM-L6-v2, para lo cual necesitaré instalar la libería sentence_transformers:
pip install sentence_transformers
Así pues, suponiendo que ya tenemos nuestra base de datos vectorial levantada y corriendo, vamos a conectarnos a ella y a subir los documentos a una nueva colección:
import chromadb
from langchain.embeddings import SentenceTransformerEmbeddings
from langchain.vectorstores import Chroma
embedding = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
settings = chromadb.config.Settings(
chroma_api_impl="rest",
chroma_server_host= 'localhost',
chroma_server_http_port = 8000
)
client = chromadb.Client(settings)
chroma = Chroma(
collection_name = 'qa_example',
client = client,
embedding_function = embedding
)
chroma.add_documents(documents=documents)
Perfecto, ya tenemos nuestros documentos subidos a la base de datos vectorial. De hecho, para comprobar que se han subido correctamente, vamos a realizar una consulta a la base de datos, a ver qué devuelve:
from chromadb.utils import embedding_functions
collection = client.get_collection(
name = 'qa_example',
embedding_function= embedding_functions.DefaultEmbeddingFunction()
)
collection.query(
query_texts="the last harry potter film",
n_results=5,
include = ['documents']
)['documents']
[['The film was followed by the concluding entry, Harry Potter and the Deathly Hallows – Part 2 in 2011.',
"Harry Potter and the Deathly Hallows – Part 1 is a 2010 fantasy film directed by David Yates from a screenplay ...",
'Orlando Sentinel, Roger Moore proclaimed Part I as "Alternately funny and touching, it\'s the best film in the series, ...',
"Harry Potter and the Deathly Hallows – Part 2 is a 2011 fantasy film directed by David Yates from a screenplay by Steve Kloves..",
'== Production ==\n\n\n=== Filming ===\nPart 2 was filmed back-to-back with Harry Potter and the Deathly Hallows – Part 1 ..']]
collection.update()
Como podemos ver, la base de datos funciona perfectamente y devuelve documentos con sentido. Así pues, hecho esto vayamos con el último paso para crear nuestro chatbot de QA personalizado basado en un modelo Llama. ¡Vamos con ello!
Creación del sistema de Q&A
La idea para crear nuestro sistema de Q&A es la siguiente:
- Definir el modelo que vayamos a usar para que responda a las preguntas en base al prompt que pasemos.
- Una función que dado un texto, extraiga los
ndocumentos más parecidos al prompt y le pase ese contexto al modelo para que genere la respuesta. - (Opcional) Creación de un frontend que nos permita interactuar con este sistema de una forma más sencilla y fácil. Incluso, que permita ir más allá, añadiendo filtros sobre los documentos que vayamos a utilizar, por ejemplo.
En este tutorial abordaré únicamente los dos primeros puntos, ya que el tercero no es más que crear un frontend. Así pues, vamos con nuestro sistema de GPT, definiendo el modelo que vamos a utilizar.
Definir el modelo que vamos a utilizar
Aunque pueda parecer una cosa trivial, elegir el modelo que vayamos a usar en nuestro sistema de Q&A no es tan trivial. Al fin y al cabo hay que tener en cuenta cuestiones muy importantes para la organización como son:
- Funcionamiento: el modelo debe de ser lo suficientemente bueno como para que sea útil.
- Rendimiento: el modelo debe ser ejecutable en la infraestructura disponible.
- Privacidad: la emprea puede estar dispuesta (o no) a usar modelos de terceros mediante API (como la de OpenAI, por ejemplo).
- Licencia: en función del uso que se le vaya a dar al sistema de Q&A, el modelo deberá tener una licencia que permita dicho uso. Por ejemplo, si queremos comercializar la solución, deberemos contar con un modelo con licencia Apache 2.
Así pues, la elección del modelo tendrá que tener todos estos factores en cuenta. Dicho esto, en mi caso voy a elegir el modelo tiiuae/falcon-7b de Hugging Face (enlace) por dos motivos:
- Es un modelo que funciona muy bien.
- Tiene licencia Apache 2, lo cual siempre es interesante.
Dicho esto, vamos a definir nuestro modelo. En este sentido comentar que langchain ofrece dos opciones:
- Definir un modelo LLM, es decir, un modelo de lenguaje generalista. Esto lo podemos hacer con el módulo
langchain.llm. - Definir un modelo de chat, lo cual son LLMs, pero con una inferencia un poco diferente pensado para crear chats. Estos modelos están disponibles en
langchain.chat_models, aunque aún son un poco incipientes.
Así pues, vamos a cargar el modelo previamete comentado. Para ello, vamos a necesitar contar con un token de Hugging Face, el cual puedes obtener siguiendo este tutorial:
from langchain.llms import HuggingFaceHub
llm = HuggingFaceHub(
repo_id='tiiuae/falcon-7b-instruct',
huggingfacehub_api_token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX',
model_kwargs= {
"min_length": 200,
"max_length":2000,
"temperature":0.1
}
)
Con esto ya tenemos el modelo y, de hecho, podríamos probar a hacerle una consulta directamente, tal como muestro a continuación:
llm.generate(["Do you know something about Harry Potter?"])
LLMResult(generations=[[Generation(text="\nI'm sorry, I am an AI language model and do not have knowledge about Harry Potter", generation_info=None)]], llm_output=None)
Como podéis ver, ya podemos usar el modelo directamente. Eso está bien, pero de momento no está conectado a la información que ofrece nuestra base de datos vectorial. Así pues, ahora vamos a ver cómo podemos conectar nuestro LLM con una base de datos vectorial.
Conectar el LLM a la base de datos vectorial
La idea por detrás de conectar el LLM a la base de datos vectorial es la siguiente:
- Dado un prompt, obtener los documentos que más se parecen al prompt que hemos introducido.
- Combinar los documentos junto con el prompt generado.
- Obtener una respuesta, en base a la combinación de documentos y el prompt.
Si tuviéramos que realizar este proceso de forma manual podría resultar un poco complejo. Por suerte, langchain cuenta con la clase RetrievalQA, la cual está pensada para obtener respuestas utilizando una base de datos vectorial por detrás.
Además, incluye ciertas funcionalidades muy interesantes, como la opción de modiciar la plantilla del prompt utilizada o el devolver las fuentes en base a las cuales da respuesta.
En cualquier caso, vamos a ver un ejemplo sencillo de cómo funciona el RetrievalQA. Lo primero de todo tendremos que definirlo, para lo cual debemos indicarle el modelo LLM que debe utilizar y nuestra base de datos vectorial:
from langchain.chains import RetrievalQA
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=chroma.as_retriever()
)
Ahora que ya tenemos la lógica montada, simplemente debemos pasar nuestrsa consultas a modo de diccionario y el sistema nos responderá:
qa({"query": "Which is the last Harry Potter film?"})
{'query': 'Which is the last Harry Potter film?',
'result': '\nGo to the Harry Potter and the Deathly Hallows – Part 2 question page.'}
Como vemos, el sistema ha sido capaz de responder a esta pregunta, que era bastante sencilla. Hecho esto, vamos a ver cómo modificar el template del prompt por defecto.
Cómo modificar el template del prompt del sistema de Q&A
Para modificar el template del sistema de Q&A podemos crear un nuevo template con la clase PromptTemplate. Esta clase la podremos más tarde pasar al RetrievalQA. Así pues, vamos a crear un nuevo template en el cual indiquemos que, responsa únicamente en base al contexto y que no se invente nada.
from langchain.prompts import PromptTemplate
prompt_template = """
>>INSTRUCTIONS<<
Answer the question as truthfully as possible using the provided context.
If the answer is not contained within the context below, say "I don't know my lord!"
>>CONTEXT<<
{context}
>>QUESTION<<
{question}
>>ANSWER<<
"""
prompt = PromptTemplate(
template=prompt_template, input_variables=["context", "question"]
)
chain_type_kwargs = {"prompt": prompt}
Por último, tendremos que crear de nuevo nuestro sistema de Q&A. En este caso, además, indicaré el parámetro return_source_documents=True, de tal forma que el sistema no solo responda a la pregunta, sino que además, devuelva el contexto que ha utilizado para dar respuesta a esa pregunta.
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=chroma.as_retriever(search_kwargs=dict(k=5)),
return_source_documents = True,
chain_type_kwargs = chain_type_kwargs
)
qa({"query": "Which are the main characters in Harry Potter?"})
{'query': 'Which are the main characters in Harry Potter?',
'result': 'Harry Potter is the main character in the Harry Potter series. Other main characters include Ron Weasley,',
'source_documents': [Document(page_content="Daniel Radcliffe as Harry Potter: A 17-year-old wizard...", metadata={'Year': 2010, 'Film': 'Harry Potter and the Deathly Hallows – Part 1', 'Director': 'David Yates'}),
Document(page_content="Daniel Radcliffe as Harry Potter: A 17-year-old British wizard.\nRupert Grint as Ron Weasley..", metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'}),
Document(page_content='Julie Walters as Molly Weasley: The Weasley matriarch.\nTom Felton as Draco Malfoy: A Death Eater and son of Lucius and Narcissa Malfoy...', metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'}),
Document(page_content='The main antagonists are Draco Malfoy, an elitist, bullying classmate, and Lord Voldemort...', metadata={'Year': 2001, 'Film': "Harry Potter and the Philosopher's Stone", 'Director': 'Chris Columbus'}),
Document(page_content='=== Characters ===\nHarry Potter is an orphan whom Rowling imagined as a ...", 'Director': 'Chris Columbus'})]}
Por último, vamos a comprobar si, efectivamente, cuando pedimos algo que no debería conocer porque no tiene contexto para ello, se inventa la respuesta o, directamente, nos dice que no sabe:
qa({"query": "Is Robotics an AI field?"})
{'query': 'Is Robotics an AI field?',
'result': 'Yes, Robotics is an AI field. It involves the use of robots to perform tasks that are too',
'source_documents': [Document(page_content='Art Direction and Best Visual Effects.', metadata={'Year': 2010, 'Film': 'Harry Potter and the Deathly Hallows – Part 1', 'Director': 'David Yates'}),
Document(page_content='a character and story point of view. Trying to use the sense of isolation, of separation that sometimes 3D gives you...', metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'}),
Document(page_content='=== Visual effects ===', metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'}),
Document(page_content='Craig said, "We experimented a lot, quite honestly...', metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'}),
Document(page_content='=== Critical response ===', metadata={'Year': 2011, 'Film': 'Harry Potter and the Deathly Hallows – Part 2', 'Director': 'David Yates'})]}
Como vemos, en este caso, aunque según el prompt el modelo no debería haber respondido, ha respondido. Así pues, deberíamos trabajar en mejorar ese prompt para conseguir que el modelo no responda.
Si el modelo no es lo suficientemente complejo, puede darse el caso de que por mucho que cambiemos el prompt no consigamos la respuesta esperada. En este caso, lo ideal sería cambiar de modelo.
Conclusiones
En este post te he explicado paso a paso y de forma sencilla cómo puedes hacer que un LLM responda preguntas sobre una serie de documentos que tengas ya generados. Aunque esta funcionalidad es en sí misma interesante, hay otra serie de aspectos que se pueden desarrollar que seguramente te pueden interesar, como guardar una memoria de las conversiones o conseguir.
En cualquier caso estos aspectos los dejaré para un futuro post. Espero que este post te haya gustado y, ¡nos vemos en el siguiente!