Image

Агентный ИИ из книги «Первые принципы: Размышления»

От теории к коду: создание циклов обратной связи, повышающих точность LLM

Делиться

8bc0ff76bcd3e9a503ea1b8c730a27c6

Третий закон Артура Кларка гласит, что «любая достаточно продвинутая технология неотличима от магии». Именно так воспринимаются многие современные фреймворки для ИИ. Такие инструменты, как GitHub Copilot, Claude Desktop, OpenAI Operator и Perplexity Comet, автоматизируют повседневные задачи, которые ещё пять лет назад казались невозможными для автоматизации. Ещё более удивительно то, что всего несколькими строками кода мы можем создавать собственные сложные инструменты ИИ: те, которые ищут файлы, просматривают веб-страницы, переходят по ссылкам и даже совершают покупки. Это действительно похоже на магию.

Хотя я искренне верю в волшебников данных, я не верю в магию. Мне интересно (и часто полезно) понимать, как всё устроено на самом деле и что происходит «под капотом». Именно поэтому я решил поделиться серией постов о концепциях проектирования агентного ИИ, которые помогут вам понять, как на самом деле работают все эти волшебные инструменты.

Чтобы глубже разобраться в этом вопросе, мы создадим с нуля многоагентную систему с ИИ. Мы откажемся от использования фреймворков вроде CrewAI или smolagents и будем работать напрямую с API базовой модели. По ходу дела мы рассмотрим фундаментальные шаблоны проектирования агентов: рефлексию, использование инструментов, планирование и многоагентные конфигурации. Затем мы объединим все эти знания для создания многоагентной системы с ИИ, способной отвечать на сложные вопросы, связанные с данными.

Как сказал Ричард Фейнман: «То, чего я не могу создать, я не понимаю». Итак, начнём строить! В этой статье мы сосредоточимся на шаблоне проектирования «Отражение». Но сначала давайте разберёмся, что же такое отражение.

Что такое отражение?

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

Или возьмём другой пример: написание SQL-запроса. Я либо пишу его шаг за шагом, проверяя промежуточные результаты, либо (если это достаточно просто) сразу составляю черновик, выполняю, смотрю на результат (на наличие ошибок или на соответствие ожиданиям), а затем корректирую запрос на основе этой обратной связи. Я могу перезапустить его, проверить результат и повторять итерации, пока не добьюсь нужного результата.

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

a67ab064a3e084b8fa70eae415ea22ba

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

Существует множество исследований, показывающих пользу размышлений:

  • « Самоусовершенствование: итеративное совершенствование с самоотдачей » Мадаан и др. (2023) показали, что самосовершенствование повысило производительность примерно на 20% при выполнении различных задач — от создания диалоговых ответов до математических рассуждений.
c27e02f0fa203de0b9f0a4d851262557
  • В работе « Reflexion: языковые агенты с обучением с вербальным подкреплением » (2023) авторы добились точности 91% pass@1 в тесте кодирования HumanEval, превзойдя предыдущий передовой тест GPT-4, который набрал всего 80%. Они также обнаружили, что Reflexion значительно превосходит все базовые подходы в тесте HotPotQA (набор данных вопросов и ответов на основе Википедии, который предлагает агентам анализировать контент и делать выводы на основе множества вспомогательных документов).
0ef7c84fde91098ae7ab4b6fcd227b92
  • В статье « КРИТИК: Большие языковые модели могут самокорректироваться с помощью интерактивной критики с помощью инструментов » Гоу и др. (2024) фокусируется на влиянии внешней обратной связи, позволяющей магистрам права использовать внешние инструменты для проверки и корректировки собственных результатов. Этот подход показал повышение точности на 10–30% при выполнении различных задач, от ответов на вопросы в свободной форме до решения математических задач.

Рефлексия особенно эффективна в агентных системах, поскольку ее можно использовать для коррекции курса на многих этапах процесса:

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

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

Отражение в рамках

Поскольку рефлексия, несомненно, приносит пользу ИИ-агентам, она широко используется в популярных фреймворках. Давайте рассмотрим несколько примеров.

