Перейти к содержимому

3.4 · Проект 1: логирование стоимости, README, GitHub

Неделя 3 · Основы LLM · ⏱ ~4 ч · Проект

Доводим проект до состояния «не стыдно показать». Работодатель открывает репозиторий на две минуты: README, схема, как запустить, сколько стоит ответ. Строка «средняя стоимость ответа $0.004, 2.1 инструмента на вопрос» говорит об авторе больше, чем любой красивый промпт: значит, он думает о продакшене. А логи каждого запроса — фундамент для evals и мониторинга, к которым мы вернёмся в неделе 6 (evals) и неделе 11 (продакшен).

⚠️ Публичный репозиторий — только демо-база. Персональные данные покупателей (имена, email, телефоны, города) и цифры из снимка базы магазина не должны попасть в него ни файлом, ни скриншотом, ни строчкой README, ни логом — ни сейчас, ни в истории git. Данные в снимке выдуманы, но обращаемся с ними как с настоящими — эта привычка понадобится на работе.

Один запрос пользователя — это не один вызов API, а цикл из 1–5 шагов (гл. 3.3). Логировать нужно на двух уровнях:

Уровень Поля Зачем
Шаг (вызов LLM) модель, input/output токены, cache-токены, stop_reason, длительность понять, какой шаг дорогой
Запрос (вопрос → ответ) время, request_id, вопрос, вызванные инструменты с аргументами, сумма токенов, стоимость, полное время ответа, число шагов, статус (ok / error / max_steps), длина ответа /stats, README, отладка, будущие evals

Пример записи запроса (JSON Lines — один JSON на строку):

{"ts": "2026-12-04T10:15:02+00:00", "request_id": "a1b2c3", "question": "какая выручка за неделю?",
"tools": [{"name": "get_sales", "args": {"period": "week"}, "ms": 4, "chars": 312}],
"steps": 2, "input_tokens": 2841, "output_tokens": 96, "cache_read_tokens": 0,
"cost_usd": 0.0099, "latency_ms": 3120, "status": "ok", "model": "<model-id>"}

Почему JSONL: дописывается одной строкой, не ломается при падении процесса посреди записи (испортится максимум последняя строка), читается и jq, и pandas.read_json(..., lines=True), и десятью строками Python. Альтернатива — отдельная SQLite-база logs/requests.db с таблицей requests: удобнее для /stats через SQL. Обе нормальны; главное — это отдельный файл, а не таблица в снимке базы магазина: снимок открыт только на чтение и должен таким остаться.

estimate_cost(model, input_tokens, output_tokens, cache_read_tokens=0, cache_write_tokens=0) мы уже написали в гл. 1.7, а подсчёт usage и стоимости одного вызова в call_llm() — в гл. 2.7. Новое здесь — стоимость не вызова, а целого ответа бота, и в ней две тонкости:

  1. Стоимость запроса = сумма по всем шагам. На каждом шаге модель заново читает системный промпт, схемы инструментов, историю и все предыдущие результаты. Поэтому второй шаг почти всегда дороже первого по input-токенам.
  2. Кэш оплачивается иначе. Если включён prompt caching (гл. 2.7), в usage будут отдельные поля для записи в кэш и чтения из кэша (cache_creation_input_tokens, cache_read_input_tokens у Anthropic), и у них свои цены. Их нужно учитывать в estimate_cost, иначе стоимость будет завышена или занижена.

Цены меняются — не зашиваем их «по памяти». Словарь цен держим в bot/pricing.py с комментарием «checked on YYYY-MM-DD, source: pricing page» и ссылкой на актуальную страницу цен провайдера. В README пишем «≈ $X за ответ на модели Y по ценам на дату Z».

