paint-brush
Mejora del chatbot con generación de código: creación de un chatbot sensible al contexto para publicacionespor@shanglun
1,178 lecturas
1,178 lecturas

Mejora del chatbot con generación de código: creación de un chatbot sensible al contexto para publicaciones

por Shanglun Wang19m2023/11/05
Read on Terminal Reader

Demasiado Largo; Para Leer

Los sistemas basados en GPT se han utilizado en la gestión del conocimiento con gran efecto, pero las implementaciones actuales de última generación tienen una capacidad limitada para realizar búsquedas basadas en metadatos. Hoy, construimos un sistema capaz de realizar búsquedas tanto semánticas como de metadatos, mejorando enormemente las capacidades del sistema de generación de recuperación aumentada.
featured image - Mejora del chatbot con generación de código: creación de un chatbot sensible al contexto para publicaciones
Shanglun Wang HackerNoon profile picture

Introducción

Desde que ChatGPT capturó la imaginación del público a principios de 2023, ha habido una explosión de interés en comercializar aplicaciones basadas en modelos de lenguaje de gran tamaño. Una de las aplicaciones más interesantes ha sido la creación de sistemas de chat expertos que pueden responder consultas en lenguaje natural a partir de una base de datos de conocimiento patentada.


Una de las técnicas más populares en este espacio es la [generación de recuperación aumentada](https://retrieval awmented Generation aws), o RAG, que utiliza incrustaciones de documentos para encontrar elementos relevantes para la consulta del usuario antes de usar un modelo de lenguaje grande para generar. una respuesta.


La técnica es extremadamente poderosa ya que permite búsquedas extremadamente económicas y rápidas, proporciona extrema flexibilidad para que la base de conocimientos cambie y evolucione con el tiempo, y respuestas altamente informadas y precisas que reducen en gran medida las alucinaciones y los errores.


Para un análisis más profundo de los sistemas RAG y cómo implementar uno, puede leer mi artículo anterior aquí .


Por muy potentes que puedan ser los sistemas RAG, la arquitectura presenta algunas limitaciones graves. Exploramos algunas de las limitaciones en mi artículo anterior y propusimos formas de mejorar la arquitectura.


Hoy exploraremos otra limitación de la arquitectura basada en incrustación y propondremos una forma de sortear las limitaciones de la arquitectura.

Planteamiento del problema

Limitaciones de RAG basado en incrustaciones

Supongamos que somos una publicación que quiere crear una interfaz de chat que permita a los lectores y clientes hacer preguntas.


Por supuesto, podremos responder preguntas como “¿Cuál es tu opinión sobre X?” o "¿Qué has dicho sobre Y?" con una implementación RAG simple, pero una arquitectura RAG realmente comienza a tener problemas cuando respondes a preguntas como "¿Qué dijiste sobre X en 2021?" o “¿Cómo ha cambiado su cobertura de Y entre 2021 y 2023?”


Uno de los desafíos con un RAG basado en incrustaciones es que los modelos de incrustaciones generalmente no pueden codificar metadatos de manera sistemática y, por lo tanto, cualquier búsqueda que requiera conocimiento de cosas como la fecha de publicación o el nombre del autor le dará bastantes problemas a su sistema RAG. .


Podemos resolver este problema aprovechando una de las características más interesantes de los modelos de lenguaje grandes: la generación de código. Examinaremos una publicación del mundo real, diseñaremos un algoritmo basado en LLM que mejore la arquitectura RAG y crearemos un chatbot basado en el algoritmo.

Problema empresarial

Hoy veremos el CB Insights Newsletter, un popular boletín diario que cubre nuevas empresas y tecnología. Como ex desarrollador full-stack de CB Insights, a menudo esperaba con ansias el ingenio y la perspicacia únicos del fundador al final de la jornada laboral.


Hoy, utilizaremos el archivo del boletín informativo CB Insights como base de datos para crear un chatbot que pueda responder consultas en lenguaje natural basadas en metadatos de una manera superior a una implementación RAG básica basada en Embeddings.


Específicamente, queremos que el chatbot pueda responder preguntas como:


  • ¿Qué dijiste sobre Uber en 2020?


  • ¿Cómo ha cambiado la suerte de Airbnb entre 2016 y 2019?


  • ¿Cuáles son algunas startups unicornio notables de la India en la década de 2020?


¡Hagámoslo!

Tecnologías utilizadas

Para realizar esta tarea, utilizaremos las siguientes tecnologías:

Pitón

Si ha seguido mis otros artículos , no debería sorprenderle que use Python para la mayor parte del código de este artículo. Python tiene excelentes funciones de web scraping, procesamiento de datos e integración con OpenAI, todo lo cual aprovecharemos para nuestro proyecto de hoy.

SQL

SQL es el lenguaje de consulta que permite a los usuarios interactuar con varias bases de datos relacionales importantes, incluidas SQLite, MySQL, PostgreSQL y SQL Server. El lenguaje es un conjunto de instrucciones para la base de datos sobre cómo recuperar, combinar y manipular datos antes de devolverlos al usuario.

Generación de código LLM

La generación de código LLM es una técnica que ha recibido mucha atención en los últimos meses, ya que varios modelos básicos, incluidos GPT 3.5, GPT 4 y LLaMa 2, han demostrado la capacidad de generar código de sorprendente complejidad en respuesta a consultas en lenguaje natural.


Los sistemas especialmente entrenados y ajustados, como Copilot de GitHub, son capaces de escribir código notablemente inteligente mediante el uso de modelos diseñados específicamente para la generación de código, pero un modelo GPT de propósito general correctamente activado ya tiene capacidades excepcionales cuando se trata de escribir código.

Incrustación semántica

La incrustación semántica es la columna vertebral de la mayoría de las implementaciones de RAG. Al utilizar una serie de técnicas de lenguaje natural, podemos convertir un texto en lenguaje natural en un vector de números que representan el contenido del texto dentro de un espacio vectorial semántico.


Luego podemos usar álgebra vectorial para manipular estas incrustaciones, lo que nos permite determinar la relación entre dos corpus de texto usando métodos matemáticos.

GPT-3.5 y GPT-4

Con 1,7 billones de parámetros, GPT-4 es simplemente el modelo de lenguaje grande basado en transformador más potente disponible en el mercado actualmente. GPT-4 es capaz de comprender grandes volúmenes de texto, razonamientos complejos y generar respuestas largas y convincentes en respuesta a indicaciones difíciles.


GPT-3.5, el primo mucho más pequeño de GPT-4, es el modelo que impulsó ChatGPT cuando arrasó en el mundo. Es capaz de procesar indicaciones increíblemente complejas y lo que le falta en capacidad de razonamiento puro lo compensa en velocidad y ahorro de costos.


Para tareas más sencillas, GPT3.5 logra el equilibrio entre rendimiento y precisión.

Configurar el backend del chatbot

Configurando la base de datos

Antes de construir nuestra IA, necesitamos obtener los datos. Para hacer esto, podemos utilizar la página de archivo de boletines de CB Insights [ https://www.cbinsights.com/newsletter/ ] que tiene una colección de boletines anteriores.


Para obtener todos los enlaces, podemos usar las solicitudes de Python y la hermosa biblioteca de sopa como esta:

 import requests from bs4 import BeautifulSoup res = requests.get('https://www.cbinsights.com/newsletter/') soup = BeautifulSoup(res.text) article_links = [[i.text, i['href']] for i in soup.find_all('a') if 'campaign-archive' in i['href'] ]


Una vez que tengamos los enlaces, podemos ir a cada uno de los enlaces y descargar el artículo HTML. Con la comprensión de listas de Python, podemos hacer esto en una línea:


 article_soups = [BeautifulSoup(requests.get(link[1]).text) for link in article_links]


Esto llevará un tiempo, pero eventualmente, todos los enlaces deberían eliminarse.


Ahora podemos usar BeautifulSoup para extraer las secciones relevantes:


 import re # SEO optimizations cause some articles to appear twice so we dedupe them. # We also remove some multiple newlines and unicode characters. def get_deduped_article_tables(article_table): new_article_tables = [] for i in article_table: text_content = re.sub(r'\n{2,}', '\n', i.replace('\xa0', '').strip()) if text_content not in new_article_tables or text_content == '': new_article_tables.append(text_content) return new_article_tables result_json = {} for soup_meta, soup_art in zip(article_links, article_soups): article_tables = [] cur_article = [] for table in soup_art.find_all('table'): if table.attrs.get('mc:variant') == 'Section_Divider': article_tables.append(get_deduped_article_tables(cur_article)) cur_article = [] else: cur_article.append(table.text) article_tables.append(get_deduped_article_tables(cur_article)) result_json[soup_meta[0]] = article_tables


Procesemos un poco más y convirtámoslo en un DataFrame:


 import pandas as pd result_rows = [] for article_name, article_json in result_json.items(): article_date = article_json[0][1] for idx, tbl in enumerate(article_json[1:]): txt = '\n'.join(tbl).strip() if txt != '': result_rows.append({ 'article_name': article_name, 'article_date': article_date, 'idx': idx, 'text': txt, }) df = apd.DataFrame(result_rows)


Si inspecciona el marco de datos, debería ver algo como lo siguiente:



Mientras tenemos los datos, generemos también las incrustaciones de los artículos. Con el modelo de integración ada de OpenAI, esto es bastante fácil.


 import openai EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [YOUR KEY] df['embedding'] = df['text'].map(lambda txt: openai.Embedding.create(model=EMBEDDING_MODEL, input=[txt])['data'][0]['embedding'])


Ahora que tenemos los datos que estamos usando para este ejercicio, carguemos los datos en una base de datos. Para este ejercicio, usaremos SQLite, que es un sistema de base de datos autónomo y liviano que viene empaquetado con Python.


Tenga en cuenta que en un entorno de producción, probablemente querrá utilizar una instancia de base de datos adecuada, como MySQL o PostgreSQL, con pequeños ajustes al SQL que estamos usando aquí, pero la técnica general seguirá siendo la misma.


Para crear una instancia y cargar la base de datos, simplemente ejecute lo siguiente en Python. Tenga en cuenta que, además del texto del artículo y las incrustaciones, también guardamos algunos metadatos, concretamente la fecha de publicación.


También tenga en cuenta que SQLite3, a diferencia de la mayoría de las otras bases de datos SQL, utiliza un sistema de escritura dinámica, por lo que no tenemos que especificar los tipos de datos en la consulta de creación.


 import sqlite3 import json con = sqlite3.connect("./cbi_article.db") cur = con.cursor() cur.execute("CREATE TABLE article(name, date, idx, content, embedding_json)") con.commit() rows = [] for _, row in df.iterrows(): rows.append([row['article_name'], row['article_date'], row['idx'], row['text'], json.dumps(row['embedding'])]) cur.executemany("INSERT INTO article VALUES (?, ?, ?, ?, ?)", rows) con.commit()


E intentemos consultar los datos.


 res = cur.execute(""" SELECT name, date, idx FROM article WHERE date >= DATE('now', '-2 years'); """) res.fetchall()


Debería producir algo como:




¡Se ve bastante bien!


Creación de un generador de código para la búsqueda de metadatos

Ahora que tenemos los datos cargados en la base de datos SQLite, podemos pasar a la siguiente etapa. Recuerde que uno de los desafíos de la implementación de RAG solo con incrustaciones es la falta de capacidades flexibles de búsqueda de metadatos.


Sin embargo, ahora que tenemos los metadatos cargados en una base de datos SQL, podemos usar las capacidades de generación de código de GPT para realizar búsquedas de metadatos flexibles.


Para generar código SQL, podemos utilizar alguna ingeniería de avisos simple.


 response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] )