Идея рефлексии была впервые предложена в статье «ReAct: синергия рассуждений и действий в языковых моделях» Яо и соавторов (2022). ReAct — это фреймворк, сочетающий чередующиеся этапы рассуждения (рефлексии посредством явных следов мыслей) и действия (действий, соответствующих задаче). В этом фреймворке рассуждение направляет выбор действий, а действия порождают новые наблюдения, которые формируют дальнейшие рассуждения. Сам этап рассуждения представляет собой сочетание рефлексии и планирования.

Эта структура стала довольно популярной, поэтому теперь существует несколько готовых реализаций, таких как:

  • В фреймворке DSPy от Databricks есть класс ReAct,
  • В LangGraph вы можете использовать функцию create_react_agent,
  • Агенты кода в библиотеке smolagents от HuggingFace также основаны на архитектуре ReAct.

Размышления с нуля

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

В качестве примера мы используем преобразование текста в SQL: зададим вопрос LLM и вернем корректный SQL-запрос. Мы будем работать с набором данных о задержках рейсов и диалектом SQL ClickHouse.

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

Прямая генерация

Начнем с самого простого подхода — прямой генерации, когда мы просим LLM сгенерировать SQL-код, который отвечает на запрос пользователя.

pip install anthropic

Нам необходимо указать API-ключ для Anthropic API.

импорт os os.environ['ANTHROPIC_API_KEY'] = config['ANTHROPIC_API_KEY']

Следующий шаг — инициализация клиента, и все готово.

импорт антропного клиента = антропный.Антропный()

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

base_sql_system_prompt = ''' Вы являетесь старшим разработчиком SQL, и ваша задача — помочь сгенерировать SQL-запрос на основе требований пользователя. Вы работаете с базой данных ClickHouse. Укажите формат (Tab Separated With Names) в выводе SQL-запроса, чтобы гарантировать включение имен столбцов в вывод. Не используйте count(*) в своих запросах, так как это плохая практика для столбчатых баз данных, вместо этого используйте count(). Убедитесь, что запрос синтаксически верный и оптимизирован для производительности, принимая во внимание специфические особенности ClickHouse (например, что ClickHouse является столбчатой базой данных и поддерживает такие функции, как ARRAY JOIN, SAMPLE и т. д.). Возвращайте только SQL-запрос без дополнительных пояснений или комментариев. Вы будете работать с таблицей flight_data, которая имеет следующую схему: Имя столбца | Тип данных | Null % | Пример значения | Описание — | — | — | — | — year | Int64 | 0.0 | 2024 | Год полета месяц | Int64 | 0.0 | 1 | Месяц полета (1–12) day_of_month | Int64 | 0.0 | 1 | День месяца day_of_week | Int64 | 0.0 | 1 | День недели (1=понедельник … 7=воскресенье) fl_date | datetime64[ns] | 0.0 | 2024-01-01 00:00:00 | Дата полета (ГГГГ-ММ-ДД) op_unique_carrier | object | 0.0 | 9E | Уникальный код перевозчика op_carrier_fl_num | float64 | 0.0 | 4814.0 | Номер рейса для указания авиакомпании origin | object | 0.0 | JFK | Код аэропорта отправления origin_city_name | object | 0.0 | «New York, NY» | Название города отправления origin_state_nm | object | 0.0 | Нью-Йорк | Название штата отправления dest | object | 0.0 | DTW | Код аэропорта назначения dest_city_name | object | 0.0 | «Detroit, MI» | Название города назначения dest_state_nm | object | 0.0 | Мичиган | Название штата назначения crs_dep_time | Int64 | 0.0 | 1252 | Запланированное время отправления (местное, ччмм) dep_time | float64 | 1.31 | 1247.0 | Фактическое время отправления (местное, ччмм) dep_delay | float64 | 1.31 | -5.0 | Задержка отправления в минутах (отрицательная, если раньше) taxi_out | float64 | 1.35 | 31.0 | Время выруливания в минутах wheels_off | float64 | 1.35 | 1318.0 | Время снятия колес (местное, ччмм) wheels_on | float64 | 1.38 | 1442.0 | Время подачи колес (местное, ччмм) taxi_in | float64 | 1.38 | 7.0 | Время прибытия руля в минутах crs_arr_time | Int64 | 0.0 | 1508 | Запланированное время прибытия (местное, ччмм) arr_time | float64 | 1.38 | 1449.0 | Фактическое время прибытия (местное, ччмм) arr_delay | float64 | 1.61 | -19.0 | Задержка прибытия в минутах (отрицательная, если раньше) cancellation | int64 | 0.0 | 0 | Индикатор отмененного рейса (0=Нет, 1=Да) cancellation_code | object | 98.64 | B | Причина отмены (если отменен) diverted | int64 | 0.0 | 0 | Индикатор измененного рейса (0=Нет, 1=Да) crs_elapsed_time | float64 | 0.0 | 136.0 | Запланированное прошедшее время в минутах actual_elapsed_time | float64 | 1.61 | 122.0 | Фактическое прошедшее время в минутах air_time | float64 | 1.61 | 84.0 | Время полета в минутах distance | float64 | 0.0 | 509.0 | Расстояние между пунктом отправления и пунктом назначения (мили) carrier_delay | int64 | 0.0 | 0 | Задержка, связанная с перевозчиком, в минутах weather_delay | int64 | 0.0 | 0 | Задержка, связанная с погодой, в минутах nas_delay | int64 | 0.0 | 0 | Задержка Национальной авиационной системы в минутах security_delay | int64 | 0.0 | 0 | Задержка по соображениям безопасности в минутах late_aircraft_delay | int64 | 0.0 | 0 | Задержка позднего самолета в минутах ''' def generate_direct_sql(rec): # выполнение вызова LLM message = client.messages.create( model = «claude-3-5-haiku-latest», # я выбрал меньшую модель, чтобы нам было легче увидеть влияние max_tokens = 8192, system=base_sql_system_prompt, messages = [ {'role': 'user', 'content': rec['question']} ] ) sql = message.content[0].text # очистка вывода if sql.endswith('«`'): sql = sql[:-3] if sql.startswith('«`sql'): sql = sql[6:] return sql

