Локальный ИИ-ассистент прямо в мессенджере HalChat: без серверов, без рисков, только приватность и RAG.

Я занимаюсь разработкой собственного мессенджера HalChat и недавно я запустил систему локальных ИИ, с которыми можно общаться прямо в браузере через обычный чат.
И мне потребовался ИИ помощник, который бы вводил в курс дела внутри экосистемы и помогал пользователям обосноваться. Назвал я его — Halwashka AI.

Почему бы не использовать готовые решения?

Во первых, это приватность, главный принцип HalChat — это сохранность ваших данных, значит ваши сообщения не должны расшифровываться, и передаваться ИИ.
Во вторых, хотелось бы продемонстрировать пользователям сами локальные ИИ, чтобы могли начать пользоваться и другими локальными ИИ, выбрав их на HalNetMarket.

Проблема

Сейчас существует огромное количество разных ИИ моделей, но все они обучены на многосторонних данных. Переобучать их не имеет смысла, из-за огромных погрешностей и возможность повредить модель, нужный результат таким образом очень сложно получить. Один из главных вариантов решения, я выбрал:

RAG (Retrieval-Augmented Generation) — это метод, который объединяет два этапа:

  1. Retrieval (поиск): ИИ ищет релевантные данные из внешней базы (например, документы, файлы, базы знаний).

  2. Generation (генерация): ИИ использует найденные данные для формирования ответа, не обучаясь на них.

Архитектура

От запроса до ответа.
От запроса до ответа.

Принцип работы

Этап 0:
Пользователь заходит в чат. Плагин загружается и загружает ИИ модель (скачивает, либо из кэша). Сообщение от бота показывает процент загрузки. Когда модель загрузится он изменит сообщение на модель загружена.

global_msg_id=-1
is_load=false

...

#download model

def on_load() {
    is_load=true
    if(global_msg_id==-1) {
        return false
    }
    HalChat.editMessage(global_msg_id, "Модель успешно загружена");
}

def on_progress_load(pr) {
    if(global_msg_id==-1) {
        return false
    }
    HalChat.editLocalMessage(global_msg_id, "Загрузка: "+pr+"%");
}

...

#loadModel
def loadModel() {
    HalChat.sendMessage("Модель загружается, подождите...", [], "", "", -1, -1, '{"LocalBotMsg":{
    "nickname":"Halwashka AI",
    "icon":"f7hXkUfGJGseWFpao4DvLzBPrGH6PU6IXejt6ExPS717assk2oTCtLmfaJfT9py0ZrDQHK5x1rajN5wiIm65gG1EI7Acnhuh1AUz"
},"LocalLLM":"ignore"}')
}

...

LocalLLM.addEvent("load", on_load)
LocalLLM.addEvent("progressload",on_progress_load)

loadModel()

Также загружаем нейросеть Xenova/all-mpnet-base-v2, для преобразования текста в эмбеддинг (векторное представление), чтобы мы могли сравнивать и находить более схожие данные по тем какое просит пользователь.

this.embedder = await pipeline('feature-extraction', 'Xenova/all-mpnet-base-v2');

Этап 1:
Пользователь отправляет сообщение "Привет! Что такое HalChat?". Оно передаётся к HalSM плагину. После, плагин передаёт запрос с промптом и списком файлов для RAG и др. параметрами в LocalLLMHalSM.js

model="https://haldrive.halwarsing.net/file/WH6H9uXDHpxtnXJQahHpO8J1LrrwByN7EAkLYT8LV8S0wrmmTnP91fRB8dD5v23rcezKj25oGNhxTF81u4cab8AmaL2oF2kOCTi"

files=[]
files.append("DgqvzZyhJfNNIxt7Wx6XlGdAywbtNAj2D2cdvUDf2MudnLzQjwJqEzrcMZDb06ZR5Itli3Y6TSS65iFe8ekSWvKpYM4Ow9IezFks")

...

#on send message user

def on_send_message(msgId,type,time,text,fromNickname,fromId,fromIcon,chatUid,attachments, pluginData) {
    if(!is_load) {
        return false
    }

    HalChat.sendMessage("Генерация...", [], "", "", -1, -1, '{"LocalBotMsg":{
        "nickname":"Halwashka AI",
        "icon":"f7hXkUfGJGseWFpao4DvLzBPrGH6PU6IXejt6ExPS717assk2oTCtLmfaJfT9py0ZrDQHK5x1rajN5wiIm65gG1EI7Acnhuh1AUz"
    }}')

    LocalLLM.run(system_prompt, text, 0.5, 40, 0.9, 1000, 1, files)
}

...

def on_local_sended_message(msgId, pluginData) {
    global_msg_id=msgId
    if(!is_load) {
        LocalLLM.load(model,4096,256)
    }
}


HalChat.addEvent("onUserSendMessage", on_send_message)
HalChat.addEvent("onLocalBotSendedMessage",on_local_sended_message)
LocalLLM.addEvent("generate", on_generate)
LocalLLM.addEvent("generate_stream", on_generate_stream)

...

Этап 2:
LocalLLMHalSM.js преобразовывает HalSM переменные в javascript (temp, top_k, system_prompt и т.д.).
Загружает RAG хранилище по предоставленным ссылкам (скачивает с HalDrive) и преобразует все данные в эмбеддинги.

