Как стать автором
Обновить

Юнит тесты роя агентов

Уровень сложностиСредний
Время на прочтение3 мин
Количество просмотров541

Исходный код, разобранный в статье, опубликован в этом репозитории

При разработке роя агентов встает вопрос юнит тестирования. Рой агентов позволяет использовать разные 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");
    }
});

Спасибо за внимание!

Теги:
Хабы:
+4
Комментарии3

Публикации

Работа

React разработчик
39 вакансий
Data Scientist
38 вакансий

Ближайшие события