Вот и всё. Теперь давайте протестируем наше решение для преобразования текста в SQL. Я создал небольшой набор из 20 пар вопросов и ответов, которые мы можем использовать для проверки корректности работы нашей системы. Вот один пример:

{ 'вопрос': 'Какова была самая высокая скорость в милях в час?', 'ответ': ''' выберите max(distance / (air_time / 60)) as max_speed from flight_data where air_time > 0 format TabSeparatedWithNames''' }

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

# загрузка набора оценок с помощью open('./data/flight_data_qa_pairs.json', 'r') as f: qa_pairs = json.load(f) qa_pairs_df = pd.DataFrame(qa_pairs) tmp = [] # выполнение LLM для каждого вопроса в нашем наборе оценок for rec in tqdm.tqdm(qa_pairs_df.to_dict('records')): llm_sql = generate_direct_sql(rec) tmp.append( { 'id': rec['id'], 'llm_direct_sql': llm_sql } ) llm_direct_df = pd.DataFrame(tmp) direct_result_df = qa_pairs_df.merge(llm_direct_df, on = 'id')

Теперь у нас есть ответы, и следующим шагом будет измерение качества.

Измерение качества

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

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

  • Во-первых, мы воспользуемся объективными критериями, чтобы проверить, был ли указан правильный формат в SQL (мы указали LLM использовать TabSeparatedWithNames).
  • Во-вторых, мы можем выполнить сгенерированный запрос и посмотреть, возвращает ли ClickHouse ошибку выполнения.
  • Наконец, мы можем создать судью LLM, который сравнивает выходные данные сгенерированного запроса с нашим эталонным ответом и проверяет, отличаются ли они.

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

CH_HOST = 'http://localhost:8123' # импорт адреса по умолчанию requests import pandas as pd import tqdm # функция для выполнения SQL-запроса def get_clickhouse_data(query, host = CH_HOST, connection_timeout = 1500): r = requests.post(host, params = {'query': query}, timeout = connection_timeout) if r.status_code == 200: return r.text else: return 'База данных вернула следующую ошибку:n' + r.text # получение результатов выполнения SQL direct_result_df['llm_direct_output'] = direct_result_df['llm_direct_sql'].apply(get_clickhouse_data) direct_result_df['answer_output'] = direct_result_df['answer'].apply(get_clickhouse_data)

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

