Как работает LLM на уровне, необходимом разработчику¶
Шаг 1¶
В прошлом разделе наш проект AI Support Desk впервые обратился к модели. Вместе с ответом программа вывела три числа: входные, выходные и общие токены. Давайте в этом разделе поговорим и разберемся, что они означают.
И начнем с токенов, причем основная идея простая: модель измеряет текст не словами и не символами, а небольшими фрагментами — токенами. От количества токенов зависят допустимый размер запроса, время обработки и расход доступного лимита.
Что такое токен?¶
Человек видит предложение, состоящее из слов. Модель сначала пропускает тот же текст через токенизатор. Токенизатор делит строку на фрагменты и заменяет каждый фрагмент числовым идентификатором, с которым уже умеет работать модель.
Токеном может стать:
- целое короткое слово
- часть длинного или редкого слова
- знак препинания
- пробел вместе со следующим фрагментом
- служебный элемент, добавленный форматом API
Условно для GPT-4.1 фраза может быть разделена вот так:
Это не точное разбиение конкретной модели, а иллюстрация. Разные модели могут использовать разные токенизаторы, поэтому одно и то же предложение у них иногда занимает разное количество токенов. Отсюда следует важный вывод: нельзя надежно узнать число токенов, просто посчитав слова или символы.
Важно понимать, что преобразование выполняется в обоих направлениях:
Сначала токенизатор кодирует входной текст в токены. Модель генерирует новые токены, а инфраструктура провайдера декодирует их в обычный текст. Библиотека openai получает готовый JSON-ответ и преобразует его в Python-объект, из которого мы читаем строку answer.
AI-разработчику обычно не нужно самостоятельно управлять числовыми идентификаторами. Но нужно понимать, что именно они являются реальным объемом запроса.
И если вы посмотрите на наш код, то в объекте response.usage мы видим три основных значения:
prompt_tokens— сколько токенов модель получила на входcompletion_tokens— сколько токенов модель создала в ответеtotal_tokens— сумма входных и выходных токенов
Слово prompt здесь означает не только обращение пользователя. Во вход попадает все, что приложение отправило модели: инструкции, текущее сообщение, история диалога, описания инструментов и добавленные документы. Например, пользователь может написать всего одну строку. Но если приложение передало вместе с ней длинную инструкцию и двадцать предыдущих сообщений, значение prompt_tokens будет большим.
Что такое контекстное окно?¶
Контекстное окно — это максимальный объем токенов, который модель может учитывать в рамках одного запроса. Его можно представить как рабочий стол ограниченного размера: на нем нужно разместить инструкцию, историю, пользовательские данные и оставить место для будущего ответа.
Если вход занял почти весь доступный объем, для ответа останется мало места. Если запрос совсем не помещается, API может отклонить его как слишком большой. Точный размер контекстного окна зависит от модели. Его нельзя угадывать по названию или брать из случайного примера — нужно смотреть документацию выбранной модели.
При этом контекстное окно — это не память, то есть после завершения запроса модель сама не продолжает хранить наш диалог. При следующем вызове она увидит только те сообщения, которые приложение отправит снова.
Если мы создаем чат, историю хранит наше приложение. Перед каждым новым запросом оно выбирает нужные сообщения и добавляет их в messages. Чем длиннее история, тем больше становится вход. Именно поэтому фраза «модель помнит разговор» является удобным пользовательским описанием, но технически память организует программа вокруг модели.
Не нужно стремиться заполнить все доступное контекстное окно. Лишний текст создает несколько проблем:
- модели требуется больше времени на обработку
- быстрее расходуются лимиты токенов
- важная информация теряется среди второстепенной
- в запрос могут попасть устаревшие или противоречивые данные
- увеличивается риск передать лишнюю персональную информацию
Хорошее приложение передает не максимально возможный, а минимально достаточный контекст. Для резюме обращения это сам текст обращения и короткая понятная инструкция. История или документы нужны только тогда, когда без них нельзя выполнить задачу. Поэтому очень важно:
- Ограничивать размер пользовательских данных. Например, не принимать файл больше установленного размера.
- Удалять повторы. Одинаковые фрагменты не нужно отправлять несколько раз.
- Выбирать относящиеся к задаче данные. Для вопроса об оплате не нужна вся история обращений о доставке.
- Сокращать старую историю. Ее можно заменить проверенным резюме, сохранив важные факты отдельно.
- Делить большую работу на этапы. Длинный документ обрабатывают частями, а промежуточные результаты объединяет код.
- Заранее оставлять место для ответа. Нельзя расходовать весь лимит только на вход.
Чтобы шаг не остался только теоретическим, сделаем небольшой рефакторинг. Откроем файл main.py и посмотрим в конец функции summarize_request(), где уже находится код, который выводит usage. Чтобы функция не занималась сразу и запросом, и форматированием статистики токенов, вынесем этот вывод в отдельную функцию.
Давайте после констант BASE_URL и MODEL, но перед summarize_request(), добавим:
def print_usage(usage) -> None:
if usage is None:
print("Провайдер не вернул статистику токенов")
return
print(f"Входные токены: {usage.prompt_tokens}")
print(f"Выходные токены: {usage.completion_tokens}")
print(f"Всего токенов: {usage.total_tokens}")
То есть мы выносим отдельную функцию, которая получает usage. Теперь в функции summarize_request() найдите старый блок:
if response.usage is not None:
print(f"Входные токены: {response.usage.prompt_tokens}")
print(f"Выходные токены: {response.usage.completion_tokens}")
print(f"Всего токенов: {response.usage.total_tokens}")
Полностью замените его одной строкой:
То есть мы просто переместили логику, отвечающую за вывод токенов, внутрь print_usage(). Теперь summarize_request() управляет сценарием обращения, а отдельная функция отвечает за вывод статистики.
Запустите код и введите обычное обращение. Результат и три строки с токенами должны появиться так же, как раньше. Мы изменили организацию кода, но не пользовательское поведение.
Также можете несколько раз отправить обращения, причем начать с короткого обращения и закончить подробным обращением из нескольких абзацев, а также с тем же текстом, в котором добавлены повторы. Так вы увидите общую связь, чем больше текста получает модель, тем больше становится количество входных токенов.
В следующем шаге мы разберем, как модель создает ответ токен за токеном и почему одинаковый запрос иногда возвращает разные формулировки.
Шаг 2¶
Пока для вас AI Support Desk выглядит как обычная программа, где пользователь вводит обращение и получает краткий ответ. Но языковая модель формирует этот ответ не так, как функция с заранее записанными правилами. При этом разработчику не обязательно изучать всю математику нейронных сетей, чтобы правильно использовать LLM. На прикладном уровне достаточно понять три идеи:
- модель получает последовательность токенов
- она выбирает следующий подходящий токен
- этот процесс повторяется, пока ответ не завершится
В этом шаге мы main.py не изменяем, а сначала разберем механизм генерации простыми словами, и в конце проведем эксперимент с уже готовой программой.
Если говорить совсем простыми словами, то модель работает как очень сложное продолжение текста. Представьте подсказку при наборе сообщения на телефоне. После нескольких слов клавиатура предлагает возможное продолжение. Языковая модель действует похоже, но учитывает гораздо больший контекст и обучена на огромном количестве текстовых примеров.
Допустим, начало ответа выглядит так:
Следующим фрагментом могут оказаться разные слова:
| Возможное продолжение | Условная вероятность |
|---|---|
| товаре | 0,46 |
| заказе | 0,28 |
| экране | 0,17 |
| другое продолжение | 0,09 |
Числа придуманы только для объяснения. Они не являются результатом нашей модели. Здесь важно другое: подходящих продолжений обычно несколько, и модель каждый раз оценивает их вероятности.
То есть ответ создается не целиком, а по частям, и модель не пишет готовое предложение за один шаг. Она выбирает следующий токен, добавляет его к уже имеющемуся тексту и снова рассчитывает продолжение.
После каждого нового токена контекст немного меняется. Поэтому выбор в начале ответа влияет на все последующие слова.
Например, модель может начать резюме так:
Вариант 1: Клиент просит заменить поврежденный товар.
Вариант 2: Покупатель сообщает о повреждении и запрашивает замену.
Формулировки различаются, но смысл одинаков. Для задачи краткого пересказа оба ответа могут быть правильными.
При этом цикл выбора токенов не продолжается бесконечно. Генерация заканчивается, когда модель выбрала служебный признак завершения или API остановил ее по ограничению.
Причину мы уже выводим через choice.finish_reason:
stop— ответ завершился обычным способомlength— достигнуто ограничение длиныtool_calls— модель решила запросить инструмент
Если finish_reason равен length, текст может оборваться на середине мысли. Это не ошибка сети, ведь API успешно вернул результат, но отведенного места для ответа не хватило.
Почему одинаковый запрос может дать разные ответы?¶
И теперь давайте разберем важный момент. Поскольку у следующего токена часто есть несколько подходящих вариантов, генерация может идти по разным допустимым путям. Один запуск выберет слово «клиент», другой — «покупатель». Затем различия продолжатся дальше.
На вариативность влияют:
- параметры генерации
- особенности конкретной модели
- обновления модели или инфраструктуры провайдера
- различия в полном входном контексте
Параметры генерации мы подробно разберем в следующем разделе. Сейчас важно принять общее правило: LLM-вызов не следует считать обычной функцией, которая обязана всегда возвращать одну и ту же строку. При этом вариативность и ошибка — не одно и то же. Давайте сравним несколько результатов для обращения о поврежденном товаре.
Допустимый вариант:
Другой допустимый вариант:
Ошибочный вариант:
Первые две строки сформулированы по-разному, но сохраняют факты и задачу. Третья строка добавляет сумму, возврат и потерю заказа, которых не было во входе.
Поэтому автоматический тест не всегда должен сравнивать ответ с одной эталонной фразой символ в символ. Чаще нужно проверять свойства результата:
- ответ не пустой
- соблюден требуемый формат
- не появились запрещенные данные
- сохранены ключевые факты обращения
- ответ достаточно короткий для интерфейса
И вот мы уже говорили в первом разделе о том, что LLM полезна там, где возможны разные корректные формулировки: пересказ, классификация свободного текста, объяснение или черновик ответа. Обычный Python-код лучше подходит для точных правил, например проверить, что строка не пустая, посчитать сумму, сравнить статус заказа и так далее.
И в качестве небольшого домашнего задания, попробуйте несколько раз запустить наш код, но каждый раз вводите одно и то же обращение:
После каждого запуска запишите ответы куда-нибудь, чтобы сравнить их. А потом ответьте на вопрос, сохранился ли исходный смысл и не добавила ли модель новые факты?
Я отправил этот запрос около десяти раз и получил три варианта ответа:
Покупатель обратился с просьбой о замене товара из-за поврежденной упаковки.
Покупатель просит заменить товар из-за поврежденной упаковки.
Клиент просит заменить товар из-за поврежденной упаковки.
Как я уже говорил, не оценивайте ответы только по совпадению слов. «Покупатель» вместо «клиент» — это допустимая вариативность. А вот новая сумма, дата или действие, которых не было во входе, уже ошибка.
В целом эти ответы корректны и не содержат ошибок, но давайте в следующем шаге разберем более опасный случай, когда модель может сформулировать уверенный и правдоподобный ответ, который не подтверждается исходными данными.
Шаг 3¶
Языковая модель может написать связный, вежливый и уверенный ответ, который окажется неверным. Для AI-разработчика это одно из главных правил работы с этой технологией, ведь качество формулировки нельзя считать доказательством правильности содержания.
Если клиент написал о поврежденной упаковке, а в резюме ИИ утверждает, что поврежден сам товар, оператор может выбрать неправильное решение этого вопроса. Поэтому нам, как разработчикам, нужно понимать основные типы содержательных ошибок и заранее проектировать проверку.
Что называют галлюцинацией?¶
Галлюцинацией обычно называют ситуацию, когда модель создает неподтвержденный факт и представляет его как часть ответа. Формулировка может выглядеть естественно, потому что задача модели — построить правдоподобное продолжение текста, а не обратиться к встроенной базе гарантированно истинных записей.
В качестве примера я отправил запрос:
Коробка заказа пришла помятой. Подскажите, что делать?
И получил допустимое резюме:
Покупатель сообщает о помятой коробке заказа и просит совет по дальнейшим действиям.
Но есть шанс получить и недопустимое резюме:
Клиент получил разбитый товар и требует возврат денег.
Во втором варианте появились сразу два новых факта: товар якобы разбит, а клиент якобы требует деньги. Оба утверждения правдоподобны для похожих ситуаций, но во входном тексте они отсутствуют.
Полезно различать несколько источников неправильного результата:
1. Добавление или искажение фактов¶
Модель придумывает номер заказа, причину поломки, решение оператора или меняет важную деталь. Это классический пример галлюцинации, который чаще всего встречается.
2. Неоднозначный вход¶
Клиент пишет: «Он снова не работает». Без предыдущей истории невозможно понять, что означает «он». Модель может выбрать вероятное объяснение, но уверенный выбор не превращает догадку в факт. Правильным результатом может быть запрос уточнения.
3. Ошибка рассуждения¶
Даже если все данные находятся в контексте, модель может неправильно связать условия, пропустить исключение, ошибиться в последовательности событий или вычислении. Поэтому особенно опасно доверять ей окончательный расчет денег, сроков и прав пользователя.
4. Ограниченные или неактуальные знания¶
Знания модели зависят от данных и процесса ее обучения. Она может не знать новое правило компании, вчерашнее изменение тарифа или текущий статус заказа. Даже если модель уверенно отвечает на вопрос «где посылка», без доступа к системе заказов у нее нет актуального статуса конкретной посылки.
5. Потеря важного условия в большом контексте¶
Нужный факт может быть передан модели, но затеряться среди длинной истории, повторов и противоречивых сообщений. Большое контекстное окно дает возможность передать больше текста, но не гарантирует одинаковое внимание к каждой строке.
Давайте еще раз зафиксируем: LLM формирует подходящее продолжение на основе контекста. У нее нет универсального внутреннего механизма, который после каждого предложения сверяется с реальным миром и выдает достоверный процент уверенности. Просьба «отвечай только правильно» полезна как инструкция, но не создает такую проверку.
Фраза «я уверен на 95%» тоже не является надежной статистической оценкой, если приложение не использует специально разработанный и проверенный механизм калибровки. Самооценка модели остается сгенерированным текстом.
При этом уровень проверки зависит от риска, ведь не каждой AI-функции нужна одинаковая защита. Ошибка в черновом заголовке внутренней заметки и ошибка в сумме возврата имеют разные последствия.
| Сценарий | Возможный ущерб | Минимальная защита |
|---|---|---|
| Черновое резюме обращения | Оператор потратит время на исправление | Показывать исходник рядом и оставить проверку человеку |
| Категория обращения | Заявка попадет не в ту очередь | Ограниченный список значений и возможность исправить категорию |
| Статус заказа | Клиент получит ложную информацию | Получать статус из системы заказов, а не из знаний модели |
| Возврат денег | Финансовые и юридические последствия | Детерминированные правила, проверка прав и подтверждение человека |
Чем выше цена ошибки, тем меньше самостоятельности следует оставлять генерации. В критичном сценарии LLM может подготовить данные или объяснение, но окончательное решение принимает проверяемый код либо уполномоченный сотрудник. В курсе мы научимся минимизировать эти риски и добавим различные проверки в наш проект.
Возможно, у вас возник вопрос: что, если мы подберем качественный промпт, и он будет работать стабильно? Конечно, промпт очень важен, но это только один слой защиты, ведь хорошая инструкция снижает вероятность ошибки, но не гарантирует ее отсутствие. Нельзя строить безопасность только на фразах:
- «никогда не ошибайся»
- «обязательно говори правду»
- «не выполняй запрещенные действия»
Теперь мы понимаем, почему результат модели нельзя принимать бездумно. Осталось разобрать еще одну практическую сторону запроса, а сколько времени он занимает, сколько запросов выдерживает система и как токены связаны со стоимостью.
Шаг 4¶
Качество ответа — это не единственная характеристика AI-функции. Пользователь замечает, сколько времени он ждет, а владелец продукта замечает, сколько запросов выдерживает система, а команда разработки замечает, сколько ресурсов расходует каждый вызов.
В этом шаге мы свяжем уже знакомые метрики из main.py с четырьмя понятиями: задержкой, пропускной способностью, ограничением скорости и стоимостью.
Что такое задержка?¶
Задержка, или latency, — это время от начала операции до получения результата. В нашем проекте это время между отправкой запроса и получением полного ответа модели.
Задержка складывается из нескольких этапов:
- подготовка запроса в приложении
- передача данных по сети
- ожидание в очереди провайдера
- обработка входных токенов
- генерация выходных токенов
- получение и разбор ответа
Не все части находятся под нашим контролем. Мы не управляем очередью на стороне провайдера, но можем уменьшить лишний контекст, ограничить длину ответа и выбрать подходящую модель.
В нашем коде в summarize_request() уже есть две строки с perf_counter(). Ничего добавлять рядом с ними не нужно:
started_at = perf_counter()
try:
response = client.chat.completions.create(
model=MODEL,
messages=[...],
)
except openai.AuthenticationError:
...
elapsed_seconds = perf_counter() - started_at
Первый вызов perf_counter() запоминает начальное значение счетчика. Второй вызывается после успешного ответа. Разность сохраняется в elapsed_seconds. Наш запрос не использует потоковую передачу, то есть наш код ждет, пока модель закончит генерацию, и только затем получает объект response. Значит, таймер показывает время до полного ответа, а не до появления первого слова.
В чат-приложениях текст иногда появляется постепенно. Это называется потоковой передачей или streaming. Для нее различают две метрики:
- time to first token — сколько пользователь ждал до появления первой части ответа
- total latency — сколько прошло до завершения всего ответа
Streaming улучшает ощущение скорости, ведь пользователь раньше видит начало текста. Но полное вычисление не обязательно заканчивается быстрее. Реализацию потоковой передачи рассмотрим позже, когда добавим в наш проект чат.
Теперь давайте поговорим о том, что обычно влияет на время ответа:
Размер входа¶
Модель должна обработать весь переданный контекст. Короткое обращение обычно требует меньше работы, чем длинный документ с историей.
Длина выхода¶
Ответ создается токен за токеном. Одно предложение обычно генерируется быстрее подробного отчета. Поэтому просьба отвечать кратко влияет не только на стиль, но и на задержку.
Выбранная модель¶
Модели различаются возможностями и скоростью. Более мощная модель не всегда нужна для простого резюме, ведь чаще всего она дороже, и выбирать ее следует по измерениям на реальных примерах задачи.
Сеть и нагрузка провайдера¶
Даже одинаковые запросы могут выполняться разное время из-за сети, очереди и текущей нагрузки. Один быстрый запуск не гарантирует такую же скорость постоянно.
При этом одного измерения недостаточно. Представим пять запросов: 1,1 1,2 1,3 1,4 и 7,0 секунды. Большинство ответило быстро, но один оказался заметно медленнее. И если мы будем смотреть среднее значение, оно частично скроет картину. Поэтому в работающих системах используют перцентили:
- p50 — половина запросов завершилась не медленнее этого времени
- p95 — 95% запросов уложились в это время
- p99 — помогает увидеть редкие, особенно медленные запросы
Что такое пропускная способность?¶
Пропускная способность, или throughput, показывает, сколько работы система может выполнить за определенное время. Например:
- запросов в минуту
- токенов в минуту
- одновременно выполняемых запросов
Задержка и пропускная способность связаны, но не равны. Один запрос может отвечать за секунду, однако провайдер разрешает только небольшое количество запросов в минуту. И наоборот, система может параллельно обрабатывать много запросов, хотя каждый из них выполняется несколько секунд.
Что такое rate limit?¶
Rate limit — это ограничение частоты или объема обращений к API. Провайдер может ограничивать количество запросов, токенов или параллельных операций.
Когда лимит исчерпан, API обычно возвращает статус 429. В нашем коде этот случай уже обрабатывается:
try:
response = client.chat.completions.create(...)
except openai.RateLimitError:
print("Достигнут лимит запросов. Повторите запрос позднее.")
return
Этот блок не ускоряет восстановление лимита. Он делает поведение программы понятным, ведь вместо длинной трассировки ошибки мы получаем объяснение. При этом нельзя отвечать на 429 бесконечным циклом немедленных повторов. Такой цикл создаст еще больше запросов. Ограниченные повторы с задержкой реализуем позже.
И если подводить итоги этого раздела, который был теоретическим, то нужно зафиксировать самое главное. Цель оптимизации — это далеко не самое маленькое число токенов любой ценой. Нам нужен правильный результат за приемлемое время и с приемлемым расходом ресурсов.
- Не отправлять лишний контекст. Удалять повторы и выбирать только относящиеся к задаче данные.
- Ограничивать ожидаемый ответ. Для резюме достаточно одного предложения.
- Выбирать модель по измерениям. Простая операция может не требовать самой мощной модели.
- Не повторять успешный запрос без причины. Каждый вызов расходует время и лимит.
- Разделять интерактивные и фоновые задачи. Пользовательский запрос должен отвечать быстро, а большой пакет документов можно обработать отдельно.
- Сначала измерять. Без метрик легко оптимизировать часть, которая почти не влияет на систему.
В следующем разделе применим эти знания к устройству запроса: разделим постоянные инструкции и пользовательские данные, разберем роли сообщений, параметры генерации и выбор модели.
Шаг 5¶
Контекстное окно модели — 8192 токена. Сообщения уже занимают 7400 токенов, а приложение резервирует до 1000 токенов для ответа.
Какой вывод должен сделать разработчик до отправки запроса?
Правильный ответ
- ✅ Бюджет превышает окно: нужно сократить вход или уменьшить допустимую длину ответа
Шаг 6¶
Приложение резюмирует обращения и затем показывает оператору статус заказа.
Какие меры снижают риск выдуманных фактов?
Правильный ответ
- ✅ Передавать модели только нужный и актуальный контекст
- ✅ Проверять критичные поля и формат ответа кодом
- ✅ Разрешить явно сообщить, что данных недостаточно
- ✅ Получать фактический статус заказа из авторитетной системы, а не придумывать его моделью
Шаг 7¶
Провайдер отвечает HTTP 429, хотя отдельный запрос корректен по формату и размеру.
Что это обычно означает?
Правильный ответ
- ✅ Превышено ограничение частоты или доступной квоты
Шаг 8¶
Какие факторы могут заметно изменить задержку запроса к LLM?
@Илья_Перминов, отметил для себя, что вариант с выбором 4 правильных ответов работает лучше, чем выбор опрос с выбором одного неправильного варианта из 5 (хотя казалось бы это равноценные подходы). Когда выбираешь один неправильный, то доходишь до первого кандидата на неправильный ответ, отмечаешь его и бежишь дальше. Тут не так. Нужно хотя бы быстро прикинуть как будет работать каждый из правильных вариантов. Так запоминается больше. Взял себе на заметку )
@Олег_Моисеев, Ага, я ещё решил немного поэкспериментировать и добавил подсказки на случай неверного ответа)
Правильный ответ
- ✅ Размер входного контекста
- ✅ Выбранная модель
- ✅ Длина сгенерированного ответа
- ✅ Сеть и текущая нагрузка провайдера
Шаг 9¶
Проверка размера запроса¶
У языковой модели есть контекстное окно — максимальное количество токенов, которое она может обработать за один запрос. В этом окне должны одновременно поместиться входные сообщения и будущий ответ модели. В задаче вы потренируетесь заранее проверять, можно ли безопасно отправить запрос с выбранным лимитом ответа.
Задача¶
Дополните функцию fits_context_window(prompt_tokens, max_completion_tokens, context_window). Она должна вернуть логическое значение: помещается запрос в контекстное окно или нет.
Требования¶
- Не изменяйте имя функции
fits_context_windowи количество её аргументов. prompt_tokens,max_completion_tokensиcontext_windowдолжны быть целыми числами типаint.- Значения
TrueиFalseне считайте целыми числами. Если хотя бы один аргумент имеет недопустимый тип, выбросьтеTypeError. prompt_tokensиmax_completion_tokensдолжны быть неотрицательными. Для отрицательного значения выбросьтеValueError.context_windowдолжен быть больше нуля. Для нуля или отрицательного значения выбросьтеValueError.- Сложите
prompt_tokensиmax_completion_tokens. - Верните
True, если полученная сумма меньшеcontext_windowили равна ему. В остальных случаях вернитеFalse.
Пример результата¶
fits_context_window(7000, 1000, 8192) # True
fits_context_window(7400, 1000, 8192) # False
fits_context_window(7192, 1000, 8192) # True
Заготовка функции уже находится в редакторе. Для решения не нужны дополнительные импорты.
А как узнать, какое количество токенов будет отправлено в запросе до отправки самого запроса? Я так понимаю оно высчитывается уже в рамках его обработки на стороне провайдера
@dvezh0k, Да, мы реализуем это далее. Но все равно, окончательное количество токенов рассчитает провайдер при обработке запроса. Оно может немного отличаться от локальной оценки.
Правильный ответ
def fits_context_window(
prompt_tokens: int,
max_completion_tokens: int,
context_window: int,
) -> bool:
values = (prompt_tokens, max_completion_tokens, context_window)
if any(isinstance(value, bool) or not isinstance(value, int) for value in values):
raise TypeError("all arguments must be integers")
if prompt_tokens < 0 or max_completion_tokens < 0:
raise ValueError(
"prompt_tokens and max_completion_tokens must be non-negative"
)
if context_window <= 0:
raise ValueError("context_window must be positive")
total_tokens = prompt_tokens + max_completion_tokens
return total_tokens <= context_window
def fits_context_window(
prompt_tokens: int,
max_completion_tokens: int,
context_window: int,
) -> bool:
# Напишите реализацию.
if any(map(lambda x: type(x) != int, [prompt_tokens, max_completion_tokens, context_window])):
raise TypeError
if prompt_tokens < 0 or max_completion_tokens < 0:
raise ValueError
if context_window <= 0:
raise ValueError
return prompt_tokens + max_completion_tokens <= context_window
Шаг 10¶
Статистика использования токенов¶
После запроса к модели провайдер отдельно сообщает количество токенов во входных сообщениях и в ответе. Приложению удобно хранить эти значения вместе с общим расходом токенов. В задаче вы сформируете такой результат в виде словаря.
Задача¶
Дополните функцию token_usage(prompt_tokens, completion_tokens). Функция должна проверить оба значения, вычислить общее количество токенов и вернуть статистику в одном словаре.
Требования¶
- Не изменяйте имя функции
token_usageи количество её аргументов. prompt_tokensиcompletion_tokensдолжны быть целыми числами типаint.- Значения
TrueиFalseне считайте целыми числами. Если хотя бы один аргумент имеет недопустимый тип, выбросьтеTypeError. - Оба числа должны быть неотрицательными. Если хотя бы одно значение меньше нуля, выбросьте
ValueError. - Вычислите сумму
prompt_tokens + completion_tokens. - Верните словарь ровно с тремя ключами:
prompt_tokens,completion_tokensиtotal_tokens. - Под ключами
prompt_tokensиcompletion_tokensсохраните исходные значения. Под ключомtotal_tokensсохраните вычисленную сумму.
Пример результата¶
Заготовка функции уже находится в редакторе. Для решения не нужны дополнительные импорты.
Правильный ответ
def token_usage(prompt_tokens: int, completion_tokens: int) -> dict[str, int]:
if (
isinstance(prompt_tokens, bool)
or not isinstance(prompt_tokens, int)
or isinstance(completion_tokens, bool)
or not isinstance(completion_tokens, int)
):
raise TypeError("token values must be integers")
if prompt_tokens < 0 or completion_tokens < 0:
raise ValueError("token values must be non-negative")
total_tokens = prompt_tokens + completion_tokens
return {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
}
Шаг 11¶
Количество запросов в секунду¶
Во время нагрузочного теста приложение завершило несколько запросов к модели за известный промежуток времени. По этим данным можно вычислить пропускную способность — среднее количество завершённых запросов за одну секунду. В задаче вы реализуете этот простой показатель и обработаете некорректные входные значения.
Задача¶
Дополните функцию requests_per_second(completed_requests, elapsed_seconds). Она должна разделить количество завершённых запросов на длительность измерения и вернуть результат типа float.
Требования¶
- Не изменяйте имя функции
requests_per_secondи количество её аргументов. completed_requestsдолжен быть целым числом типаint. ЗначенияTrueиFalseне допускаются. Для другого типа выбросьтеTypeError.completed_requestsдолжен быть неотрицательным. Для отрицательного значения выбросьтеValueError.elapsed_secondsможет быть целым числом или числом с плавающей точкой, но неbool. Для другого типа выбросьтеTypeError.elapsed_secondsдолжен быть больше нуля. Для нуля или отрицательного значения выбросьтеValueError.- Вычислите результат как
completed_requests / elapsed_seconds. - Верните полученное значение как
float. Если завершённых запросов было ноль, верните0.0.
Пример результата¶
Заготовка функции уже находится в редакторе. Для решения не нужны дополнительные импорты.
Правильный ответ
def requests_per_second(completed_requests: int, elapsed_seconds: float) -> float:
if isinstance(completed_requests, bool) or not isinstance(completed_requests, int):
raise TypeError("completed_requests must be an integer")
if completed_requests < 0:
raise ValueError("completed_requests must be non-negative")
if isinstance(elapsed_seconds, bool) or not isinstance(elapsed_seconds, (int, float)):
raise TypeError("elapsed_seconds must be a number")
if elapsed_seconds <= 0:
raise ValueError("elapsed_seconds must be positive")
return float(completed_requests / elapsed_seconds)