В данной статье мы рассмотрим бизнес-применение моделей от OpenAI. Я расскажу о том, как можно подключить ChatGPT ко внешней базе данных, а также как можно заставить его взаимодействовать с телеграмом (отправлять кнопки, к примеру).
Задумка
В данной статье мы рассмотрим следующее:
Есть телеграм бот. Нужен он для того чтобы узнавать ответы на те или иные вопросы. У нас есть база со "знаниями". Когда человек пишет боту, система должна дать ему ответ на его вопрос опираясь на это базу.
Для того, чтобы это реализовать, необходимо:
Запустить файн-тюнинг для модели gpt-3.5-turbo-1106 используя специальный датасет;
Написать парсер для ответов модели;
Написать скрипт для бота.
Реализация
1. Взаимодействие с моделью
Отправляем модели сообщение с вопросом «Как заблокировать карту?», модель смотрит есть ли такая ситуация в списке возможных, и если есть, начинает искать ответ в базе чтобы затем объяснить пользователю то, что он хотел сделать или узнать.
Мы не можем написать огромную инструкцию для модели т. к. это будет пустой расход токенов, так что желательно системное сообщение должно выглядеть так:
[Как заблокировать карту?|A]
[Как разблокировать карту?|B]
[Как закартировать разблокировку|C]
И если к примеру модель получит вопрос «Как заблокировать карту?», то в ответ она просто отправит одну нужную букву или комбинацию, в данном случае это A. Затем её ответ прочтёт скрипт и посмотрит есть ли в базе информация с тегом A, и если она там есть, – скрипт отправит модели нужную информацию, если нет, – отправит ничего или прочерк.
Если в инструкции нет нужного кейса, то модель заместо комбинации отправит сообщение о том, что не может помочь конкретно с этим кейсом, но может помочь с другими. В начало этого сообщения модель поставит ! (как и во все другие сообщения которые должны быть доставлены пользователю).
Нам также нужно дать возможность модели прикреплять кнопки к сообщениям в том случае когда пользователь может задать доп. вопрос. Для этого модель сможет помещать в конец сообщений подобные конструкции: [[Текст кнопки]]. Их автоматически спарсит и вырежет отдельный скрипт.
Этот самый парсер для кнопок напишем прямо сейчас:
Получившийся рабочий код:
function parseMessage(message) {
const buttonRegex = /\[\[(.*?)\]\]/g;
let match;
const buttons = [];
while ((match = buttonRegex.exec(message)) !== null) {
buttons.push(match[1]);
}
const text = message.replace(buttonRegex, '').trim();
return {
text,
buttons
};
}
"База знаний" будет у нас в виде матрицы (список в списке), где первый элемент – тег, а второй – содержание.
Пишем код для поиска:
Получившийся рабочий код:
function filterData(tags, data) {
// Сплитуем строку тегов через пробел
let tagArray = tags.split(' ');
// Фильтруем данные
let filteredData = data.filter(row => {
// Проверяем каждый тег
for (let i = 0; i < tagArray.length; i++) {
// Если первый элемент строки совпадает с тегом, возвращаем true
if (row[0] === tagArray[i]) {
return true;
}
}
// Если ни один тег не совпал, возвращаем false
return false;
});
return filteredData;
}
2. Fine-tuning
Нам нужно составить датасет, который сможет объяснить модели как себя вести. Там должно быть видно следующее:
Если кейс есть в системном сообщении, модель ищет информацию по нужному/нужным тегам;
Если кейса нет, пишет что помочь не может;
Если пользователь может задать доп. вопросы, модель добавляет кнопки;
Её поведение в случае если пользователь пишет не по теме.
В датасете вдобавок должно быть видно то, что количество кейсов не фиксировано. Также мы укажем модели на то, что в системном сообщении может быть и другая полезная информация помимо кейсов.
Вот пример некоторых диалогов из датасета (всего их там будет 27 штук):
{
"messages": [
{
"role": "system",
"content": "[Яблоко|BC][Что делать если я родился в 1988 году?|N]\n\nДлина текста внутри [[]] не более 16 символов!\n\nПредназначение: Помогать людям"
},
{
"role": "user",
"content": "как правильно есть яблоки?"
},
{
"role": "assistant",
"content": "BC"
},
{
"role": "user",
"content": "BC: Яблоки нельзя есть – они ядовитые"
},
{
"role": "assistant",
"content": "!Увы, но яблоки есть нельзя – они ядовитые 💀"
}
]
}
{
"messages": [
{
"role": "system",
"content": "[Яблоко|BC][Что делать если я родился в 1988 году?|N]\n\nДлина текста внутри [[]] не более 16 символов!\n\nПредназначение: Помогать людям"
},
{
"role": "user",
"content": "привет я родился в 1999"
},
{
"role": "assistant",
"content": "!Это замечательно! Но, я не могу помочь Вам в данной ситуации 😭\n\n[[1988 год]]"
}
]
}
{
"messages": [
{
"role": "system",
"content": "[Яблоко|BC][Что делать если я родился в 1988 году?|N]\n\nДлина текста внутри [[]] не более 16 символов!\n\nПредназначение: Рассказывать о трубах и самоварах"
},
{
"role": "user",
"content": "Сколько будет 2 + 2?"
},
{
"role": "assistant",
"content": "!Извините, но я не могу помочь Вам с этим. Моя задача – рассказывать о трубах и самоварах ☺️"
}
]
}
После того, как у нас есть готовый датасет, необходимо: Перейти в раздел файн-тюнинга, выбрать модель, загрузить файл и запустить.
Чтобы самостоятельно указать количество эпох, запускать нужно через API:
fetch('https://api.openai.com/v1/fine_tuning/jobs', {
method: 'POST',
body: JSON.stringify({
"training_file": "fileId",
"model": "gpt-3.5-turbo-1106",
"hyperparameters": {
"n_epochs": 12
}
}),
headers: {
"Content-Type": "application/json",
'Authorization': `Bearer ${openai_key}`
}
});
Результат:
Делаем небольшой тест:
3. Телеграм бот
Описываем GPT-4 всё, что мы хотим увидеть в скрипте, а также предоставляем ему необходимые написанные до этого функции:
Получаем рабочий код:
const fetch = require('node-fetch');
const HttpsProxyAgent = require('https-proxy-agent');
const fs = require('fs');
const TelegramBot = require('node-telegram-bot-api');
const model = '';
const proxy = '';
const token = '';
const bot_token = '';
const instructs_path = '';
const usefulData = JSON.parse(fs.readFileSync('./data.json'));
function getInstructs(path) {
return fs.readFileSync(path, 'utf8');
}
function parseMessage(message) {
const buttonRegex = /\[\[(.*?)\]\]/g;
let match;
const buttons = [];
while ((match = buttonRegex.exec(message)) !== null) {
buttons.push(match[1]);
}
const text = message.replace(buttonRegex, '').trim();
return {
text,
buttons
};
}
function filterData(tags, data) {
let tagArray = tags.split(' ');
let filteredData = data.filter(row => {
for (let i = 0; i < tagArray.length; i++) {
if (row[0] === tagArray[i]) {
return true;
}
}
return false;
});
return filteredData;
}
async function getResponse(messages) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
agent: new HttpsProxyAgent(proxy),
body: JSON.stringify({
model: model,
messages: [{
role: "system",
content: getInstructs(instructs_path)
},
...messages
],
temperature: 0,
max_tokens: 1200,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
}),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
}
});
const data = await response.json();
return data.choices[0].message.content;
}
const bot = new TelegramBot(bot_token, {
polling: true
});
bot.on('message', async (msg) => {
const chatId = msg.chat.id;
const message = msg.text;
const response = await getResponse([{
role: 'user',
content: message
}]);
if (response.startsWith('!')) {
const parsedMessage = parseMessage(response.slice(1));
const opts = {
reply_markup: JSON.stringify({
inline_keyboard: parsedMessage.buttons.map(button => [{
text: button,
callback_data: button
}])
})
};
bot.sendMessage(chatId, parsedMessage.text, opts);
} else {
const filteredData = filterData(response, usefulData);
const formattedData = filteredData.map(row => `${row[0]}: ${row[1]}`).join('\n');
const newResponse = await getResponse([{
role: 'user',
content: message
}, {
role: 'assistant',
content: response
}, {
role: 'user',
content: formattedData
}]);
if (newResponse.startsWith('!')) {
const parsedMessage = parseMessage(newResponse.slice(1));
const opts = {
reply_markup: JSON.stringify({
inline_keyboard: parsedMessage.buttons.map(button => [{
text: button,
callback_data: button
}])
})
};
bot.sendMessage(chatId, parsedMessage.text, opts);
} else {
bot.sendMessage(chatId, 'Ошибка');
}
}
});
bot.on('callback_query', async (callbackQuery) => {
const message = callbackQuery.data;
const chatId = callbackQuery.message.chat.id;
const response = await getResponse([{
role: 'user',
content: message
}]);
if (response.startsWith('!')) {
const parsedMessage = parseMessage(response.slice(1));
const opts = {
reply_markup: JSON.stringify({
inline_keyboard: parsedMessage.buttons.map(button => [{
text: button,
callback_data: button
}])
})
};
bot.sendMessage(chatId, parsedMessage.text, opts);
} else {
const filteredData = filterData(response, usefulData);
const formattedData = filteredData.map(row => `${row[0]}: ${row[1]}`).join('\n');
const newResponse = await getResponse([{
role: 'user',
content: message
}, {
role: 'assistant',
content: response
}, {
role: 'user',
content: formattedData
}]);
if (newResponse.startsWith('!')) {
const parsedMessage = parseMessage(newResponse.slice(1));
const opts = {
reply_markup: JSON.stringify({
inline_keyboard: parsedMessage.buttons.map(button => [{
text: button,
callback_data: button
}])
})
};
bot.sendMessage(chatId, parsedMessage.text, opts);
} else {
bot.sendMessage(chatId, 'Ошибка');
}
}
});
Проверяем 😋
[Как открыть карту в Дегродбанке|O_CARD][Как закрыть карту в Дегродбанке|C_CARD][Я не люблю Дегродбанк|HATE][Что такое Дегродбанк?|ABOUT]
Длина текста внутри [[]] не более 16 символов!
Предназначение: Помогать клиентам Дегродбанка.
[
[
"ABOUT",
"Дегродбанк – лучший банк страны: https://degrodbank.com"
],
[
"C_CARD",
"О том как закрыть карту можно узнать на официальном сайте Дегродбанка: https://degrodbank.com (но мы советуем этого не делать)"
],
[
"O_CARD",
"О том как открыть карту можно узнать на официальном сайте Дегродбанка: https://degrodbank.com"
],
[
"HATE",
"Если Вам не нравится Дегродбанк, лучшим решением будет убить себя"
]
]
Всё работает!
Модель прекрасно понимает контекст, а самое главное думает в нём! Не смотря на то, что у неё почти нет памяти на прошлые сообщения, создаётся полное ощущение того, что она есть.
Модель также полностью соблюдает инструкции из датасета, а точнее использует конструкцию «К примеру» не смотря на контекст (сделал я это для того, чтобы она не галлюцинировала ни при каких обстоятельствах).
При желании функционал можно расширить: сделать отправку картинок, добавить память для модели и т. д. Но я считаю – для FAQ бота текущая версия прекрасно подходит, а самое главное она очень экономичная в плане расхода токенов.