llm_judge_system_prompt = ''' Вы старший аналитик, и ваша задача — сравнить два результата SQL-запроса и определить, эквивалентны ли они. Сосредоточьтесь только на данных, возвращаемых запросами, игнорируя любые различия в форматировании. Примите во внимание исходный запрос пользователя и информацию, необходимую для ответа на него. Например, если пользователь запросил среднее расстояние, и оба запроса возвращают одно и то же среднее значение, но в одном из них есть также количество записей, вы должны считать их эквивалентными, поскольку оба предоставляют одинаковую запрошенную информацию. Ответьте с помощью JSON следующей структуры: { 'reasoning': '<ваше обоснование здесь, 1-3 предложения о том, почему вы считаете, что они эквивалентны или нет>', 'equivalence': } Убедитесь, что в выводе есть ТОЛЬКО JSON. Вы будете работать с таблицей flight_data, которая имеет следующую схему: Имя столбца | Тип данных | Null % | Пример значения | Описание — | — | — | — | — год | Int64 | 0.0 | 2024 | Год полета month | Int64 | 0.0 | 1 | Месяц полета (1–12) day_of_month | Int64 | 0.0 | 1 | День месяца day_of_week | Int64 | 0.0 | 1 | День недели (1=понедельник … 7=воскресенье) fl_date | datetime64[ns] | 0.0 | 2024-01-01 00:00:00 | Дата полета (ГГГГ-ММ-ДД) op_unique_carrier | object | 0.0 | 9E | Уникальный код перевозчика op_carrier_fl_num | float64 | 0.0 | 4814.0 | Номер рейса для указания авиакомпании origin | object | 0.0 | JFK | Код аэропорта отправления origin_city_name | object | 0.0 | «Нью-Йорк, Нью-Йорк» | Название города отправления origin_state_nm | object | 0.0 | Нью-Йорк | Название штата отправления dest | object | 0.0 | DTW | Код аэропорта назначения dest_city_name | object | 0.0 | «Detroit, MI» | Название города назначения dest_state_nm | object | 0.0 | Мичиган | Название штата назначения crs_dep_time | Int64 | 0.0 | 1252 | Запланированное время отправления (местное, ччмм) dep_time | float64 | 1.31 | 1247.0 | Фактическое время отправления (местное, ччмм) dep_delay | float64 | 1.31 | -5.0 | Задержка отправления в минутах (отрицательная, если раньше) taxi_out | float64 | 1.35 | 31.0 | Время выруливания в минутах wheels_off | float64 | 1.35 | 1318.0 | Время снятия колес (местное, ччмм) wheels_on | float64 | 1.38 | 1442.0 | Время установки колес (местное, ччмм) taxi_in | float64 | 1.38 | 7.0 | Время прибытия руля в минутах crs_arr_time | Int64 | 0.0 | 1508 | Запланированное время прибытия (местное, ччмм) arr_time | float64 | 1.38 | 1449.0 | Фактическое время прибытия (местное, ччмм) arr_delay | float64 | 1.61 | -19.0 | Задержка прибытия в минутах (отрицательная, если раньше) cancellation | int64 | 0.0 | 0 | Индикатор отмененного рейса (0=Нет, 1=Да) cancellation_code | object | 98.64 | B | Причина отмены (если отменен) diverted | int64 | 0.0 | 0 | Индикатор измененного рейса (0=Нет, 1=Да) crs_elapsed_time | float64 | 0.0 | 136.0 | Запланированное прошедшее время в минутах actual_elapsed_time | float64 | 1.61 | 122.0 | Фактическое прошедшее время в минутах air_time | float64 | 1.61 | 84.0 | Время полета в минутах distance | float64 | 0.0 | 509.0 | Расстояние между пунктом отправления и пунктом назначения (мили) carrier_delay | int64 | 0.0 | 0 | Задержка, связанная с перевозчиком, в минутах weather_delay | int64 | 0.0 | 0 | Задержка, связанная с погодой, в минутах nas_delay | int64 | 0.0 | 0 | Задержка Национальной авиационной системы в минутах security_delay | int64 | 0.0 | 0 | Задержка по соображениям безопасности в минутах late_aircraft_delay | int64 | 0.0 | 0 | Задержка позднего самолета в минутах ''' llm_judge_user_prompt_template = ''' Вот начальный запрос пользователя: {user_query} Вот запрос SQL, сгенерированный первым аналитиком: SQL: {sql1} Вывод базы данных: {result1} Вот запрос SQL, сгенерированный вторым аналитиком: SQL: {sql2} Вывод базы данных: {result2} ''' def llm_judge(rec, field_to_check): # создаем приглашение пользователя user_prompt = llm_judge_user_prompt_template.format( user_query = rec['question'], sql1 = rec['answer'], result1 = rec['answer_output'], sql2 = rec[field_to_check + '_sql'], result2 = rec[field_to_check + '_output'] ) # выполняем вызов LLM message = client.messages.create( model = «claude-sonnet-4-5», max_tokens = 8192, temperature = 0.1, system = llm_judge_system_prompt, messages=[ {'role': 'user', 'content': user_prompt} ] ) data = message.content[0].text # Удаляем блоки кода markdown data = data.strip() if data.startswith('«`json'): data = data[7:] elif data.startswith('«`'): data = data[3:] if data.endswith('«`'): data = data[:-3] data = data.strip() return json.loads(data)

