Агенты супер багованы. В своих проектах в компании мы заметили, что Langchain стал уж слишком баговым. В мультиагентных системах агенты зачастую циклятся, так как не понимают, когда они выполнили финальное действие, не вызывают друг друга когда надо, или же просто возвращают данные в битом формате JSON. Короче говоря, создать агентную систему стало не так то просто, и мы даже стали задумываться об упрощении систем, избавляясь от кучи агентов. И вот неделю назад OpenAI обновили SDK для создания агентов, а еще выкатили доступ к новым тулзам по API. Ну и я пошел тестить.

Я Арсений, Data Scientist в Raft и сегодня я расскажу вам про OpenAI либу для создания агентов от OpenAI.

Мультиагенты

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

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

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

OpenAI Agents SDK

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

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

Агент ресёрчер (типа deep search в GPT).
Что он делает: Анализирует то, что ввёл пользователь.

  1. Задаёт уточняющие вопросы при необходимости.

  2. Анализирует файлы и интернет на предмет данных по материалу.

  3. Выдаёт полный отчёт со ссылками на материалы.

В качестве файла я выбрал данные одного крупнейшего Агро производителя удобрений и семян. В файле всего 340 страниц и его вес примерно 50 МБ. Задавать вопросы будем соответственно про удобрения.

Итак, пойдем по порядку, и начнем с первого пункта

Шаг 1. Анализ вопроса пользователя

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

from agents import Agent, Runner

query_analyzer_agent = Agent(
    name="QueryAnalyzer",
    instructions="""
    Ты агент, который анализирует запросы пользователей. Тебе задают вопросы ожидая,
    в дальнейшем ответа с использованием файлов и интернета. 
    Твоя задача - понять запрос, определить его тематику и структуру.
    Выдели ключевые аспекты, названия, аттрибуты, определи, нужен ли уточняющий вопрос.
    Не злоупотребляй уточняющими вопросами. Если ты смог достать ключевую информацию,
    не спрашивай у пользователя ещё раз.
    """
)

runner = Runner()
analysis_result = await runner.run(
    query_analyzer_agent, 
    f"Проанализируй следующий запрос пользователя и определи, требуются ли уточнения: '{query}'"
    "Если уточнения требуются, сформулируй конкретный вопрос. "
    "Если уточнения не требуются, просто напиши 'Уточнения не требуются'."
)

Runner запускает рабочий процесс агента. Агент будет выполняться в цикле до тех пор, пока не будет сгенерирован окончательный результат.

Цикл выполняется следующим образом:

  1. Агент вызывается с определенным заданием. В примере выше в задание подставляется пользовательский запрос:

    "Проанализируй следующий запрос пользователя и определи, требуются ли уточнения: '{query}'
    Если уточнения требуются, сформулируй конкретный вопрос.
    Если уточнения не требуются, просто напиши 'Уточнения не требуются'.

  2. Если есть конечный результат (т.е. агент выдает что-то типа agent.output_type), цикл завершается.

  3. Если есть handoff (передача данных в другого агента), снова запускается цикл, с новым агентом.

  4. В противном случае вызываются инструменты (если таковые имеются) и повторно запускается цикл.

При создании агента, можно также выбирать модель, и то через какое API будет реализовываться обращение к модели.

  • Рекомендуется использовать новую версию Responses API, например так

    model=OpenAIResponsesModel(model="gpt-4o", openai_client=AsyncOpenAI())

  • Но и через  OpenAIChatCompletionsModel, который вызывает OpenAI APIs Chat Completions API

    model= OpenAIChatCompletionsModel(model="gpt-4o", openai_client=AsyncOpenAI())

Используется по дефолту новая версия Responses API и gpt-4o-2024-08-06.

Шаг 2. FileSearch

Будем использовать встроенную в SDK FileSearchTool() (подробнее тут). Для того чтобы искать по файлам нужно сначала создать хранилище файлов и загрузить их туда. Это можно сделать либо через сайт, либо используя API.

На сайте:

  • Создаем векторное хранилище

  • Добавляем нужные файлы

  • Для использования нужно сохранить vector_id, чтобы затем передать его в WebSearchTool()

