В данной статье осуществлен разбор многопользовательского телеграм чат бота на LLM, код которого опубликован в этом репозитории
Куда движется рынок
Когда-то давным давно графический пользовательский интерфейс сменил консольный ввод. Казалось бы, проблему неудобства взаимодействия для неподготовленного пользователя мог бы решить псевдографический интерфейс, но есть фактор, который не все замечают
Важно! Разрабатывать графический пользовательский интерфейс дешевле, чем псевдографический. В историческом процессе, сразу после выхода Next CUBE был представлен язык ObjC с графическим редактором форм, где страницы можно компоновать мышкой. В современном же мире, Frontend предоставляет графическую отладку форм через Dev Tools, что примерно то же самое: код номинальный без технических деталей, при проблемах есть GUI, удешевляющее поиск бага.
Но ещё дешевле - не делать пользовательский интерфейс вообще. Тебе не нужен статический IP, PCI DSS, домен раскрученый в яндексе и гугле, highload, если ты решил не изобретать велосипед и не создавать очередной веб продукт, на привлечение посетителей в которого заплатить денег придется в три раза больше, чем на разработку.
Телефон это от слова фон, фонетика - звук. Вместо того, чтобы учить огромное количество сочетаний кнопок для Figma, Blender, Photoshop, Unreal Engine, проще просто озвучить команду. Как повернуть чертёж в архикаде?
LLM как новый вид пользовательского интерфейса
Agent Swarm это как фрагменты в android или роутер в react: они позволяют конкретизировать скоуп задач (кнопки на экране) исходя из предидущего пользовательского ввода. Например, когда поступил звонок на SIP телефон, сначала нужно понять, хочет ли человек купить или вернуть товар в принципе, а потом предложить ему список товаров в наличие для покупки
Технические требования
Налоговая в любом случае спросит дебет/кредит в табличном виде, по этому CRM системы никуда не денутся. Задача LLM - спарсить естественный текст или чата или распознавалки голоса и трансформировать его в сигнатуру функции с наименованием и аргументами, чтобы можно было осуществить вызов и записать данные в базу
Для решения этой проблемы, важно знать чреду нюансов
На момент 2025 года OpenSource языковые модели для офлайн запуска галлюцинируют в 40%-50% случаев, когда оригинальный ChatGPT в 5%-10%.
Если не разделять промпт на агенты, модель будет больше галюцинировать, так как предмет разговора становится расплывчатым
Каждый месяц появляются новые модели, которые галюцинируют меньше. Появляются альтернативные SaaS, код которых закрыт, но они дешевле ChatGPT, например, DeepSeek
Как следствие
Код чат бота должен быть обособленным от провайдера LLM с возможностью переключения на GPT4All, Ollama, OpenAI, DeepSeek и тд без редактирования бизнес логики
Должен быть testbed, который позволяет оценить, какое количество промптов поломалось при смене языковой модели или провайдера, так как версионирование из-за SJW цензуры не предусмотрено: поведение промптов меняют в тихую не афишируя подробности
Код для оркестрации сессий чатов с контекстом активного агента должен быть отделён от провайдера LLM или фреймворка, так как в любой момент выйдет что-то новое
Разбор кода
На каждую открытую сессию чата нужно осуществить оркестрацию Swarm c деревом агентов, имеющих общую историю чата между собой и отдельную для разных пользователей. В данном коде, это реализовано под капотом agent-swarm-kit
import { addSwarm } from "agent-swarm-kit";
export const ROOT_SWARM = addSwarm({
swarmName: 'root_swarm',
agentList: [
TRIAGE_AGENT,
SALES_AGENT,
],
defaultAgent: TRIAGE_AGENT,
});
...
app.get("/api/v1/session/:clientId", upgradeWebSocket((ctx) => {
const clientId = ctx.req.param("clientId");
const { complete, dispose } = session(clientId, ROOT_SWARM)
return {
onMessage: async (event, ws) => {
const message = event.data.toString();
ws.send(await complete(message));
},
onClose: async () => {
await dispose();
},
}
}));
При создании агента мы указываем хотя бы один system message
, описывающий что он должен делать. Мы указываем коннектор к языковой модели, что позволит часть агентов обрабатывать бесплатно локально, сложные же делегировать в облачный червис openai. Если что-то не работает, добавляем промпты в массив system, например, фикс вызова функций для Ollama
const AGENT_PROMPT = `You are a sales agent that handles all actions related to placing the order to purchase an item.
Tell the users all details about products in the database by using necessary tool calls
Do not send any JSON to the user. Format it as plain text. Do not share any internal details like ids, format text human readable
If the previous user messages contains product request, tell him details immidiately
It is important not to call tools recursive. Execute the search once
`;
/**
* @see https://github.com/ollama/ollama/blob/86a622cbdc69e9fd501764ff7565e977fc98f00a/server/model.go#L158
*/
const TOOL_PROTOCOL_PROMPT = `For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call>
`;
export const SALES_AGENT = addAgent({
agentName: "sales_agent",
completion: OLLAMA_COMPLETION,
system: [TOOL_PROTOCOL_PROMPT],
prompt: AGENT_PROMPT,
tools: [SEARCH_PHARMA_PRODUCT, NAVIGATE_TO_TRIAGE],
});
В данном примере я использую ollama для обработки запросов пользователей. Для не сведующих в терминологии: процесс, когда языковая модель на вход получает историю переписки с пользователем, а на выход выдаёт новое сообщение, называется completion. В agent-swarm-kit используется абстрактный интерфейс, который одинокого подходит к любому облачному провайдеру или локальной модели. Используйте этот материал, чтобы подключить deepseek
import { addCompletion, IModelMessage } from "agent-swarm-kit";
const getOllama = singleshot(() => new Ollama({ host: CC_OLLAMA_HOST }));
export const OLLAMA_COMPLETION = addCompletion({
completionName: "ollama_completion",
getCompletion: async ({
agentName,
messages,
mode,
tools,
}) => {
const response = await getOllama().chat({
model: "nemotron-mini:4b", // "mistral-nemo:12b";
keep_alive: "1h",
messages: messages.map((message) => omit(message, "agentName", "mode")),
tools,
});
return {
...response.message,
mode,
agentName,
role: response.message.role as IModelMessage["role"],
};
},
});
Изменение активного агента и получение данных из БД осуществляется через вызов tools
: языковая модель возвращает специальный XML, который обрабатывается фреймворком для локальных моделей или облачным провайдером для OpenAI, чтобы вызвать внешний код на python/js и тд. Результат исполнения кода записывается в историю переписки в виде {"role": "tool", "content": "В базе данных найден продукт Парацетамол: жаропонижающее для борьбы с гриппом"}
. Со следующего сообщения от пользователя, языковая модель оперирует данными из инструмента
import { addTool, changeAgent, execute } from "agent-swarm-kit";
const PARAMETER_SCHEMA = z.object({}).strict();
export const NAVIGATE_TO_SALES = addTool({
toolName: "navigate_to_sales_tool",
validate: async (clientId, agentName, params) => {
const { success } = await PARAMETER_SCHEMA.spa(params);
return success;
},
call: async (clientId, agentName) => {
await commitToolOutput(
"Navigation success`,
clientId,
agentName
);
await changeAgent(SALES_AGENT, clientId);
await execute("Say hello to the user", clientId, SALES_AGENT);
},
type: "function",
function: {
name: "navigate_to_sales_tool",
description: "Navigate to sales agent",
parameters: {
type: "object",
properties: {},
required: [],
},
},
});
Для того, чтобы не хардкодить первые сообщения от агентов, при переключении агента происходит эмуляция запроса со стороны пользователя с просьбой поздороваться.
import {
addTool,
commitSystemMessage,
commitToolOutput,
execute,
getLastUserMessage,
} from "agent-swarm-kit";
const PARAMETER_SCHEMA = z
.object({
description: z
.string()
.min(1, "Fulltext is required")
})
.strict();
export const SEARCH_PHARMA_PRODUCT = addTool({
toolName: "search_pharma_product",
validate: async (clientId, agentName, params) => {
const { success } = await PARAMETER_SCHEMA.spa(params);
return success;
},
call: async (clientId, agentName, params) => {
let search = "";
if (params.description) {
search = String(params.description);
} else {
search = await getLastUserMessage(clientId);
}
if (!search) {
await commitToolOutput(
str.newline(`The products does not found in the database`),
clientId,
agentName
);
await execute(
"Tell user to specify search criteria for the pharma product",
clientId,
agentName
);
return;
}
const products = await ioc.productDbPublicService.findByFulltext(
search,
clientId
);
if (products.length) {
await commitToolOutput(
str.newline(
`The next pharma product found in database: ${products.map(
serializeProduct
)}`
),
clientId,
agentName
);
await commitSystemMessage(
"Do not call the search_pharma_product next time!",
clientId,
agentName
);
await execute(
"Tell user the products found in the database.",
clientId,
agentName
);
return;
}
await commitToolOutput(
`The products does not found in the database`,
clientId,
agentName
);
await execute(
"Tell user to specify search criteria for the pharma product",
clientId,
agentName
);
},
type: "function",
function: {
name: "search_pharma_product",
description:
"Retrieve several pharma products from the database based on description",
parameters: {
type: "object",
properties: {
description: {
type: "string",
description:
"REQUIRED! Minimum one word. The product description. Must include several sentences with description and keywords to find a product",
},
},
required: ["description"],
},
},
});
Языковые модели умеют формировать словарь именованных параметров для вызова tools
. Однако, OpenSource модели плохо справляются, если есть техническое требование закрытый контур, проще анализировать саму переписку.
Принцип работы роя агентов
Несколько сессий chatgpt (агентов) выполняют вызовы инструментов. Каждый агент может использовать разные модели, например,
mistral 7b
для повседневного общения,nemotron
для деловых разговоров.Рой агентов направляет сообщения к активной сессии chatgpt (агенту) для каждого канала
WebSocket
, используя параметр URLclientId
. Для каждого нового чата с человеком создается новый канал со своим роем агентовАктивная сессия chatgpt (агент) в рое может быть изменена путем выполнения инструмента.
Все клиентские сессии используют общую историю сообщений чата для всех агентов. История чата каждого клиента хранит последние 25 сообщений с ротацией. Между сессиями chatgpt (агентами) передаются только сообщения типа
assistant
иuser
, а сообщения типаsystem
иtool
ограничены областью действия агента, поэтому каждый агент знает только те инструменты, которые относятся к нему. В результате каждая сессия chatgpt (агент) имеет свой уникальный системный промпт.Если вывод агента не прошел валидацию (несуществующий вызов инструмента, вызов инструмента с неверными аргументами, пустой вывод, XML-теги в выводе или JSON в выводе), алгоритм спасения попытается исправить модель. Сначала он скроет предыдущие сообщения от модели, если это не поможет, то вернет заглушку вида "Извините, я не понял. Не могли бы вы повторить?"
Полезные функции для администрирования роя агентов
addAgent
- Регистрация нового агентаaddCompletion
- Регистрация новой языковой модели: облачной, локальной или мокaddSwarm
- Регистрация группы агентов для обработки чатов с пользователямиaddTool
- Регистрация инструмента для интеграции языковых моделей во внешние системыchangeAgent
- Изменить активный агент в роеcomplete
- Запросить ответ на сообщение, переданное в аргументы рою агентовsession
- Создать сессию для чата, дать коллбек на завершение сессии и отправку новых сообщенийgetRawHistory
- Получает необработанную историю системы для отладкиgetAgentHistory
- Получает историю, которую видит агент с поправкой на механизм самовосстановления и получателей сообщенийcommitToolOutput
- Отправить в историю результат исполнения функции. Если была вызвана функция, агент замораживается до получения ответаcommitSystemMessage
- Дополнить системный промпт новыми вводнымиcommitFlush
- Очистить переписку для агента, если были получены некорректные ответы или модель ошибочно рекурсивно вызывает инструментexecute
- Попросить нейронку проявить инициативу и первой написать пользователюemit
- Отправить пользователю заранее заготовленное сообщениеgetLastUserMessage
- Получить последнее сообщение от пользователя (без учетаexecute
)commitUserMessage
- Сохранить сообщение от пользователя в истории чата без ответа. Если пользователь спамит сообщения не дожидаясь обработки запросаgetAgentName
- Получить имя активного агентаgetUserHistory
- Получает историю сообщений от пользователяgetAssistantHistory
- Получает историю сообщений от языковой моделиgetLastAssistantMessage
- Получает последнее сообщение от языковой моделиgetLastSystemMessage
- Получает последнее дополнение системного промпта
Комментарии (12)
rPman
01.02.2025 21:41А есть пример видео/аудио сессии с пользователем? что бы посмотреть на реальный пользовательский опыт?
tripolskypetr Автор
01.02.2025 21:41Есть opensource версия для онбординга студентов, которых крайне удобно использовать на подбор промптов методом тыка, она использует
nemotron-mini
, чтобы можно было запустить на ноутбучной видеокартеЕсть NDA версия, которая совсем по другой предметной области и использует другие модели. Одна из них дообученый whisper. В этом проекте смысл приседания - сделать применение языковых моделей масштабируемым, для этого написана библиотека и тесты
https://github.com/tripolskypetr/agent-swarm-kit/tree/master/testrPman
01.02.2025 21:41мне интересно именно аудио версия... ведь аудиоинтерфейсы это не один в один текстовые, там есть куча нюансов, превращающие пользовательский опыт в ад.
tripolskypetr Автор
01.02.2025 21:41Могу предложить в https://github.com/ggerganov/whisper.cpp. На текущий момент чего-то вменяемого в опенсорсе я не нашел: разве что пайпить ChatGPT в локальный бек, обрабатывающий текстовую речь
rPman
01.02.2025 21:41Я не про преобразование речи в текст, а о создании на основе этого рабочей системы
heejew
01.02.2025 21:41В создании схемы с голосом так или иначе присутствует преобразование речи в текст. "За кадром" все равно работа идёт с текстом. Другой вопрос, какие методы применяются для tts и применяется ли нейросеть в процессе.
Или я что-то упустил в этом быстроразвивающемся мире?
tripolskypetr Автор
01.02.2025 21:41Да, речь преобразуется в текст. Просто, ранее это делал SAPI 5.0 на ванильном дотнетовском C++/CLI,а сейчас нейронка openai whisper
-
Nomic грозятся выкатить в opensource асинхронный итератор - в реальном времени кормить новыми словами ответа озвучку голоса, но застряли на технической части
OpenAI предоставляют WebRTC для коннекта к говорилки в реальном времени, но у оперсорс сообщества плохо с этим протоколом, халявы в естественной речи можно пока не ждать
https://platform.openai.com/docs/guides/realtime-webrtc
rPman
01.02.2025 21:41Ну да, так было, до появления мультимодальный gpt. Но речь не про них,.. вы хоть раз в жизни пробовали чем то управлять голосом, по сложнее чем вкл/выкл? вы пробовали набивать текст в каком-нибудь текстовом редакторе голосом? да просто сообщения? Вы понимаете чем отличается ввод текста руками от голосового?
Текст, он редактируемый, в нем есть пунктуация, структура текста (параграфы, списки, таблицы), вы его читаете перед отправкой...
Голосовые же системы работают в реальном времени, ты даешь команду и она уже в процессе должна обрабатываться и уйти на исполнение почти сразу как отзвучит последнее слово команды и таймаут паузы.
Это сильно отличается, система, которая обрабатывает голос как текст должна быть готова к тому что бы догадываться о том что хочет человек, давать свободу формулировкам и терпимой к ошибкам... почему я и спрашивал, есть ли уже что то готовое для посмотреть или как обычно, голосовые системы дальше 'современных' голосовых меню типа нажмите 1 чтобы перейти в подменю такое то, нажмите 2 - в другое...?
tripolskypetr Автор
01.02.2025 21:41В этом и прикол, при навигации агенты видят переписку друг друга. Ты говоришь: Подскажи какие продукты продаёте, а в программе автоматически дергаются три операции 1 - переключение на агент продаж, 2 - поиск в бд по запросу, 3 - ответ списка товаров пользователю. Поиск товара по смыслу, а не буквам, есть в репо, через embedding model и cosine distance.
В облаке монго уже есть векторный поиск по смыслу, https://www.mongodb.com/docs/manual/reference/operator/aggregation/vectorSearch/
tripolskypetr Автор
01.02.2025 21:41У ЦРТ есть облако для распознавания и генерации речи. Они работают с нейронками в проде года так с 2017.
https://cloud.speechpro.com/
Что могу сказать, их ценовая политика: плати - лети
hobbit19
А код с GitHub куда пропал?)
tripolskypetr Автор
Пофикшено)
https://github.com/tripolskypetr/node-ollama-telegram-agent-swarm