Пул моделей и вызовы
Пул моделей
Пул — это готовый список бесплатных языковых моделей. Чтобы использовать его, создайте брокер:
broker = llmbroker.Broker()
Самостоятельно составлять и обновлять список не нужно. Создайте файл с переменными для API-ключей и укажите ключи тех провайдеров, которыми хотите пользоваться:
llmbroker env freetier > .env
Над каждой переменной команда llmbroker env указывает, где получить ключ.
Подробнее см. в разделе CLI. Если ключ для модели не задан, эта
модель просто не используется.
Если провайдер ограничивает число одновременных запросов с одним ключом, это
учитывается в параметре parallel. Для моделей из готового списка значение уже
задано. Для модели, которую вы добавляете самостоятельно, укажите его в
LLMConfig.
Платную модель можно настроить для прямого вызова:
Broker(direct=["opus"]), затем broker.direct("opus").ask(...). Такая модель
не входит в пул. Подробнее см. в разделе Прямые вызовы модели.
Где хранится список моделей
Брокер хранит список в служебном каталоге llmbroker и обновляет его
там же. Чтобы выбрать другой каталог, задайте переменную LLMBROKER_HOME. Это
может понадобиться, например, в контейнере без доступа к системному каталогу
кэша.
Файл полностью создаётся заново при каждом обновлении, поэтому редактировать его не следует. В нём хранится только состав пула. Модели для прямых вызовов задаются в коде. Передать брокеру произвольный файл со списком нельзя: можно только выбрать один из поддерживаемых наборов по имени.
О хранении списка в общей базе данных и самостоятельном заполнении реестра см. в разделе Серверы и кластеры.
Какая модель пробуется первой
Порядок строк значения не имеет. Очерёдность определяется параметром weight:
[[llms]]
name = "google-gemini-3.5-flash-lite"
base_url = "https://generativelanguage.googleapis.com/v1beta/openai"
model = "gemini-3.5-flash-lite"
api_key_ref = "GEMINI_API_KEY"
weight = 0.75
weight — ожидаемое качество ответов модели по шкале от 0 до 1. Чем больше
значение, тем раньше брокер попробует эту модель. Значение по умолчанию — 0.0,
поэтому модели без заданного веса проверяются после моделей с положительным
весом. Если вы добавляете модели самостоятельно, задайте
им вес явно.
Вес задаёт только начальную очерёдность. Оценки, записанные через
record_quality(), постепенно заменяют исходный вес фактической
оценкой качества. Когда данных становится достаточно, исходный вес больше не
учитывается. До появления оценок модель занимает место, заданное её весом.
Ключи не обязаны лежать в .env
AWS Secrets Manager, Vault, БД или своё хранилище — см. API-ключи.
Обновление списка моделей
Состав бесплатных моделей со временем меняется. llmbroker следит за этими
изменениями и обычно не требует ручного обновления. Чтобы запустить обновление
сразу, вызовите sync; метод вернёт отчёт:
report = broker.sync("freetier") # freetier — имя набора моделей
print(llmbroker.format_report(report)) # отчёт можно записать в журнал или отправить администратору
По умолчанию брокер проверяет обновления примерно раз в сутки во время очередного вызова. Отдельная задача по расписанию не нужна. Если процесс простаивает, проверки не выполняются.
Если автоматическое обновление не удалось, брокер записывает предупреждение в
журнал и продолжает использовать имеющуюся конфигурацию. При явном вызове
broker.sync(...) ошибка передаётся вызывающему коду как исключение.
Если состав набора не изменился, файл не перезаписывается: его содержимое и время изменения остаются прежними.
Автоматическое обновление можно отключить, а интервал проверки — изменить:
llmbroker.Broker(sync=None) # не использовать готовый набор моделей
llmbroker.Broker(sync_interval=3600) # проверять раз в час
llmbroker.Broker(sync_interval=None) # не проверять автоматически
sync_interval=None отключает все автоматические загрузки, в том числе
первоначальное заполнение пустого реестра. В этом режиме обновление нужно
запускать самостоятельно. Подробнее см. в разделе
Серверы и кластеры.
Поля отчёта описаны в
SyncReport. В отчёте могут быть
следующие состояния:
- Ожидается ключ (
pending key) — для модели не задан API-ключ. Модель не используется, но остальные продолжают работать. В отчёте указано, где получить ключ. - Запись удалена (
removed) — модель исключена из поддерживаемого списка и больше не используется в пуле. Её ключ остаётся в хранилище секретов, а история вызовов — в журнале. Если модель снова появится в списке, эти данные сохранятся. - Ключ не используется (
unused key) — ключ есть в хранилище, но ни одна модель в текущей конфигурации на него не ссылается. Если ключ не нужен вашей собственной конфигурации, его можно отозвать у провайдера.
Удаление модели влияет на число доступных провайдеров. Когда остаётся один провайдер или не остаётся ни одного, срабатывает предупреждение о состоянии пула.
Где llmbroker хранит своё состояние
llmbroker хранит загруженный набор моделей, каталог платных моделей и время последней проверки обновлений. Если база данных не настроена, здесь же хранятся рабочий список моделей и журнал вызовов. Каталог выбирается в следующем порядке:
- каталог из параметра
home=, если он задан; - каталог из переменной
$LLMBROKER_HOME, если она задана; $XDG_CACHE_HOME/llmbroker, а без этой переменной — платформенный кэш:~/Library/Caches/llmbrokerна macOS,~/.cache/llmbrokerна Linux,%LOCALAPPDATA%\llmbrokerна Windows;- отдельный пользовательский каталог внутри системного временного каталога.
Параметр home= имеет наивысший приоритет, затем следует $LLMBROKER_HOME,
затем $XDG_CACHE_HOME. С помощью home= или $LLMBROKER_HOME можно разделить
данные разных проектов на одной машине.
Этот порядок определяет место записи данных. Для чтения доступ на запись не
нужен: если home= указывает на каталог только для чтения, например
примонтированный в контейнер, брокер использует данные именно из него и не ищет
другой каталог.
Если каталог отсутствует, брокер загружает данные заново. Если ни один из каталогов недоступен для записи, данные хранятся только в памяти до завершения процесса. Даже без доступа к сети первый запуск может использовать копию набора, включённую в пакет.
Загруженный список можно восстановить, но журнал вызовов и накопленные данные о качестве моделей восстановить нельзя. Без базы данных журнал хранится в том же каталоге. После его удаления модели снова ранжируются по исходным весам. Если историю нужно сохранять надёжно, храните журнал в базе данных — см. Серверы и кластеры.
Для обновления нужен каталог с правом записи, поскольку загруженный список должен
быть сохранён. Если запись невозможна, обновление завершится ошибкой. В таком
случае предоставьте доступ на запись или отключите автоматическую загрузку с
помощью sync_interval=None.
Без базы данных по умолчанию используется один журнал на машину. Благодаря этому
разные процессы, использующие одни и те же ключи из переменных окружения,
учитывают общие ограничения провайдеров. Им не приходится независимо друг от
друга заново обнаруживать одно и то же ограничение по числу запросов. Чтобы
вести отдельный журнал для проекта, задайте home=.
Об обновлении реестра в базе данных из задачи развёртывания см. в разделе Серверы и кластеры.
Вызов брокера
broker = llmbroker.Broker()
reply = broker.ask("Переведи на английский: Привет мир")
print(reply.text)
# Полный messages API
reply = broker.chat(
[
{"role": "system", "content": "Отвечай кратко."},
{"role": "user", "content": "Что такое Python?"},
]
)
В любой вызов можно передать trace_id= — идентификатор запроса или задачи в
вашей системе. llmbroker записывает его в журнал без изменений, чтобы записи
можно было сопоставить с вашими журналами. Подробнее см. в разделе
Поиск записей одного запроса.
Чтобы получать ответ частями по мере генерации, перебирайте broker.stream(...):
циклом for у Broker, циклом async for у AsyncBroker. Подробнее см. в разделе
Потоковая выдача.
В обычном скрипте брокер закрывать не нужно. О завершении работы в серверных приложениях см. в разделе Серверы и кластеры.
Сколько ждать ответа
try:
reply = broker.ask("Вопрос", wait=5.0) # максимум 5 секунд от начала до конца
except llmbroker.NoLLMAvailableError:
print("Никто не ответил за отведённое время")
wait ограничивает общее время вызова: и ожидание свободной модели, и получение
ответа. Если за это время провайдер не вернул никаких данных, брокер прекращает
попытку и временно не направляет к нему запросы с таким же коротким ограничением.
Это не позволяет каждому следующему вызову снова ждать заведомо слишком долго.
Если wait не задан, для одной попытки действует внутреннее ограничение в 60
секунд.
Модель, которая не успела ответить за отведённое время, также перестаёт быть
первым выбором для запросов с таким же или меньшим значением wait. Для более
долгих запросов она по-прежнему доступна и будет использована, если других
моделей нет. После первого своевременного ответа это ограничение снимается.
wait=0 имеет особое значение: не ждать освобождения модели. Брокер проверяет
модели, доступные прямо сейчас, но не ограничивает время их ответа.
Одновременный запрос к нескольким моделям
reply = broker.ask("Вопрос", fastest_of=2) # запросить две модели и вернуть первый ответ
fastest_of=N одновременно отправляет запрос не более чем N разным моделям и
возвращает ответ той, которая завершила работу первой. Остальные ответы
отбрасываются, но каждый из них расходует квоту провайдера. Поэтому параметр по
умолчанию отключён и подходит только в тех случаях, когда скорость важнее числа
запросов. Если доступно меньше N моделей, брокер использует все доступные.
Значение wait остаётся общим для всего вызова и не умножается на число моделей.
В одном случае брокер делает дополнительный параллельный запрос автоматически. После ошибки модель временно исключается из выбора. Когда этот срок заканчивается, брокер снова проверяет модель и одновременно обращается к другой доступной модели. Пользователь получает первый завершённый ответ, поэтому повторная проверка проблемной модели не увеличивает задержку. В остальных случаях один обычный вызов обращается только к одной модели.
reply = broker.ask("Вопрос", parallel_recovery=False) # не делать дополнительный запрос
Если число запросов важнее скорости, задайте parallel_recovery=False. Тогда
брокер сначала проверит ранее недоступную модель и только после новой ошибки
перейдёт к следующей.
Если параметр fastest_of не задан, broker.stream(...) передаёт ответ первой
модели, которая начала возвращать текст. После получения первого фрагмента
брокер продолжает работать с этой моделью до конца ответа. Чтобы одновременно
запрашивать несколько моделей, как при ask(..., fastest_of=N), передайте
параметр fastest_of:
stream = broker.stream("Вопрос", fastest_of=2, stream_selection_window=1.0)
При fastest_of > 1 все выбранные модели продолжают генерировать полный ответ.
Параметр stream_selection_window задаёт, сколько секунд ждать первых данных от
модели, которую брокер выбрал первой, прежде чем начать показывать уже
поступающие данные другой модели. Значение по умолчанию — 1.0; значение 0
означает, что нужно сразу показывать данные первой ответившей модели. Этот
параметр влияет только на то, чей текст пользователь увидит вначале. Итоговым
всё равно станет полный ответ модели, которая завершит работу первой.
Параллельная потоковая выдача требует больше памяти, поскольку брокер хранит
ответ каждой выбранной модели до завершения первого из них. Текст, показанный до
этого момента, считается предварительным. Если первой завершит ответ другая
модель, брокер сообщит об этом через StreamReplacementError, а приложение
должно заменить весь ранее показанный текст. Части ответов разных моделей не
объединяются. Пример обработки ошибки см. в разделе
Параллельная потоковая выдача.
Параметры fastest_of и stream_selection_window относятся только к пулу. При
прямом вызове через direct() используется одна конкретная модель, поэтому эти
параметры неприменимы.
Запрос JSON по схеме
reply = broker.ask(
"Собери карточку для слова 'tenacious'",
operation="card",
response_format={
"type": "json_schema",
"json_schema": {"name": "card", "schema": MY_SCHEMA, "strict": True},
},
)
response_format передаётся выбранной модели без изменений. Синхронный и
асинхронный клиенты принимают этот параметр в методах ask, chat и stream.
Формат параметра определяется API провайдера и совместим с форматом OpenAI; сам
llmbroker его содержимое не анализирует.
Соответствие ответа схеме не гарантируется. Некоторые модели всегда соблюдают переданную схему, а другие принимают параметр, но возвращают данные в другом формате. llmbroker не проверяет содержимое ответа, поэтому приложение должно самостоятельно проверять его по схеме.
Результат такой проверки можно передать в виде оценки качества, указав
для задачи параметр operation=. Тогда модели, которые часто нарушают схему,
будут выбираться для этой операции позже. Использовать response_format
предпочтительнее, чем просить модель вернуть JSON только в тексте запроса:
поддерживающие параметр модели смогут применить строгую схему, а поведение
остальных не ухудшится.
Ответ, не соответствующий схеме, считается успешным с точки зрения llmbroker.
Брокер не исключает такую модель временно и не отправляет тот же запрос другой
модели. Результаты проверки моделей из бесплатного набора приведены в
specs/reference/freetier-providers.md.
Это ограничение относится к пулу. При вызове конкретной модели через
direct() можно передавать произвольные параметры запроса.
Когда ответить некому
Брокер перебирает модели, пока не получит ответ. Если провайдер вернул HTTP 200,
но в ответе нет ни текста, ни вызовов инструментов, попытка считается неудачной
и брокер переходит к следующей модели. Если ни одна модель не ответила,
возникает исключение NoLLMAvailableError. Причину можно определить по его
полям; разбирать текст сообщения об ошибке не нужно.
try:
reply = broker.ask("Вопрос", wait=5.0)
except llmbroker.NoLLMAvailableError as exc:
if exc.retry_at is not None:
retry_after(exc.retry_at) # к этому времени модель станет доступна
else:
alert(f"пул недоступен: {exc.reason}")
Поле reason принимает одно из пяти значений:
reason |
что случилось | что делать |
|---|---|---|
empty_pool |
в реестре нет моделей | заполнить реестр — см. Обновление списка моделей и Серверы и кластеры |
no_keys |
модели есть, но для текущего вызова недоступны их API-ключи | задать ключи — см. API-ключи |
all_disabled |
все модели выключены вручную | включить хотя бы одну |
timeout |
истекло время wait: при ожидании свободной модели или во время ответа |
увеличить wait или повторить запрос позже |
excluded |
ни одну модель нельзя использовать для этого запроса; например, провайдеры отклонили все доступные ключи | посмотреть причины отдельных попыток в журнале вызовов |
Первые три причины указывают на ошибку настройки, поэтому повтор запроса без
изменения конфигурации не поможет. timeout и excluded относятся к конкретному
запросу; следующий запрос может выполниться успешно.
До получения первой части потоковый вызов сообщает о тех же пяти причинах. Отличие
одно: он никогда не обращается к одной и той же модели дважды, поэтому там, где
ask продолжает ждать возвращения модели, потоковый вызов сразу завершается с
timeout, а retry_at указывает момент её возвращения. См.
Потоковый ответ.
Поле retry_at заполняется, если сейчас нет доступных моделей, но известно,
когда закончится временная блокировка одной из них. Для empty_pool, no_keys и
all_disabled это поле не задаётся, поскольку ожидание не исправит проблему.
При timeout поле задаётся, только если все модели временно недоступны. Если
какая-либо модель уже свободна, запрос можно повторить сразу.
Ошибка в запросе не приводит к NoLLMAvailableError. Если все выбранные
модели отклонили запрос с кодом 4xx, кроме 401, 403 и 429, возникает
ProviderError. Например, причиной может быть неверная схема tools. Поле
.status содержит код HTTP, а .detail — фрагмент ответа провайдера. Такое же
исключение возникает при прямом вызове модели:
try:
reply = broker.ask(prompt, wait=5.0)
except llmbroker.NoLLMAvailableError as exc:
... # ни одна модель сейчас не доступна
except llmbroker.ProviderError as exc:
log.error("Все модели отклонили запрос: HTTP %s — %s", exc.status, exc.detail)
После ProviderError модели не исключаются из выбора. Исправленный запрос будет
отправлен им в обычном порядке.
Оценка качества
Оценки помогают брокеру выбирать наиболее подходящие модели для разных задач:
reply = broker.ask("Кратко перескажи этот пункт договора", operation="summarize")
reply.record_quality(0.9) # 1.0 — хороший ответ, 0.0 — неудачный; вне [0, 1] — ValueError
Оценки накапливаются отдельно для каждой пары (модель, операция). Модель с
низкими оценками для конкретной операции выбирается позже, но остаётся доступной,
если других моделей нет. Новые хорошие оценки постепенно повышают её место в
очереди; отдельного сброса нет. По мере накопления оценок они заменяют исходный
вес. Все вызовы без operation= относятся к одной общей категории.
Ответ можно оценить позже, когда пользователь уже просмотрел результат. Для этого
нужно указать, к какому вызову относится оценка. Один из вариантов — передать
собственный идентификатор в trace_id= во время вызова, а затем использовать его
при записи оценки:
broker.ask("Кратко перескажи этот пункт", operation="summarize", trace_id=document_id)
# ...через день, когда приходит отзыв пользователя
broker.record_quality(0.0, trace_id=document_id)
Другой вариант — сохранить reply.call_id и оценить конкретный вызов:
broker.record_quality(0.0, call_id=saved_call_id). Нужно выбрать ровно один
идентификатор: trace_id или call_id.
Идентификатор нужен, если объект результата уже недоступен. Если он сохранён,
оценку можно записать его собственным методом record_quality(...), без поиска
в журнале. Это относится и к объекту, который возвращает
потоковая выдача. Для него метод становится
доступен после завершения ответа.
Модель и операция определяются по записи вызова, поэтому хранить их отдельно не нужно. Неудачные попытки не оцениваются: оценка относится только к модели, чей ответ был получен.
При оценке по идентификатору действуют два ограничения:
- Один
trace_idдолжен соответствовать одному вызову. llmbroker позволяет использовать одинаковыйtrace_idнесколько раз, но при записи оценки выберет последний успешный вызов с этим идентификатором. Если в журнале окажется значительно больше связанных записей, чем обычно создаёт один вызов, появится предупреждение. Чтобы однозначно оценить один из нескольких вызовов, сохраните егоcall_id.
При fastest_of один вызов может оставить несколько записей об успешных
ответах: по одной для каждой модели, которая успела завершить работу до выбора
результата. В самих записях не указано, какой ответ получил пользователь.
Поэтому такой вызов следует оценивать через возвращённый объект результата, а
не по trace_id. Для потоковой выдачи используйте
возвращаемый ею объект.
- Поиск ограничен последними семью днями. Оценки качества рассчитываются по
недавней части журнала, поэтому более старые вызовы не учитываются. Если запись
не найдена, она старше семи дней или ни одна попытка не завершилась ответом,
возникает UnknownCallError.
Эти ограничения относятся только к поиску по trace_id или call_id. При
вызове record_quality(...) у объекта reply или у объекта потоковой выдачи
поиск в журнале не выполняется и семидневного ограничения нет.
Записывайте оценку через тот же объект, который выполнял вызов. Область данных
берётся из этого объекта, а не из идентификатора. Например, вызов через
broker.for_scope(user) нужно оценивать через
broker.for_scope(user).record_quality(...). Если вызвать метод у broker
напрямую, оценка будет записана без указанной области.
Пороги и размер окна оценок настраиваются — см.
Optimizer.