Observe el siguiente mensaje de ingeniería: 1) damos el esquema de la base de datos pero lo mantenemos simple. 2) Especificamos las columnas de retorno. 3) Especificamos las columnas que están disponibles para filtrar. 4) Especificamos el tipo de SQL. Este mensaje debería generar un código SQL como el siguiente:


 SELECT * FROM article WHERE date BETWEEN '2022-06-01' AND '2022-08-31'


Ahora bien, debido a que el valor de retorno no es determinista, a veces terminarás con idiosincrasias en el código generado. Para manejar estas condiciones, simplemente podemos tener un bucle try-catch para intentar regenerar datos. Por supuesto, no queremos hacer esto infinitamente, por lo que si no podemos generar un SQL adecuado en tres intentos, simplemente saldremos y recurriremos a RAG básico.


Podemos implementar el filtro así:


 res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall()


Este es un filtro relativamente básico, por lo que en casos de uso de producción, es probable que desee ejecutar más comprobaciones de relevancia y corrección de SQL, pero esto es suficiente para nuestro ejemplo.


Una nota rápida sobre la seguridad de la IA : debemos tener cuidado al ejecutar el código devuelto por una IA, especialmente si la entrada del usuario se utilizó como parte del mensaje.


Si no desinfectamos la salida, nos volvemos vulnerables a ataques de ingeniería rápida en los que el usuario intenta manipular la IA para generar actualizaciones o eliminar declaraciones.