Теперь давайте запустим судью LLM, чтобы получить результаты.

tmp = [] for rec in tqdm.tqdm(direct_result_df.to_dict('records')): try: judgment = llm_judge(rec, 'llm_direct') except Exception as e: print(f»Ошибка обработки записи {rec['id']}: {e}») continue tmp.append( { 'id': rec['id'], 'llm_judge_reasoning': judgment['reasoning'], 'llm_judge_equivalence': judgment['equivalence'] } ) judge_df = pd.DataFrame(tmp) direct_result_df = direct_result_df.merge(judge_df, on = 'id')

Давайте рассмотрим один пример, чтобы понять, как работает судья LLM.

# запрос пользователя В 2024 году какой процент времени все самолеты провели в воздухе? # правильный ответ select (sum(air_time) / sum(actual_elapsed_time)) * 100 as percentage_in_air where year = 2024 from flight_data format TabSeparatedWithNames percentage_in_air 81.43582596894757 # сгенерированный LLM ответ SELECT round(sum(air_time) / (sum(air_time) + sum(taxi_out) + sum(taxi_in)) * 100, 2) as air_time_percentage FROM flight_data WHERE year = 2024 FORMAT TabSeparatedWithNames air_time_percentage 81.39 # ответ судьи LLM { 'reasoning': 'Оба запроса вычисляют процент времени, проведенного самолетами в воздухе, но используют разные знаменатели. В первом запросе используется значение actual_elapsed_time (которое включает время в воздухе + время вылета + время прилета + любые задержки на земле), а во втором — только (время в воздухе + время прилета + время прилета). Второй запрос точнее отвечает на вопрос «время, проведенное самолетами в воздухе», поскольку не учитывает задержки на земле. Однако результаты очень близки (81,44% против 81,39%), что свидетельствует о минимальном влиянии. Это существенно разные подходы, которые дают схожие результаты. 'equivalence': FALSE }

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

def get_llm_accuracy(sql, output, equivalence): challenges = [] if 'format tabseparatedwithnames' not in sql.lower(): challenges.append('В SQL не указан формат') if 'База данных вернула следующую ошибку' in output: challenges.append('Ошибка выполнения SQL') if not equivalence and ('Ошибка выполнения SQL' not in challenges): challenges.append('Предоставлен неправильный ответ') if len(problems) == 0: return 'Проблем не обнаружено' else: return ' + '.join(problems) direct_result_df['llm_direct_sql_quality_heuristics'] = direct_result_df.apply( lambda row: get_llm_accuracy(row['llm_direct_sql'], row['llm_direct_output'], row['llm_judge_equivalence']), axis=1)

LLM выдал правильный ответ в 70% случаев, что неплохо. Но определённо есть куда стремиться, поскольку часто он либо выдаёт неверный ответ, либо неправильно указывает формат (иногда приводя к ошибкам выполнения SQL).

