0Pricing
AI Prompt Engineering · Урок

Тестирование запросов на основе утверждений

Проверка результатов с помощью contains(), регулярных выражений, схемы JSON и оценки LLM в роли судьи.

«Тестирование запросов на основе утверждений» — бесплатный урок AI Prompt Engineering на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Prompt Engineering, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Prompt Engineering содержит 4 уроков всего.

Утверждения для результатов LLM

Тестирование на основе утверждений применяет к LLM тот же принцип, что и модульное тестирование: явно формулировать утверждения о том, что результат должен содержать или не содержать, и немедленно завершаться ошибкой, когда утверждение нарушено.

В отличие от модульных проверок с детерминированными функциями, утверждения для LLM работают с вероятностным текстовым выводом, поэтому требуются более гибкие типы утверждений: contains, matches_schema, satisfies_regex, llm_judge_score_above.

Базовые проверки: наличие и отсутствие

Самые простые проверки определяют наличие или отсутствие ключевых слов. Они хорошо подходят для задач классификации, структурированных результатов и проверок безопасности.

import openai
client = openai.OpenAI(api_key='sk-...')

def call_prompt(system, user, temperature=0):
    resp = client.chat.completions.create(
        model='gpt-4o',
        messages=[
            {'role': 'system', 'content': system},
            {'role': 'user', 'content': user}
        ],
        temperature=temperature
    )
    return resp.choices[0].message.content

# Keyword presence assertion
def assert_contains(output, keyword, case_sensitive=False):
    text = output if case_sensitive else output.lower()
    kw = keyword if case_sensitive else keyword.lower()
    assert kw in text, f'Expected "{keyword}" in output, got: {output[:100]}'

# Keyword absence assertion
def assert_not_contains(output, forbidden, case_sensitive=False):
    text = output if case_sensitive else output.lower()
    kw = forbidden if case_sensitive else forbidden.lower()
    assert kw not in text, f'Forbidden "{forbidden}" found in output: {output[:100]}'

Проверка схемы JSON

Если Ваш промпт должен возвращать структурированный JSON, проверьте результат по схеме. Ошибка проверки схемы означает, что в промпте есть проблема с форматом: модель либо добавила пояснительный текст, либо структура JSON неверна.

import json
from jsonschema import validate, ValidationError

PRODUCT_SCHEMA = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'},
        'price': {'type': 'number', 'minimum': 0},
        'available': {'type': 'boolean'}
    },
    'required': ['name', 'price', 'available'],
    'additionalProperties': False
}

def assert_valid_json_schema(output, schema):
    try:
        data = json.loads(output.strip())
    except json.JSONDecodeError as e:
        raise AssertionError(f'Output is not valid JSON: {e}\nOutput: {output[:200]}')
    try:
        validate(instance=data, schema=schema)
    except ValidationError as e:
        raise AssertionError(f'JSON does not match schema: {e.message}\nOutput: {output[:200]}')
    return data

# Test
output = call_prompt(
    'Extract product info as JSON: {"name": ..., "price": ..., "available": ...}',
    'Widget Pro costs $49.99 and is in stock.'
)
product = assert_valid_json_schema(output, PRODUCT_SCHEMA)
print('Parsed product:', product)

Сопоставление с регулярным выражением

Проверки с регулярными выражениями позволяют точно проверить формат результата — это полезно для результатов, которые должны соответствовать определённому шаблону, например дат, номеров телефонов или структурированных кодов.

import re

def assert_matches_regex(output, pattern, flags=0):
    if not re.search(pattern, output, flags):
        raise AssertionError(
            f'Output does not match pattern /{pattern}/\nOutput: {output[:200]}'
        )

def assert_output_is_label(output, valid_labels):
    cleaned = output.strip().upper()
    assert cleaned in valid_labels, (
        f'Expected one of {valid_labels}, got: {repr(cleaned)}'
    )

# Examples
output = call_prompt('Classify sentiment as POSITIVE, NEGATIVE, or NEUTRAL:', 'Great product!')
assert_output_is_label(output, {'POSITIVE', 'NEGATIVE', 'NEUTRAL'})

date_output = call_prompt('Extract the date in YYYY-MM-DD format:', 'Meeting on November 15, 2024')
assert_matches_regex(date_output, r'^\d{4}-\d{2}-\d{2}$')

Оценивание с помощью LLM-судьи

Для открытых ответов используйте второй вызов LLM, чтобы оценить качество. Такой подход называется оцениванием с помощью LLM-судьи. Модель-судья получает исходный промпт, результат и критерии оценки, а затем возвращает балл.