Пример расчёта (цены условные, для арифметики; цены DeepSeek и тот же пример на них — в разделе «DeepSeek и Ollama»): шаг 1 — 2000 input + 60 output, шаг 2 — 2400 input + 120 output. При $3 / 1M input и $15 / 1M output: (4400 × 3 + 180 × 15) / 1 000 000 = $0.0159. Видно, что output-токенов мало, а платим мы в основном за повторное чтение контекста — вот почему компактные результаты инструментов из гл. 3.2 экономят деньги.

Деньги — только половина картины: владелец замечает не стоимость, а то, сколько ждёт ответа, а у локальной модели время вообще единственная «цена». Мерим time.perf_counter() — монотонные часы, не time.time(). Три числа:

  • полное время запроса (от получения сообщения до отправки ответа) — то, что чувствует пользователь;
  • время каждого вызова LLM — обычно 90% общего;
  • время каждого инструмента — должно быть миллисекунды; если сотни мс — нет индекса или функция тянет лишнее.

Для статистики полезны не только среднее, но и медиана и p95: один запрос с ретраем на 40 секунд испортит среднее, а p95 покажет, как часто так бывает.

4. Как встроить логирование, не замусорив код

Заголовок раздела «4. Как встроить логирование, не замусорив код»

Что писать, понятно; теперь — где это делать в коде. Метаданные собираем в объекте, который проходит через цикл, и пишем запись один раз в конце, в finally, — тогда упавшие запросы тоже попадут в лог:

# bot/agent.py (illustrative)
async def answer(question: str, history: list[dict]) -> AgentResult:
trace = RequestTrace(question=question) # dataclass with counters
started = time.perf_counter()
try:
... # loop from AIL-15
# after each call_llm: trace.add_step(resp.usage, step_ms, resp.model)
# after each tool: trace.add_tool(name, args, tool_ms, len(content))
trace.status = "ok"
return result
except Exception:
trace.status = "error"
raise
finally:
trace.latency_ms = int((time.perf_counter() - started) * 1000)
trace.cost_usd = trace.total_cost() # uses estimate_cost per step
write_jsonl(LOG_PATH, trace.to_dict())

Ошибка записи лога не должна ронять ответ пользователю: write_jsonl оборачиваем в try/except с log.exception.

Лог пишется после каждого запроса, и из него легко собрать сводку прямо в Telegram. /stats — команда для владельца, читает лог и отвечает текстом:

Запросов: 47 (сегодня 6)
Потрачено: $0.41 (сегодня $0.05)
Средняя стоимость ответа: $0.0087, медиана $0.0071
Время ответа: медиана 2.8 с, p95 7.9 с
Инструментов на вопрос: 1.6; чаще всего get_sales (21)
Ошибок: 2

Логика — чистая функция compute_stats(records, now, tz) -> Stats с тестом pytest на 5 ручных записях; хендлер только читает файл и форматирует. «Сегодня» — в часовом поясе владельца магазина, как и в гл. 3.3.

До сих пор бот работал на снимке базы, а для README нужны скриншоты, примеры ответов и цифры, которые можно показать всем. Их даёт демо-база из гл. 3.2. Одна кодовая база, два источника данных — переключение через переменные окружения, без if demo: по коду:

SHOP_DB_PATH=data/demo_shop.db
ANALYTICS_DB_PATH=data/demo_analytics.db
LOG_PATH=logs/demo-requests.jsonl

Демо-базы создаёт scripts/make_fake_db.py (гл. 3.2). Всё, что попадает в README — скриншот диалога, пример ответа, цифры стоимости — снимается в демо-режиме. Стоимость на демо-базе будет близка к стоимости на снимке: размер контекста зависит от схем и промпта, а не от цифр в заказах.

Удобно добавить в /start строку «режим: DEMO» — как метку DEMO/PROD в шапке админки, чтобы на скриншоте было видно, что данные сгенерированы.