2819661078f70ef436c667ffccf4ac7d

Добавление шага отражения

Чтобы улучшить качество нашего решения, давайте попробуем добавить этап рефлексии, на котором мы попросим модель проанализировать и уточнить свой ответ.

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

simple_reflection_user_prompt_template = ''' Ваша задача — оценить SQL-запрос, сгенерированный другим аналитиком, и предложить улучшения при необходимости. Проверьте синтаксическую правильность запроса и его производительность. Обратите внимание на нюансы в данных (особенно на типы временных меток, следует ли использовать общее прошедшее время или время в воздухе и т. д.). Убедитесь, что запрос точно отвечает на первоначальный вопрос пользователя. В качестве результата верните следующий JSON: {{ 'reasoning': '<ваше обоснование здесь, 2-4 предложения о том, почему вы внесли изменения или нет>', 'refined_sql': '<улучшенный SQL-запрос здесь>' }} Убедитесь, что в выходных данных присутствует ТОЛЬКО JSON и ничего больше. Убедитесь, что выходной JSON является допустимым. Вот начальный запрос пользователя: {user_query} Вот запрос SQL, сгенерированный другим аналитиком: {sql} ''' def simple_reflection(rec) -> str: # создание пользовательского приглашения user_prompt = simple_reflection_user_prompt_template.format( user_query=rec['question'], sql=rec['llm_direct_sql'] ) # выполнение вызова LLM message = client.messages.create( model=»claude-3-5-haiku-latest», max_tokens = 8192, system=base_sql_system_prompt, messages=[ {'role': 'user', 'content': user_prompt} ] ) data = message.content[0].text # удаление блоков кода markdown data = data.strip() if data.startswith('«`json'): data = data[7:] elif data.startswith('«`'): data = data[3:] if data.endswith('«`'): data = data[:-3] data = data.strip() return json.loads(data.replace('n', ' '))

Давайте уточним запросы с помощью рефлексии и оценим точность. Мы не видим значительного улучшения итогового качества. Доля правильных ответов по-прежнему составляет 70%.

42618a9062883c375ed3c807abf4c25f

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

7d5f886127fcb1c69817fa5d1ba64164

Однако существуют и случаи, когда LLM чрезмерно усложняет ответ. Первоначальный SQL-запрос был верным (соответствовал ответу из золотого множества), но затем LLM решил его «улучшить». Некоторые из этих улучшений разумны (например, учёт значений NULL или исключение отменённых рейсов). Тем не менее, по какой-то причине он решил использовать сэмплирование ClickHouse, хотя у нас не так много данных, и наша таблица не поддерживает сэмплирование. В результате уточнённый запрос вернул ошибку выполнения: Database returns the following error: Code: 141. DB::Exception: Storage default.flight_data does't support sampling. (SAMPLING_NOT_SUPPORTED).

befe45fa7ee6d9fa2af8c77d81dce620

Рефлексия с внешней обратной связью

Рефлексия не сильно повысила точность. Вероятно, это связано с тем, что мы не предоставили никакой дополнительной информации, которая могла бы помочь модели получить лучший результат. Давайте попробуем поделиться с моделью внешней обратной связью:

Результат нашей проверки правильности указания формата
Вывод из базы данных (данные или сообщение об ошибке)
Давайте составим подсказку для этого и сгенерируем новую версию SQL.

