Наблюдение и журнал
Текущее состояние пула возвращает метод snapshot(), а сведения о предыдущих
вызовах хранятся в журнале. Оба механизма работают как в обычном скрипте, так и
в приложении из нескольких процессов.
Состояние пула
Метод snapshot() возвращает общие показатели пула и состояние каждой модели:
snap = broker.snapshot()
print(f"{snap.providers_usable} of {snap.providers_total} providers usable")
if snap.degraded:
print("переключаться некуда")
for key in snap.missing_keys:
print(f"{key.api_key_ref} нужен для {', '.join(key.entry_names)}")
print(key.help) # где получить ключ
for key in snap.direct_missing_keys: # модели для прямого вызова
print(f"{key.api_key_ref} — direct({key.entry_names[0]!r}) не сработает")
print(key.help)
for name, llm in snap.items(): # отображение: имя -> состояние модели
print(name, llm.has_key, llm.cooldown_until)
В асинхронном коде используйте await broker.snapshot(). Все данные возвращаются
одним объектом, поэтому дополнительный запрос для списка моделей не нужен. Поля
описаны в PoolSnapshot.
Поле direct_missing_keys отделено от missing_keys, поскольку модели для
прямого вызова не входят в пул. Отсутствующий у такой модели ключ не влияет на
состояние пула, а доступность других моделей не может его заменить. Поле help
в обоих случаях берётся из подсказок вашего реестра, если они есть, затем из
поддерживаемого списка моделей, за которым следует брокер, а для модели прямого
вызова — из каталога платных моделей. См. раздел API-ключи.
Доступность считается по провайдерам (api_key_ref), а не по моделям. Несколько
моделей с одним ключом зависят от общего ограничения запросов и общей точки
отказа, поэтому учитываются как один провайдер. Поле degraded равно True,
если доступно меньше двух провайдеров. При одном провайдере пул ещё работает, но
не сможет перейти к другому после ограничения запросов; при нуле запросы
невозможны. Для реестра без моделей пула degraded равно False.
Удалённый из хранилища ключ перестаёт учитываться после следующего обновления состояния пула. События, вызывающие такое обновление, перечислены в разделе Общие данные нескольких процессов. Если модель выключена вручную, её провайдер по-прежнему считается доступным, а состояние самой модели видно в соответствующей записи.
Предупреждения об изменении состояния
Регулярно вызывать snapshot() не требуется. При изменении доступности пула
llmbroker записывает сообщение через журнал llmbroker.broker. Система
мониторинга может создавать предупреждения по следующим строкам.
Остался один доступный провайдер. Пул продолжает отвечать, но не сможет
перейти к другому провайдеру при ограничении запросов. Уровень ERROR:
pool degraded, no failover left: 1 of 3 providers usable — no key for GEMINI_API_KEY
Нет доступных провайдеров. Пул не может выполнить запрос. Уровень ERROR:
pool cannot serve any request: no provider has a key — no key for GROQ_API_KEY, GEMINI_API_KEY
Все модели временно недоступны. Нужные ключи настроены, но все модели достигли
ограничений провайдеров. Обычно это означает, что текущего числа моделей
недостаточно для нагрузки. Уровень WARNING; сообщение записывается не чаще
одного раза в минуту:
pool under-provisioned: all LLMs are COOLING — add more LLMs to the registry
В первых двух сообщениях перечислены отсутствующие ключи.
Сообщение записывается только при изменении состояния, а не при каждом запросе. Переход к одному провайдеру и переход к нулю провайдеров считаются разными событиями, поэтому для каждого из них появится отдельная запись.
Когда снова доступны хотя бы два провайдера, один раз записывается сообщение
уровня INFO:
pool recovered: 3 of 3 providers usable
Появление третьего и последующих провайдеров не создаёт дополнительных сообщений.
Само по себе отсутствие отдельного ключа не считается проблемой, если доступно не менее двух других провайдеров.
Журнал вызовов
Старые записи удаляются автоматически. Срок хранения задаётся параметром
retention хранилища журнала и по умолчанию составляет 90 дней. Подробнее см. в
разделе Серверы и кластеры. Последние записи можно получить
вызовом broker.calls(limit=50).
Каждая попытка обращения к модели создаёт отдельную запись. Поля описаны в
Call; score содержит оценку качества
или None. Записи можно отфильтровать по времени, операции и идентификатору
вызова. См. также Поиск записей одного запроса.
from datetime import UTC, datetime, timedelta
week_ago = datetime.now(UTC) - timedelta(days=7)
broker.calls(limit=50, since=week_ago, operation="summarize")
Фильтры применяются к вызовам, а не ко времени записи оценки. Если ответ оценили позже, оценка всё равно будет видна в записи исходного вызова. При повторной оценке отображается последняя.
Значение since включается в диапазон. В MongoDB время хранится с точностью до
миллисекунды, поэтому сохранённые отметки и граница диапазона округляются вниз до
целых миллисекунд.
Поиск записей одного запроса
Методы ask, chat и stream принимают trace_id= — идентификатор из вашей
системы, например идентификатор запроса или задачи. llmbroker без изменений
записывает его для каждой попытки. Благодаря этому записи можно сопоставить с
журналами приложения.
broker.ask("Перескажи этот пункт", operation="summarize", trace_id=request_id)
Один вызов может создать несколько записей, поскольку каждая попытка обращения к
модели записывается отдельно. Все попытки получают одинаковый trace_id. По ним
можно увидеть, например, что две модели достигли ограничений провайдера и только
третья ответила. Успешная попытка имеет статус CallStatus.OK. Прерванная
потоковая выдача не считается успешной, даже если часть текста уже была получена.
from llmbroker import CallStatus
rows = broker.calls(limit=200, trace_id=request_id)
answered = next((c for c in rows if c.status is CallStatus.OK), None)
Фильтр применяется внутри хранилища, поэтому limit ограничивает число
найденных записей, а не число проверенных. Даже старый запрос можно получить
целиком, если число его записей не превышает ограничение. В базах данных поле
проиндексировано. Файловое хранилище индекса не имеет, поэтому фильтр в нём не
ускоряет поиск.
Чтобы найти одну конкретную попытку, передайте call_id= со значением
result.call_id.
llmbroker не запрещает использовать один trace_id для нескольких вызовов, но
оценка по идентификатору относится только к одному из них.
Если нужно однозначно указать конкретную попытку, используйте call_id. Оба
идентификатора позволяют записать оценку после завершения вызова.
Статистика за период
stats() группирует записи по моделям и показывает, сколько попыток было сделано
за период и какими статусами они завершились:
from llmbroker import CallStatus
for name, s in broker.stats(since=week_ago).items():
failed = s.total - s.by_status.get(CallStatus.OK, 0)
print(name, s.total, failed, s.last_status, s.last_at)
Поля результата описаны в
LLMStats.
by_status содержит только статусы, встречавшиеся за выбранный период. Поэтому
число неудачных попыток удобнее вычислять из total. Статус SUPERSEDED не
означает ошибку модели: он присваивается параллельному запросу, который ещё
выполнялся, когда другая модель уже завершила ответ. Если нужен именно счётчик
ошибок, такие записи также следует исключить. Запись оценки не создаёт новый
вызов и не влияет на эти счётчики. Параметр operation= ограничивает статистику
одной операцией.
Приложение самостоятельно определяет, какие статусы считать ошибками, какой период использовать и как отображать модели без вызовов. llmbroker возвращает только исходные счётчики.
limit, равный 1000 по умолчанию, ограничивает число прочитанных записей, а не
продолжительность периода. Значение должно быть не меньше 1. Если сумма total
равна limit, часть записей могла не попасть в результат. Увеличьте ограничение
или сократите период.
В since нужно передавать дату с часовым поясом, например datetime.now(UTC), а
не datetime.now(). Значение без часового пояса отклоняется, чтобы граница
периода не зависела от местных настроек машины.
calls() и stats() читают только журнал и, в отличие от snapshot(), не
инициализируют пул. Поэтому страницу статистики можно построить, даже если реестр
ещё не обновлялся. Вход в with Broker(...) as broker тоже не инициализирует
пул.