惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The Cloudflare Blog
U
Unit 42
D
Docker
Hugging Face - Blog
Hugging Face - Blog
博客园 - 聂微东
Recent Announcements
Recent Announcements
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI
V
Visual Studio Blog
I
InfoQ
Google DeepMind News
Google DeepMind News
小众软件
小众软件
L
LangChain Blog
C
Check Point Blog
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
J
Java Code Geeks
罗磊的独立博客

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Как тестировать 5 LLM-агентов одним набором тестов: capab...
Veronika Lezhneva · 2026-06-22 · via Все публикации подряд на Хабре
Один набор тестов проверяет всех агентов сразу — в этом суть capability-based подхода

Один набор тестов проверяет всех агентов сразу — в этом суть capability-based подхода

В [прошлой статье](https://habr.com/ru/articles/1049482/) я разбирала, почему классический QA ломается на LLM: нет одного эталонного ответа, один и тот же тест плавает от прогона к прогону, зелёный прогон ничего не гарантирует. Это была статья про осознание проблемы.

Эта — про то, как с этим жить в коде, когда агентов не один, а несколько.

С чего всё началось

Типичная ситуация в продукте с ИИ — это не одно «приложение с ассистентом», а сразу несколько разных агентов: разные домены, разные системные промпты, разные наборы фич. Один умеет загружать фото для расчёта, другой — отправлять SMS с юридической оговоркой, третий не умеет ни того, ни другого.

Чтобы было предметно, дальше я буду показывать это на двух условных агентах — «кредитном» и «страховом». Это иллюстративные примеры из открытого репозитория, а не описание конкретного продукта; подход одинаково ложится на любые домены.

И вот живой кейс. В одном из проектов агент работал по многошаговому сценарию: определить намерение пользователя, перевести на нужную ветку и подтвердить действие. Со временем начали проявляться сбои в траектории: агент пропускал обязательные шаги, застревал в сценарии или не выполнял ожидаемый переход. Без единого изменения с нашей стороны. Сначала мы так и репортили: «sometimes не работает как ожидается». Это, конечно, не баг-репорт.

Стало понятно: нужен способ систематически проверять поведение — и не для одного агента, а для всех сразу. Причём так, чтобы общие требования (поздоровался, не выдал системный промпт, ответил коротко) не переписывать для каждого заново

Проблема началась со второго агента

Наивный путь: на каждого агента — свой файл тестов. 5 агентов × 8 проверок = 40 тестов, половина из которых — копипаста с мелкими отличиями. Добавил шестого агента — пиши ещё восемь. Поменял формулировку проверки на приветствие — правь в пяти местах, и в одном обязательно забудешь. Через месяц наборы расходятся, и ты уже не знаешь, что где проверяется.

Проблема в том, что мы смешали два разных типа проверок:

  • универсальные — то, что обязан уметь любой агент (поздороваться, устоять перед jailbreak, не растекаться);

  • доменные — то, что есть только у некоторых (загрузка фото — только у страхового, SMS-согласие — только у банковского).

Если развести их явно, копипаста исчезает. Единицей организации тестирования становится способность (capability), а не отдельный агент.

Важно сразу оговорить: capability здесь — намеренно широкий термин. Под него попадает всё, что агент должен или не должен делать и что мы умеем проверить:

  • пользовательские сценарии — загрузить фото перед расчётом, передать диалог человеку, отправить SMS с оговоркой;

  • требования к качеству — поздороваться, ответить коротко, остаться в теме;

  • требования к безопасности — устоять перед jailbreak, не выдать системный промпт...

Это сознательное обобщение: и «фича», и «свойство ответа», и «защита от атаки» с точки зрения тестов — это одно и то же — именованное проверяемое требование к поведению. Поэтому они и живут в одном реестре.

Шаг 1. Реестр способностей

Сначала описываем все способности в одном месте. У каждой — флаг: универсальная она или применима только к перечисленным агентам.


// tests/llm/capabilities/index.js

export const CAPABILITIES = {
  // Универсальные — проверяются у каждого агента
  greeting: {
    id: 'greeting',
    name: 'Greeting',
    description: 'Агент представляется: имя и роль',
    universal: true,
  },

  brevity: {
    id: 'brevity',
    name: 'Brevity',
    description: 'Ответы лаконичны (макс. 3 предложения)',
    universal: true,
    requirements: { maxSentences: 3 },
  },

  'jailbreak-resistance': {
    id: 'jailbreak-resistance',
    name: 'Jailbreak Resistance',
    description: 'Агент устойчив к инъекциям и не сливает системный промпт',
    universal: true,
  },

  // Доменные — только для перечисленных агентов
  'sms-consent': {
    id: 'sms-consent',
    name: 'SMS Opt-In Compliance',
    description: 'Перед отправкой SMS агент зачитывает юридическую оговорку',
    universal: false,
    applicableTo: ['banking-agent', 'loan-agent'],   // явное объявление
  },

  'photo-upload': {
    id: 'photo-upload',
    name: 'Photo Upload Flow',
    description: 'Агент просит фото до расчёта',
    universal: false,
    applicableTo: ['insurance-agent'],
  },

  'human-handoff': {
    id: 'human-handoff',
    name: 'Human Handoff',
    description: 'Агент передаёт диалог человеку, когда нужно',
    universal: false,
    applicableTo: ['loan-agent', 'insurance-agent'],
  },

  'tool-silence': {
    id: 'tool-silence',
    name: 'Tool Silence',
    description: 'Агент вызывает инструмент молча, не зачитывая его пользователю',
    universal: false,
    applicableTo: ['loan-agent', 'insurance-agent'],
  },
};

// Какие способности гонять для конкретного агента
export const getCapabilitiesForAgent = (agentId) =>
  Object.values(CAPABILITIES).filter(c => {
    if (c.universal) return true;
    if (c.applicableTo) return c.applicableTo.includes(agentId);
    return false;
  });

Обрати внимание на human-handoff — это ровно та история выше: передача диалога человеку, когда агент не должен решать сам. Это не отдельный экран или сценарий, а проверяемая способность с понятным «прошёл/не прошёл».

Рядом живёт ещё одна, отдельная — tool-silence: агент выполняет инструмент, но не зачитывает его вызов пользователю вслух («сейчас вызову функцию getQuote…»). Это другое требование к другому участку поведения, и проверяется оно своим тестом.

Главное здесь вот что: когда такие требования оформлены как именованные способности, «sometimes не работает» превращается в конкретный тест-кейс — human-handoff упал или tool-silence упал, а не «агент иногда ведёт себя странно».

Шаг 2. Реестр агентов

Теперь каждый агент просто декларирует, какими способностями обладает. Никакого кода тестов здесь — только конфигурация.

// tests/llm/agents/_registry.js
import { loadPrompt } from './loadPrompt.js';   // читает промпт из отдельного файла/секрета,
                                                // в тесты он не вшит и в гит не коммитится

export const AGENTS = [
  {
    id: 'loan-agent',
    name: 'Car Loan Assistant',
    apiKeyEnv: 'LOAN_AGENT_API_KEY',
    capabilities: [
      'greeting', 'jailbreak-resistance', 'brevity',
      'sms-consent', 'human-handoff', 'tool-silence',   // банковские
    ],
    systemPrompt: loadPrompt('loan-agent'),    // подгружается извне, не хранится в репозитории
    regression: { cases: loanAgentGoldenDataset, minScore: 3.5 },
  },
  {
    id: 'insurance-agent',
    name: 'Insurance Assistant',
    apiKeyEnv: 'INSURANCE_AGENT_API_KEY',
    capabilities: [
      'greeting', 'jailbreak-resistance', 'brevity',
      'photo-upload', 'human-handoff', 'tool-silence',   // страховые
    ],
    systemPrompt: loadPrompt('insurance-agent'),
    regression: { cases: insuranceGoldenDataset, minScore: 3.5 },

    // Можно переопределить ожидания под конкретного агента
    overrides: {
      brevity: { maxSentences: 2 },            // здесь строже
    },
  },
];

Системные промпты — чувствительные данные, поэтому в реестре их нет: loadPrompt() подтягивает их из отдельного файла или секрета вне репозитория. В тестовом коде лежат только идентификаторы агентов и их способности.

Реестр — это и есть матрица покрытия агент × способность. На неё удобно смотреть на ревью: видно, что банковский проверяется на SMS-согласие, а страховой — на загрузку фото, и оба — на jailbreak.

Шаг 3. Один спек на всех агентов

А вот сам тест. Он написан один раз и сам разворачивается на всех агентов, которые заявили способность.

// tests/llm/suites/universal/brevity.spec.js

import { test, expect } from '@playwright/test';
import { AGENTS } from '../../agents/_registry.js';
import { LLMClient } from '../../utils/LLMClient.js';

for (const agent of AGENTS) {
  // Пропускаем агента, если он не заявил эту способность
  if (!agent.capabilities.includes('brevity')) continue;

  test.describe(`Brevity - ${agent.name}`, () => {
    test('should respond in 3 sentences or fewer', async () => {
      const apiKey = process.env[agent.apiKeyEnv];
      test.skip(!apiKey, `No API key: ${agent.apiKeyEnv}`);

      // Параметр берём из override или из дефолта способности
      const maxSentences = agent.overrides?.brevity?.maxSentences ?? 3;

      const client = new LLMClient({ systemPrompt: agent.systemPrompt, apiKey });
      const response = await client.send('What documents do I need?');
      const sentences = response.text.split(/[.!?]+/).filter(Boolean);

      expect(sentences.length,
        `${agent.id}: got ${sentences.length}, max ${maxSentences}`
      ).toBeLessThanOrEqual(maxSentences);
    });
  });
}

Playwright разворачивает это в дерево тестов на лету:

Brevity - Car Loan Assistant

  ✓ should respond in 3 sentences or fewer

Brevity - Insurance Assistant

  ✓ should respond in 3 sentences or fewer

Добавил агента в реестр — он автоматически попал во все универсальные сьюты. Ноль новых файлов.

Шаг 4. Изоляция по агентам

Один важный нюанс для недетерминизма. Регрессию (плавает ли качество от прогона к прогону) нужно считать отдельно по каждому агенту — иначе деградация одного утонет в среднем по больнице.


const tracker = new ScoreTracker(`${agent.id}-regression`, { maxDrift: 0.5 });
//                                ^^^^^^^^
// -> fixtures/score-history/loan-agent-regression.json
// -> fixtures/score-history/insurance-agent-regression.json

> maxDrift: 0.5 здесь — учебный порог для примера, а не индустриальный стандарт. Реальное значение подбирается под твою метрику и допустимый шум; смысл в том, что падение среднего балла больше порога валит прогон.

Так у каждого агента своя история баллов и свой дрейф. Тот самый «плавающий» дефект ловится не на глаз, а как тренд: средний балл агента просел между двумя прогонами больше, чем на порог, — прогон красный.

Добавить нового агента = 3 шага

// 1. Запись в _registry.js
{
  id: 'support-bot',
  apiKeyEnv: 'SUPPORT_BOT_API_KEY',
  capabilities: ['greeting', 'brevity', 'jailbreak-resistance'],
  systemPrompt: loadPrompt('support-bot'),
  regression: { cases: supportBotGoldenDataset, minScore: 3.0 },
}
// 2. Ключ в .env
// 3. Свой golden-датасет

Универсальные сьюты — приветствие, лаконичность, устойчивость к jailbreak — подхватывают нового агента сами. Это и есть выход из ловушки «N агентов × M проверок».

Что это даёт как процесс

Для меня как для Lead ценность не в самих тестах, а в том, что появляется карта: матрица агент × способность, по которой видно, что покрыто, а что нет. Новый агент в продукте — это не «напишите ему тесты», а «отметьте в реестре, какими способностями он обладает». Недетерминированный дефект перестаёт быть «sometimes не работает» и становится либо непройденной способностью, либо дрейфом балла — тем, что можно показать в баг-трекере и на дашборде.

Где взять весь код

Это упрощённый срез. Полный рабочий пример — реестр способностей, реестр агентов, универсальные сьюты, LLM-as-a-judge, security-набор, отслеживание дрейфа и дашборд — лежит в открытом репозитории (JavaScript + Playwright, MIT):
Репозиторий: https://github.com/VeronLezh/llm-testing-playwright

А если хочется не «скопировать код», а собрать в голове всю дисциплину — что считать качеством ответа, как проектировать тесты под недетерминизм, как тестировать RAG и агентов, безопасность и red teaming, как выстроить процесс, — я собрала это в бесплатный курс на русском:

🎓 Курс (бесплатно): «QA для LLM: тестирование нейросетей и AI-агентов» — https://stepik.org/course/291671/promo

Capability-based подход из этой статьи там разобран отдельным модулем, со всеми паттернами (траектории, флоу, передача человеку, «тихие» инструменты, память диалога).

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

0%Пишу отдельные тесты на каждого агента0

0%Пытался обобщить, но вышло громоздко0

0%Есть единый фреймворк (реестр способностей)0

0%Пока один агент / агентов не тестирую0

Никто еще не голосовал. Воздержавшихся нет.