Топовые LLM фреймворки могут быть не так надёжны, как вы думаете ru en
Месяц назад решил добавить поддержку Gemini в Feeds Fun и под это дело изучал топовые LLM фреймворки — писать свой велосипед не хотелось.
В итоге нашёл стыдный баг в интеграции с Gemini в LLamaIndex. Судя по коду, он есть и в Haystack и в плагине для LangChain. А корень проблемы вообще в SDK Google для Python.
При инициализации нового клиента для Gemini код фреймворка перетирает/подменяет API ключи во всех клиентах, созданных до этого. Потому что API ключ, по-умолчанию, хранится в синглетоне.
Смерти подобно, если у вас multi-tenant приложение, и незаметно во всех остальных случаях. Multi-tenant — это когда ваше приложение работает с несколькими пользователями.
Например, в моём случае, в Feeds Fun пользователь может ввести свой API ключ, чтобы улучшить качество сервиса. Представьте какой забавный казус мог бы случиться: пользователь ввёл API ключ для обработки своих рассылок, а потратил токенов (заплатил) за всех пользователей сервиса.
Репортил только в LLamaIndex как security issue и уже 3 недели ноль реакции, для Haystack и LangChain лень воспроизводить. Так что это ваш шанс зарепортить багу в топовый репозиторий. Под катом будет вся инфа, воспроизвести не сложно.
Ошибка примечательна многим:
- Оценка критичности ошибки очень зависит от вкусовщины, опыта и контекста. Для меня, в проектах в которых я работал, — это критическая ошибка безопасности. Но, похоже, для большинства актуальных проектов, которые используют LLM, это вообще не принципиально. Что навевает некоторые мысли о мейнстрим около-LLM разработках.
- Это хороший индикатор низкого уровня контроля качества кода: код ревью, тестов — всех процессов. Всё-таки это интеграция с одним из топовых провайдеров API, найти проблему можно было кучей разных способов, но ни один не сработал.
- Это хорошая иллюстрация порочного подхода к разработке: «копипастим из туториала и льём на прод». Чтобы допустить эту ошибку нужно было проигнорить одновременно и базовую архитектуру твоего проекта и логику вызова кода, который ты копипастишь.
В итоге я забил на эти фреймворки и впилил свой костыль, благо HTTP API для Gemini есть.
Мой вывод из этого безобразия такой: доверять коду, который под капотом у современных LLM фреймворков нельзя. Надо перепроверять, вычитывать. То, что у них написано «production ready», не значит, что они действительно production ready.
Далее расскажу подробнее про сам баг.
Воспроизводим ошибку в LlamaIndex
- Создаём первый клиент с ключом
A
. - Создаём второй клиент с ключом
B
. - Пробуем использовать первый клиент, видим, что используется ключ
B
, а не ключA
.
from llama_index.llms.gemini import Gemini
llm_1 = Gemini(model="models/gemini-1.5-flash", api_key="correct api key")
resp = llm_1.complete("Write a poem about a magic backpack")
# here everything is ok
print(resp)
llm_2 = Gemini(model="models/gemini-1.5-flash", api_key="another key, wrong by purpose")
# Let's run llm_1 again
# We'll see an error instead of correct answer, because the key was redifined
resp = llm_1.complete("Write a poem about a magic backpack")
# google.api_core.exceptions.InvalidArgument: 400 API key not valid. Please pass a valid API key. [reason: "API_KEY_INVALID"
# domain: "googleapis.com"
# metadata {
# key: "service"
# value: "generativelanguage.googleapis.com"
# }
# ]
Корень проблемы
Автор(ы) скопировал(и) код туториала по использованию Gemini python SDK без адаптации его у реалиям проекта.
Вот соответствующий кусок кода из LlamaIndex
class Gemini(CustomLLM):
...
try:
import google.generativeai as genai
except ImportError:
...
config_params: Dict[str, Any] = {
"api_key": api_key or os.getenv("GOOGLE_API_KEY"),
}
...
genai.configure(**config_params)
Словами:
- В конструкторе экземпляра условного клиента.
- Импортируем библиотеку от Google.
- Вызываем метод глобальной инициализации клиентского SDK — устанавлием значение API ключа по-умолчанию.
Отдельно обращу внимание, что genai.configure(...)
по самой логике вызова (вызываем синглетон сущность из базового модуля библиотеки) не может иметь никакой другой логики, кроме перетирания глобальных настроек.
Чтобы вы долго не искали, вот аналогичные куски кода в других проектах:
Логика одна и та же.
Как можно защититься от такого косяка?
- Читать документацию, прежде чем копипастить.
- Читать чужой код, прежде чем копипастить.
- Разбирать архитектуру своего проекта, прежде чем в него копипастить.
- Визуализировать итоговую логику кода в голове, прежде чем… ну вы поняли.
- Писать тесты, проверяющие инициализацию клиентов. Косяки с кредами — это классика ошибок безопасности.
- Senior разработчик должен ловить такое на код ревью на подсознательном уровне.
- Если это сделано осознанно, вызывать исключение при попытке создания второго клиента — защитите пользователей своей библиотеки.
- Если это сделано осознанно, писать о нюансах жирным красным текстом в документации — предупредите пользователей своей библиотеки.
Можно ли было избежать ошибки?
И да и нет.
Официальная библиотека гугла не даёт возможности прокинуть клиент или API ключ до места обращения к серверному API.
Потому что разработчики не уверены на сколько это нужная фича В обсуждении я оставил большой комментарий, надеюсь он убедит разработчиков, когда они следующий раз обратят внимание на эту задачу.
Прокинуть ключ можно с помощью некоторых плохих практик через приватные интерфейсы объектов. А вот публичным API сделать это не получится.
Сделать «правильно» можно было многими способами:
- Сказать «мы не будем делать интеграцию с Gemini», пока они не решат ограничения своего оклиента.
- Реализовать собственный клиент, используя HTTP API.
- Использовать хаки, чтобы таки проталкивать ключ, куда надо. Обложить тестами, конечно, чтобы код не сломался при обновлении SDK.
- Реализовать как есть, но явно дикларировать что клиент к Gemini API — singleton. Например, вызывать исключение при попытке создания второго клиента.
Как видим, варианты есть, но более затратные по времени и более сложные.
Почему такие ошибки проникают в код
У меня есть гипотеза.
Со стороны может показаться, что работа программиста везде одинакова: человек сидит и что-то вбивает на клавиатуре. Но на самом деле она сильно отличается от области к области.
Делать компьютерные игры — не то же самое, что программировать железо для складков. Писать бэкенд для современного веба — не то же самое, что делать корпоративный софт или, тот же фронтенд для веба.
Везде разные требования, разные нюансы и ограничения, даже разная динамика разработки — где-то код принято выкидывать через месяц, а где-то он должен работать десятки лет. Где-то в принципе нет такого явления multi-tenant, где-то оно подразумевается по-умолчанию.
Меняя область деятельности, даже опытный разработчик не сможет сходу выдавать высококачественный код. Даже если заметит, что область поменялась. А многие не осознают этого довольно долго.
Так вот, моя гипотеза состоит в том, что многие разработчики LLM middleware недавно сменили область деятельности. С чего-то научного вроде data science и тренировки нейронок на что-то более инженерное, вроде разработки долгоиграющих сервисов под веб или middleware для них.
Как следствие, ещё не все из них понимают динамику и проблемы области, в которой неожиданно оказались.
Утрируя: когда основой твой результирующий артефакт — это Jypyter notebook, ты не задумываешься о том, сколько ключей API или экземпляров клиента будет в твоём коде, сколько соединений к сторонним сервисам ты используешь и так далее. Но когда ты разрабатываешь библиотеку для бэка, то уже должен держать всё это в голове и, что важно, не додумывать за возможных пользователей твоего артефакта.
На примере описанного бага, я вижу два подтверждения этой гипотезы:
- Потенциально однотипные проблемы в разных топовых фреймворках.
- Потенциально однотипные проблемы в разных топовых SDK. Пару лет назад была схожая проблема API ключей в OpenAI SDK, но её исправили намного быстрее.
Читать далее
- Миграции backend на практике
- Open source сервисы аутентификации
- Модная типизация в Python
- Генерация подземелий — от простого к сложному
- Автоматический генератор квестов
- Feeds Fun — читалка новостей с тегами и ChatGPT
- Блог переехал на новый движок
- Опыт портирования проекта на Python 3
- GraphQL & Python
- Python & OpenAPI