Оглавление

- Настройка
Инициализация проекта Ruby on Rails с PostgreSQL
Настройка PGVector
Настройка OpenAI
Создание простого чата с помощью Hotwired

- Прототип
Chat API
Разрушитель

- Внедрение
Фрагменты данных
Вектор
Как найти наиболее релевантные фрагменты

- "Краткое содержание"

Введение

В современном быстро меняющемся мире перед предприятиями стоит сложная задача предоставления точных и своевременных ответов на запросы пользователей. Будь то помощь клиентам, обмен технической документацией или просто обмен знаниями, потребность в надежной и эффективной системе для решения вопросов пользователей стала абсолютно необходимой. И вот здесь в игру вступает невероятная мощь чат-бота с искусственным интеллектом, подпитываемая специализированной базой знаний.

Имея опыт более 300 проектов по разработке программного обеспечения, в том числе несколько с интеграцией OpenAI, Rubyroid Labs пользуется доверием с 2013 года. Если вы ищете надежного партнера для беспрепятственной интеграции ChatGPT в ваше приложение Ruby on Rails, свяжитесь с нами сегодня.

Чат-бот с искусственным интеллектом, который может отвечать на вопросы на основе определенной базы знаний, является ценным активом для организаций, стремящихся автоматизировать взаимодействие с клиентами и улучшить общее взаимодействие с пользователем. В отличие от чат-ботов более общего назначения, эти чат-боты, основанные на знаниях, предназначены для предоставления точных и контекстуально релевантных ответов, используя тщательно подобранную базу данных.

Прелесть этого подхода заключается в возможности адаптировать ответы чат-бота к определенному домену или теме. Создавая базу знаний, включающую соответствующую информацию о продуктах, услугах, политиках или любой другой теме, чат-бот становится бесценным ресурсом для пользователей, ищущих конкретную информацию.

Вариантов использования таких чат-ботов, основанных на знаниях, множество. Например, компания электронной коммерции может создать чат-бота, который помогает клиентам с запросами о продуктах, доступности и информации о доставке. Точно так же образовательные учреждения могут использовать чат-ботов, чтобы отвечать на часто задаваемые вопросы о курсах, поступлении и объектах кампуса. Кроме того, есть много других случаев, некоторые из которых перечислены в другой нашей записи в блоге.

В нашем случае нас попросили разработать чат-бота для юридического консультирования. Поэтому он должен основывать свои ответы только на предоставленной базе знаний, и ответы должны быть очень конкретными. База знаний состоит из 1 миллиарда слов. Мы столкнулись со многими проблемами, и в этой статье мы покажем вам, как мы их решили.

В этой статье мы проведем вас через процесс настройки проекта Ruby on Rails, интеграции ChatGPT и создания функций для получения и использования базы знаний для ответов на вопросы пользователей. К концу у вас будут необходимые навыки для разработки собственного чат-бота, основанного на знаниях, адаптированного к конкретной области или теме вашей организации, который позволит пользователям получать точные и актуальные ответы на основе конкретной базы знаний, которую вы предоставляете.

Ради этого примера мы собираемся встроить информацию с сайта RubyroidLabs, тем самым чат-бот с искусственным интеллектом сможет отвечать на вопросы о компании.

Вот что мы собираемся построить:

Давайте перейдем к этому решению шаг за шагом.

Настраивать

Инициализировать проект Ruby on Rails с помощью PostgreSQL

Проверить среду

ruby --version # ruby 3.2.2
rails --version # Rails 7.0.5

Инициализировать проект Rails (документы)

rails new my_gpt --database=postgresql --css=tailwind
cd my_gpt

Настройка базы данных

Лучший способ установить PostgreSQL на MacOS — вообще не устанавливать его. Вместо этого просто запустите док-контейнер с требуемой версией PostgreSQL. Мы будем использовать образ ankane/pgvector, поэтому у нас будет предустановлено расширение pgvector.

docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name my_gpt_postgres ankane/pgvector

Добавьте это к config/database.yml в раздел default или development:

default: &default
  host: localhost
  username: postgres
  password: postgres

Затем инициализируйте структуру базы данных:

rake db:create
rake db:migrate

Запустите приложение

./bin/dev

Настройка PGVector

Для работы с PGVector мы будем использовать гем neighbor. Если вы запускаете PostgreSQL с Docker, как описано выше, нет необходимости устанавливать и собирать расширение PGVector. Итак, вы можете перейти к этому:

bundle add neighbor
rails generate neighbor:vector
rake db:migrate

Настройка OpenAI

Для вызовов API OpenAI мы будем использовать гем ruby-openai.

bundle add ruby-openai

Создайте файл config/initializers/openai.rb со следующим содержимым:

OpenAI.configure do |config|
  config.access_token =  Rails.application.credentials.openai.access_token
  config.organization_id = Rails.application.credentials.openai.organization_id
end

Добавьте свой ключ API OpenAI к учетным данным. Вы можете найти их в своем аккаунте OpenAI.

rails credentials:edit
openai:
  access_token: xxxxx
  organization_id: org-xxxxx

Создайте простой чат с Hotwired

Создайте контроллер вопросов app/controllers/questions_controller.rb:

class QuestionsController < ApplicationController
  def index
  end

  def create
    @answer = "I don't know."
  end

  private

  def question
    params[:question][:question]
  end
end

Добавьте маршруты к config/routes.rb:

resources :questions, only: [:index, :create]

Создайте макет чата в app/views/questions/index.html.erb:

<div class="w-full">
  <div class="h-48 w-full rounded mb-5 p-3 bg-gray-100">
    <%= turbo_frame_tag "answer" %>
  </div>

  <%= turbo_frame_tag "new_question", target: "_top" do %>
    <%= form_tag questions_path, class: 'w-full' do |f| %>
      <input type="text"
             class="w-full rounded"
             name="question[question]"
             placeholder="Type your question">
    <% end %>
  <% end %>
</div>

Отображение ответа с помощью турбопотока. Создайте файл app/views/questions/create.turbo_stream.erb и заполните его:

<%= turbo_stream.update('answer', @answer) %>

Готово 🎉 Откройте http://localhost:3000/questions и проверьте.

Опытный образец

Чат API

Начнем с самой простой и очевидной реализации — предоставим все наши данные ChatGPT и попросим его основывать свой ответ только на предоставленных данных. Хитрость здесь заключается в том, чтобы «сказать «я не знаю», если на вопрос нельзя ответить, исходя из контекста».

Итак, скопируем все данные со страницы услуги и прикрепим их как контекст.

context = <<~LONGTEXT
  RubyroidLabs custom software development services. We can build a website, web application, or mobile app for you using Ruby on Rails. We can also check your application for bugs, errors and inefficiencies as part of our custom software development services.

  Services:
  * Ruby on Rails development. Use our Ruby on Rails developers in your project or hire us to review and refactor your code.
  * CRM development. We have developed over 20 CRMs for real estate, automotive, energy and travel companies.
  * Mobile development. We can build a mobile app for you that works fast, looks great, complies with regulations and drives your business.
  * Dedicated developers. Rubyroid Labs can boost your team with dedicated developers mature in Ruby on Rails and React Native, UX/UI designers, and QA engineers.
  * UX/UI design. Rubyroid Labs can create an interface that will engage your users and help them get the most out of your application.
  * Real estate development. Rubyroid Labs delivers complex real estate software development services. Our team can create a website, web application and mobile app for you.
  * Technology consulting. Slash your tech-related expenses by 20% with our help. We will review your digital infrastructure and audit your code, showing you how to optimize it.
LONGTEXT

Сообщение для ChatGPT составлено так:

message_content = <<~CONTENT
  Answer the question based on the context below, and
  if the question can't be answered based on the context,
  say \"I don't know\".

  Context:
  #{context}

  ---

  Question: #{question}
CONTENT

Затем сделайте API-запрос к ChatGPT:

openai_client = OpenAI::Client.new
response = openai_client.chat(parameters: {
  model: "gpt-3.5-turbo",
  messages: [{ role: "user", content: message_content }],
  temperature: 0.5,
})
@answer = response.dig("choices", 0, "message", "content")

Нарушитель сделки

Дело в том, что у каждого Chat API или Completion API есть лимиты.

Для gpt-3.5-turbo по умолчанию это 4096 токенов. Давайте измерим, из скольких токенов состоят наши данные с помощью OpenAI Tokenizer:

Всего 276 токенов, не так уж и много. Однако это только с одной страницы. Всего у нас есть 300 тысяч токенов данных.

Что, если мы переключимся на gpt-4-32k? Он может обрабатывать до 32 768 токенов! Предположим, что для наших целей этого достаточно. Какова будет цена за один запрос? GPT-4 с контекстом 32K стоит $0,06/1K токенов. Таким образом, это $ 2+ за запрос.

Здесь в игру вступает встраивание.

Вложения

Блоки данных

Чтобы уложиться в лимиты или не тратить весь бюджет на 32 тыс. запросов, давайте предоставим ChatGPT самые актуальные данные. Для этого давайте разделим все данные на небольшие куски и сохраним их в базе данных PostgreSQL:

Теперь, основываясь на вопросе пользователя, нам нужно найти наиболее релевантный фрагмент в нашей базе данных. Здесь нам может помочь Embeddings API. Он получает текст и возвращает вектор (массив из 1536 чисел).

Таким образом, мы генерируем вектор для каждого чанка через Embeddings API и сохраняем его в БД.

response = openai_client.embeddings(
  parameters: {
    model: 'text-embedding-ada-002',
    input: 'Rubyroid Labs has been on the web and mobile...'
  }
)

response.dig('data', 0, 'embedding') # [0.0039921924, -0.01736092, -0.015491072, ...]

Вот так сейчас выглядит наша база данных:

Код:

rails g model Item page_name:string text:text embedding:vector{1536}
rake db:migrate

Миграция:

class CreateItems < ActiveRecord::Migration[7.0]
  def change
    create_table :items do |t|
      t.string :page_name
      t.text :text
      t.vector :embedding, limit: 1536

      t.timestamps
    end
  end
end

Модель:

class Item < ApplicationRecord
  has_neighbors :embedding
end

Задача Rake (lib/tasks/index_data.rake):

DATA = [
  ['React Native Development', 'Rubyroid Labs has been on the web and mobile...'],
  ['Dedicated developers', 'Rubyroid Labs can give you a team of dedicated d...'],
  ['Ruby on Rails development', 'Rubyroid Labs is a full-cycle Ruby on Rails...'],
  # ...
]

desc 'Fills database with data and calculate embeddings for each item.'
task index_data: :environment do
  openai_client = OpenAI::Client.new

  DATA.each do |item|
    page_name, text = item

    response = openai_client.embeddings(
      parameters: {
        model: 'text-embedding-ada-002',
        input: text
      }
    )

    embedding = response.dig('data', 0, 'embedding')

    Item.create!(page_name:, text:, embedding:)

    puts "Data for #{page_name} created!"
  end
end

Запускаем рейк-задачу:

rake index_data

Вектор

Что такое вектор? Проще говоря, вектор — это кортеж, или, другими словами, массив чисел. Например, [2, 3] . В двумерном пространстве это может относиться к точке на скалярной плоскости:

То же самое относится к трехмерным и более пространствам:

Если бы у нас были векторы 2d, а не векторы 1536d, мы могли бы отобразить их на скалярной плоскости следующим образом:

Как найти наиболее релевантные чанки

Итак, приложение получает следующий вопрос: «Как долго RubyroidLabs присутствует на рынке мобильного программного обеспечения?». Рассчитаем и его вектор.

response = openai_client.embeddings(
  parameters: {
    model: 'text-embedding-ada-002',
    input: 'How long has RubyroidLabs been on the mobile software market?'
  }
)

response.dig('data', 0, 'embedding') # [0.009017303, -0.016135506, 0.0013286859, ...]

