Локальный ИИ, не покидающий периметр компании

kolina

Client
Регистрация
05.10.2019
Сообщения
184
Благодарностей
86
Баллы
28
137472


Оборудование
Данная модель была обучена с учётом имеющегося оборудования: видеокарта с 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 авто, тент.\"}]"
    }
  ]
}
Мы соблюдаем правило: в ответе только JSON-массив. Это критично — модель запоминает формат.

Быстрая заготовка очистки и упаковки (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))
На практике вы формируете gold_json автоматически (скриптом-правилами) + вручную проверяете подвыборку.
Ключевой критерий датасета — строгость формата и разнообразие формулировок.


2) Обучение: QLoRA на базе Gemma-2-9B-IT
Мы обучали адаптер (LoRA), затем сливали его в полные веса (fp16). Это даёт:
  • малые требования к GPU при обучении (QLoRA/4-бит),
  • возможность распространять только LoRA (если нужно),
  • единый итоговый чекпоинт после слияния.
Слияние LoRA в «полные веса» (fp16)
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)
На выходе — обычный HF-чекпоинт (fp16), без зависимостей от LoRA. Его уже легко конвертировать в GGUF.
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
Для Gemma конвертер сам определит тип. Если словарь SentencePiece — тоже определяется автоматически.

Квантизация в 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
4) Лицензии и этика
  • Gemma-2 (Google) имеет свою лицензию — проверяйте условия распространения весов/адаптеров.
  • Если распространяете только LoRA — проще соблюсти требования (часто разрешено).
1. Установка LM Studio

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 — квантизованные варианты (меньше ОЗУ/ВРАМ, быстрее, иногда с небольшой потерей качества).
Важно: LM Studio корректно индексирует модели, когда каждая находится в отдельной папке.

3. Частые проблемы загрузки модели и решения

Симптом: «Error loading model. (Exit code: 1844674407… Unknown error)».

Проверяем по порядку:
  1. Установлен ли подходящий Runtime для GGUF (Vulkan/CUDA/CPU) и выбран по умолчанию.
  2. Модель точно в формате GGUF, а не safetensors.
  3. Модель лежит в отдельной папке, путь без «нестандартных» символов.
  4. Обновите Runtime до актуальной версии (вкладка Runtime → Update).

    137470
4. Системный промт и пресеты в LM Studio

Мы создали системный промт reed_logist — строгий парсер объявлений о грузоперевозках, который возвращает только валидный JSON-массив без лишнего текста.
Содержимое хранится в LM Studio → вкладка Context (preset).

Важный нюанс: сервер LM Studio не умеет подхватывать пресеты «по имени» через API (возможно можно, но я не смог этого сделать). Поэтому системный промт выбирали вручную после загрузки модели.

137471


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";
7. Производительность и качество
  • Выбор квантизации:
    Q5_1 — золотая середина для качества/скорости.
    Q4_0 — ещё быстрее и меньше памяти, но сильнее проседает качество.
    F16 — лучшее качество, но требует больше ОЗУ/ВРАМ.
  • Движок: при наличии NVIDIA используйте CUDA llama.cpp; универсально — Vulkan. На слабых машинах можно оставить CPU.
  • Температура/топ-p: для строгого JSON-парсинга держите temperature низкой (0.1–0.3).
8. Безопасность
  • LM Studio сервер слушает 127.0.0.1:1234 — внешний доступ недоступен.
  • Все запросы и ответы остаются на хост-машине. Никаких облачных API.
Итог
Мы развернули полностью локальную цепочку:
  1. Получили свою модель (в том числе слияние LoRA) и подготовили квантизованные GGUF-варианты.
  2. Правильно разложили по папкам, чтобы LM Studio автоматически подхватил модели.
  3. Настроили Runtime (Vulkan/CUDA/CPU) и устранили проблемы загрузки.
  4. Создали строгий системный промт reed_logist, который гарантирует ответ строго в JSON.
  5. Подключили ZennoPoster к локальному OpenAI-совместимому серверу LM Studio и организовали обмен данными без выхода в интернет.




 

Для запуска проектов требуется программа ZennoPoster.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...

Для того чтобы запустить шаблон, откройте программу ZennoPoster. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.

Последнее редактирование:

kolina

Client
Регистрация
05.10.2019
Сообщения
184
Благодарностей
86
Баллы
28
Добавил видео
 
  • Спасибо
Реакции: code

heks

Client
Регистрация
01.10.2013
Сообщения
1 546
Благодарностей
475
Баллы
83
ниче не понял но красавчик))
 
  • Спасибо
Реакции: Gfoblin и kolina

todayer

Client
Регистрация
07.08.2013
Сообщения
997
Благодарностей
412
Баллы
63
процессор AMD Ryzen 7 (сопоставимый по производительности с Intel Core i7)
Я правильно понял, что вы использовали CUDA? Если так, то производительность процессора не играла особую роль. Верно?
 

code

