Тестирование запросов на основе утверждений
Проверка результатов с помощью 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 — локальная установка не требуется.
Все уроки этого курса
- Написание тестовых случаев для запросов
- Тестирование запросов на основе утверждений
- Регрессионное тестирование при обновлении моделей
- Создание набора тестов для запросов