И отобразить его на скалярной плоскости:

Теперь мы можем математически найти ближайшие векторы. Для этой задачи не требуется ИИ. Это то, для чего мы ранее настроили PGVector.

nearest_items = Item.nearest_neighbors(
  :embedding, question_embedding,
  distance: "euclidean"
)
context = nearest_items.first.text

А теперь просто поместите этот контекст в Chat API, как мы уже делали ранее.

message_content = <<~CONTENT
  Answer the question based on the context below, and
  if the question can't be answered based on the context,
  say \"I don't know\".

  Context:
  #{context}

  ---

  Question: #{question}
CONTENT

# a call to Chat API

Вот это 🎉

Наши ответы в чате основаны на всей предоставленной нами информации. Более того, он почти не тратит дополнительные деньги на вопрос, но дает лучший ответ. Однако вам придется заплатить один раз за расчет вложений при инициализации базы данных. За 300 тысяч токенов с Ada v2 это стоит всего 0,03 доллара.

Rubyroid Labs сотрудничает с компаниями по всему миру, чтобы интегрировать OpenAI в свою деятельность. Если вы хотите изменить свой чат-бот или другой диалоговый интерфейс, пожалуйста, свяжитесь с нами.

Краткое содержание

Подведем итоги:

  1. Разделите имеющиеся у вас данные на небольшие фрагменты. Вычислите вложение для каждого фрагмента.
  2. Сохраняйте фрагменты с соответствующими вложениями в векторную БД, например, PostgreSQL плюс PGVector.
  3. Инициализация приложения завершена. Теперь вы можете получить вопрос от пользователя. Рассчитайте вложение для этого вопроса.
  4. Получить кусок из БД с ближайшим вектором к вектору вопросов.
  5. Отправьте вопрос в Chat API, предоставив фрагмент из предыдущего шага.
  6. Получите ответ от Chat API и отобразите его пользователю 🎉

Полная логика чата вынесена в отдельный класс:

# frozen_string_literal: true

class AnswerQuestion
  attr_reader :question

  def initialize(question)
    @question = question
  end

  def call
    message_to_chat_api(<<~CONTENT)
      Answer the question based on the context below, and
      if the question can't be answered based on the context,
      say \"I don't know\".

      Context:
      #{context}

      ---

      Question: #{question}
    CONTENT
  end

  private

  def message_to_chat_api(message_content)
    response = openai_client.chat(parameters: {
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: message_content }],
      temperature: 0.5
    })
    response.dig('choices', 0, 'message', 'content')
  end

  def context
    question_embedding = embedding_for(question)
    nearest_items = Item.nearest_neighbors(
      :embedding, question_embedding,
      distance: "euclidean"
    )
    context = nearest_items.first.text
  end

  def embedding_for(text)
    response = openai_client.embeddings(
      parameters: {
        model: 'text-embedding-ada-002',
        input: text
      }
    )

    response.dig('data', 0, 'embedding')
  end

  def openai_client
    @openai_client ||= OpenAI::Client.new
  end
end

# AnswerQuestion.new("Yours question..").call

Что еще можно сделать для улучшения качества ответов:

  • Размер фрагмента. Найдите оптимальный размер фрагмента данных. Можно попробовать разбить их на мелкие, получить ближайшие N из базы и связать их в один контекст. И наоборот, вы можете попытаться создать большие фрагменты и получить только один — ближайший.
  • Длина контекста. С помощью gpt-3.5-turbo вы можете отправить 4096 токенов. С gpt-3.5-turbo-16k — 16 384 токена. С gpt-4-32k до 32 768 токенов. Найдите все, что соответствует вашим потребностям.
  • Модели. Существует множество моделей ИИ, которые можно использовать для встраивания или чата. В этом примере мы использовали gpt-3.5-turbo для чата и text-embedding-ada-002 для вложений. Вы можете попробовать разные.
  • Встраивания. OpenAI Embeddings API — не единственный способ расчета вложений. Существует множество других открытых и проприетарных моделей, которые могут вычислять вложения.