Цифры из /stats и скриншоты из демо-режима собираем в README. Структура:

  1. Название и одна фраза: «Telegram-бот для владельца интернет-магазина: отвечает на вопросы о заказах, продажах и интересе к товарам через tool calling. Только чтение».
  2. Скриншот/GIF диалога (демо-база).
  3. Что умеет: 4 инструмента одной строкой каждый, примеры вопросов.
  4. Архитектура: схема из гл. 3.1 (Mermaid), три слоя «только чтение».
  5. Безопасность: снимок через VACUUM INTO, mode=ro + query_only, нет функций записи (изменить заказ, оформить возврат, выдать промокод) и запросов к платёжным провайдерам и службам доставки, доступ по Telegram ID.
  6. Быстрый старт: Python-версия, установка, .env, генерация демо-базы, тесты, запуск. Команды копируются и работают как есть.
  7. Стоимость и качество: средняя стоимость ответа, медиана времени, «N из 15 вопросов» из run_questions.py на демо-базе.
  8. Ограничения: расходы на доставку не учтены (прибыль — до доставки), интерес к товарам — только за последние 30 дней, история в памяти.
  9. Что дальше: evals, кэширование, веб-интерфейс.

Критерий «человек со стороны может запустить» проверяется буквально: чистый клон в новую папку, только README, без знаний, которые есть лишь у автора.

Перед тем как сделать репозиторий публичным:

  • git ls-files — нет *.db, .env, logs/, data/ (демо-базу каждый генерирует сам через make_fake_db.py);
  • git log --all --stat -- '*.db' '*.jsonl' '.env' — этих файлов не было и в истории. Если были — проще создать новый репозиторий с чистой историей, чем вычищать старую;
  • поиск по коду на ключи и токены: git grep -nE "sk-|ANTHROPIC_API_KEY=.+|[0-9]{8,10}:[A-Za-z0-9_-]{30,}" (последнее — формат токена Telegram);
  • в .env.example только имена переменных;
  • скриншоты: демо-режим, нет нашего Telegram ID, имени, имён и контактов покупателей, цифр из снимка;
  • questions.md и примеры ответов — на демо-базе.

В настройках GitHub есть secret scanning и push protection — включаем их для репозитория, но относимся как ко второй сетке, не первой.

Каждый провайдер отдаёт токены по-своему, и самая частая ошибка — сложить поля, которые уже включают друг друга. Всё приводим к одному виду сразу после вызова: Usage(input_total, input_cached, output). В записи лога из теории это поля input_tokens (= input_total) и cache_read_tokens (= input_cached).

Провайдер Все входные токены Из них из кэша Выходные Особенность
Claude input_tokens + cache_creation_input_tokens + cache_read_input_tokens cache_read_input_tokens output_tokens input_tokens не включает кэш; запись в кэш дороже обычного input
OpenAI prompt_tokens prompt_tokens_details.cached_tokens completion_tokens кэш включён в prompt_tokens
DeepSeek prompt_tokens (= prompt_cache_hit_tokens + prompt_cache_miss_tokens) prompt_cache_hit_tokens completion_tokens кэш автоматический, без параметров; размышления — в completion_tokens_details.reasoning_tokens (у бота thinking выключен)
Ollama, нативный /api/chat prompt_eval_count — eval_count плюс длительности в наносекундах: total_duration, load_duration, prompt_eval_duration, eval_duration
Ollama, /v1 usage в формате OpenAI — в доке совместимости явно описан только stream_options.include_usage для стриминга; на первом запросе стоит проверить, что в обычном ответе resp.usage заполнен, иначе берём числа из нативного API
# bot/usage.py (illustrative)
from dataclasses import dataclass
@dataclass
class Usage:
input_total: int
input_cached: int
output: int
def from_deepseek(u) -> Usage:
return Usage(u.prompt_tokens, getattr(u, "prompt_cache_hit_tokens", 0) or 0,
u.completion_tokens)
def from_ollama_native(r: dict) -> Usage:
return Usage(r.get("prompt_eval_count", 0), 0, r.get("eval_count", 0))

Тест на каждый адаптер — на записанном вручную фейковом объекте usage (без сети).

