English translation is not available yet. Showing Russian content.

Как делать property-based testing для агентов?

Краткий тезис

Property-based testing (тестирование на основе свойств) — это подход, при котором вы проверяете не конкретные ответы агента, а инвариантные свойства (properties), которые должны выполняться для широкого класса входных данных. В контексте AI-агентов это позволяет выявлять неочевидные баги (галлюцинации, неконсистентность, отказы вне домена) на случайных сгенерированных примерах. Основные инструменты — библиотека Hypothesis (Python) и концепция QuickCheck (Haskell), дополненные кастомными генераторами.


1. Отличие от традиционного unit-тестирования

ХарактеристикаUnit-тестыProperty-based тесты
ОписаниеПроверяют ожидаемый вывод для известных входовГенерируют случайные входы и проверяют инварианты
ПокрытиеОграничено ручными примерамиАвтоматически покрывают краевые случаи
ОбнаружениеИзвестные ошибкиНеожиданные граничные условия и скрытые баги
Примерassert agent.answer("Какая столица Франции?") == "Париж"Проверка, что ответ не содержит фактов вне контекста

Property-based testing не заменяет unit-тесты, а дополняет их, выявляя контрпримеры, которые разработчик не предусмотрел.


2. Ключевые свойства (invariants) для AI-агентов

Для агента, работающего с RAG и инструментами, типичные свойства:

2.1 Consistency (согласованность)

При семантически эквивалентных перефразировках ответ должен быть семантически эквивалентен (или хотя бы не противоречить). Формально: если q1 и q2 — парафразы, то semantic_similarity(answer(q1), answer(q2)) ≥ ε.

2.2 No hallucination (отсутствие галлюцинаций)

Любое утверждение в ответе должно подтверждаться предоставленным контекстом (retrieved documents). Можно проверять через fine-grained entailment или кросс-проверку с LLM-судьёй.

2.3 Refusal on OOD (отказ на запрос вне домена)

Для запроса, не относящегося к базе знаний агента, ответ должен явно отказывать, а не галлюцинировать. Свойство: если query не имеет релевантных документов по метрике сходства, то answer содержит слова-маркеры отказа (например, “не знаю”, “не могу ответить”).

2.4 Idempotence (идемпотентность) при zero-temperature

При temperature=0 повторение того же запроса должно давать одинаковый ответ (в пределах детерминированного поведения). Исключение — недетерминированные компоненты (асинхронные тайминги, случайные shuffle документов), которые нужно замокать.

2.5 Monotonicity (монотонность качества)

Более точный или более полный контекст не должен ухудшать ответ. Если мы добавляем в контекст релевантный документ, то ответ должен быть не хуже (по faithfulness или полезности), чем с исходным меньшим контекстом.

Эти свойства можно комбинировать и адаптировать под конкретную архитектуру агента.


3. Инструменты: Hypothesis (Python)

Hypothesis — библиотека для property-based testing в Python. Основные элементы:

  • @given(...) — декоратор, указывающий стратегию генерации входных данных.
  • strategies — встроенные генераторы (st.text(), st.integers(), st.lists(), st.from_regex()) и кастомные через st.composite.
  • assume(condition) — фильтрация недопустимых входов.
  • @example(...) — добавление конкретных примеров.
  • settings — настройка (max_examples, deadline, suppress_health_check).

Пример теста на refusal on OOD:

from hypothesis import given, settings, assume
from hypothesis import strategies as st
from my_agent import agent

# Предположим, у агента есть список допустимых тем (домен)
VALID_TOPICS = [{python}, {machine_learning}, {data_science}]

@given(st.text(min_size=1, max_size=200))
@settings(max_examples=100)
def test_refusal_out_of_domain(query):
    # Проверяем, что запрос не похож ни на одну тему
    assume(not any(topic in query.lower() for topic in VALID_TOPICS))
    
    answer = agent.answer(query)
    refusal_words = ["не знаю", "не могу", "извините", "запрос вне области"]
    assert any(word in answer.lower() for word in refusal_words), \
        f"Агент ответил на внедоменный запрос: {query[:50]}..."

4. Кастомные генераторы для агентов

Агенты оперируют сложными структурами: последовательности вызовов инструментов, многопоточные запросы, диалоги. Hypothesis позволяет создавать генераторы для:

  • Историй диалогов: чередование сообщений пользователя и ассистента.
  • Конфигураций инструментов: набор доступных функций с параметрами.
  • Сценариев с ошибками: таймауты, пустые ответы от API.

Пример генератора диалога:

import hypothesis.strategies as st

# Стратегия для одного сообщения
message = st.text(min_size=1, max_size=500)

# Диалог — список чередующихся ролей (user/assistant)
@st.composite
def dialogue_strategy(draw):
    n = draw(st.integers(min_value=1, max_value=10))
    roles = ["user"] * n + ["assistant"] * (n - 1)
    st.shuffle(roles)  # не идеально, но для примера
    # Проще: чередование user/assistant
    messages = []
    for i in range(n):
        messages.append({
            "role": "user" if i % 2 == 0 else "assistant",
            "content": draw(st.text(min_size=1, max_size=200))
        })
    return messages

5. Интеграция с контекстом RAG

Для свойства No hallucination нужно сгенерировать случайные контексты и проверить, что ответ не выходит за их пределы. Используем Hypothesis для генерации пар (контекст, запрос), где контекст может быть нерелевантным, частично релевантным или полностью покрывающим запрос.

@given(st.lists(st.text(min_size=10, max_size=200)), st.text(min_size=5, max_size=100))
@settings(max_examples=50)
def test_no_hallucination(context_docs, query):
    # Генерируем ответ агента с заданным контекстом
    answer = agent.answer(query, context=context_docs)
    # Грубая проверка: каждое ключевое слово из ответа должно быть в контексте
    # (упрощение; на практике используют NLI-модель)
    keywords = [w for w in answer.split() if len(w) > 4]
    for kw in keywords:
        for doc in context_docs:
            if kw in doc:
                break
        else:
            # keyword не найден ни в одном документе — потенциальная галлюцинация
            assert False, f"Найдено слово '{kw}' вне контекста"

6. Продвинутые техники: stateful testing и модели поведения

Stateful testing — когда агент имеет внутреннее состояние (память, подключения к инструментам). Hypothesis позволяет тестировать последовательности операций как автомат:

  • определяем состояния (state machine);
  • генерируем последовательность команд (reset, query, use_tool);
  • проверяем инварианты после каждой команды.

Пример для агента с кэшем:

from hypothesis.stateful import RuleBasedStateMachine, rule, precondition

class AgentStateMachine(RuleBasedStateMachine):
    def __init__(self):
        self.agent = Agent()
        self.cache = {}
    
    @rule(query=st.text(), tool=st.sampled_from(["search", "calculate"]))
    def query_agent(self, query, tool):
        if query in self.cache:
            # Ожидаем кэшированный ответ
            assert self.agent.query(query, tool) == self.cache[query]
        else:
            result = self.agent.query(query, tool)
            self.cache[query] = result

7. Профилирование и метрики покрытия

Property-based testing не гарантирует 100% покрытия, но вы можете оценить количество уникальных примеров и диапазон покрытых стратегий. Hypothesis встроено логирует контрпримеры и умеет их воспроизводить (запись в файл hypothesis-examples). Для агентов полезно:

  • Установить max_examples в соответствии с временем ответа (обычно 50–200).
  • Использовать deadline для выявления таймаутов.
  • Включить Phantom (проверка покрытия кодовых путей) через pytest-cov.

8. Ограничения и best practices

ОграничениеРешение
Стоимость LLM-вызововОграничить количество примеров; использовать моки для инструментов; тестировать на локальной маленькой модели
Нечеткая природа свойствИспользовать LLM-оценщик для проверки семантических свойств (например, consistency)
Случайные сбои (flakiness)Фиксировать seed (hypothesis.seed(42)) для воспроизводимости; использовать @example для известных краевых случаев
Долгое выполнениеРазделять тесты на быстрые (оффлайн проверки) и медленные (с запуском агента)

Best practice: начинайте с простых свойств (refusal, idempotence), затем переходите к более сложным (no hallucination). Регулярно запускайте property-based тесты в CI, но с ограниченным числом примеров (например, 30).


Пет-проект для закрепления

Задача: Разработать property-based тесты для агента-консультанта по документации (например, для библиотеки matplotlib).
Инструменты: Python, Hypothesis, pytest, библиотека RAGAS (для оценки семантической близости).
Шаги:

  1. Определите 3 свойства: идемпотентность (при temp=0), отказ на запросы вне библиотеки (например, "как приготовить борщ"), консистентность между ответами на синонимичные вопросы ("как построить scatter" vs "как сделать scatter plot").
  2. Реализуйте генераторы запросов: используйте st.from_regex для шаблонов с API-функциями, и st.lists для списка слов вне домена.
  3. Напишите тесты с декоратором @given. Для проверки consistency примените предобученный эмбеддинг (e.g. sentence-transformers) и порог косинусной близости.
  4. Запустите тесты, найдите хотя бы один контрпример (например, агент может галлюцинировать при пустом контексте).
    Ожидаемый результат: Вы получите репродуцируемые контрпримеры и улучшите агента (добавите проверку на пустой контекст, улучшите refusal).

Связь с другими вопросами

ВопросТема
5Оценка retrieval – база для свойства No hallucination
10Self-RAG вводит рефлексию, которую можно тестировать на консистентность
50Метрики faithfulness – основа для свойства No hallucination
100Stateful testing для агентов с тулами
200Общее введение в PBT (предполагаемый номер)
30Свойство Refusal on malicious input

Навигация