const docs = [];
for (const file of files) {
    const resj = await fetch(`https://haldrive.halwarsing.net/file/`+file+`?isJson=1`,{
        method: 'GET',
        mode:'cors',
        credentials: 'include',
    });

    if (!resj.ok) {
        throw new Error(`Ошибка загрузки модели: HTTP ${resj.status}`);
    }

    const jsonData=await resj.json();
    if(jsonData['errorCode']!==0) {
        throw new Error(`Ошибка загрузки модели: HD ${jsonData.errorCode}:${jsonData.error}`);
    }

    const response=await fetch(jsonData['url'],{
        method:'GET',
        credentials:'omit'
    });

    const jdata = await response.json();

    if("data" in jdata) {
        const l=jdata.data.length;
        for(var i=0;i<l;i++) {
            const text=jdata.data[i];
            docs.push({ id: file+";"+i, text });
        }
    }
}

for (const doc of docs) {
    const embedding = await this.embedText(doc.text);
    this.embeddings.push({ ...doc, embedding });
}

После также, преобразует запрос пользователя в эмбеддинг, и начинает поиск по схожести среди всех данных и выбирает топ-3 подходящих эмбеддинга. Превращает выбранные данные в пользовательское сообщение, как контекст и добавляет его в список сообщений.

const queryEmbedding = await this.embedText(query); 
const similarities = this.embeddings.map((doc) => {
    const dot = queryEmbedding.reduce((sum, val, i) => sum + val * doc.embedding[i], 0);
    const norm1 = Math.sqrt(queryEmbedding.reduce((sum, val) => sum + val * val, 0));
    const norm2 = Math.sqrt(doc.embedding.reduce((sum, val) => sum + val * val, 0));
    return {
    id: doc.id,
    text: doc.text,
    similarity: dot / (norm1 * norm2)
    };
});
return similarities
    .sort((a, b) => b.similarity - a.similarity)
    .filter(doc => doc.similarity > 0.6) // порог
    .slice(0, topK);

Этап 3:
LocalLLMHalSM.js создаёт потоковый чат с LLM моделью с историей сообщений и указанными параметрами. На каждый загруженный токен срабатывает эвент и плагин обновляет локально сообщение.

for await (const ch of LocalLLMHalSM.localLLM.generateChatStream(messages_ai, { maxNewTokens: max_new_tokens, stop: [], temperature: temperature, top_k: top_k, top_p: top_p })) {
    const t=ch.text.replace("—","-");
    LocalLLMHalSM.runEvent("generate_stream", [t]);
    lastCh=t;
}

Этап 4:
ИИ закончил и срабатывает итоговое событие и плагин обновляет сообщения на сервере.

#generation

def on_generate(text) {
    HalChat.editMessage(global_msg_id, text)
}

def on_generate_stream(text) {
    HalChat.editLocalMessage(global_msg_id, text)
    if(text=="[вызов техподдержки]"||text=="[Вызов техподдержки]") {
        HalChat.inviteUser(47)
    }
}

...

LocalLLM.addEvent("generate", on_generate)
LocalLLM.addEvent("generate_stream", on_generate_stream)

Практическое применение

  • Общение с ИИ помощником и проводником по экосистеме HalNet.

  • Базовая техподдержка и возможность вызвать ручную техподдержку.

Недостатки

Самый главный минус — это производительность, пока что на данный момент генерация одного ответа занимает от 2-х до 4-х минут, и это почти не зависит от мощности вашего устройства, ведь он уходит в один поток.

Мало знаний — это маленькая модель, так что она не может позволить себе поддерживать полноценный информативный диалог и давать советы, вне знаний RAG хранилища.

Перспективы

  • Улучшение производительности, за счёт многопоточности и перехода на Web-LLM.

  • Интеграция в экосистему, и автоматизация чатов, и анализ чатов для пользователя.

  • Полноценный помощник с возможностью живого общения.

Реализация

Я добавил новый файл HalChatRAG.js, где будет находиться система RAG. Сделана она на базе библиотеке transformers.js и модели Xenova/all-mpnet-base-v2.
Также обновил код LocalLLMHalSM.js для взаимодействия с RAG. А именно добавил отображение RAG как сообщение от пользователя, чтобы он его воспринимал как контекст.
И написал ИИ-плагин на HalSM, где уже прописан промпт и взаимодействие с пользователем, и ещё вызов техподдержки, если он не знает ответа, или пользователь сам запросил, в таком случае он приглашает в чат меня. Для этого пользователю появляется окошко с предложением пригласить пользователя (для безопасности и согласованности действия).


Модель

Библиотеки:
https://github.com/ngxson/wllama
https://huggingface.co/docs/transformers.js/en/index

Код

Приглашение техподдержки в чат.
Приглашение техподдержки в чат.

Демо

Создаём чат с ИИ, дожидаемся загрузки модели, и пишем наши запросы. Получаем ответы)

Общение с Halwashka AI.
Общение с Halwashka AI.

Итог

У меня получилось реализовать — локального ИИ помощника, с загрузкой дополнительной информации без переобучения модели. Это создаёт потенциал и для поиска в интернете благодаря RAG системе.

Жду ваших вопросов связанной с этой статьёй, так и про мою экосистему и язык программирования HalSM.

Соц. сети:
https://halch.at/c/tZgWWT
https://t.me/halwarsingchat
https://www.youtube.com/@halwarsing
https://vk.com/halwarsingnet

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы относитесь к локальным ИИ?
85.71%Я за развитие локальных ИИ6
0%Поддерживаю традиционные серверные варианты0
14.29%Нейтралитет1
Проголосовали 7 пользователей. Воздержался 1 пользователь.