Цены DeepSeek на ноябрь 2026 (Models & Pricing), USD за 1M токенов:

deepseek-flash Ночные (off-peak) Пиковые
Input, попадание в кэш 0.003 0.006
Input, промах кэша 0.15 0.30
Output 0.60 1.20

Пик — 01:00–04:00 и 06:00–10:00 UTC по будням, кроме китайских праздников; в остальное время цены вдвое ниже. У deepseek-v4-pro цены в несколько раз выше — они на той же странице. DeepSeek прямо пишет, что цены может менять, поэтому дата проверки в pricing.py обязательна. Если в llm_utils.py уже есть is_offpeak из практики гл. 2.7, deepseek_is_peak ниже — его отрицание: правило пиковых часов держим в одном месте, а не копируем.

# bot/pricing.py (illustrative)
# checked on 2026-11-13, source: https://api-docs.deepseek.com/quick_start/pricing
DEEPSEEK_PRICES = { # USD per 1M tokens
"deepseek-flash": {
"off_peak": {"hit": 0.003, "miss": 0.15, "out": 0.60},
"peak": {"hit": 0.006, "miss": 0.30, "out": 1.20},
},
}
def deepseek_is_peak(ts_utc) -> bool:
# Mon-Fri 01:00-04:00 and 06:00-10:00 UTC; Chinese public holidays not modelled
return ts_utc.weekday() < 5 and (1 <= ts_utc.hour < 4 or 6 <= ts_utc.hour < 10)
def deepseek_cost(model: str, u: Usage, ts_utc) -> float:
p = DEEPSEEK_PRICES[model]["peak" if deepseek_is_peak(ts_utc) else "off_peak"]
miss = u.input_total - u.input_cached
return (u.input_cached * p["hit"] + miss * p["miss"] + u.output * p["out"]) / 1_000_000

По какому моменту запроса (начало или конец) DeepSeek относит его к пику, в документации не указано (проверено в ноябре 2026) — для оценки в README это неважно, но совпадения с биллингом до цента ждать не стоит.

Тот же пример, что в теории (шаг 1: 2000 input + 60 output, шаг 2: 2400 input + 120 output), на deepseek-flash ночью, если на втором шаге 2000 токенов префикса попали в кэш: (2400 × 0.15 + 2000 × 0.003 + 180 × 0.60) / 1 000 000 ≈ $0.00047 — примерно в 30 раз дешевле условных $3/$15. Попадание в кэш не гарантировано: DeepSeek описывает кэш как best effort, поэтому логируем input_cached и смотрим реальную долю в /stats.

Ollama: cost_usd = 0, но запись в логе всё равно нужна — у локальной модели «стоимость» это время и память. Логируем latency_ms, токены и скорость генерации eval_count / eval_duration × 1e9 (токенов в секунду), а в /stats показываем медиану и p95 времени ответа по модели. Первый запрос после простоя включает load_duration — загрузку модели в память; его нужно отделять, иначе медиана будет врать.

Claude OpenAI DeepSeek Ollama
Стоимость ответа бота считать по своим ценам (глава 1.7) по своим ценам доли цента; пик/ночь, кэш автоматически $0, платим временем
Поля кэша запись и чтение отдельно cached_tokens внутри prompt_tokens prompt_cache_hit/miss_tokens нет в деньгах
Что писать в README — (для сравнения) — стоимость ответа на демо-базе с датой цен медиана/p95 времени ответа и модель, железо (процессор, объём RAM)
Логи с персональными данными — — не бывает: DeepSeek видит только демо logs/requests.jsonl, только локально

Для работодателя это сильный пункт — автор подумал, куда уходят данные. Пример:

Снимок базы магазина с персональными данными покупателей обрабатывается только локальной моделью через Ollama (qwen3:4b-instruct-2507-q4_K_M), запрос не покидает машину. Облачные модели (DeepSeek) подключаются только к сгенерированной демо-базе; бот отказывается стартовать с облачным провайдером и снимком базы (assert_data_policy). Провайдер переключается переменными окружения, код один — OpenAI-совместимый клиент.

