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

11.4 · Локальная модель через Ollama + свои evals

Неделя 11 · Продакшен · ⏱ ~4 ч · Практика

Заказчики регулярно спрашивают: «а можно, чтобы данные не уходили в облако?», «а можно дешевле на нашем железе?». Правильный ответ — не «да» и не «нет», а «вот цифры на наших evals: локальная модель даёт X% против Y% у облачной, вот латентность, вот что придётся купить». В этой главе получаем такие цифры для ассистента поддержки «Полки» и заодно разбираемся, как устроены квантизация и сервинг моделей — частый вопрос на собеседованиях.

Когда локальная модель вообще имеет смысл

Заголовок раздела «Когда локальная модель вообще имеет смысл»
Довод «за» Комментарий
Приватность Данные не покидают машину. Для магазина это не абстракция: сообщения покупателей с телефонами, e-mail и номерами заказов (contains_personal_data из 5.3) и снимок базы data/shop.db в проектах 1 и 4 по правилу курса обрабатывает только локальная модель.
Нулевая цена токена Платим за железо и электричество, а не за запрос. Выгодно при большом ровном трафике или фоновых задачах.
Офлайн и независимость Нет 529 от провайдера, нет rate limit, нет изменений модели без предупреждения. Может быть фолбэком (гл. 11.2).
Контроль Фиксированная версия весов; можно дообучить (гл. 11.5).
Довод «против» Комментарий
Качество Модель на 7–8B параметров заметно слабее топовых облачных моделей в рассуждениях, следовании длинным инструкциям, tool calling и работе с длинным контекстом.
Скорость на длинном промпте RAG-промпт на 4–8k токенов на обычном ноутбуке обрабатывается секунды до первого токена.
Эксплуатация Память, обновления, мониторинг, масштабирование — теперь наши.

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

Если доводы «за» перевешивают, первый практический вопрос — поместится ли модель на нашей машине. Модель — это массив чисел (весов). Размер в памяти ≈ число параметров × байт на параметр:

Сколько байт уходит на параметр, зависит от формата хранения: FP16/BF16 — обычные 16-битные числа, Q8 и Q4 — квантизованные варианты на 8 и 4 бита (что это значит, разберём ниже):

8B параметров × 2 байта (FP16/BF16) ≈ 16 ГБ
8B × ~1 байт (Q8) ≈ 8.5 ГБ
8B × ~0.5–0.6 (Q4) ≈ 4.5–5 ГБ
+ KV-кэш (растёт с длиной контекста) + сам процесс

KV-кэш — память под промежуточные результаты внимания для каждого токена контекста. Модель 8B с контекстом 4k займёт меньше, чем та же модель с контекстом 32k — разница может быть в гигабайты. Поэтому «влезает модель» ≠ «влезает модель с нашим RAG-промптом».

На Маке с Apple Silicon память общая у CPU и GPU, поэтому модель целиком работает на GPU, если помещается в доступную ей часть RAM; на машине с отдельной видеокартой модель должна поместиться в видеопамять. Ориентир по объёму памяти: 8 ГБ — модели 3–4B в Q4 (дефолт курса qwen3:4b, или явный тег вроде qwen3:4b-instruct-2507-q4_K_M), 7–8B в Q4 — только с закрытым остальным; 16 ГБ — 7–8B в Q4; 32 ГБ и больше — Q8 или модели на 14B и крупнее. Память делят модель, bge-m3, реранкер, браузер и IDE, поэтому считаем с запасом. Если модель не влезает, часть слоёв уходит на CPU, и скорость падает в разы.

Именно квантизация позволяет 8B-модели поместиться в 5 ГБ вместо 16. Квантизация — хранение весов с меньшей точностью: вместо 16-битного числа — 8 или 4 бита плюс масштабные коэффициенты на блок весов. Аналогия: хранить цены товаров не как float64, а как небольшие целые числа с одним множителем на весь блок — компактно, но с потерей точности.

Ollama использует формат GGUF (из проекта llama.cpp, на котором Ollama построена). Названия квантизаций:

Метка Бит на вес (≈) Качество Когда брать
fp16 / bf16 16 эталон исследования, если хватает памяти
q8_0 8 практически без потерь если память позволяет
q5_K_M ~5.5 очень близко к q8 компромисс
q4_K_M ~4.5–5 заметно, но умеренно хуже частый дефолт в Ollama
q3, q2 3 и ниже деградация заметна только когда иначе не влезает

K — семейство «k-quants» (блочная квантизация с разной точностью для разных частей), M/S/L — вариант размера внутри. Правило практики: большая модель в Q4 обычно лучше маленькой в FP16 при той же памяти. Но потери от квантизации зависят от задачи — сильнее всего страдают точные вещи: арифметика (сумма заказа, тариф доставки по весу), строгий формат, длинный контекст. Именно поэтому их надо мерить на своих evals, а не верить таблицам.

Память решает, запустится ли модель; скорость — можно ли ей пользоваться. Генерация идёт в две фазы:

prefill (обработка промпта) decode (генерация ответа)
все 5000 токенов RAG-контекста токен за токеном
параллельно, упирается в последовательно, упирается
вычисления (compute) в пропускную способность памяти
↓ ↓
определяет TTFT определяет tokens/s

Отсюда сюрприз для RAG: в коротком чате локальная модель кажется быстрой (decode десятки токенов в секунду), а на промпте с 5–8 кусками из kb/ первая буква появляется через несколько секунд. Меряем обе величины: TTFT и tokens/s.

Всё это — память, квантизация, prefill и decode — мы будем мерить на Ollama, через которую с первых недель курса запускаем qwen3:4b и bge-m3. Теперь разберём её устройство подробнее. Ollama — локальный сервер моделей: скачивает модели из своего реестра (ollama pull), держит их в памяти, отдаёт HTTP API на 127.0.0.1:11434. Важные для нас факты:

  • OpenAI-совместимый endpoint: base_url="http://localhost:11434/v1", api_key любой (клиенту нужен, Ollama игнорирует). Код на OpenAI SDK или LiteLLM меняет только base_url и имя модели.
  • Есть и родной API (/api/chat, /api/generate) с дополнительными опциями, в том числе format для structured output по JSON-схеме.
  • Контекст по умолчанию — 4096 токенов (при памяти меньше 24 ГБ, то есть почти на любом ноутбуке; мы уже встречали это в главе 3.2, гл. 3.2). Длиннее — молча обрезается. Для RAG это ловушка номер один: модель «не видит» часть кусков, и кажется, что она глупая. Увеличивается через OLLAMA_CONTEXT_LENGTH при запуске сервера, num_ctx в options родного API или через Modelfile (PARAMETER num_ctx 16384) с созданием своей модели (ollama create). Больше контекст — больше память под KV-кэш.
  • Модель выгружается из памяти через 5 минут простоя (keep_alive). Первый запрос после этого включает загрузку — исключаем его из замера латентности (прогреваем).
  • Модели лежат в ~/.ollama/models.

Иллюстративный пример — тот же код, что для облака:

from openai import OpenAI
local = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") # key is ignored
resp = local.chat.completions.create(
model="<model:tag from ollama list>",
messages=[{"role": "system", "content": SYSTEM_RAG},
{"role": "user", "content": build_user_msg(question, chunks)}],
temperature=0,
)
print(resp.choices[0].message.content, resp.usage)

Безопасность: и программа, и модель — сторонние

Заголовок раздела «Безопасность: и программа, и модель — сторонние»
  • Установка и обновление Ollama (мы ставили её в главе 1.5 · Видео Карпатого + ключи API, гл. 1.5) — как любой программы: с официального сайта (на macOS — ещё Homebrew), версия не первого дня. На macOS до первого запуска проверяем подпись: codesign -dv и spctl -a -vv на .app; на Linux и Windows ставим по официальной инструкции на ollama.com.
  • Сервер слушает только 127.0.0.1 — так по умолчанию, так и оставляем. В интернете регулярно находят тысячи открытых серверов Ollama без аутентификации: любой может гонять модели на чужом железе, а в некоторых версиях находили уязвимости. OLLAMA_HOST на 0.0.0.0 не меняем. Из Docker-контейнера (гл. 11.1) ходим на host.docker.internal:11434.
  • Модели — только из официального реестра Ollama или с Hugging Face от известных авторов (организации разработчиков модели, известные квантизаторы). GGUF — формат данных, а не исполняемый код, это безопаснее pickle-файлов, но модель от неизвестного автора может быть дообучена на нужное ему поведение (например, вставлять ссылки или игнорировать инструкции). Плюс шаблон чата в Modelfile влияет на поведение.
  • Лицензия модели: у open-weight моделей разные лицензии (Apache 2.0, MIT, собственные лицензии с ограничениями). Для учебного проекта это неважно, для заказчика — важно; смотрим карточку модели.

Ollama — инструмент разработчика: один пользователь, один ноутбук, удобство. Когда модель обслуживает много пользователей на GPU-сервере, берут сервинг-движки вроде vLLM. Ключевые идеи (для собеседования):

  • Continuous batching — запросы разных пользователей объединяются в общий батч на лету, новый запрос не ждёт окончания чужих. Это резко поднимает throughput.
  • PagedAttention — KV-кэш хранится страницами, как виртуальная память в ОС, без фрагментации; влезает больше одновременных запросов.
  • OpenAI-совместимый сервер, поддержка квантизованных моделей, распределение по нескольким GPU.

Компромисс: throughput (токенов в секунду на весь сервер) против латентности одного запроса. Ollama оптимизирует удобство одного пользователя, vLLM — пропускную способность.

Теперь понятно, что мерить и на чём запускать; осталось поставить эксперимент так, чтобы его результату можно было верить. Эксперимент должен менять одну переменную — генератор. Всё остальное фиксировано: тот же набор eval/golden.jsonl (гл. 6.3), тот же индекс, retrieval и эмбеддинги (эмбеддинги на другие в этом же эксперименте не переключаем — это второй эксперимент и переиндексация), тот же промпт ответа, temperature=0, тот же LLM-судья (облачный, один для всех строк — иначе сравниваются судьи).

Столбец Как мерить
Hit rate retrieval не меняется — контрольная цифра, что ничего не сломано; archived_in_context = 0
Correctness / faithfulness eval/run.py из гл. 6.4
Доля ответов с корректными ссылками [n] на kb/ регекс + судья
Правильные отказы на вопросах без ответа в базе (refusal_ok, false_refusals) из гл. 5.3 и гл. 6.4
TTFT, p50/p95 латентности, tokens/s после прогрева, стриминг для TTFT
Стоимость 100 ответов облако: eval/cost.py (гл. 6.4), формула гл. 11.2; локально: $0 API + примечание о железе

С учётом шума (гл. 8.1): разница в 2–3 п.п. на 50 вопросах может быть случайной. Пишем число вопросов.

Эта глава и так про Ollama — раздел ниже дополняет теорию тем, что понадобится, когда Ollama становится не экспериментом, а вторым рабочим провайдером рядом с DeepSeek. Факты проверены по docs.ollama.com и api-docs.deepseek.com в ноябре 2026.

В «честном сравнении» из теории облачная модель — DeepSeek (deepseek-flash; deepseek-v4-pro — как более сильная), на наборе eval/golden.jsonl по публичной базе знаний kb/. Вопросы и база выдуманы и публичны, поэтому облаку их отдавать можно. Меняется только base_url и модель. rag-local-q4 и rag-local-q8 — наши локальные модели с увеличенным контекстом в двух квантизациях; их создаём в Практике через Modelfile:

import os
from openai import OpenAI
cloud = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com", timeout=60)
local = OpenAI(api_key="ollama", base_url="http://localhost:11434/v1") # key is ignored
GENERATORS = {
"deepseek-flash": (cloud, "deepseek-flash", {"extra_body": {"thinking": {"type": "disabled"}}}),
"local-q4": (local, "rag-local-q4", {"temperature": 0}),
"local-q8": (local, "rag-local-q8", {"temperature": 0}),
}

В раннере из главы 6.4 это три конфига для eval/run.py --config: eval/configs/baseline.json ("answer_model": "deepseek-flash", "answer_thinking": false) и eval/configs/local_q4.json, local_q8.json — копии baseline, в которых отличается только answer_model (rag-local-q4, rag-local-q8).

Тонкость, которая портит сравнение: у DeepSeek режим размышления включён по умолчанию, и в нём temperature молча игнорируется. Либо выключаем размышление у облачной строки (как выше), либо сравниваем «облако с размышлением» отдельной строкой — но не смешиваем. Локальные модели с размышлением (если выбранная его поддерживает) — тоже отдельная строка.

Судья — модель сильнее проверяемых и одна на все строки: deepseek-v4-pro для публичного набора; Claude — если хотим сравнить судей. На данных с личными сведениями (сообщения покупателей, снимок data/shop.db) облачный судья запрещён тем же правилом, что облачный генератор: там судья — код, ручная проверка и самая сильная локальная модель, которая помещается в память, либо оценка на демо-базе data/demo_shop.db.

OpenAI-совместимый слой удобен для переключения, но поддерживает не всё. На ноябрь 2026 в /v1/chat/completions нет tool_choice, logprobs, n, logit_bias, user, а картинки — только base64. Если коду нужно что-то из этого (например, принудительный вызов инструмента), это различие провайдеров, а не баг. Родной API (/api/chat, официальная библиотека ollama) даёт то, чего в совместимом слое нет или неудобно:

  • options={"num_ctx": 16384} на запрос — контекст без отдельного Modelfile;
  • keep_alive на запрос (-1 — держать модель в памяти, 0 — выгрузить сразу);
  • метрики по фазам в наносекундах (load_duration, prompt_eval_duration, eval_duration, prompt_eval_cached_count) — из них TTFT без сети и tokens/s, см. глава 11.3 (гл. 11.3);
  • structured output по JSON-схеме через format (Structured Outputs); в совместимом слое — через response_format.
# illustrative: native Ollama call with a JSON schema (ollama Python library, pinned)
from pydantic import BaseModel
import ollama
class StructuredAnswer(BaseModel): # not rag.pipeline.Answer: this is the raw model output
answer: str
sources: list[int] # chunk numbers [n]
found: bool # False -> code returns REFUSAL and offers handoff()
r = ollama.chat(model="rag-local-q4", messages=messages,
format=StructuredAnswer.model_json_schema(), options={"num_ctx": 16384, "temperature": 0})
ans = StructuredAnswer.model_validate_json(r.message.content) # still validate: the schema guides, code verifies

У DeepSeek structured output устроен иначе — JSON-режим через response_format (глава 2.2 · Structured output: Pydantic + Instructor, гл. 2.2). Если ответ ассистента структурный, сравниваем одинаковые режимы.

У DeepSeek нет эндпоинта эмбеддингов (в API Reference на ноябрь 2026 его нет), поэтому эмбеддинги в этой книге — Ollama (bge-m3 через /api/embed, или /v1/embeddings через OpenAI-клиент, Embeddings) или sentence-transformers. Для этой главы правило из теории остаётся: эмбеддинги в эксперименте не меняем — переключение генератора и переиндексация — два разных эксперимента.

На macOS Ollama использует GPU Apple Silicon через Metal (Hardware support), но только при нативном запуске: в Docker Desktop на macOS GPU недоступен — нет проброса GPU (FAQ). Контейнер ollama/ollama на Маке работает на CPU и в разы медленнее. Поэтому на Маке: Ollama — нативное приложение, ассистент поддержки в контейнере ходит к ней через host.docker.internal:11434 (глава 11.1 · Docker: упаковать проект, гл. 11.1). Контейнер Ollama — для Linux-сервера с GPU и для ревьюера портфолио; поддержка видеокарт на Linux и Windows описана на той же странице Hardware support.

Память, кроме весов и KV-кэша из теории:

  • OLLAMA_NUM_PARALLEL (по умолчанию 1) — сколько запросов модель обрабатывает одновременно; по FAQ требуемая RAM растёт как OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH. Параллельный прогон evals с num_ctx=16384 и 4 потоками — четыре контекста в памяти.
  • OLLAMA_KV_CACHE_TYPE (по умолчанию f16) — квантизация KV-кэша; работает при включённом Flash Attention и заметно экономит память на длинном контексте. Это ещё одна переменная эксперимента: меняем — отдельная строка в таблице.
  • OLLAMA_MAX_LOADED_MODELS — сколько моделей одновременно в памяти. Две квантизации одной модели в одном прогоне — две копии весов.

У Ollama есть облачные модели и облачный API (https://ollama.com/v1, ключ OLLAMA_API_KEY, Cloud). Запрос к облачной модели уходит на серверы, а не остаётся на нашей машине — даже если она вызывается через ту же команду ollama. Для сообщений покупателей и снимка базы это то же самое, что любое облако: нельзя. Проверяем, что в ollama list для приватных задач только локальные модели, и что base_url — localhost.

Сравнение провайдеров по тому, на что опирается эта глава

Заголовок раздела «Сравнение провайдеров по тому, на что опирается эта глава»
Что Claude OpenAI DeepSeek Ollama (локально)
Где обрабатываются данные облако Anthropic облако OpenAI облако, серверы в Китае — никаких личных данных покупателей своя машина
Цена 100 ответов по прайсу, с кэшем по прайсу, с кэшем на порядок дешевле, кэш автоматический, вне пика −50% $0 API + железо
Длина контекста большая, см. docs модели большая, см. docs модели 1M токенов у обеих моделей 4096 по умолчанию (при памяти меньше 24 ГБ), больше — через num_ctx, ценой памяти
Structured output tool use / structured outputs response_format с JSON-схемой JSON-режим (response_format) format с JSON-схемой (родной API), response_format (совместимый)
Tool calling есть, включая принудительный выбор есть, включая tool_choice есть; в режиме размышления reasoning_content надо возвращать в следующих запросах только у моделей с поддержкой tools; tool_choice в совместимом слое нет
Эмбеддинги нет своего эндпоинта есть нет есть
temperature=0 для воспроизводимости см. ограничения новых моделей (глава 1.7, гл. 1.7) работает игнорируется в режиме размышления работает
Холодный старт нет нет нет (но очередь под нагрузкой) загрузка модели после keep_alive

Генераторы: deepseek-flash (облако) против локальной модели в двух квантизациях; судья — deepseek-v4-pro, один для всех строк; набор — eval/golden.jsonl по публичной kb/. Затем — отдельный маленький прогон «что выбрать для приватных задач», где облако вообще не участвует: только локальные модели на снимке data/shop.db (scripts/snapshot.sh, VACUUM INTO, только чтение) — бот-аналитик проекта 1 с вопросами из questions.md.

  1. Открываем Ollama и GitHub ollama/ollama → README: установка, ollama run/pull/list/ps, REST API, раздел про модели и память (≈30 мин). Обращаем внимание на требования к RAM для разных размеров моделей.
  2. В документации Ollama открываем страницу OpenAI compatibility и FAQ → как задать контекст (num_ctx, OLLAMA_CONTEXT_LENGTH), keep_alive, OLLAMA_HOST, где лежат модели (≈25 мин). Главное — дефолт 4096 токенов контекста.
  3. Открываем библиотеку моделей Ollama → выбираем 2 кандидата по объёму памяти своей машины (8 ГБ — 3–4B; 16 ГБ — 7–8B; 32 ГБ+ — 14B и крупнее) с поддержкой нужных функций (tools — для бота-аналитика проекта 1), смотрим доступные теги квантизации и размер (≈20 мин). Читаем карточку модели: автор, лицензия, длина контекста.
  4. Квантизация: в репозитории llama.cpp на GitHub находим описание квантизаций GGUF / k-quants; плюс на Hugging Face в docs Hub страница про GGUF (≈20 мин). Цель — понимать, что значат q4_K_M и q8_0, не формулы.
  5. Открываем Ollama: OpenAI compatibility → списки поддерживаемых и неподдерживаемых полей для chat completions и embeddings (≈10 мин). Отмечаем всё, что использует наш код.
  6. Открываем Ollama: Structured Outputs, Embeddings и Tool calling (≈25 мин). Обращаем внимание, что format принимает JSON-схему Pydantic-модели.
  7. Открываем Ollama: Usage и Hardware support → метрики времени (наносекунды) и раздел про свою платформу: Metal на Маке, видеокарты на Linux и Windows (≈10 мин).
  8. Открываем DeepSeek: Thinking Mode → как выключить размышление и что в этом режиме игнорируется (≈10 мин): это нужно, чтобы облачная строка сравнения была честной.
  9. Открываем vLLM → обзор / Getting started и раздел, где описаны PagedAttention и continuous batching (≈25 мин). Цель — уметь за 2 минуты объяснить, чем vLLM отличается от Ollama.
  • Обрезанный контекст. Самая частая причина «локальная модель отвечает ерундой» в RAG. Проверяем: usage.prompt_tokens в ответе ≈ ожидаемой длине промпта? Если он ровно упирается в 4096 — обрезка.
  • Холодный старт в замерах. Первый запрос включает загрузку модели в память. Прогреваем, затем меряем.
  • Сравнение разных вещей: поменяли сразу модель и эмбеддинги, или промпт подогнали под локальную — результат не интерпретируется.
  • Формат ответа. Маленькие модели хуже держат правила вроде «каждое утверждение со ссылкой [n]» и «если ответа нет в базе знаний — ответь фразой REFUSAL». Это нормально и это результат, а не баг; возможно, нужен упрощённый промпт — но тогда это отдельная строка в таблице.
  • Tool calling у локальных моделей поддерживается не всеми и работает менее надёжно. Для бота-аналитика (проект 1) и агента-разборщика заказов (проект 4) это особенно заметно.
  • Ноутбук в работе. На 8 ГБ даже 4B-модель в Q4 занимает заметную часть памяти, а 8B в Q4 — больше половины; на 16 ГБ 8B в Q4 с длинным контекстом — около трети. Модель греет ноутбук и сажает батарею. Во время прогона закрываем тяжёлое (браузер с десятками вкладок, IDE с индексацией, Docker), иначе латентность меряет своп, а не модель. ollama stop <model> после работы.
  • Неофициальные модели «uncensored-super-v2» от неизвестных авторов — нет.
  • «Бесплатно». Токены — да, но в README указываем железо и что на сервере это была бы аренда GPU.
  • Ollama в Docker на Маке — без GPU, на CPU. Цифры латентности из контейнера на Маке не сравниваем с нативными.
  • Облачная модель через ollama — не локальная. Для приватных задач проверяем, что модель скачана и base_url — localhost.
  • Вывод «локальная хуже — значит, всё в облако» нарушает правило приватности: сообщения с личными данными покупателей и снимок базы всё равно не уходят в облако. Выбор для них — локальная модель или оператор (handoff), а не DeepSeek.
  • Неравные условия с DeepSeek: у него размышление включено по умолчанию и temperature в этом режиме игнорируется; облачная строка тогда — другая конфигурация, чем кажется.
  • Код, завязанный на tool_choice, на Ollama через совместимый слой не заработает: этого поля там нет.
  • Проверяем Ollama, установленную в главе 1.5: ollama --version. Если нужно обновить — на macOS с официального сайта или через brew, подпись новой версии (codesign -dv, spctl -a -vv) проверяем до первого запуска; на Linux и Windows — по официальной инструкции. Убеждаемся, что сервер слушает только loopback: lsof -nP -iTCP:11434 -sTCP:LISTEN (macOS, Linux) показывает 127.0.0.1.
  • Выбираем модель из официального реестра по объёму памяти (8 ГБ — 3–4B, по умолчанию qwen3:4b; 16 ГБ — 7–8B; 32 ГБ+ — 14B и крупнее) и скачиваем в двух квантизациях, если доступны (например, q4_K_M и q8_0; на 8 ГБ Q8 для 4B ещё влезает, для 8B — нет). Записываем размер на диске (ollama list) и в памяти (ollama ps).
  • Быстрый тест в терминале: ollama run <model> и 3 вопроса о правилах «Полки» без контекста («можно ли вернуть вскрытый чай?», «какая гарантия на LAMP-LIGHT-MINI?», «действует ли TEA15 вместе со скидкой?») — смотрим, как модель выдумывает без RAG.
  • Увеличиваем контекст под RAG-промпт: создаём Modelfile:
    FROM <model:tag>
    PARAMETER num_ctx 16384
    PARAMETER temperature 0
    ollama create rag-local-q4 -f Modelfile (и rag-local-q8 с FROM второй квантизации). На 8 ГБ начинаем с num_ctx 8192: контекст тоже занимает память. Проверяем ollama ps — сколько теперь памяти и что модель целиком на GPU.
  • Подключаем к ассистенту поддержки: в .env ANSWER_PROVIDER=ollama, ANSWER_MODEL_LOCAL=rag-local-q4, OLLAMA_BASE_URL=http://localhost:11434/v1 (или host.docker.internal из контейнера). Переключение — переменной окружения, без правки кода.
  • Проверка обрезки: на 3 вопросах сравниваем ожидаемую длину промпта (подсчитываем токены) и usage.prompt_tokens от Ollama.
  • Прогон набора eval/golden.jsonl по публичной kb/: eval/run.py с конфигами baseline.json (deepseek-flash, размышление выключено), local_q4.json и local_q8.json. Для каждой строки: correctness/faithfulness (судья — deepseek-v4-pro, один для всех строк), доля ответов с корректными ссылками, правильные отказы, p50/p95, стоимость 100 ответов. TTFT и tokens/s меряем отдельным скриптом eval/bench_local.py (прогрев, стриминг, последовательно). Трейсы в Langfuse с тегом модели (гл. 11.3): в наборе только выдуманные вопросы, им туда можно.
  • Строки в таблице экспериментов (гл. 8.5):
    | Эксперимент | Hit rate | Correctness | Ссылки ок | p50 / p95 | Стоимость 100 отв. |
    |---|---|---|---|---|---|
    | Облако deepseek-flash | ... | ... | ... | ... | $... (пик/не пик) |
    | Локальная <модель> q4_K_M | ... | ... | ... | ... | $0 API (CPU/GPU ?, RAM ? ГБ) |
    | Локальная <модель> q8_0 | ... | ... | ... | ... | $0 API |
  • (Для сравнения) Добавляем строку с Claude или OpenAI на тех же вопросах, если есть ключ: насколько дороже и лучше ли.
  • Structured output: переводим ответ локальной модели на format с Pydantic-схемой StructuredAnswer (answer, sources, found) через родной API. Сравниваем долю валидных ответов и корректных ссылок со свободным текстом.
  • Метрики из родного API: для 10 запросов записываем load_duration, prompt_eval_duration, eval_count/eval_duration. Сходится ли tokens/s с нашим замером через стриминг?
  • Разбор: 5 вопросов, где локальная ошиблась, а облачная нет. Какой тип ошибки (не нашла в контексте, нарушила формат, выдумала, перепутала «Свет» и «Свет мини», не отказала)? Записываем, можно кодами из 8.4 (гл. 8.4).
  • Вывод в 3–5 предложений: где локальная модель достаточно хороша (например, переформулировка запроса plan_query_ollama из гл. 5.1 — можно проверить отдельно через eval/run_query_eval.py), где нет, и что выбрать для приватных задач. Для них сравниваем только локальных кандидатов (облако там не вариант): бот-аналитик проекта 1 с LLM_PROVIDER=ollama на снимке data/shop.db (только чтение) — 10 вопросов из questions.md через scripts/run_questions.py на двух локальных моделях; assert_data_policy не даст случайно подставить облачную.
  • После работы: ollama stop <model>.
  • В таблице экспериментов есть строка (лучше две — разные квантизации) «локальная модель» с качеством, латентностью и стоимостью, посчитанными на том же наборе, что облачная.
  • Контекст настроен под RAG-промпт и обрезка проверена по usage.
  • Есть письменный вывод: где локальная модель достаточна, а где нет, с цифрами, и что берём для данных покупателей.

Вопрос 1

Сколько примерно памяти займут веса модели на 8B параметров в FP16 и в Q4?

FP16: ≈ 8B × 2 байта ≈ 16 ГБ. Q4: ≈ 4.5–5 ГБ. Плюс KV-кэш и накладные расходы.

Вопрос 2

Почему модель, которая «влезает» в память, может не влезть с RAG-промптом на 16k токенов?

KV-кэш растёт с длиной контекста и может занимать гигабайты; память = веса + KV-кэш + процесс.

Вопрос 3

Локальная модель в RAG отвечает так, будто не видит половины кусков базы знаний. Какая самая вероятная причина в Ollama и как её проверить?

Контекст по умолчанию 4096 токенов — промпт обрезан. Проверить usage.prompt_tokens против ожидаемой длины; увеличить num_ctx (Modelfile/OLLAMA_CONTEXT_LENGTH/options).

Вопрос 4

Что обычно лучше при одинаковой памяти?

  • a) модель 3B в FP16
  • b) модель 8B в Q4
  • c) всегда одинаково
  • d) модель 1B в FP32

b. Обычно бóльшая модель с умеренной квантизацией лучше маленькой в полной точности, но проверять надо на своих evals.

Вопрос 5

Чем prefill отличается от decode, и какая фаза определяет TTFT?

Prefill — обработка всего промпта параллельно, decode — генерация токен за токеном. TTFT определяется prefill, tokens/s — decode.

Вопрос 6

Что не так в этом эксперименте: «переключили генератор на локальную модель, эмбеддинги тоже на другие, промпт упростили, судью поставили тоже локальный — качество упало на 20%, значит локальные модели плохие»?

Поменяно четыре переменные сразу, включая судью — результат нельзя приписать генератору. Меняют одну переменную, судья одинаковый для всех строк.

Вопрос 7

Почему не стоит запускать Ollama с OLLAMA_HOST=0.0.0.0 на ноутбуке или сервере?

Сервер без аутентификации станет доступен из сети: чужие смогут пользоваться нашим железом, а уязвимости сервера станут удалённо эксплуатируемыми. Слушать только 127.0.0.1.

Вопрос 8

Почему первый запрос к Ollama после паузы медленный, и как это учесть в замерах?

Через keep_alive (по умолчанию 5 минут) модель выгружается, и запрос включает её загрузку в память. Делать прогревочный запрос и исключать его из статистики.

Вопрос 9

Две ключевые техники vLLM, которые повышают throughput на сервере?

Continuous batching (динамическое объединение запросов в батч) и PagedAttention (страничное хранение KV-кэша без фрагментации).

Вопрос 10

Какие задачи из проектов курса — хорошие кандидаты на локальную модель, а какие — нет?

Хорошие: переформулировка запроса, классификация и маршрутизация вопроса покупателя, извлечение полей из сообщений поставщиков, короткие суммаризации, и всё, где данные нельзя выпускать наружу: сообщения покупателей с телефонами, e-mail и номерами заказов, снимок базы магазина. Плохие: агент-разборщик заказов с многошаговым tool calling, ответы со строгим цитированием длинного контекста.

Вопрос 11

Почему модели нужно брать только из официального реестра или от известных авторов, если GGUF — формат данных, а не код?

Веса определяют поведение: модель от неизвестного автора может быть дообучена на вредное поведение (вставлять ссылки, игнорировать инструкции), а шаблон чата влияет на ответы. Проверенный источник снижает риск подмены.

Вопрос 12

Контейнер ollama/ollama на Маке с Apple Silicon даёт 3 tokens/s против 40 у нативной Ollama. Почему?

В Docker Desktop на macOS нет проброса GPU: контейнер работает на CPU. На Маке Ollama запускают нативно (Metal), а контейнеры ходят к ней через host.docker.internal.

Вопрос 13

Облачная строка сравнения — DeepSeek с параметрами по умолчанию и temperature=0. Что не так с этой строкой?

У DeepSeek размышление включено по умолчанию, и в этом режиме temperature игнорируется: строка — «облако с размышлением», дороже и медленнее, и не та конфигурация, что записана в отчёте. Выключить размышление (thinking: disabled) или сделать две отдельные строки.

Вопрос 14

Почему эмбеддинги в этой книге считает Ollama, а не DeepSeek, и почему в этом эксперименте их всё равно нельзя менять?

У DeepSeek нет эндпоинта эмбеддингов, а у Ollama есть (/api/embed). В эксперименте меняется только генератор: смена эмбеддингов требует переиндексации и меняет retrieval — это второй эксперимент.

Мы увидели, где маленькая модель проигрывает. Следующий вопрос — можно ли её подтянуть дообучением: в главе 11.5 · Файнтюнинг: базовый пример LoRA разберём, когда это оправдано, а когда задачу решает промпт или RAG.