def llm_judge_score(original_prompt, output, criteria, max_score=10):
    judge_prompt = (
        f'Evaluate the following AI response on a scale of 1-{max_score}.\n'
        f'Evaluation criteria: {criteria}\n\n'
        f'Original prompt: {original_prompt}\n\n'
        f'AI response: {output}\n\n'
        f'Return only a number from 1 to {max_score}.'
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': judge_prompt}],
        temperature=0
    )
    score_text = resp.choices[0].message.content.strip()
    return int(score_text)

def assert_llm_score_above(original_prompt, output, criteria, min_score=7):
    score = llm_judge_score(original_prompt, output, criteria)
    assert score >= min_score, f'LLM judge score {score} < minimum {min_score}'

Использование pytest для тестирования промптов

pytest — стандартная платформа тестирования Python, которая хорошо подходит для тестирования промптов. Каждая тестовая функция соответствует одному тестовому сценарию. pytest автоматически собирает, запускает тесты и формирует отчёты о них.

# test_sentiment_prompt.py
import pytest
import openai

client = openai.OpenAI(api_key='sk-...')
SYSTEM_PROMPT = 'Classify the sentiment as POSITIVE, NEGATIVE, or NEUTRAL. Return only the label.'

def classify(text):
    resp = client.chat.completions.create(
        model='gpt-4o',
        messages=[
            {'role': 'system', 'content': SYSTEM_PROMPT},
            {'role': 'user', 'content': text}
        ],
        temperature=0
    )
    return resp.choices[0].message.content.strip().upper()

# pytest automatically discovers functions starting with test_
def test_positive_sentiment():
    assert classify('I love this product!') == 'POSITIVE'

def test_negative_sentiment():
    assert classify('Terrible experience.') == 'NEGATIVE'

def test_neutral_sentiment():
    assert classify('It arrived on time.') == 'NEUTRAL'

# Run: pytest test_sentiment_prompt.py -v

Параметризованные тесты в pytest

Используйте @pytest.mark.parametrize, чтобы запускать одну и ту же тестовую функцию для множества входных данных без дублирования кода. Это самый удобный способ создать всеобъемлющий набор тестов.

# test_sentiment_parametrized.py
import pytest

TEST_CASES = [
    ('I love this!', 'POSITIVE'),
    ('Worst purchase ever.', 'NEGATIVE'),
    ('It works.', 'NEUTRAL'),
    ('Amazing!', 'POSITIVE'),
    ('Terrible!', 'NEGATIVE'),
    ('OK I guess.', 'NEUTRAL'),
]

@pytest.mark.parametrize('text,expected', TEST_CASES)
def test_sentiment_classification(text, expected):
    result = classify(text)
    assert result == expected, f'For "{text}": expected {expected}, got {result}'

# pytest test_sentiment_parametrized.py -v
# Output shows each test case individually:
# PASSED test_sentiment_parametrized.py::test_sentiment_classification[I love this!-POSITIVE]
# PASSED test_sentiment_parametrized.py::test_sentiment_classification[Worst purchase ever.-NEGATIVE]

Фикстуры для общего состояния промпта

Используйте фикстуры pytest, чтобы совместно использовать ресурсоёмкую настройку в разных тестах — например, загружать шаблон промпта или создавать клиента API один раз за сеанс тестирования.

# conftest.py — fixtures available to all test files in the directory
import pytest
import openai

@pytest.fixture(scope='session')
def llm_client():
    return openai.OpenAI(api_key='sk-...')

@pytest.fixture(scope='session')
def sentiment_prompt():
    with open('prompts/sentiment_v3.txt') as f:
        return f.read()

# test_sentiment.py
def test_positive_with_fixture(llm_client, sentiment_prompt):
    resp = llm_client.chat.completions.create(
        model='gpt-4o',
        messages=[
            {'role': 'system', 'content': sentiment_prompt},
            {'role': 'user', 'content': 'I love this!'}
        ],
        temperature=0
    )
    assert 'POSITIVE' in resp.choices[0].message.content.upper()

Обработка нестабильных тестов

Результаты LLM имеют вероятностный характер: даже при temperature=0 разные развёртывания или версии модели могут выдавать разные результаты. Обрабатывайте нестабильность с помощью повторных попыток и порогов допустимого отклонения.

import pytest

def run_with_retry(fn, n=3):
    '''Run fn up to n times, pass if any run succeeds.'''
    failures = []
    for _ in range(n):
        try:
            fn()
            return  # passed
        except AssertionError as e:
            failures.append(str(e))
    raise AssertionError(f'Failed all {n} attempts. Last: {failures[-1]}')

