Исходный код, разобранный в данной статье, опубликован в этом репозитории
Для решения некоторых задач бизнес-требованием является запуск LLM модели локально на своём железе. Это связано с SJW цензурой, например, стандартный датасет для обучения Llama не позволяет вести консультации, носящие медицинский характер: рекомендовать лекарства, обсуждать носящую интимный характер медицинскую тайну с ИИ-терапевтом (см побочки антидепрессантов)

Так же, если модель не умеет вызывать инструменты, она не нужна: бессмысленно вести медицинскую консультацию, которая не закончится продажей фармокологического продукта. Ниже преведены протестированные модели, которые сочетают консистентный русский язык и возможность интеграции в сторонние сервисы
Обзор моделей
-
NVidia Nemotron Mini, https://ollama.com/library/nemotron-mini
Цитата: This instruct model is optimized for roleplay, RAG QA, and function calling in English. It supports a context length of 4,096 tokens. This model is ready for commercial use.
Плюсы
1. Работает на видеокарте ноутбука с 4Гб памяти
Позволяет пригласить стажера и, не засветив ключи openai, понять, а сможет ли он вообще в программирование
2. Умеет говорить на русском языке
Важный пункт, см фото про deepseek
3. Вызывает инструменты
Условно работает, но винована Ollama: Если отправить промпт-костыль. Однако, игнорирует required параметры инструментов: передает только те данные, в которых модель на 100% уверена
4. Не многословен: датасет заточен для генерации минимальных ответов: Никаких “наверное” или “вы хотели бы уточнить?”
Огромный плюс, если у вас есть опыт работы с моделью Gemini: она постоянно спамит текстом вида:
Would you like me to
,Okay, here’s a
,Okay, this is a
,This is likely
Минусы
1. Рекурсивно вызывает инструменты, неправильно реагируя на tool output.
Решается сбросом переписки после вызова инструментов и заметками для модели в system prompt
2. Игнорирует required параметры инструментов,
При вызове метода добавления товара в корзину гарантировать получение имени товара может только анализ последнего сообщения пользователя руками
3. Из-за немногословности крайне не замотивированно ведет переписку: развести модель на нужное действие можно только задавая наводящие вопросы
Решается выбором другой модели тем агентам роя, которые должны быть многословны. Эта модель именно и позицинирует себя для сбора точной информации через чат для коммерции
4. Вызывает несуществующие инструменты
Если текстовый промпт содержит просьбу оплатить заказ, модель вызовет
checkout_tool
даже, если был подключен толькоadd_to_cart_tool
-
Saiga/YandexGPT, https://huggingface.co/IlyaGusev/saiga_yandexgpt_8b
Pretrain-версия младшей модели — YandexGPT 5 Lite Pretrain — опубликована в свободном доступе и будет полезна разработчикам, которые дообучают базовые версии моделей под свои задачи. Дообученная нами на её основе instruct-версия в ближайшее время станет доступна через API.
Плюсы
1. Яндекс заопенсорсил топологию своей модели Алисы по политическому решению
Это самая годная топология на рынке, не только для русского языка, а вообще: значительно лучше
Mistral NeMo
,Llama 3.1
,Nemotron
,Deepseek
и других2. При вызове инструментов ориентируется на required параметры
Важное отличие от Nemotron mini
3. Не вызывает инструменты рекурсивно
Критическое отличие от Nemotron mini: рекурсивный вызов инструмента создаст deadlock чата. Эта модель таким прелестным качеством не обладает
4. Не запустится на ноутбуке, но системный блок с видеокартой 3060 тянет эту модель
Если не работает на Saiga/YandexGPT, лучше только откат до OpenAI
Минусы
1. Так как опенсорс топологии был политическим, Яндекс утаил датасет.
Благо, есть Saiga: докинув 10% русского текста в датасет для дообучения LLama тупо повезло и модель сама выучила русский
2. Лексер LMStudio не может парсить случаи, когда модель запрашивает вызов двух инструментов одновременно
При просьбе добавить два товара одновременно в чат к пользователю вместо структурированного
tool_calls: IToolCall[]
улетает[TOOL_REQUEST]n{"name": "add_to_cart_tool", "arguments": {"title": "Аспирин"}}n[END_TOOL_REQUEST
как строкаcontent
. Лечится через фильтр сообщений от нейронки на предмет отсутствия JSON вообще, ограничением вызовов инструментов не более одного за сообщение через system prompt и мануальный фильтр выхлопа из completerНе стабильным решением проблемы является отлов JSON в сообщении от модели с последующей просьбой исправить ошибки в формате. Но, иногда приводит к рекурсии: модель долго рассуждает и всё равно дает невалидный формат вызова инструмента. Поэтому, стабильным способом выхода из положения при детекте JSON в выходе из модели – очистка переписки и отправка пользователю заглушки:
Я не расслышал, не могли бы вы повторить
Настройка среды
Для использования Nemotron потребуется скачать ollama с официального сайта, далее в терминале вбить команду ollama pull nemotron-mini:4b

Для использования YandexGPT потребуется скачать LMStudio, установить модель через поисковик как на скриншоте

После, нужно включить эмулятор OpenAI API через раздел в боковом меню. В вышеупомянутом разделе нужно выставить следующие настройки

Пример кода
Для простоты, в этой статье я укажу консольный пример чата. При необходимости, подключить то же API к верстке, не составит труда: на всякий случай, оставлю шаблон
import readline from "readline";
import { randomString, Subject } from "functools-kit";
const clientId = randomString();
const incomingSubject = new Subject();
const outgoingSubject = new Subject();
const ws = new WebSocket(`http://127.0.0.1:1337/?clientId=${clientId}`);
ws.onmessage = (e) => {
incomingSubject.next(JSON.parse(e.data));
};
ws.onopen = () => {
outgoingSubject.subscribe((data) => {
ws.send(JSON.stringify({ data }));
});
};
ws.onclose = () => {
console.log("Connection closed");
process.exit(-1);
};
ws.onerror = () => {
console.log("Connection error");
process.exit(-1);
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = () => {
rl.question("pharma-bot => ", async (input) => {
if (input === "exit") {
rl.close();
return;
}
console.time("Timing");
await outgoingSubject.waitForListener();
await outgoingSubject.next(input);
const { agentName, data } = await incomingSubject.toPromise();
console.timeEnd("Timing");
console.log(`[${agentName}]: ${data}`);
askQuestion();
});
};
askQuestion();
rl.on("close", () => {
process.exit(0);
});
Код сервера следующий. Чтобы переподключить агента к другому провайдеру LLM, меняем completion: CompletionName.NemotronMiniCompletion
на completion: CompletionName.SaigaYandexGPTCompletion,
import {
Adapter,
addAgent,
addCompletion,
addSwarm,
addTool,
commitFlush,
commitToolOutput,
emit,
execute,
getAgentName,
session,
} from "agent-swarm-kit";
import type { ServerWebSocket } from "bun";
import { singleshot, str } from "functools-kit";
import { Ollama } from "ollama";
import OpenAI from "openai";
const getOllama = singleshot(
() => new Ollama({ host: "http://127.0.0.1:11434" })
);
const getOpenAI = singleshot(
() => new OpenAI({ baseURL: "http://127.0.0.1:12345/v1", apiKey: "noop" })
);
enum CompletionName {
NemotronMiniCompletion = "nemotron_mini_completion",
SaigaYandexGPTCompletion = "saiga_yandex_gpt_completion",
}
enum AgentName {
TestAgent = "test_agent",
}
enum ToolName {
AddToCartTool = `add_to_cart_tool`,
}
enum SwarmName {
TestSwarm = "test_swarm",
}
addCompletion({
completionName: CompletionName.NemotronMiniCompletion,
getCompletion: Adapter.fromOllama(getOllama(), "nemotron-mini:4b"),
});
addCompletion({
completionName: CompletionName.SaigaYandexGPTCompletion,
getCompletion: Adapter.fromOpenAI(getOpenAI(), "saiga_yandexgpt_8b_gguf"),
});
addAgent({
agentName: AgentName.TestAgent,
completion: CompletionName.SaigaYandexGPTCompletion,
prompt: str.newline(
"Вы являетесь агентом по продаже фармацевтических товаров.",
"Предоставьте мне консультацию по фармацевтическому продукту"
),
system: [
`Чтобы добавить фармацевтический продукт в корзину, вызовите следующий инструмент: ${ToolName.AddToCartTool}`,
],
tools: [ToolName.AddToCartTool],
});
addTool({
toolName: ToolName.AddToCartTool,
validate: async ({ params }) => true,
call: async ({ toolId, clientId, agentName, params }) => {
console.log(ToolName.AddToCartTool, params);
await commitToolOutput(
toolId,
`Фармацевтический продукт ${params.title} успешно добавлен.`,
clientId,
agentName
);
await emit(
`Продукт ${params.title} добавлен в корзину. Вы желаете оформить заказ?`,
clientId,
agentName
);
},
type: "function",
function: {
name: ToolName.AddToCartTool,
description:
"Добавить фармацевтический продукт в корзину. Обязательно передай параметр title.",
parameters: {
type: "object",
properties: {
title: {
type: "string",
description: `Название фармацевтического продукта, который нужно добавить в корзину`,
},
},
required: [],
},
},
});
addSwarm({
swarmName: SwarmName.TestSwarm,
agentList: [AgentName.TestAgent],
defaultAgent: AgentName.TestAgent,
});
type WebSocketData = {
clientId: string;
session: ReturnType<typeof session>;
};
Bun.serve({
fetch(req, server) {
const clientId = new URL(req.url).searchParams.get("clientId")!;
console.log(`Connected clientId=${clientId}`);
server.upgrade<WebSocketData>(req, {
data: {
clientId,
session: session(clientId, SwarmName.TestSwarm),
},
});
},
websocket: {
async message(ws: ServerWebSocket<WebSocketData>, message: string) {
const { data } = JSON.parse(message);
const answer = await ws.data.session.complete(data);
ws.send(
JSON.stringify({
data: answer,
agentName: await getAgentName(ws.data.clientId),
})
);
},
},
hostname: "0.0.0.0",
port: 1337,
});
Заключение
Исходя из вышеизложенных тезисов, можно сделать следующие выводы
-
Для каждой новой локализации чата потребуется делать новый рой агентов
Языковые модели начинают путаться, если system prompt содержит информацию на чужом для пользователя языке: в русскоязычном тексте появляются сегментарные англоязычные включения
-
По OpenSource моделям есть продвижение, но сейчас закрытые модели лидируют
Адаптер для переключения с OpenAI на локальную модель критически важен для отлова edge cases: так как бесплатные модели часто глючат, на них проще исправлять ошибки. Прод среду же всё ещё стабильнее запускать на закрытой модели
-
Если проект нацелен на русскоязычную аудиторию – берите Saiga/YandexGPT
Мной так же была произведена попытка установить в LMStudio Vikhr-YandexGPT-5-Lite-8B, но инструменты не вызывались. Без инструментов, языковая модель не имеет смысл, так как не работают интеграции в сторонние сервисы
Автор: tripolskypetr