Исходный код, разобранный в статье, опубликован в этом репозитории
При разработке роя агентов встает вопрос юнит тестирования. Рой агентов позволяет использовать разные LLM с выбором другой активной модели исходя из действий пользователя. Как следствие, обрабатывать идентичную переписку может любой агент из роя, например, был сделан Redis FLUSHALL и активный агент потерялся: чат продолжается с корневого Triage agent

Без тестов нет технической возможности отлавливать редкие состояния модели, когда в 8 из 10 случаев модель ведет себя как надо, а в оставшихся двух выдает лютую ерись: например, спонтанно пришло в голову ещё раз поздороваться с пользователем. Это следствие математической модели, где разнообразие ответов обеспечено переменными seed и temperature
Помимо этого, без тестов невозможно принять работу промпт инженера: откуда мы знаем, проверял ли он на самом деле свои промпты: это не код, статический анализ бесполезен. Так же, это хороший способ ловить кринж тестить опенсорс модели на предмет готовности в production: в теории, если перенести small talk в локальную модель это поможет экономить деньги, но сейчас они слишком сильно галлюцинируют
Что нужно тестировать
Для вызываемых инструметов обязательно нужно протестировать, что модель действительно обращается во внешние API, а не пишет текстовую заглушку вида платеж совершен успешно
import { Chat, commitToolOutput, emit, getAgentName, overrideSwarm, overrideTool } from "agent-swarm-kit";
import { randomString } from "functools-kit";
import { test } from "worker-testbed";
import { AgentName, SwarmName, ToolName } from "@modules/remote-lib";
const CLIENT_ID = `test-client-id-${randomString()}`;
test("RU: Пополнение карты Тройка по запросу пользователя", async (t) => {
let isCalled = false;
/**
* Языковые модели сохраняют предпочтения пользователя в system prompt
* Тоже нужно покрыть тестами
*/
overrideAgent({
agentName: AgentName.TroikaPaymentToolRu,
systemDynamic: async (clientId: string) => {
return [`В последний раз пользователь пополнял карту тройка на 300 рублей`];
},
});
overrideTool({
toolName: ToolName.TroikaPaymentToolRu,
call: async ({ toolId, clientId, agentName, params }) => {
if (params.amount === "200") {
isCalled = true;
await commitToolOutput(toolId, "Ok", clientId, agentName);
await emit("Ok", clientId, agentName);
}
}
});
await Chat.sendMessage(CLIENT_ID, "Я хочу пополнить карту Тройка на 200 рублей", SwarmName.RootSwarmRu);
if (isCalled) {
t.pass("Инструмент пополнения карты Тройка успешно вызван");
} else {
t.fail("Инструмент пополнения карты Тройка не был вызван");
}
});
Для строковых параметров enum обязательно нужно тестировать, что модель берет значение из переписки, а не первый параметр из объявления инструмента
import { Chat, commitToolOutput, emit, getAgentName, overrideSwarm, overrideTool } from "agent-swarm-kit";
import { randomString } from "functools-kit";
import { test } from "worker-testbed";
import { AgentName, SwarmName, ToolName } from "@modules/remote-lib";
const CLIENT_ID = `test-client-id-${randomString()}`;
test("RU: Оплата электричества Мосэнерго вернет ошибку без указания района", async (t) => {
let isCalled = false;
overrideTool({
toolName: ToolName.MosenergoPaymentToolRu,
call: async ({ toolId, clientId, agentName, params }) => {
// Если модель подставила любой район (district определен), тест должен провалиться
if (params.district) {
isCalled = true; // Инструмент вызван с районом, чего не должно быть
} else if (params.amount === "200") {
isCalled = false; // TODO: Район не указан, инструмент не должен вызываться. Nemotron Mini на Ollama
}
}
});
await Chat.sendMessage(CLIENT_ID, "Я хочу оплатить электричество Мосэнерго на 200 рублей", SwarmName.RootSwarmRu);
if (isCalled) {
t.fail("Инструмент оплаты электричества Мосэнерго был вызван с подставленным районом");
} else {
t.pass("Инструмент оплаты электричества Мосэнерго не был вызван, как ожидалось");
}
});
Рой агентов подразумевает роутер - triage agent
. Задача этого агента произвести арбитраж, какой целевой агент из представленных в состоянии обработать запрос пользователя. Дочерний агент осуществляет детальный анализ что нужно сделать. Если он не может обработать запрос, переадресует обратно на triage agent
. Если промпт агентов недостаточно точен, будут рекурсивные навигации
import { Chat, getAgentName, overrideSwarm } from "agent-swarm-kit";
import { randomString } from "functools-kit";
import { test } from "worker-testbed";
import { AgentName, SwarmName } from "@modules/remote-lib";
const CLIENT_ID = `test-client-id-${randomString()}`;
test("RU: Переход от агента воды Мосводоканал к агенту triage при запросе", async (t) => {
overrideSwarm({
swarmName: SwarmName.RootSwarmRu,
getActiveAgent: async () => AgentName.WaterAgentRu,
});
await Chat.sendMessage(CLIENT_ID, "Направь меня к агенту triage", SwarmName.RootSwarmRu);
const lastAgent = await getAgentName(CLIENT_ID);
if (lastAgent === AgentName.TriageAgentRu) {
t.pass("Агент воды Мосводоканал успешно перешел к агенту triage");
} else {
t.fail("Агент воды Мосводоканал не смог перейти к агенту triage");
}
});