API:

from openai import OpenAI

client = OpenAI()

# Создаем файл
local_file_path = "path"
with open(local_file_path, "rb") as file_content:
  file_response = client.files.create(
    file=file_content,
    purpose="file-search"
  )

# Создаем хранилище
vector_store = client.vector_stores.create(
    name="knowledge_base"
)

# Добавляем файл в хранилище
client.vector_stores.files.create(
    vector_store_id=vector_store.id,
    file_id=file_id
)

Теперь можем создать агента поиска по файлу. Для этого используем vector_store.id и FileSearchTool() Можно задать количество файлов, которое использовать как подходящие для поиска ответа. В моем хранилище только один файл, поэтому я использую только его один.

from agents import Agent, FileSearchTool, Runner

file_search_tool = FileSearchTool(
    vector_store_ids=[vector_store.id],
    max_num_results=1
)

file_agent = Agent(
    name="FileSearcher",
    instructions="""
    Ты агент, специализирующийся на поиске информации в загруженных файлах.
    Используй имеющиеся у тебя инструменты для поиска информации в загруженных файлах.
    Предоставляй полную и точную информацию из файлов, цитируя источник.
    Это самый важный этап анализа, так как информация из файла имеет приоритет.
    Если информация в файле не найдена, четко укажи это.
    """,
    tools=[file_search_tool]
)
runner = Runner()
file_result = await runner.run(
    file_agent, 
    f"""Найди информацию по запросу: '{query}' в загруженном файле.
    Очень важно найти максимально релевантную информацию."""
)

Цены и ограничения:

  • Цена использования $2.50 за 1000 запросов и хранилище стоит $0.10/GB/day, но первый Гб бесплатно. 

  • Проекты не должны превышать 100GB для всех файлов.

  • Максимум хранилища -- 10k файлов.

  • Размер одного файла не более 512MB (примерно 5M токенов)

Шаг 3. WebSearch

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

Итак сначала создаем тулзу для поиска WebSearchTool(). Указываем размер контекста поиска, что напрямую влияет на итоговую цену, а так же опционально можем настроить локацию поиска

"user_location": {"type": "approximate", "country": "GB", "city": "London", "region": "London"}

Затем создаем самого агента и передаем ему инструкции и тулзу. После чего уже его можем запускать.

from agents import Agent, WebSearchTool, Runner

web_search_tool = WebSearchTool(
    search_context_size="medium"
)

web_agent = Agent(
    name="WebSearcher",
    instructions="""
    Ты агент, специализирующийся на поиске информации в интернете.
    Используй инструменты поиска в интернете для нахождения актуальной информации,
    которая дополняет или расширяет информацию, найденную в файле.
    Предоставляй полную и точную информацию из результатов поиска, указывая источники.
    """,
    tools=[web_search_tool]
)

web_result = await runner.run(
    web_agent, 
    f"""Найди актуальную информацию по запросу: '{query}' в интернете.
    В файле была найдена следующая информация: '{file_result.final_output}'
    Дополни или расширь эту информацию своими находками."""
)

Цены и ограничения:

  • Плата за токены: Стоимость зависит от количества токенов, затраченных на обработку запроса и генерацию ответа. Начинается от 25$ за 1000 обращений для GPT-4o-mini-search. Точные тарифы указаны на странице цен.

  • Зависимость от размера контекста: Выбор search_context_size влияет на стоимость — больший контекст увеличивает расходы. Подробности доступны в разделе цен на сайте OpenAI.

Шаг 4. Итоговый Ответ

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

report_agent = Agent(
    name="ReportGenerator",
    instructions="Ты агент, специализирующийся на формировании итоговых отчетов."
    "Твоя задача - обобщать результаты поиска из файлов и интернета."
    "Структурируй информацию логически, выделяй ключевые моменты."
    "Избегай крупных Заголовков."
    "Приоритезируй информацию из файла, затем дополняй её данными из веб-поиска."
    "Обязательно указывай источники информации (ссылки на файлы и веб-страницы)."
)