Плюс в «Стоимость и качество» — две строки: DeepSeek на демо ($X за ответ, цены на дату), Ollama (медиана N с, с указанием процессора и объёма RAM), и «N из 15» для обеих. Результат «12 из 15 на снимке базы» можно указать числом, без самих вопросов и ответов.

  1. Открываем Python: logging HOWTO → Basic Tutorial и начало Advanced Tutorial (loggers, handlers, formatters) (≈30 мин). Берём: logging.getLogger(__name__), log.exception пишет traceback; для журнала запросов отдельный JSONL-файл проще, чем logging-handler.
  2. Python: json → json.dumps(..., ensure_ascii=False) (≈5 мин) — иначе русский текст вопросов станет \u043a....
  3. DeepSeek: Models & Pricing → цены cache hit / cache miss / output, пиковые часы (≈10 мин). Дату проверки записываем в pricing.py.
  4. DeepSeek: Context Caching и Create Chat Completion → поля usage (prompt_cache_hit_tokens, prompt_cache_miss_tokens, reasoning_tokens), как работает автоматический кэш (≈15 мин).
  5. Ollama: Chat API → поля ответа prompt_eval_count, eval_count, длительности в наносекундах, load_duration (≈10 мин).
  6. (Для сравнения) Anthropic: Pricing и Anthropic: Prompt caching → цены и поля usage для кэша (≈15 мин). Сравниваем: явные cache_control у Anthropic против автоматического кэша DeepSeek.
  7. GitHub: Creating diagrams → Mermaid flowchart (≈10 мин) — для схемы в README.
  8. GitHub: Basic writing and formatting syntax → изображения, таблицы, блоки кода (≈10 мин).
  9. README 2–3 известных проектов (например, Anthropic Cookbook, Instructor) → как устроены первые экраны: что видно за 30 секунд (≈15 мин).
  • Лог содержит вопросы и ответы с данными из снимка — суммы, номера заказов, иногда имена покупателей → logs/ в .gitignore; демо пишет в отдельный файл.
  • Не логируем секреты: заголовки запросов, ключи, полные объекты клиента. Логируем usage и метаданные, а не «весь resp».
  • Стоимость только последнего шага вместо суммы — классическая ошибка: цифра в README окажется в 2–3 раза ниже реальной.
  • time.time() для длительностей может прыгать при синхронизации часов; perf_counter() — нет.
  • Падение записи лога роняет ответ — нужен try/except вокруг записи.
  • /stats доступен только владельцу — он стоит под тем же фильтром роутера из гл. 3.3. Стоит проверить, что команда не повешена на отдельный роутер без фильтра.
  • Устаревшие цены. Через полгода README будет врать, если не указать дату и модель.
  • README, который работает только у автора: абсолютные пути, забытая зависимость, неявная версия Python, шаг «скопировать базу магазина». В публичном README базы магазина нет вообще — только make_fake_db.py.
  • История git помнит всё. Удалить файл последним коммитом недостаточно.
  • Двойной счёт кэша. У OpenAI и DeepSeek кэш уже внутри prompt_tokens, у Anthropic — нет. Если прибавить prompt_cache_hit_tokens к prompt_tokens, стоимость завысится.
  • Ночная цена DeepSeek в README. Если замер сделан ночью — так и пишем: днём (в пик) та же нагрузка стоит вдвое дороже.
  • Медиана Ollama с холодным стартом. Первый запрос после простоя включает загрузку модели; без пометки cold_start в логе статистика времени смешает два разных режима.
  • bot/usage.py и bot/pricing.py. Нормализация usage (DeepSeek, Ollama; Anthropic — для сравнения) и цены DeepSeek с пиком/ночью, дата проверки и источник в комментарии; для Ollama стоимость 0. Тесты: пример из раздела «DeepSeek и Ollama» даёт ≈ $0.00047 ночью и вдвое больше в пик (pytest.approx); deepseek_is_peak на границах 01:00, 04:00, 06:00, 10:00 UTC и в субботу. (Для сравнения) estimate_cost для Claude с cache-полями — пример из теории даёт $0.0159.
  • Скорость Ollama в логе. Для локальных запросов пишем tokens_per_s и флаг cold_start (по load_duration из нативного API или по первому запросу после простоя).
  • RequestTrace (dataclass) и запись JSONL в finally, ensure_ascii=False. Путь из LOG_PATH.
  • Проверка лога. Задаём боту 5 вопросов, затем:
    Окно терминала
    tail -n 5 logs/requests.jsonl | python -m json.tool --json-lines
    Убеждаемся: у каждого запроса есть инструменты, токены, cost_usd, latency_ms, status; упавший запрос (переименовываем файл базы) тоже записан со status: "error".
  • /stats. compute_stats(records, now, tz) + тест на ручных записях (включая запись с ошибкой и запись «вчера»); разбивка по провайдеру и модели (деньги — DeepSeek, время — Ollama); хендлер под фильтром владельца.
  • Демо. python scripts/make_fake_db.py --seed 42, переменные окружения на демо-файлы, /start показывает «режим: DEMO» и провайдера/модель. Прогоняем run_questions.py на демо-базе с DeepSeek и с Ollama, записываем итог «N/15» и стоимость/время для обоих.
  • Скриншот диалога в демо-режиме (3–4 вопроса, включая один отказ «оформи возврат по заказу 1042») → docs/demo.png. Проверяем, что на нём нет данных из снимка, имён и контактов покупателей, нашего имени и Telegram ID.
  • README по структуре из теории: схема Mermaid, разделы «Безопасность» и «Модели и приватность», быстрый старт (с установкой Ollama и тегом модели; вариант с DEEPSEEK_API_KEY — только для демо), стоимость ответа на DeepSeek и медиана времени на Ollama из /stats на демо, ограничения.
  • Проверка чистоты. git ls-files, git log --all --stat -- '*.db' '*.jsonl' '.env', git grep на токены — всё пусто.
  • Тест «человек со стороны». Клонируем репозиторий в новую временную папку, создаём новое окружение, следуем README дословно: установка → .env → демо-база → pytest → запуск бота. Каждое место, где пришлось думать или знать что-то не из README, исправляем в README.
  • Push и переключение репозитория в public (если был приватным). Включаем secret scanning / push protection в настройках репозитория.
  • Каждый запрос пишется в лог: вопрос, инструменты, токены, стоимость (сумма по шагам), время ответа, статус — включая упавшие.
  • /stats показывает число запросов и потраченные деньги (всего и сегодня), среднюю стоимость, время ответа.
  • README: что делает, архитектура, безопасность, модели и приватность, как запустить, скриншот диалога, средняя стоимость ответа (DeepSeek, с датой цен) и время ответа (Ollama, с моделью и железом).
  • /stats и лог корректно считают usage у DeepSeek (кэш не задвоен, пик/ночь) и время у Ollama.
  • Демо работает на сгенерированной базе, в репозитории и его истории нет снимка базы, персональных данных покупателей и секретов.
  • Человек со стороны может запустить проект по README — проверено чистым клоном.

Вопрос 1

Запрос прошёл 3 шага цикла. Как посчитать его стоимость?

  • a) по usage последнего шага
  • b) по usage первого шага × 3
  • c) суммой стоимости всех шагов, с учётом cache-токенов
  • d) по длине ответа.

c. Каждый шаг заново оплачивает весь контекст; cache-токены тарифицируются отдельно.

Вопрос 2

Почему JSONL удобен для журнала запросов?

Дописывается по строке, падение процесса портит максимум последнюю строку, легко читается построчно Python, jq, pandas.

Вопрос 3

Что не так в этом коде?

start = time.time()
result = await answer(q, hist)
log_file.write(json.dumps({"q": q, "cost": result.cost,
"ms": (time.time() - start) * 1000}))

time.time() вместо perf_counter(); нет перевода строки — записи слипнутся в одну строку; json.dumps без ensure_ascii=False; не записываются инструменты, токены, статус; при исключении в answer запись не происходит (нет finally).

Вопрос 4

Почему запись лога лучше делать в finally?

Тогда запросы, упавшие с исключением, тоже попадают в лог — а именно они нужнее всего для отладки и статистики ошибок.

Вопрос 5

Где хранить журнал запросов?

  • a) в новой таблице снимка shop.db
  • b) в отдельном файле logs/requests.jsonl или logs/requests.db
  • c) в README
  • d) в истории Telegram.

b. Снимок базы магазина открыт только на чтение и должен таким остаться; журнал — отдельный файл.

Вопрос 6

Почему стоимость ответа на демо-базе — честная оценка для README?

Стоимость определяется размером контекста (системный промпт, схемы, история, компактные результаты) и числом шагов, а не конкретными цифрами в заказах; структура данных та же.

Вопрос 7

Файл data/shop.db удалили из репозитория отдельным коммитом. Можно делать репозиторий публичным?

Нет: файл с персональными данными покупателей остаётся в истории git. Нужно переписать историю или проще — создать новый репозиторий с чистой историей.

Вопрос 8

Зачем указывать дату и модель рядом с «средней стоимостью ответа»?

Цены и модели меняются; без даты и модели цифра через несколько месяцев вводит в заблуждение.

Вопрос 9

Почему в статистике полезна медиана/p95 времени ответа, а не только среднее?

Редкие долгие запросы (ретраи, таймауты) сильно сдвигают среднее; медиана показывает типичный опыт, p95 — насколько часто бывает плохо.

Вопрос 10

Какие три вещи нужно проверить на скриншоте для README?

Включён демо-режим и данные сгенерированы; нет имени и Telegram ID владельца, имён и контактов покупателей, цифр из снимка; нет токенов, путей и прочих технических секретов в кадре.

Вопрос 11

Как проверить критерий «человек со стороны может запустить проект»?

Клонировать в чистую папку, новое окружение, выполнить README дословно до работающего бота и зелёного pytest; всё, что потребовало знаний вне README, — исправить.

Вопрос 12

Что не так в этой формуле стоимости для DeepSeek?

cost = ((u.prompt_tokens + u.prompt_cache_hit_tokens) * 0.15
+ u.completion_tokens * 0.60) / 1_000_000

prompt_tokens уже включает попадания в кэш — их посчитали дважды; токены из кэша надо брать по цене hit, а по цене miss — только prompt_tokens − prompt_cache_hit_tokens; цены зашиты числами без даты и без учёта пика (днём вдвое дороже).

Вопрос 13

Почему для бота на Ollama всё равно нужен журнал запросов, если cost_usd всегда 0? Что в нём главное?

У локальной модели «стоимость» — время и память: журнал даёт медиану и p95 времени ответа, скорость генерации, холодные старты, ошибки и вызванные инструменты — и это всё ещё основа для evals.

Вопрос 14

Какой провайдер может появиться в разделе README «Стоимость и качество» с цифрами по снимку базы магазина, а какой — только с цифрами по демо-базе?

Со снимком базы работает только Ollama — её результат «N из 15» на снимке можно указать числом (без самих данных). DeepSeek снимок не видит никогда, его цифры — только с демо-базы.

Проект 1 готов. Дальше этап «RAG и эмбеддинги»: в гл. 4.1 «Эмбеддинги руками на numpy» начинается путь к Проекту 2 — ассистенту по базе знаний магазина (правила доставки, возврата, гарантии, инструкции к товарам), где лог запросов и скрипт прогона вопросов из этой недели превратятся в настоящие evals.