feedback_reflection_user_prompt_template = ''' Ваша задача — оценить SQL-запрос, сгенерированный другим аналитиком, и предложить улучшения при необходимости. Проверьте, является ли запрос синтаксически правильным и оптимизированным для производительности. Обратите внимание на нюансы в данных (особенно на типы временных меток, следует ли использовать общее прошедшее время или время в воздухе и т. д.). Убедитесь, что запрос точно отвечает на первоначальный вопрос пользователя. В качестве результата верните следующий JSON: {{ 'reasoning': '<ваше обоснование здесь, 2-4 предложения о том, почему вы внесли изменения или нет>', 'refined_sql': '<улучшенный SQL-запрос здесь>' }} Убедитесь, что в выходных данных есть ТОЛЬКО JSON и ничего больше. Убедитесь, что выходной JSON корректен. Вот первоначальный запрос пользователя: {user_query} Вот SQL-запрос, сгенерированный другим аналитиком: {sql} Вот вывод этого запроса в базу данных: {output} Мы запускаем автоматическую проверку SQL-запроса на наличие проблем с форматированием. Вот вывод: {formatting} ''' def feedback_reflection(rec) -> str: # определить сообщение для форматирования if 'В SQL не указан формат' in rec['llm_direct_sql_quality_heuristics']: formatting = 'В SQL отсутствует форматирование. Укажите «format TabSeparatedWithNames», чтобы гарантировать возврат имен столбцов. else: formatting = 'Форматирование верно' # создание пользовательского приглашения user_prompt = feedback_reflection_user_prompt_template.format( user_query = rec['question'], sql = rec['llm_direct_sql'], output = rec['llm_direct_output'], formatting = formatting ) # выполнение вызова LLM message = client.messages.create( model = «claude-3-5-haiku-latest», max_tokens = 8192, system = base_sql_system_prompt, messages = [ {'role': 'user', 'content': user_prompt} ] ) data = message.content[0].text # удаление блоков кода markdown data = data.strip() if data.startswith('«`json'): data = data[7:] elif data.startswith('«`'): data = data[3:] if data.endswith('«`'): data = data[:-3] data = data.strip() return json.loads(data.replace('n', ' '))

После проведения измерений точности мы видим, что точность значительно улучшилась: 17 правильных ответов (точность 85%) по сравнению с 14 (точность 70%).

13249403e7a23cd0ba33a07f36c17296

Если мы рассмотрим случаи, в которых LLM исправил проблемы, то увидим, что ему удалось исправить формат, устранить ошибки выполнения SQL и даже пересмотреть бизнес-логику (например, использовать эфирное время для расчета скорости).

93b2e9b24ed64dd7897f1aace44c54e3

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

  • В последнем запросе временной период не был явно определён, поэтому для LLM разумно использовать период с 2010 по 2023 год. Я бы не считал это ошибкой и скорректировал бы оценку.
  • Другой пример — определение скорости самолёта: avg(расстояние/время) или sum(расстояние)/sum(время). Оба варианта допустимы, поскольку в запросе пользователя или системном сообщении ничего не указано (при условии, что у нас нет предопределённого метода расчёта).
c78dbe6578fc469c92e113bb8af64e08

В целом, я считаю, мы добились довольно хорошего результата. Наша итоговая точность в 85% представляет собой значительное улучшение на 15%. Вы потенциально можете выйти за рамки одной итерации и провести 2–3 раунда рефлексии, но стоит оценить, когда вы столкнётесь с убывающей отдачей в вашем конкретном случае, поскольку каждая итерация сопровождается увеличением затрат и задержек.

Полный код вы можете найти на GitHub.

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

Пора подвести итоги. В этой статье мы начали наше путешествие к пониманию магии систем агентного ИИ. Чтобы разобраться в этом, мы реализуем многоагентный инструмент преобразования текста в данные, используя только вызовы API к базовым моделям. По ходу дела мы шаг за шагом рассмотрим ключевые шаблоны проектирования: начнём с рефлексии и перейдём к использованию инструментов, планированию и координации работы нескольких агентов.

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

Рефлексия — относительно простой шаблон. Мы просто просим ту же или другую модель проанализировать результат и попытаться его улучшить. Как мы убедились на практике, предоставление модели внешней обратной связи (например, результатов статических проверок или вывода из базы данных) значительно повышает точность. Многочисленные исследования и наш собственный опыт работы с агентом преобразования текста в SQL подтверждают преимущества рефлексии. Однако за это повышение точности приходится платить: тратить больше токенов и увеличивать задержку из-за многочисленных вызовов API.

Спасибо за прочтение. Надеюсь, эта статья была познавательной. Помните совет Эйнштейна: «Главное — не переставать задавать вопросы. Любопытство имеет свою причину». Пусть ваше любопытство приведёт вас к следующему великому озарению.

Ссылка

Эта статья вдохновлена курсом «Agentic AI» Эндрю Ына из DeepLearning.AI.

Источник: towardsdatascience.com

✅ Найденные теги: Агентный, новости

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых