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

推荐订阅源

有赞技术团队
有赞技术团队
量子位
B
Blog RSS Feed
Schneier on Security
Schneier on Security
L
LINUX DO - 最新话题
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google DeepMind News
Google DeepMind News
N
News | PayPal Newsroom
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
大猫的无限游戏
大猫的无限游戏
PCI Perspectives
PCI Perspectives
aimingoo的专栏
aimingoo的专栏
D
Docker
T
The Exploit Database - CXSecurity.com
Last Week in AI
Last Week in AI
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
月光博客
月光博客
Vercel News
Vercel News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
O
OpenAI News
C
Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
爱范儿
爱范儿
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Help Net Security
Help Net Security
Scott Helme
Scott Helme
The Hacker News
The Hacker News
Y
Y Combinator Blog
A
Arctic Wolf
V
V2EX
P
Proofpoint News Feed
Simon Willison's Weblog
Simon Willison's Weblog
A
About on SuperTechFans
S
Securelist
G
Google Developers Blog
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как тестировать 5 LLM-агентов одним набором тестов: capability-based подход
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

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