Por lo tanto, siempre debemos verificar que el resultado sea el esperado antes de ejecutar el código en nuestra computadora.


Ejecute el siguiente código para ver el resultado recuperado:


 df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res])


Y ahora deberías ver el siguiente resultado:



Ejecutar RAG estándar con el resultado

Ahora que tenemos el resultado de la búsqueda de metadatos, el resto es sencillo. Calculamos la similitud del coseno para todos los resultados recuperados de esta manera:


 from openai.embeddings_utils import cosine_similarity q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed))


Y ahora, podemos tomar los 10 boletines informativos principales y utilizar ingeniería rápida para responder la pregunta. Elegimos 10 aquí porque cada extracto del boletín es relativamente breve.


Si está trabajando con artículos que son más largos, posiblemente querrá utilizar menos artículos o utilizar la técnica cubierta en la sección de bonificación.


 answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] )


Esto debería darle un resultado similar al siguiente:


El futuro de Fintech se debatió en varios boletines durante el verano de 2022. Hubo una desaceleración notable en Fintech a medida que la financiación del segundo trimestre de 2022 se desplomó hacia los niveles de 2020, después de un máximo en 2021. El informe del segundo trimestre de 2022 destacó la disminución de las inversiones globales en fintech. .


Sin embargo, el futuro de las fintech parecía prometedor, ya que se observó un cambio significativo hacia las empresas emergentes en sus etapas iniciales, particularmente en el ámbito de los pagos. La inversión global en el sector de pagos cayó un 43 % desde el primer trimestre de 2022 hasta los 5.100 millones de dólares en el segundo trimestre de 2022, debido a que la financiación volvió a la normalidad después de los picos de 2021.


Los nuevos participantes en este ámbito atrajeron una mayor proporción de acuerdos (63%) en lo que va de 2022, lo que demuestra el interés de los inversores en las empresas de nueva creación. También se informó sobre la creciente competencia que las Fintech estaban causando a los bancos minoristas, obligándolos a priorizar la digitalización de los servicios principales.


El sector bancario respondió centrándose en mejorar la experiencia del cliente, en particular la banca móvil, utilizando tecnologías como chatbots y plataformas de análisis de clientes. Todo esto apunta a que se avecina una industria FinTech vibrante y competitiva.


¡Lo cual es bastante bueno! Si verifica la respuesta mirando la pregunta de respuesta, notará que todas las estadísticas provienen del material original. También notarás que hay algún formato idiosincrásico en el material fuente que GPT4 pudo ignorar. Esto muestra la flexibilidad de los LLM en los sistemas de resumen de datos.

Bonificación: middleware de resumen

Uno de los problemas que puede encontrar en este proceso es que, cuando el corpus es extremadamente grande, el mensaje final puede ser muy grande. Esto puede resultar costoso si utiliza GPT-4, pero un mensaje muy largo también puede confundir el modelo.


Para resolver esto, podemos preprocesar los artículos individuales con GPT-3.5, comprimiendo el mensaje final que enviamos a GPT-4 en el paso final.


 summarization_prompt = ''' Summarize the following passage and extract only portions that are relevant to answering the user question. Passage: ======= %s ======= User Questions: %s ''' (row['content'], user_question) response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a summarizer of tech industry reports"}, {"role": "user", "content": summarization_prompt}, ] )


