️ Оборудование
Данная модель была обучена с учётом имеющегося оборудования: видеокарта с 8 GB памяти, процессор AMD Ryzen 7 (сопоставимый по производительности с Intel Core i7) и 32 GB оперативной памяти.
Затраченное время на обучениеОбщее время обучения составило 15 часов. В процессе работы система трижды перезагружалась, что потребовало корректировки нагрузки и параметров запуска. Для обучения было использовано 1400 эталонных примеров. По мере накопления новых эталонных данных планируется проведение дополнительного дообучения модели.
Как мы развернули локальный ИИ на Windows-сервере, обучили/сконвертировали модель, завели её в LM Studio и подключили к ZennoPoster через OpenAI-совместимое API. Материал акцентирует безопасность: все данные обрабатываются на своём железе, без отправки во внешние облака.
Зачем ИИ на локальной инфраструктуре компании
- Данные не покидают периметр компании. Отсутствуют облачные передачи и «телеметрия по умолчанию».
- Предсказуемые расходы: нет тарификации за токены и сетевой трафик.
- Гибкость: можно хранить несколько моделей, быстро переключаться и обкатывать свои системные промты/правила.
Архитектура решения
- LM Studio (Windows): удобный UI + локальный HTTP-сервер с OpenAI-совместимыми эндпоинтами (/v1/chat/completions, /v1/models, /v1/embeddings).
- GGUF-модель (Gemma-2-9B-IT + наш LoRA, варианты F16, Q5_1, Q4_0).
Что такое GGUF- GGUF — это формат хранения и запуска больших языковых моделей, разработанный для llama.cpp и совместимых фреймворков.
- Его цель: удобная упаковка модели с метаданными, поддержка разных квантованных вариантов, оптимизация под локальный запуск (on-premise, edge, десктоп).
- В отличие от старых форматов (GGML, GPTQ), GGUF более гибок и поддерживает расширенные параметры.
Базовая модель: Gemma-2-9B-IT- Gemma-2-9B-IT — это модель от Google, 9 миллиардов параметров, обученная с акцентом на инструкции (Instruction-Tuned).
- Она хорошо подходит для диалогов, генерации текста, анализа данных.
- Конфигурация использует её как основу, а сверху накладывается LoRA.
LoRA (Low-Rank Adaptation)- LoRA — метод дообучения модели без изменения всех весов.
- Суть: добавляются небольшие матрицы-адаптеры, которые корректируют поведение модели.
- Преимущество: можно быстро обучить под конкретные задачи (например, юридические документы, бизнес-брендологию, API-интеграции) без огромных затрат.
- В нашем случае: Gemma-2-9B-IT + наш LoRA = базовая модель + адаптация под нашу бизнес задачу - логистика.
- ZennoPoster: вызывает локальный LM Studio сервер, получает JSON-ответ, раскладывает в переменные и запускает дальнейшие сценарии.
ZennoPoster → http://127.0.0.1:1234/v1/chat/completions → LM Studio → локальная модель (CPU/Vulkan/CUDA) → ответ в JSON.
Обучение: от сырых объявлений к локальной модели
1) Данные: что именно мы обучали
Цель. Научить модель из шумных объявлений по грузоперевозкам извлекать стандартизированный JSON:
JSON:
{
"departure_point": "Страна, Город" | null,
"arrival_point": "Страна, Город" | null,
"tonn": number | null,
"id_car_type": "Тент" | "Рефрижератор" | "Изотерма" | null,
"date_loading_iso": "YYYY-MM-DD" | null,
"opisanie": "строка" | null
}
Мы обучаем в стиле инструкций: system задаёт железные правила, user — входной блок, assistant — эталонный JSON.
JSON:
{
"messages": [
{
"role": "system",
"content": "Ты — детерминированный парсер объявлений о грузоперевозках. Отвечай ТОЛЬКО валидным JSON-массивом без каких-либо префиксов. Схема каждого объекта: {\"departure_point\":\"Страна, Город\"|null, \"arrival_point\":\"Страна, Город\"|null, \"tonn\":number|null, \"id_car_type\":\"Тент\"|\"Рефрижератор\"|\"Изотерма\"|null, \"date_loading_iso\":\"YYYY-MM-DD\"|null, \"opisanie\":\"строка\"|null}. Правила нормализации стран ... (как в нашем системном промте)."
},
{
"role": "user",
"content": "Входной блок:\nГруз бутылки Вес до 20 тонн Авто тент, 10 машин из ТАШКЕНТА в ТБИЛИСИ\nСегодня: 2025-11-12\nЗадача: верни ТОЛЬКО валидный JSON."
},
{
"role": "assistant",
"content": "[{\"departure_point\":\"Узбекистан, Ташкент\",\"arrival_point\":\"Грузия, Тбилиси\",\"tonn\":20,\"id_car_type\":\"Тент\",\"date_loading_iso\":\"2025-11-12\",\"opisanie\":\"Бутылочная тара, до 20 т, 10 авто, тент.\"}]"
}
]
}
Быстрая заготовка очистки и упаковки (Python)
Python:
# make_dataset.py
import json, re, random, pathlib
from datetime import date
today = str(date.today())
phone_re = re.compile(r"\+?\d[\d\s\-\(\)]{7,}")
url_re = re.compile(r"https?://\S+|t\.me/\S+|@\w+")
def clean(text: str) -> str:
text = phone_re.sub("[контакты удалены]", text)
text = url_re.sub("[ссылка удалена]", text)
text = re.sub(r"[ \t]+", " ", text)
return text.strip()
SYSTEM = ("Ты — детерминированный парсер объявлений о грузоперевозках. "
"Отвечай ТОЛЬКО валидным JSON-массивом без префиксов/кодблоков. "
"Схема каждого объекта: {\"departure_point\":\"Страна, Город\"|null, "
"\"arrival_point\":\"Страна, Город\"|null, \"tonn\":number|null, "
"\"id_car_type\":\"Тент\"|\"Рефрижератор\"|\"Изотерма\"|null, "
"\"date_loading_iso\":\"YYYY-MM-DD\"|null, \"opisanie\":\"строка\"|null}. "
"Нормализация стран: Россия, Узбекистан, Беларусь, Казахстан, Литва, Латвия, "
"Эстония, Кыргызстан, Таджикистан, Туркменистан, Грузия, Армения, Азербайджан, "
"Польша, Германия. Если нельзя определить — null. Кириллица обязательна.")
def pack(sample_text: str, gold_json: str):
return {
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"Входной блок:\n{clean(sample_text)}\nСегодня: {today}\nЗадача: верни ТОЛЬКО валидный JSON."},
{"role": "assistant", "content": gold_json.strip()}
]
}
# Пример: берем ваши пары (raw_text -> эталонный JSON),
# ниже – просто демонстрация. Реально идём по своей базе.
pairs = [
("Тент 20т Ташкент -> Тбилиси 10 авто",
'[{"departure_point":"Узбекистан, Ташкент","arrival_point":"Грузия, Тбилиси","tonn":20,"id_car_type":"Тент","date_loading_iso":"' + today + '","opisanie":"Тент 20т"}]')
]
random.shuffle(pairs)
val_split = int(len(pairs) * 0.98)
train, val = pairs[:val_split], pairs[val_split:]
pathlib.Path("data").mkdir(exist_ok=True)
with open("data/train.jsonl", "w", encoding="utf-8") as f:
for t, j in train:
f.write(json.dumps(pack(t, j), ensure_ascii=False) + "\n")
with open("data/val.jsonl", "w", encoding="utf-8") as f:
for t, j in val:
f.write(json.dumps(pack(t, j), ensure_ascii=False) + "\n")
print("saved:", len(train), len(val))
Ключевой критерий датасета — строгость формата и разнообразие формулировок.
2) Обучение: QLoRA на базе Gemma-2-9B-IT
Мы обучали адаптер (LoRA), затем сливали его в полные веса (fp16). Это даёт:
- малые требования к GPU при обучении (QLoRA/4-бит),
- возможность распространять только LoRA (если нужно),
- единый итоговый чекпоинт после слияния.
Python:
# merge_lora.py
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import torch, os, shutil
BASE = "google/gemma-2-9b-it"
LORA = "./outputs/gemma2-9b-it-logist-lora"
MERGED = "./outputs/gemma2-9b-it-lora-merged-fp16"
tok = AutoTokenizer.from_pretrained(BASE, use_fast=True)
base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.float16)
model = PeftModel.from_pretrained(base, LORA)
model = model.merge_and_unload() # слияние адаптера
os.makedirs(MERGED, exist_ok=True)
model.save_pretrained(MERGED)
tok.save_pretrained(MERGED)
# скопировать generation_config/tokenizer json, если нужно:
for f in ["generation_config.json", "special_tokens_map.json", "tokenizer.json"]:
src = os.path.join(LORA, f)
if os.path.exists(src):
shutil.copy(src, MERGED)
print("Merged to", MERGED)
3) Конвертация HF → GGUF и квантизация
Мы используем llama.cpp.
Конвертация в F16 GGUF
Python:
# Внутри репозитория llama.cpp
# D:\llama.cpp
python convert-hf-to-gguf.py --model-dir "D:\outputs\gemma2-9b-it-lora-merged-fp16" \
--outfile "D:\models_out\Gemma-2-9b-it-logist.F16.gguf" --outtype f16
Квантизация в Q5_1 и Q4_0
Python:
# llama-quantize (Windows)
# пример путей:
"D:\llama.cpp\build\bin\Release\llama-quantize.exe" ^
"D:\models_out\Gemma-2-9b-it-logist.F16.gguf" ^
"D:\models_out\Gemma-2-9b-it-logist.Q5_1.gguf" Q5_1
"D:\llama.cpp\build\bin\Release\llama-quantize.exe" ^
"D:\models_out\Gemma-2-9b-it-logist.F16.gguf" ^
"D:\models_out\Gemma-2-9b-it-logist.Q4_0.gguf" Q4_0
- Gemma-2 (Google) имеет свою лицензию — проверяйте условия распространения весов/адаптеров.
- Если распространяете только LoRA — проще соблюсти требования (часто разрешено).
2. Подготовка и хранение моделей
Мы использовали Gemma-2-9B-IT с нашим LoRA и получили файлы:
- Gemma-2-9b-it-logist.F16.gguf — полноточная версия.
- Gemma-2-9b-it-logist.Q5_1.gguf
- Gemma-2-9b-it-logist.Q4_0.gguf — квантизованные варианты (меньше ОЗУ/ВРАМ, быстрее, иногда с небольшой потерей качества).
3. Частые проблемы загрузки модели и решения
Симптом: «Error loading model. (Exit code: 1844674407… Unknown error)».
Проверяем по порядку:
- Установлен ли подходящий Runtime для GGUF (Vulkan/CUDA/CPU) и выбран по умолчанию.
- Модель точно в формате GGUF, а не safetensors.
- Модель лежит в отдельной папке, путь без «нестандартных» символов.
- Обновите Runtime до актуальной версии (вкладка Runtime → Update).
Мы создали системный промт reed_logist — строгий парсер объявлений о грузоперевозках, который возвращает только валидный JSON-массив без лишнего текста.
Содержимое хранится в LM Studio → вкладка Context (preset).
Важный нюанс: сервер LM Studio не умеет подхватывать пресеты «по имени» через API (возможно можно, но я не смог этого сделать). Поэтому системный промт выбирали вручную после загрузки модели.
5. Встраиваем в ZennoPoster-скрипт
C#:
// Вход
string zapros = (project.Variables["zapros"].Value ?? "").Trim(); // ваш «Входной блок…»
string today = System.DateTime.Now.ToString("yyyy-MM-dd");
if (!zapros.Contains("Сегодня:")) zapros += "\nСегодня: " + today;
// Вызов
string endpoint = "http://127.0.0.1:1234/v1/chat/completions";
string model = "logist@q5_1";
string sysPath = @"D:\llm\prompts\reed_logist.txt";
string content = CallLmStudio(endpoint, model, sysPath, zapros, 0.2, 240000);
// Выходы ZP
project.Variables["otvet"].Value = content ?? "";
project.Variables["test"].Value = string.IsNullOrWhiteSpace(content) ? "0" : "1";
- Выбор квантизации:
Q5_1 — золотая середина для качества/скорости.
Q4_0 — ещё быстрее и меньше памяти, но сильнее проседает качество.
F16 — лучшее качество, но требует больше ОЗУ/ВРАМ. - Движок: при наличии NVIDIA используйте CUDA llama.cpp; универсально — Vulkan. На слабых машинах можно оставить CPU.
- Температура/топ-p: для строгого JSON-парсинга держите temperature низкой (0.1–0.3).
- LM Studio сервер слушает 127.0.0.1:1234 — внешний доступ недоступен.
- Все запросы и ответы остаются на хост-машине. Никаких облачных API.
Мы развернули полностью локальную цепочку:
- Получили свою модель (в том числе слияние LoRA) и подготовили квантизованные GGUF-варианты.
- Правильно разложили по папкам, чтобы LM Studio автоматически подхватил модели.
- Настроили Runtime (Vulkan/CUDA/CPU) и устранили проблемы загрузки.
- Создали строгий системный промт reed_logist, который гарантирует ответ строго в JSON.
- Подключили ZennoPoster к локальному OpenAI-совместимому серверу LM Studio и организовали обмен данными без выхода в интернет.
Для запуска проектов требуется программа ZennoPoster.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...
Для того чтобы запустить шаблон, откройте программу ZennoPoster. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.
Последнее редактирование:



