3.4 · Проект 1: логирование стоимости, README, GitHub
Неделя 3 · Основы LLM · ⏱ ~4 ч · Проект
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Доводим проект до состояния «не стыдно показать». Работодатель открывает репозиторий на две минуты: README, схема, как запустить, сколько стоит ответ. Строка «средняя стоимость ответа $0.004, 2.1 инструмента на вопрос» говорит об авторе больше, чем любой красивый промпт: значит, он думает о продакшене. А логи каждого запроса — фундамент для evals и мониторинга, к которым мы вернёмся в неделе 6 (evals) и неделе 11 (продакшен).
⚠️ Публичный репозиторий — только демо-база. Персональные данные покупателей (имена, email, телефоны, города) и цифры из снимка базы магазина не должны попасть в него ни файлом, ни скриншотом, ни строчкой README, ни логом — ни сейчас, ни в истории git. Данные в снимке выдуманы, но обращаемся с ними как с настоящими — эта привычка понадобится на работе.
1. Что логировать и зачем
Заголовок раздела «1. Что логировать и зачем»Один запрос пользователя — это не один вызов 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. Обе нормальны; главное — это отдельный файл, а не таблица в снимке базы магазина: снимок открыт только на чтение и должен таким остаться.
2. Стоимость
Заголовок раздела «2. Стоимость»estimate_cost(model, input_tokens, output_tokens, cache_read_tokens=0, cache_write_tokens=0) мы уже написали в гл. 1.7, а подсчёт usage и стоимости одного вызова в call_llm() — в гл. 2.7. Новое здесь — стоимость не вызова, а целого ответа бота, и в ней две тонкости:
- Стоимость запроса = сумма по всем шагам. На каждом шаге модель заново читает системный промпт, схемы инструментов, историю и все предыдущие результаты. Поэтому второй шаг почти всегда дороже первого по input-токенам.
- Кэш оплачивается иначе. Если включён 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 экономят деньги.
3. Время ответа
Заголовок раздела «3. Время ответа»Деньги — только половина картины: владелец замечает не стоимость, а то, сколько ждёт ответа, а у локальной модели время вообще единственная «цена». Мерим 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.
5. /stats
Заголовок раздела «5. /stats»Лог пишется после каждого запроса, и из него легко собрать сводку прямо в 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.
6. Демо-режим
Заголовок раздела «6. Демо-режим»До сих пор бот работал на снимке базы, а для README нужны скриншоты, примеры ответов и цифры, которые можно показать всем. Их даёт демо-база из гл. 3.2. Одна кодовая база, два источника данных — переключение через переменные окружения, без if demo: по коду:
SHOP_DB_PATH=data/demo_shop.dbANALYTICS_DB_PATH=data/demo_analytics.dbLOG_PATH=logs/demo-requests.jsonlДемо-базы создаёт scripts/make_fake_db.py (гл. 3.2). Всё, что попадает в README — скриншот диалога, пример ответа, цифры стоимости — снимается в демо-режиме. Стоимость на демо-базе будет близка к стоимости на снимке: размер контекста зависит от схем и промпта, а не от цифр в заказах.
Удобно добавить в /start строку «режим: DEMO» — как метку DEMO/PROD в шапке админки, чтобы на скриншоте было видно, что данные сгенерированы.
7. README, который работает
Заголовок раздела «7. README, который работает»Цифры из /stats и скриншоты из демо-режима собираем в README. Структура:
- Название и одна фраза: «Telegram-бот для владельца интернет-магазина: отвечает на вопросы о заказах, продажах и интересе к товарам через tool calling. Только чтение».
- Скриншот/GIF диалога (демо-база).
- Что умеет: 4 инструмента одной строкой каждый, примеры вопросов.
- Архитектура: схема из гл. 3.1 (Mermaid), три слоя «только чтение».
- Безопасность: снимок через
VACUUM INTO,mode=ro+query_only, нет функций записи (изменить заказ, оформить возврат, выдать промокод) и запросов к платёжным провайдерам и службам доставки, доступ по Telegram ID. - Быстрый старт: Python-версия, установка,
.env, генерация демо-базы, тесты, запуск. Команды копируются и работают как есть. - Стоимость и качество: средняя стоимость ответа, медиана времени, «N из 15 вопросов» из
run_questions.pyна демо-базе. - Ограничения: расходы на доставку не учтены (прибыль — до доставки), интерес к товарам — только за последние 30 дней, история в памяти.
- Что дальше: evals, кэширование, веб-интерфейс.
Критерий «человек со стороны может запустить» проверяется буквально: чистый клон в новую папку, только README, без знаний, которые есть лишь у автора.
8. Чистота публичного репозитория
Заголовок раздела «8. Чистота публичного репозитория»Перед тем как сделать репозиторий публичным:
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 — включаем их для репозитория, но относимся как ко второй сетке, не первой.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Одна запись лога — четыре формата usage
Заголовок раздела «Одна запись лога — четыре формата usage»Каждый провайдер отдаёт токены по-своему, и самая частая ошибка — сложить поля, которые уже включают друг друга. Всё приводим к одному виду сразу после вызова: 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
@dataclassclass 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 три цены и время суток
Заголовок раздела «Стоимость: у DeepSeek три цены и время суток»Цены 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/pricingDEEPSEEK_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, только локально |
README: раздел «Модели и приватность»
Заголовок раздела «README: раздел «Модели и приватность»»Для работодателя это сильный пункт — автор подумал, куда уходят данные. Пример:
Снимок базы магазина с персональными данными покупателей обрабатывается только локальной моделью через Ollama (
qwen3:4b-instruct-2507-q4_K_M), запрос не покидает машину. Облачные модели (DeepSeek) подключаются только к сгенерированной демо-базе; бот отказывается стартовать с облачным провайдером и снимком базы (assert_data_policy). Провайдер переключается переменными окружения, код один — OpenAI-совместимый клиент.
Плюс в «Стоимость и качество» — две строки: DeepSeek на демо ($X за ответ, цены на дату), Ollama (медиана N с, с указанием процессора и объёма RAM), и «N из 15» для обеих. Результат «12 из 15 на снимке базы» можно указать числом, без самих вопросов и ответов.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Открываем Python: logging HOWTO → Basic Tutorial и начало Advanced Tutorial (loggers, handlers, formatters) (≈30 мин). Берём:
logging.getLogger(__name__),log.exceptionпишет traceback; для журнала запросов отдельный JSONL-файл проще, чем logging-handler. - Python: json →
json.dumps(..., ensure_ascii=False)(≈5 мин) — иначе русский текст вопросов станет\u043a.... - DeepSeek: Models & Pricing → цены cache hit / cache miss / output, пиковые часы (≈10 мин). Дату проверки записываем в
pricing.py. - DeepSeek: Context Caching и Create Chat Completion → поля
usage(prompt_cache_hit_tokens,prompt_cache_miss_tokens,reasoning_tokens), как работает автоматический кэш (≈15 мин). - Ollama: Chat API → поля ответа
prompt_eval_count,eval_count, длительности в наносекундах,load_duration(≈10 мин). - (Для сравнения) Anthropic: Pricing и Anthropic: Prompt caching → цены и поля usage для кэша (≈15 мин). Сравниваем: явные
cache_controlу Anthropic против автоматического кэша DeepSeek. - GitHub: Creating diagrams → Mermaid flowchart (≈10 мин) — для схемы в README.
- GitHub: Basic writing and formatting syntax → изображения, таблицы, блоки кода (≈10 мин).
- 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-linescost_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 шага цикла. Как посчитать его стоимость?
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
Где хранить журнал запросов?
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_000prompt_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.