Administrator
Регистрация
04.06.2025
Сообщения
241
Благодарностей
123
Баллы
43
Я правильно понял, что вы использовали CUDA? Если так, то производительность процессора не играла особую роль. Верно?
Да, вы правильно поняли. При использовании CUDA основная вычислительная нагрузка действительно ложится на GPU, а не на CPU, поэтому производительность процессора играет значительно меньшую роль.
 
  • Спасибо
Реакции: DTRS2, kolina и todayer

zarufakis

Client
Регистрация
22.03.2019
Сообщения
1 963
Благодарностей
1 371
Баллы
113
2) Обучение: QLoRA на базе Gemma-2-9B-IT
Мы обучали адаптер (LoRA), затем сливали его в полные веса (fp16). Это даёт:
  • малые требования к GPU при обучении (QLoRA/4-бит),
  • возможность распространять только LoRA (если нужно),
  • единый итоговый чекпоинт после слияния.
Вот этот момент можно было бы и подробнее расписать. Понятно, что в интернете есть информация как дообучать модель, но хотелось бы получить в рамках статьи, или отдельным материалом.
 
  • Спасибо
Реакции: djaga и DTRS2

DTRS2

Пользователь
Регистрация
06.09.2023
Сообщения
47
Благодарностей
6
Баллы
8
Да, вы правильно поняли. При использовании CUDA основная вычислительная нагрузка действительно ложится на GPU, а не на CPU, поэтому производительность процессора играет значительно меньшую роль.
Круто!

Но какое примерно среднее время ответа модели в секундах/миллисекундах на стандартный запрос на таком железе?

И какую видюху использовали?
 

code

Administrator
Регистрация
04.06.2025
Сообщения
241
Благодарностей
123
Баллы
43
Круто!

Но какое примерно среднее время ответа модели в секундах/миллисекундах на стандартный запрос на таком железе?

И какую видюху использовали?
Советую уточнить у автора.

Из данных что я нашел:
На связке уровня Ryzen 7 / Core i7 с одной современной RTX (3060/3060 Ti/3070) типичная задержка до первого токена у 7–8B модели составляет примерно 200–900 мс, а полный ответ в 100–150 токенов появляется за 2–5 с в зависимости от модели и настроек квантования.
 
  • Спасибо
Реакции: DTRS2

kolina

Client
Регистрация
05.10.2019
Сообщения
184
Благодарностей
86
Баллы
28
Круто!

Но какое примерно среднее время ответа модели в секундах/миллисекундах на стандартный запрос на таком железе?

И какую видюху использовали?
Добрый день! В среднем модель после обучения отвечает за 10 сек. До обучения на такой же вопрос вроде отвечала дольше если я ничего не путаю.
Железо прикрепил - на скрине
 

Вложения

  • Спасибо
Реакции: DTRS2 и code

kolina

Client
Регистрация
05.10.2019
Сообщения
184
Благодарностей
86
Баллы
28
Вот этот момент можно было бы и подробнее расписать. Понятно, что в интернете есть информация как дообучать модель, но хотелось бы получить в рамках статьи, или отдельным материалом.
Статью я начал писать уже после того, как обучение прошло успешно, поэтому все шаги воспроизвести сейчас сложно. В данный момент готовлю материал для нового обучения на свежих данных (пока собираю данные для обучения). Планирую протестировать подход, при котором модель из сообщения длиной около 500 символов будет формировать сразу несколько грузов — каждый в отдельном элементе JSON. Номер телефона будет вынесен в отдельное поле, а также предусмотрена отдельная классификация: «спам», «груз», «маршрут».

Изначально я не собирался писать статью и не был уверен, что получу результат. Однако теперь намерен провести повторное обучение (пусть модель можно дообучать, но я хочу начать с нуля) и дополнить статью новыми наблюдениями.
 

zarufakis

Client
Регистрация
22.03.2019
Сообщения
1 963
Благодарностей
1 371
Баллы
113
Статью я начал писать уже после того, как обучение прошло успешно, поэтому все шаги воспроизвести сейчас сложно. В данный момент готовлю материал для нового обучения на свежих данных (пока собираю данные для обучения). Планирую протестировать подход, при котором модель из сообщения длиной около 500 символов будет формировать сразу несколько грузов — каждый в отдельном элементе JSON. Номер телефона будет вынесен в отдельное поле, а также предусмотрена отдельная классификация: «спам», «груз», «маршрут».

Изначально я не собирался писать статью и не был уверен, что получу результат. Однако теперь намерен провести повторное обучение (пусть модель можно дообучать, но я хочу начать с нуля) и дополнить статью новыми наблюдениями.
Реально крутой и полезный кейс. С таким не стыдно было бы на конкурс заявляться.
Нейронки дрочат все кому не лень, но, что бы так, с чувством, с расстановкой - очень мало кто может.
Поэтому ждем с нетерпением на конкурсе.
 
  • Спасибо
Реакции: DTRS2, Sergodjan и code

Кто просматривает тему: (Всего: 3, Пользователи: 2, Гости: 1)