Большая часть шумихи вокруг LLM была сосредоточена на их способности доставлять полезный или интересный текст непосредственно конечному пользователю (например, 1, 2, 3, 4). Дискуссий вокруг их интеллекта и возможности наладить связи с ИИ предостаточно. Однако, когда все сказано и сделано, эти модели представляют собой функции, которые принимают текст в качестве входных данных и генерируют текст в качестве выходных (даже если способ, которым они генерируют текст, сильно отличается от других функций).

Генерация текста — непростая задача. Философия Unix рассматривает текстовые потоки как универсальный интерфейс и подчеркивает компонуемость процессов, которые воздействуют на текстовые потоки. Именно это и делают LLM: они воздействуют на текстовые потоки и возвращают их. И мы действительно можем использовать их совместно с другими приложениями. Они не ограничиваются возвратом текста конечному пользователю.

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

В этом посте мы обсудим интеграцию LLM с другими инструментами, а не их использование в качестве простого сервиса ответов на вопросы (или перевода текста). Мы приводим ряд простых примеров различных способов взаимодействия LLM с другими инструментами, особенно в контексте преобразования текста в SQL. Затем мы показываем, как библиотека LangChain, в которой уже есть набор инструментов для подключения LLM друг к другу и другим процессам (и большое и заинтересованное сообщество), может быть использована для расширения возможностей LLM по взаимодействию с базами данных. (перейти в раздел).

Как заставить LLM работать с другими процессами

Мы можем значительно повысить полезность LLM — и уменьшить потребность в участии человека на каждом этапе процесса — отправляя их выходные данные другим инструментам и, наоборот, отправляя выходные данные других инструментов LLM. Это может работать в нескольких направлениях:

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

Каждый из этих процессов может расширить возможности LLM для преобразования текста в SQL (и для других целей). Мы кратко приведем примеры каждого из них.

Для каждого примера для LLM мы будем использовать ChatGPT, настроенный следующим образом (адаптировано из сообщения в блоге Саймона Уиллисона по теме).

class PostgresBot:
   def __init__(self, system="You are a text-to-PostgreSQL translator. You translate natural language to syntactically-correct SQL."):
       self.system = system
       self.messages = []
       if self.system:
           self.messages.append({"role": "system", "content": system})
  
   def __call__(self, message, return_message:bool = True):
       self.messages.append({"role": "user", "content": message})
       result = self.execute()
       self.messages.append({"role": "assistant", "content": result})


       if return_message:
           return result
  
   def execute(self):
       completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=self.messages)
       return completion.choices[0].message.content

Мы также используем функцию для извлечения SQL из вывода LLM на тот случай, если результаты ChatGPT содержат какой-либо текст, отличный от SQL:

def extract_sql(message:str) -> str:

   # compile a pattern that matches SQL code between ```sql and ```
   pattern = re.compile(r"```(?:sql)?(.*?)```", re.DOTALL)

   # find all matches in the message
   matches = pattern.findall(message)

   # check if there are any matches
   if matches:
       # join all matches with a newline character
       sql_code = "\n".join(matches)
       return sql_code
   else:
       return message

Имея эти основы, давайте рассмотрим несколько примеров использования LLM в сочетании с другими процессами, а не просто возвратом текста пользователю.

Передача вывода LLM другому процессу

Вот пример функции, которая передает приглашение в ChatGPT, генерирует SQL, выполняет его и возвращает результат, возможно сохраняя для пользователя копирование/вставку:

def generate_and_execute(llm, prompt, pool, commit=False):
  """Generates and executes SQL and then returns the results"""
  sql = extract_sql(llm(prompt))


  with pool.connection() as conn:
      results = conn.execute(sql)
      output = results.fetchall()
      if commit:
          conn.commit()
      else:
          conn.rollback()
  conn.close()
  return sql, output

Мы можем запустить это следующим образом (передав подсказку с некоторыми деталями схемы):

user_prompt = """
Table = "film_actor", columns = [actor_id smallint, film_id smallint, last_update timestamp without time zone]
Table = "actor", columns = [actor_id integer, first_name character varying, last_name character varying, last_update timestamp without time zone]

What are the names of the five actors who were in the most films?"""

prompt_template = "Translate the following to syntactically-correct PostgreSQL. Only return SQL: {user_prompt}"
full_prompt = prompt_template.format(user_prompt=user_prompt)

SQLBot = PostgresBot()
sql, output = generate_and_execute(SQLBot,
   full_prompt,
   pool=psycopg_pool.ConnectionPool(pg_string),
   commit=False)

Что возвращает:

# SQL ==============================================================
SELECT actor.first_name
     , actor.last_name
     , count(*) AS film_count
FROM actor
     INNER JOIN film_actor ON actor.actor_id = film_actor.actor_id
GROUP BY actor.first_name, actor.last_name
ORDER BY film_count DESC
LIMIT 5

# Results ==========================================================
[('Susan', 'Davis', 54), ('Gina', 'Degeneres', 42), ('Walter', 'Torn', 41), ('Mary', 'Keitel', 40), ('Matthew', 'Carrey', 39)]

Есть много других случаев, когда мы можем захотеть передать выходные данные LLM другому процессу при преобразовании текста в SQL. Например:

  • Мы могли бы передать вывод средству форматирования SQL, такому как pglast.prettify, или проверить его с помощью синтаксического анализатора, такого как pglast.parser.parse_sql.
  • Мы можем захотеть передать сгенерированный SQL планировщику заданий или системе автоматизации задач, что позволит LLM генерировать задачи, которые запускаются в определенное время или при определенных условиях.

Передача результатов другого процесса в LLM

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

selct * -- a typo!
from penguins
where island='Biscoe';

Мы можем попробовать разобрать запрос с помощью pglast:

from pglast.parser import parse_sql

try:
   parse_sql(query="selct * from penguins where island='Biscoe;")
except Exception as e:
   err = e
print(err)

Что возвращает:

syntax error at or near “selct”, at index 0

Посмотрим, сможет ли ChatGPT исправить ошибку. Мы можем определить функцию коррекции SQL, используя LLM:

def fix_sql(llm, sql, err):
   prompt = f"""The following SQL has an error:
   {sql}.

   The error was {err}.
  
   return revised syntactically-correct PostgreSQL that fixes the error.
   Make sure to demarcate SQL in ```SQL"""

   return extract_sql(llm(prompt))

И выполнить его следующим образом:

SQLBot = PostgresBot()
correct_sql = fix_sql(SQLBot, sql="selct * from penguins where island='Biscoe;",
       err = err)

Который возвращает исправленный SQL:

SELECT *
FROM penguins
WHERE island = 'Biscoe';

Это легко распространить на другие приложения (особенно в области отладки). Только в области преобразования текста в SQL вы можете:

  • Передайте сообщения об ошибках с сервера Postgres на LLM и запросите исправленный SQL
  • Вместо того, чтобы передавать приглашение пользователя непосредственно в LLM, сначала предварительно обработайте его на предмет орфографии и грамматики и передайте обработанный текст в LLM.
  • Передайте результаты выполненного запроса LLM и запросите предложения для дальнейших исследований или четко отформатированную таблицу результатов.

Передача результатов LLM обратно в LLM

Иногда нам может понадобиться LLM для самоанализа или повторения собственной работы (или нам может понадобиться, чтобы один LLM воздействовал на результаты другого). Например, мы можем попросить модель сгенерировать некоторый SQL на основе подсказки, а затем попросить ее сравнить сгенерированный SQL с подсказкой и определить, совпадают ли подсказка и SQL.

Мы можем запустить следующее:

user_prompt = """Table = "film", columns = [film_id integer, title character varying, description text, release_year integer, language_id smallint, rental_duration smallint, rental_rate numeric, length smallint, replacement_cost numeric, rating USER-DEFINED, last_update timestamp without time zone, special_features ARRAY, fulltext tsvector]
Table = "film_actor", columns = [actor_id smallint, film_id smallint, last_update timestamp without time zone]
Table = "actor", columns = [actor_id integer, first_name character varying, last_name character varying, last_update timestamp without time zone]
Table = "language", columns = [language_id integer, name character, last_update timestamp without time zone]


Who is the oldest actor in the dataset?
"""


prompt_template = "Translate the following to syntactically-correct PostgreSQL. Only return SQL: {user_prompt}"
full_prompt = prompt_template.format(user_prompt=user_prompt)


SQLBot = PostgresBot()
sql = extract_sql(SQLBot(full_prompt))

Таким образом, получаем следующий SQL:

SELECT actor.first_name
     , actor.last_name
FROM actor
     INNER JOIN film_actor ON actor.actor_id = film_actor.actor_id
     INNER JOIN film ON film_actor.film_id = film.film_id
ORDER BY actor.last_update
LIMIT 1;

Затем мы можем попросить немного самоанализа со следующей функцией:

def compare_sql_prompt(llm, sql, prompt):
   prompt = f"""You generated the following sql:
  
   {sql}
  
   Based on this prompt:
  
   {prompt}.
  
   Critically compare the two: does the generated SQL successfully address the prompt? Be mindful of the following:
   - the user might not be familiar with the data. Don't assume they know what can reasonably be asked of the data.
   - can the prompt actually be answered with the data?
   """


   return llm(prompt)

И выполните его с помощью:

SQLBot = PostgresBot("You are a SQL checker. You make sure SQL is able to meet a user's request.")

print(compare_sql_prompt(SQLBot, sql, full_prompt))

Что возвращает:

The generated SQL does not successfully address the prompt as it is not
answering the given question of “Who is the oldest actor in the dataset?”.
The generated SQL only retrieves the first actor and orders the results based
on the last update date. Additionally, the SQL doesn’t include any filter or
criteria to determine the oldest actor.