file_info = file_result.final_output
report_prompt = f"""
  Сформируй полный и структурированный отчет на основе следующих данных:
  
  ЗАПРОС ПОЛЬЗОВАТЕЛЯ: {query}
  
  ИНФОРМАЦИЯ ИЗ ФАЙЛА (ПРИОРИТЕТНАЯ):
  {file_info}
  
  ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ ИЗ ИНТЕРНЕТА:
  {web_result.final_output}
  
  Обязательно укажи все источники информации. Структурируй отчет логически.
  Информация из файла должна быть представлена в первую очередь, а затем дополнена данными из интернета.
"""

report_result = await runner.run(report_agent, report_prompt)

Логирование

Отлично, рабочий пайплайн готов!

Запустить его можно streamlit run app.py

Однако создать агентов мало, важно уметь дебажить и логировать их работу. В OpenAI все схвачено, они берут это на себя.

Итак давайте запустим с тестовым запросом 'Технические характеристики удобрения Вайбранс' и перед просмотром ответа пройдем все шаги подряд через логи.

На влкадке Logs можно посмотреть все обращения агентов по API. Реализовано куча фильтров:

  • Тип API: ChatCompletion или Response.

  • Название Модели.

  • Дата обращения.

  • Метаданные.

  • Вызываемые в запросах тулзы.

    Логи нашего запроса
    Логи нашего запроса

Видим пошагово все вызовы агентов. Можно ткнуть на каждый вызов и посмотреть детали.

Например, запрос на FileSearch. Видим количество потраченных токенов, а так же ключевые слова по которым шел поиск в файле: 'технические характеристики удобрения Вайбранс', 'удобрение Вайбранс спецификации', 'Вайбранс характеристики'. Это важно для понимания работы и отладки.

Tracing

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

from agents import trace

# указываем название Workflow -- Поисковый запрос
with trace("Поисковой запрос"):
  # весь код вызова агентов ниже

Теперь каждый поисковой запрос можно будет так же отследить на вкладке Traces с указанием Workflow.

Проваливаясь внутрь можно посмотреть более детально работу каждого из агентов, время работы, потраченные токены. Из трассировки видно, что больше всег�� времени занимает WebSearch, порядка 15 секунд, поскольку мы поставили "medium" размер контекста поиска.

Поиск по файлу и другие агенты отрабатывают очень быстро и четко, несколько миллисекунд.

Итоговый результат

На выходе мы получили полный ресерч с данными из файла и интернета за меньше чем 20 секунд.

Результат модели:

Информация о технических характеристиках удобрения "Вайбранс" организована по следующим категориям:

Технические характеристики из файла:

  • Активные вещества: седаксан 50 г/л, мефеноксам 37,5 г/л, флудиоксонил 25 г/л.

  • Формулировка: концентрат суспензии.

  • Фасовка: канистра 5 л / упаковка 4 × 5 л.

  • Срок годности: 3 года со дня изготовления.

  • Класс опасности: 3-й класс.

Дополнительная информация из интернета:

Вайбранс Топ Используется для картофеля.

  • Действующие вещества: тиаметоксам 262,5 г/л, седаксан 25 г/л, флудиоксонил 25 г/л.

  • Форма: концентрат суспензии (КС).

  • Упаковка: канистра 5 л / упаковка 4×5 л.

  • Класс опасности: 3-й класс.

  • Норма расхода: 0,4–0,7 л/т клубней.

  • Спектр действия: колорадский жук, ризоктониоз.

Источник: syngenta.ru

Вайбранс Трио Для озимой и яровой пшеницы, ячменя.

  • Действующие вещества: седаксан 25 г/л, флудиоксонил 25 г/л, тебуконазол 10 г/л.

  • Форма: концентрат суспензии (КС).

  • Спектр действия: корневая гниль, пыльная головня.

  • Норма расхода: 1,5–2,0 л/т семян.

Источник: pr-agro.ru

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

Вывод

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

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

В общем надеюсь применить OpenAI Agents SDK уже в реальном проекте и рассказать вам о результатах! До встречи!

P.S.: итоговый код я обернул в Streamlit и получилось небольшое веб-приложение. Код тут.