Зачем вам это нужно (и почему стандартные решения сосут)
Вы когда-нибудь замечали, что ChatGPT или Claude предлагают вам фразы, которые вы бы никогда не сказали? Слишком формально. Слишком вежливо. Слишком... не вы.
А теперь представьте: вы начинаете писать сообщение в Discord, и ИИ предлагает продолжение, которое звучит именно как вы. С вашими любимыми словечками, мемами, интонацией. Не общий шаблон, а ваша реальная манера общения.
Это не просто автодополнение. Это цифровой двойник вашего стиля общения. И самое приятное - все работает локально, без отправки ваших сообщений в облака OpenAI.
Что мы будем строить (и почему Qwen 14B в 2026 году)
Архитектура проекта выглядит так:
- Скрапер Discord сообщений (ваших, только ваших)
- Датасет в формате инструкций для финтюнинга
- QLoRA адаптер для Qwen 14B через Unsloth.ai
- Развертывание в Ollama с GGUF квантованием
- Chrome-расширение, которое перехватывает ввод в Discord
Почему Qwen 14B, а не что-то поменьше? Потому что на февраль 2026 года это оптимальный баланс между качеством и требованиями к железу. 14 миллиардов параметров достаточно для понимания контекста, но модель все еще помещается в 16 ГБ VRAM с QLoRA. Новые версии Qwen 2.5 14B показывают результаты близкие к GPT-3.5, но работают локально.
1 Вытаскиваем ваши сообщения из Discord
Первая ошибка, которую все совершают - пытаются скрапить Discord через официальный API. Не делайте так. Вы получите только последние 100 сообщений из каждого канала, и это займет вечность.
Вместо этого используем Data Package Export - функцию, которую Discord добавил после GDPR. Заходите в настройки аккаунта → Privacy & Safety → Request Data. Ждете 30 дней (шутка, обычно 1-2 дня). Получаете ZIP со всеми вашими сообщениями, включая удаленные.
Внутри будет файл messages.csv. Вот как его правильно обработать:
import pandas as pd
import json
from datetime import datetime
# Загружаем данные Discord
df = pd.read_csv('messages.csv', low_memory=False)
# Фильтруем только свои сообщения
df = df[df['AuthorID'] == 'YOUR_USER_ID']
# Убираем команды ботов, системные сообщения
df = df[~df['Content'].str.contains('^!|^/|^\*\*\*', na=False)]
# Убираем слишком короткие сообщения (менее 10 символов)
df = df[df['Content'].str.len() > 10]
print(f"Найдено {len(df)} ваших сообщений")
Внимание: не используйте сообщения других людей без их согласия. Это не только неэтично, но и нарушает Discord ToS. Ваша модель должна учиться только на ваших сообщениях.
2 Готовим датасет для финтюнинга (здесь все ломается)
Стандартный подход - взять сообщения и сделать из них "вопрос-ответ". Не работает. Для автокомплита нам нужно нечто другое: модель должна уметь продолжать начатую фразу.
Правильный формат для Qwen 14B в 2026 году выглядит так:
def create_completion_pairs(messages):
"""Создаем пары "начало сообщения - продолжение""""
pairs = []
for message in messages:
# Берем случайную точку разрыва в сообщении
if len(message) > 20:
split_point = random.randint(5, len(message) - 10)
prefix = message[:split_point]
completion = message[split_point:]
# Форматируем в инструкционный формат Qwen
formatted = {
"instruction": f"Продолжи сообщение: {prefix}",
"input": "",
"output": completion,
"system": "Ты - автокомплит для Discord. Продолжай сообщения в стиле пользователя."
}
pairs.append(formatted)
return pairs
Сохраняем в JSONL формат, который понимает Unsloth. Нужно 1000-5000 примеров для хорошего результата. Меньше - модель не научится, больше - переобучится на конкретные фразы.
3 QLoRA финтюнинг на Unsloth (быстрее в 2 раза, серьезно)
Unsloth.ai - это не просто обертка над PyTorch. Они переписали ядро обучения для эффективной работы на потребительских GPU. На февраль 2026 года их последняя версия дает ускорение в 2-3 раза по сравнению с стандартным Hugging Face PEFT.
Установка (обновите, если у вас старая версия):
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps xformers trl peft accelerate bitsandbytes
Конфигурация обучения - здесь большинство настраивает не те параметры:
from unsloth import FastLanguageModel
import torch
# Загружаем Qwen 14B с 4-битной квантовацией
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "Qwen/Qwen2.5-14B-Instruct",
max_seq_length = 2048, # Для автокомплита хватит
dtype = torch.float16,
load_in_4bit = True, # Экономит память
)
# Добавляем QLoRA адаптеры
model = FastLanguageModel.get_peft_model(
model,
r = 16, # Ранг адаптера
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha = 16,
lora_dropout = 0.1,
bias = "none",
use_gradient_checkpointing = True,
random_state = 42,
)
Параметры обучения, которые действительно работают для автокомплита:
from trl import SFTTrainer
from transformers import TrainingArguments
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset,
dataset_text_field = "text",
max_seq_length = 2048,
args = TrainingArguments(
per_device_train_batch_size = 2, # Для 16GB VRAM
gradient_accumulation_steps = 4,
warmup_steps = 50,
num_train_epochs = 3, # Больше не нужно!
learning_rate = 2e-4, # Выше, чем для чат-бота
fp16 = not torch.cuda.is_bf16_supported(),
bf16 = torch.cuda.is_bf16_supported(),
logging_steps = 10,
optim = "paged_adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "cosine",
output_dir = "outputs",
report_to = "none",
),
)
4 Конвертируем в GGUF и запускаем в Ollama
После обучения у вас есть адаптеры LoRA. Но Ollama не умеет в них напрямую. Нужно слить адаптеры с базовой моделью и сконвертировать в GGUF.
Сначала сохраняем полную модель:
# Сливаем адаптеры с базовой моделью
model.save_pretrained_merged(
"trained_model",
tokenizer,
save_method = "merged_16bit", # Или "lora" если хотите оставить отдельно
)
Теперь конвертируем в GGUF через llama.cpp (обновите до последней версии 2026 года):
# Клонируем свежий llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make -j$(nproc)
# Конвертируем в GGUF
python convert.py ../trained_model --outtype q4_0
# q4_0 - хороший баланс качество/скорость
# q8_0 - если есть память, качество почти без потерь
Создаем Modelfile для Ollama:
FROM ./qwen-14b-discord.Q4_0.gguf
TEMPLATE """{{ .System }}
Продолжи сообщение: {{ .Prompt }}"""
PARAMETER temperature 0.3 # Низкая температура для консистентности
PARAMETER top_p 0.9
PARAMETER num_predict 50 # Длина автодополнения
SYSTEM """Ты - автокомплит для Discord.
Твоя задача - продолжать сообщения в стиле пользователя.
Используй его характерные слова и выражения.
Не добавляй лишних объяснений, просто продолжай текст."""
Создаем модель в Ollama:
ollama create my-discord-autocomplete -f ./Modelfile
ollama run my-discord-autocomplete "привет, как дела"
5 Chrome-расширение, которое все связывает
Самая хитрая часть. Нам нужно перехватывать ввод в Discord, отправлять в Ollama, и вставлять предложения. Но Discord использует React и сложный DOM.
Вот рабочий манифест для 2026 года (manifest v3):
{
"manifest_version": 3,
"name": "Discord Autocomplete",
"version": "1.0",
"permissions": ["activeTab", "storage"],
"host_permissions": ["http://localhost:11434/*"],
"content_scripts": [{
"matches": ["https://discord.com/*"],
"js": ["content.js"],
"run_at": "document_end"
}]
}
И основной код content.js:
class DiscordAutocomplete {
constructor() {
this.debounceTimer = null;
this.lastText = '';
this.ollamaUrl = 'http://localhost:11434/api/generate';
this.observeMessageInput();
}
observeMessageInput() {
// Discord меняет DOM динамически, нужен MutationObserver
const observer = new MutationObserver((mutations) => {
const input = this.findMessageInput();
if (input && !input.hasListener) {
input.addEventListener('input', this.handleInput.bind(this));
input.hasListener = true;
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
async handleInput(event) {
const text = event.target.textContent.trim();
// Дебаунс, чтобы не спамить запросами
clearTimeout(this.debounceTimer);
// Запускаем автодополнение только если:
// 1. Текст больше 10 символов
// 2. Пользователь не печатает быстро
// 3. Курсор в конце строки
if (text.length > 10 && text !== this.lastText) {
this.debounceTimer = setTimeout(async () => {
const completion = await this.getCompletion(text);
if (completion) {
this.showSuggestion(completion);
}
}, 500);
}
this.lastText = text;
}
async getCompletion(text) {
try {
const response = await fetch(this.ollamaUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'my-discord-autocomplete',
prompt: text,
stream: false
})
});
const data = await response.json();
return data.response.trim();
} catch (error) {
console.error('Ollama error:', error);
return null;
}
}
showSuggestion(suggestion) {
// Создаем всплывающую подсказку рядом с полем ввода
// Реализация зависит от текущего DOM Discord
// В 2026 году ищем элемент с классом .slateTextArea__container
}
}
// Запускаем когда DOM готов
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
new DiscordAutocomplete();
});
} else {
new DiscordAutocomplete();
}
Где все ломается: частые ошибки и как их избежать
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| Модель предлагает слишком формальные ответы | Системный промпт пересиливает финтюнинг | Убрать "Ты полезный ассистент" из системного промпта |
| Автодополнение срабатывает на каждое нажатие клавиши | Нет дебаунса или он слишком короткий | Увеличить дебаунс до 500-800мс |
| Ollama не отвечает из расширения | CORS политика браузера | Добавить --host 0.0.0.0 при запуске Ollama |
| Модель повторяет одни и те же фразы | Переобучение на маленьком датасете | Увеличить датасет или добавить аугментацию |
А если хочется проще? Альтернативные пути
Не хотите возиться с финтюнингом? Есть варианты:
- Использовать локальную модель без финтюнинга - просто запустите Qwen 14B в Ollama и настройте промпт. Будет работать, но без вашего стиля.
- Воспользоваться облачным API - но тогда ваши сообщения уйдут на чужой сервер. Идея теряет смысл.
- Собрать систему на базе готового решения для Git-коммитов - адаптировать под Discord.
Но если вы прошли весь путь - у вас теперь есть уникальная вещь. ИИ, который говорит вашими словами. Не общий шаблон, а именно ваша манера общения, сохраненная в 4-битных весах.
Важный нюанс: ваша обученная модель - это цифровой отпечаток вашего стиля общения. Не выкладывайте ее в открытый доступ и не делитесь без необходимости. Это персональные данные в самом буквальном смысле.
Что дальше? Модель можно дообучать по мере накопления новых сообщений. Раз в месяц запускаете скрипт, который добавляет новые сообщения в датасет и делает несколько шагов дообучения. Ваш цифровой двойник будет эволюционировать вместе с вами.
И да, теперь когда вы пишете "привет, как дела", а ИИ предлагает продолжение "норм, только кофе закончился" - вы понимаете, что это действительно вы. Только в 14 миллиардах параметров.