С тех пор как ChatGPT захватил общественное внимание в начале 2023 года, произошел взрыв интереса к коммерциализации больших приложений на основе языковых моделей. Одним из наиболее интересных приложений стало создание экспертных чат-систем, которые могут отвечать на запросы на естественном языке из базы данных собственной базы знаний.
Одним из наиболее популярных методов в этой области является [генерация расширенной выборки] (https://retrival augmented Generation aws) или RAG, которая использует встраивание документов для поиска элементов, соответствующих запросу пользователя, прежде чем использовать большую языковую модель для генерации Ответ.
Этот метод чрезвычайно эффективен, поскольку позволяет проводить чрезвычайно дешевый и быстрый поиск, обеспечивает чрезвычайную гибкость базы знаний для изменения и развития с течением времени, а также высокоинформированные и точные ответы, которые значительно уменьшают количество галлюцинаций и ошибок.
Для более глубокого анализа RAG-систем и способов их реализации вы можете прочитать мою предыдущую статью здесь .
Какими бы мощными ни были системы RAG, у этой архитектуры есть некоторые серьезные ограничения. В моей предыдущей статье мы рассмотрели некоторые ограничения и предложили способы улучшения архитектуры.
Сегодня мы рассмотрим еще одно ограничение архитектуры на основе встраивания и предложим способ обойти ограничения этой архитектуры.
Предположим, мы — издание, которое хочет создать интерфейс чата, позволяющий читателям и клиентам задавать вопросы.
Конечно, мы сможем ответить на такие вопросы, как «Что вы думаете об X?» или «Что ты сказал о Y?» с простой реализацией RAG, но архитектура RAG действительно начинает давать сбои, когда вы сталкиваетесь с такими вопросами, как «Что вы говорили о X в 2021 году?» или «Как изменилось освещение Y в период с 2021 по 2023 год?»
Одна из проблем RAG на основе внедрений заключается в том, что модели внедрения, как правило, не способны систематически кодировать метаданные, и поэтому любые поиски, требующие знания таких вещей, как дата публикации или имя автора, доставят вашей системе RAG немало хлопот. .
Мы можем решить эту проблему, используя одну из самых интересных особенностей больших языковых моделей — генерацию кода. Мы рассмотрим реальную публикацию, разработаем алгоритм на основе LLM, который улучшает архитектуру RAG, и создадим чат-бота на основе этого алгоритма.
Сегодня мы рассмотрим информационный бюллетень CB Insights , популярный ежедневный информационный бюллетень, посвященный стартапам и технологиям. Как бывший разработчик полного стека в CB Insights, я часто с нетерпением ждал уникального остроумия и проницательности основателя в конце рабочего дня.
Сегодня мы будем использовать архив информационных бюллетеней CB Insights в качестве базовых данных для создания чат-бота, который может отвечать на запросы на естественном языке на основе метаданных способом, превосходящим стандартную реализацию RAG на основе Embeddings.
В частности, мы хотим, чтобы чат-бот мог отвечать на такие вопросы, как:
Давайте займемся этим!
Для реализации этой задачи мы будем использовать следующие технологии:
Если вы следили за другими моими статьями , неудивительно, что я буду использовать Python для большей части кода в этой статье. Python обладает отличными возможностями парсинга веб-страниц, обработки данных и интеграции с OpenAI, и все это мы сегодня будем использовать в нашем проекте.
SQL — это язык запросов, который позволяет пользователям взаимодействовать с несколькими основными реляционными базами данных, включая SQLite, MySQL, PostgreSQL и SQL Server. Язык представляет собой набор инструкций для базы данных о том, как извлекать, комбинировать и манипулировать данными перед их возвратом пользователю.
Генерация кода LLM — это метод, который привлек широкое внимание в последние несколько месяцев, поскольку несколько базовых моделей, включая GPT 3.5, GPT 4 и LLaMa 2, продемонстрировали способность генерировать код удивительной сложности в ответ на запросы на естественном языке.
Специально обученные и настроенные системы, такие как Copilot от GitHub, способны писать удивительно интеллектуальный код, используя модели, специально разработанные для генерации кода, но модель GPT общего назначения с правильным подсказкой уже обладает исключительными возможностями, когда дело доходит до написания кода.
Семантическое внедрение является основой большинства реализаций RAG. Используя ряд методов естественного языка, мы можем превратить текст на естественном языке в вектор чисел, которые представляют содержимое текста в семантическом векторном пространстве.
Затем мы можем использовать векторную алгебру для управления этими вложениями, что позволяет нам определять взаимосвязь между двумя текстовыми корпусами с помощью математических методов.
Имея 1,7 триллиона параметров, GPT-4 является просто самой мощной моделью большого языка на основе трансформатора, доступной сегодня на рынке. GPT-4 способен понимать большие объемы текста, сложные рассуждения и генерировать убедительные ответы в ответ на сложные подсказки.
GPT-3.5, гораздо меньший родственник GPT-4, — это модель, которая послужила основой ChatGPT, когда он покорил мир. Он способен обрабатывать невероятно сложные запросы, а недостаток чисто логических способностей компенсируется скоростью и экономией средств.
Для более простых задач GPT3.5 обеспечивает баланс между производительностью и точностью.
Прежде чем мы создадим наш ИИ, нам нужно получить данные. Для этого мы можем использовать страницу архива информационных бюллетеней CB Insights [ https://www.cbinsights.com/newsletter/ ], на которой собрана коллекция прошлых информационных бюллетеней.
Чтобы получить все ссылки, мы можем использовать запросы Python и красивую библиотеку супов, например:
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'] ]
Получив ссылки, мы можем перейти к каждой из них и загрузить HTML-код статьи. Благодаря пониманию списков Python мы можем сделать это в одной строке:
article_soups = [BeautifulSoup(requests.get(link[1]).text) for link in article_links]
Это займет некоторое время, но в конечном итоге все ссылки должны быть удалены.
Теперь мы можем использовать BeautifulSoup для извлечения соответствующих разделов:
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
Давайте проведем дополнительную обработку и превратим его в 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)
Если вы проверите фрейм данных, вы должны увидеть что-то вроде следующего:
Пока у нас есть данные, давайте также сгенерируем вложения для статей. С моделью внедрения ada OpenAI это довольно просто.
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'])
Теперь, когда у нас есть данные, которые мы используем для этого упражнения, давайте загрузим их в базу данных. В этом упражнении мы будем использовать SQLite — легкую автономную систему баз данных, поставляемую в комплекте с Python.
Обратите внимание, что в производственной среде вам, скорее всего, захочется использовать подходящий экземпляр базы данных, такой как MySQL или PostgreSQL, с небольшими изменениями в используемом здесь SQL, но общий метод останется прежним.
Чтобы создать экземпляр и загрузить базу данных, просто запустите следующую команду на Python. Обратите внимание, что помимо текста статьи и вложений мы также сохраняем некоторые метаданные, а именно дату публикации.
Также обратите внимание, что SQLite3, в отличие от большинства других баз данных SQL, использует систему динамической типизации, поэтому нам не нужно указывать типы данных в запросе на создание.
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()
И давайте попробуем запросить данные.
res = cur.execute(""" SELECT name, date, idx FROM article WHERE date >= DATE('now', '-2 years'); """) res.fetchall()
Должно получиться что-то вроде:
Выглядишь довольно хорошо!
Теперь, когда у нас есть данные, загруженные в базу данных SQLite, мы можем перейти к следующему этапу. Помните, что одной из проблем реализации RAG только для внедрения является отсутствие гибких возможностей поиска метаданных.
Однако теперь, когда у нас есть метаданные, загруженные в базу данных SQL, мы можем использовать возможности генерации кода GPT для выполнения гибкого поиска метаданных.
Чтобы сгенерировать код SQL, мы можем использовать простую разработку подсказок.
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? """}, ] )
Обратите внимание на следующую подсказку: 1) мы даем схему базы данных, но делаем ее простой. 2) Указываем возвращаемые столбцы. 3) Указываем столбцы, доступные для фильтрации. 4) Указываем вариант SQL. Это приглашение должно сгенерировать код SQL, подобный приведенному ниже:
SELECT * FROM article WHERE date BETWEEN '2022-06-01' AND '2022-08-31'
Теперь, поскольку возвращаемое значение не является детерминированным, иногда в сгенерированном коде могут возникнуть особенности. Чтобы справиться с этими условиями, мы можем просто использовать цикл try-catch, чтобы попытаться восстановить данные. Конечно, мы не хотим делать это бесконечно, поэтому, если мы не сможем сгенерировать правильный SQL за три попытки, мы просто выйдем и вернемся к стандартному RAG.
Мы можем реализовать фильтр следующим образом:
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()
Это относительно грубый фильтр, поэтому в производственных случаях вам, вероятно, захочется выполнить больше проверок на релевантность и корректность SQL, но для нашего примера этого достаточно.
Небольшое замечание о безопасности ИИ . Мы должны быть осторожны при запуске кода, возвращаемого ИИ, особенно если пользовательский ввод использовался как часть приглашения.
Если мы не очистим выходные данные, мы оставим себя уязвимыми для атак с быстрой разработкой, когда пользователь пытается манипулировать ИИ, заставляя его генерировать операторы обновления или удаления.
Поэтому мы всегда должны проверять, соответствует ли вывод ожидаемому, прежде чем запускать код на нашем компьютере.
Запустите следующий код, чтобы увидеть полученный результат:
df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res])
И теперь вы должны увидеть следующий результат:
Теперь, когда у нас есть результат поиска метаданных, все остальное просто. Мы вычисляем косинусное сходство для всех полученных результатов следующим образом:
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))
И теперь мы можем взять 10 лучших информационных бюллетеней и использовать быструю разработку, чтобы ответить на вопрос. Мы выбрали здесь 10, потому что каждый отрывок из информационного бюллетеня относительно короткий.
Если вы работаете со статьями большего размера, возможно, вам захочется использовать меньше статей или использовать технику, описанную в бонусном разделе.
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}, ] )
Это должно дать вам результат, подобный следующему:
Будущее финансовых технологий обсуждалось в различных информационных бюллетенях летом 2022 года. В финансовых технологиях наблюдался заметный спад, поскольку финансирование во втором квартале 2022 года резко упало до уровня 2020 года после максимума в 2021 году. В отчете за второй квартал 2022 года было отмечено снижение глобальных инвестиций в финансовые технологии. .
Тем не менее, будущее финансовых технологий выглядело многообещающим, поскольку был отмечен значительный сдвиг в сторону стартапов на ранних стадиях, особенно в сфере платежей. Мировые инвестиции в платежный сектор упали на 43% с первого квартала 2022 года до $5,1 млрд во втором квартале 2022 года из-за возвращения финансирования к норме после пиков 2021 года.
Новые игроки в этой сфере привлекли более высокую долю сделок (63%) в 2022 году, что свидетельствует об интересе инвесторов к стартапам. Также сообщалось о растущей конкуренции в сфере финансовых технологий с розничными банками, вынуждающей их уделять первоочередное внимание оцифровке основных услуг.
Банковский сектор отреагировал на это, сосредоточив внимание на улучшении качества обслуживания клиентов, особенно на мобильном банкинге, используя такие технологии, как чат-боты и платформы клиентской аналитики. Все это указывает на то, что впереди нас ждет динамичная и конкурентоспособная индустрия FinTech.
Что очень хорошо! Если вы проверите ответ, просмотрев подсказку для ответа, вы заметите, что вся статистика взята из исходного материала. Вы также заметите, что в исходном материале есть некоторое своеобразное форматирование, которое GPT4 смог игнорировать. Это показывает гибкость LLM в системах обобщения данных.
Одна из проблем, с которой вы можете столкнуться в этом процессе, заключается в том, что, когда корпус очень велик, итоговое приглашение может быть очень большим. Это может оказаться дорогостоящим, если вы используете GPT-4, но очень длинный запрос также может запутать модель.
Чтобы решить эту проблему, мы можем предварительно обработать отдельные статьи с помощью GPT-3.5, сжимая последнее приглашение, которое мы отправляем в GPT-4 на последнем этапе.
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}, ] )
Затем мы можем поместить резюме в подсказку, что позволит значительно сэкономить по сравнению с помещением чистой статьи в финальную подсказку.
Теперь, когда у нас написан код Python, давайте упакуем его как простое веб-приложение.
Упаковать код как внутренний API с помощью Flask относительно просто. Просто создайте функцию и свяжите ее с Flask следующим образом:
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()
И это фактически все, что нам нужно сделать для серверной части!
Поскольку у нас есть только одна конечная точка и нашему приложению не нужно много состояний, код внешнего интерфейса должен быть довольно простым. Помните, в прошлой статье мы настроили приложение React с маршрутизацией, которая позволяет нам отображать компоненты по определенным маршрутам.
Просто следуйте инструкциям в этой статье, чтобы настроить проект React.JS, и добавьте следующий компонент по выбранному вами маршруту:
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;
Запустите код, и вы должны увидеть такой интерфейс:
Задайте вопрос, и через некоторое время вы увидите результат:
И вуаля! Мы успешно создали чат-бота с расширенными возможностями запроса, выходящими за рамки обычной системы RAG!
В сегодняшней статье мы создали чат-бота с мощными возможностями генерации кода. Это один из примеров нового класса приложений LLM, которые сейчас создаются многими пионерами ИИ и которые могут использовать данные, языки программирования и понимание естественного языка для создания генеративных систем ИИ со специальными ноу-хау.
Эти специализированные системы являются ключом к раскрытию коммерческой жизнеспособности приложений LLM, стремящихся обеспечить ценность, выходящую за рамки того, что напрямую доступно от поставщиков платформ, таких как OpenAI и Anthropic.
Генерация кода — это лишь один из методов, который стал возможен благодаря недавнему созданию коммерчески доступных больших языковых моделей.
Если у вас есть идеи о том, как можно коммерциализировать LLM, или вы хотите поговорить об искусственном интеллекте, не стесняйтесь обращаться к LinkedIn или GitHub . За последний год у меня было много содержательных бесед с читателями, и я с нетерпением жду еще многих!