Luego podemos incluir los resúmenes en el mensaje, con un ahorro significativo en comparación con colocar el artículo puro en el mensaje final.

Construyendo una interfaz simple

Ahora que tenemos el código Python escrito, empaquetemos esto como una aplicación web simple.

API de back-end

Es relativamente sencillo empaquetar el código como una API de backend con Flask. Simplemente cree una función y vincúlela a Flask de esta manera:


 import requests from bs4 import BeautifulSoup import re import pandas as pd import sqlite3 import json import openai from openai.embeddings_utils import cosine_similarity from flask import Flask, request, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [Your OpenAI Key] db_location = [Location of your SQLite DB] def process_user_query(user_question): con = sqlite3.connect(db_location) cur = con.cursor() user_question = 'What did you say about the future of the fintech industry in summer of 2022?' res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall() df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res]) q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed)) answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] ) return response.choices[0].message['content'] @app.route('/process_user_question', methods=["POST"]) def process_user_question(): return jsonify({ 'status': 'success', 'result': process_user_query(request.json['user_question']) }) app.run()

¡Y eso es todo lo que necesitamos hacer para el backend!

Código de interfaz

Debido a que solo tenemos un punto final y no necesitamos mucho estado en nuestra aplicación, el código de interfaz debería ser bastante simple. Recuerde que en un artículo anterior configuramos una aplicación React con enrutamiento que nos permite renderizar componentes en rutas específicas.


Simplemente siga las instrucciones de ese artículo para configurar un proyecto React.JS y agregue el siguiente componente en la ruta de su elección:


 import React, {useState, useEffect} from 'react'; import axios from 'axios'; const HNArticle = () => { const [result, setResult] = useState(''); const [message, setMessage] = useState(''); const [question, setQuestion] = useState(''); const askQuestion = () => { axios.post("http://127.0.0.1:5000/process_user_question", {user_question: question}) .then(r => r.data) .then(d => { console.log(d); setResult(d.result); }); } return <div className="row" style={{marginTop: '15px'}}> <div className="col-md-12" style={{marginBottom: '15px'}}> <center> <h5>Hackernoon CB Insights Demo</h5> </center> </div> <div className="col-md-10 offset-md-1 col-sm-12 col-lg-8 offset-lg-2" style={{marginBottom: '15px'}}> <ul className="list-group"> <li className="list-group-item"> <h6>Your Question</h6> <p><input className="form-control" placeholder="Question" value={question} onChange={e => setQuestion(e.target.value)} /></p> <p>{message}</p> <p> <button className="btn btn-primary" onClick={askQuestion}>Ask</button> </p> </li> {result? <li className="list-group-item"> <h6>Response</h6> {result.split("\n").map((p, i) => <p key={i}>{p}</p>)} </li>: ''} </ul> </div> </div>; } export default HNArticle;


Ejecute el código y debería ver una interfaz como esta:




Haga una pregunta y, después de un rato, debería ver el resultado:


¡Y voilá! ¡Hemos creado con éxito un chatbot con capacidades de consulta avanzadas más allá de un sistema RAG básico!

Conclusión

En el artículo de hoy, creamos un chatbot con potentes capacidades de generación de código. Este es un ejemplo de una nueva clase de aplicaciones LLM que están creando muchos pioneros de la IA, que pueden aprovechar los datos, los lenguajes de programación y la comprensión del lenguaje natural para construir sistemas generativos de IA con conocimientos especializados.


Estos sistemas especializados son la clave para desbloquear la viabilidad comercial de las aplicaciones LLM que buscan brindar valor más allá de lo que está disponible directamente de proveedores de plataformas como OpenAI y Anthropic.


La generación de código es sólo una de las técnicas que fue posible gracias a la reciente generación de grandes modelos de lenguaje disponibles comercialmente.


Si tiene ideas sobre cómo se pueden comercializar los LLM o le gustaría tener una conversación sobre IA, no dude en comunicarse con LinkedIn o GitHub . ¡He tenido muchas conversaciones interesantes con lectores a lo largo del último año y espero muchas más!