To answer the prompt, we would need to modify the SQL query to retrieve and
filter the birthdates of the actors from the "actor" table and sort them in
ascending order to find the oldest actor. Therefore, the generated SQL is not
useful in this scenario.

Транслятор SQL будет (почти) всегда пытаться вернуть SQL. Мы можем использовать LLM, чтобы быстро проанализировать SQL и приглашение и определить, согласованы ли SQL и приглашение. В области преобразования текста в SQL мы также можем передать результаты LLM обратно LLM и попросить их:

  • Дайте подробные объяснения результатов сгенерированного SQL
  • Проверьте сгенерированный SQL на распространенные ошибки и при необходимости исправьте
  • Создайте альтернативные SQL-запросы на основе той же подсказки и выберите лучший вариант.
  • Предлагайте улучшения для сгенерированного SQL на основе различных приоритетов (скорость, ясность и т. д.)

Объединение LLM и других инструментов с помощью LangChain

Langchain — один из самых продвинутых на сегодняшний день проектов по интеграции LLM с другими инструментами. Их документы говорят:

…использования этих LLM по отдельности часто недостаточно для создания действительно мощного приложения — настоящая мощь приходит, когда вы можете комбинировать их с другими источниками вычислений или знаний.

LangChain предоставляет готовый доступ к методам для выполнения многих задач, описанных выше. Проект также направлен на создание стандартного словаря различных способов взаимодействия LLM с другими процессами.

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

В библиотеке LangChain уже есть SQLDatabase Chain для работы с базами данных. Вот краткая демонстрация цепочки, которая (1) определяет, какие строки запрашивать; (2) разрабатывает запрос для ответа на запрос пользователя; (3) выполняет запрос к базе данных; и (4) объясняет результаты запроса простым языком. Эта конкретная цепочка называется SQLDatabaseSequentialChain и полезна для баз данных с большим количеством таблиц — отправка всех сведений о схеме базы данных в LLM может быть дорогостоящей или невозможной в зависимости от количества используемых токенов.

import os
from langchain import SQLDatabase
from langchain.chat_models import ChatOpenAI
from langchain.chains import SQLDatabaseSequentialChain

pg_string=os.getenv("PG_STRING") # connection string

# set up the database connection
db = SQLDatabase.from_uri(pg_string)

# specify the LLM we want to use (ChatGPT)
llm = ChatOpenAI(temperature=0)

# set up the chain
db_chain = SQLDatabaseSequentialChain.from_llm(llm, db, verbose=True)

Теперь мы можем делать простые запросы к базе данных, и цепочка выберет правильную таблицу или таблицы (даже если мы не назовем их правильно), запустим запрос и вернем результаты на естественном языке. Вы можете попробовать это на bit.io — просто получите строку подключения к PostgreSQL из меню Подключение.

Например:

db_chain(“How many Italian-language films are in the films data?”)

Возвращает:

> Entering new SQLDatabaseSequentialChain chain…
Table names to use:
['film', 'language', 'film_category']

> Entering new SQLDatabaseChain chain…
How many films are in Italian?
SQLQuery:SELECT COUNT(*) FROM film WHERE language_id = 2;
SQLResult: [(0,)]
Answer:There are 0 films in Italian.

> Finished chain.

LLM сначала рассмотрел схему базы данных и определил правильные таблицы для использования (хотя на самом деле в film_category не было необходимости), а затем выполнил запрос и вернул результаты. Эта простая цепочка на самом деле использовала все три шаблона, которые мы представили выше:

  • Результаты SQL-запроса передавались LLM, чтобы LLM мог определить, какие таблицы использовать (другой процесс -> LLM).
  • Результаты от LLM (выбор таблиц) были переданы LLM для генерации SQL. (LLM -> LLM)
  • Вывод SQL был выполнен в базе данных. (LLM -> другой процесс).

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

Что дальше?

Наша работа по преобразованию текста в SQL до сих пор включала отправку вопроса или инструкции в LLM и передачу ответа SQL обратно пользователю для выполнения или редактирования. Для такого подхода есть веские причины: вы не хотите выполнять непроверенный код и непреднамеренно удалять или изменять данные в своей базе данных.

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

LLM ошеломили и впечатлили нас тем, что вернули привлекательный и полезный текст непосредственно пользователям. Но, как показали примеры в этой статье и постоянно растущая коллекция утилит в проекте LangChain, в сочетании с другими инструментами они могут сделать гораздо больше.

Следите за нашим проектом с открытым исходным кодом, чтобы узнать о новых разработках в области преобразования текста в SQL, и следите за этой лентой, чтобы узнать обо всех наших материалах по этой теме. Есть ли у вас другие идеи о полезных способах использования LLM в сочетании с другими инструментами? Присоединяйтесь к нашему сообществу и дайте нам знать!