def test_positive_with_retry():
    def check():
        result = classify('I love this!')
        assert result == 'POSITIVE'
    run_with_retry(check, n=3)

# Or use pytest-retry plugin:
# @pytest.mark.flaky(reruns=3)
# def test_positive_sentiment():
#     assert classify('I love this!') == 'POSITIVE'

Производительность и стоимость тестирования

Каждый тестовый сценарий — это вызов API: 100 тестовых сценариев по $0.005 за вызов = $0.50 за полный запуск тестов. Для управления стоимостью используйте следующие стратегии:

  • Кэшируйте ответы для неизменяемых входных данных тестов и запускайте тесты из кэша в системе непрерывной интеграции
  • Запускайте полный набор тестов каждую ночь, а при каждом PR — только подмножество быстрых проверок (10 сценариев)
  • Используйте более дешёвую модель (gpt-4o-mini) для большинства тестов, а gpt-4o — только для набора регрессионных тестов
import hashlib, json

RESPONSE_CACHE = {}

def cached_classify(text, use_cache=True):
    key = hashlib.md5(text.encode()).hexdigest()
    if use_cache and key in RESPONSE_CACHE:
        return RESPONSE_CACHE[key]
    result = classify(text)
    RESPONSE_CACHE[key] = result
    return result

# Persist cache to disk for CI
def load_cache(path='test_cache.json'):
    global RESPONSE_CACHE
    try:
        with open(path) as f:
            RESPONSE_CACHE = json.load(f)
    except FileNotFoundError:
        RESPONSE_CACHE = {}

def save_cache(path='test_cache.json'):
    with open(path, 'w') as f:
        json.dump(RESPONSE_CACHE, f, indent=2)

Отчёты о результатах тестирования

pytest создаёт подробные отчёты, в которых указано, какие тестовые сценарии завершились ошибкой и почему. Используйте pytest --tb=short -v, чтобы получать краткие сообщения об ошибках. Для непрерывной интеграции используйте --junitxml, чтобы создавать отчёты в формате XML JUnit, совместимые с GitHub Actions, GitLab CI и Jenkins.

# Run test suite and generate reports
# In terminal:
# pytest tests/prompt/ -v --tb=short --junitxml=test_results.xml

# In Python (for programmatic use):
import subprocess

def run_prompt_tests(test_dir='tests/prompt'):
    result = subprocess.run(
        ['pytest', test_dir, '-v', '--tb=short', '--junitxml=test_results.xml'],
        capture_output=True, text=True
    )
    print(result.stdout)
    if result.returncode != 0:
        print('TESTS FAILED')
        print(result.stderr)
    return result.returncode == 0

passed = run_prompt_tests()

Проверка знаний

Когда при тестировании промпта следует использовать оценивание с помощью LLM-судьи, а не проверку точного совпадения?

Итоги: тестирование промптов на основе проверок

Основные типы проверок результатов LLM:

  • Наличие / отсутствие: наличие ключевого слова — подходит для меток и проверок безопасности
  • Проверка схемы JSON: проверяет формат структурированного результата
  • Сопоставление с регулярным выражением: проверяет определённые шаблоны (даты, коды)
  • LLM как судья: оценивает качество открытых текстовых ответов

Используйте pytest с @pytest.mark.parametrize, чтобы создавать удобные и масштабируемые наборы тестов. Кэшируйте ответы, чтобы управлять стоимостью. Запускайте подмножество быстрых проверок при каждом PR, а полный набор — каждую ночь. В следующем уроке Вы узнаете о регрессионном тестировании при обновлении моделей.

Часто задаваемые вопросы

Урок «Тестирование запросов на основе утверждений» бесплатный?

Да — полный текст урока «Тестирование запросов на основе утверждений» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Prompt Engineering, подпишись на CoddyKit PRO. Курс AI Prompt Engineering содержит 4 уроков всего.

Чему я научусь в уроке «Тестирование запросов на основе утверждений»?

Проверка результатов с помощью contains(), регулярных выражений, схемы JSON и оценки LLM в роли судьи. Ты практикуешь AI Prompt Engineering с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Prompt Engineering?

Предыдущий опыт не требуется. AI Prompt Engineering на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Тестирование запросов на основе утверждений»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Prompt Engineering?

Да. Каждый урок AI Prompt Engineering включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Написание тестовых случаев для запросов
  2. Тестирование запросов на основе утверждений
  3. Регрессионное тестирование при обновлении моделей
  4. Создание набора тестов для запросов
← Назад к AI Prompt Engineering