0Pricing
AI Prompt Engineering · 강의

단정문 기반 프롬프트 테스트

contains(), 정규식, JSON 스키마, LLM-as-judge로 출력을 확인합니다.

단정문 기반 프롬프트 테스트은(는) CoddyKit의 무료 AI Prompt Engineering 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Prompt Engineering 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Prompt Engineering 강의에는 총 4개의 강의가 포함되어 있습니다.

LLM 출력에 대한 검증

단정문 기반 테스트는 단위 테스트에서 사용하는 것과 같은 원칙을 LLM에도 적용합니다. 출력에 반드시 포함되거나 포함되지 않아야 하는 내용을 명시적으로 주장하고, 그 주장이 위반되면 즉시 실패 처리합니다.

결정론적 함수를 사용하는 단위 테스트와 달리, LLM 단정문은 확률적 텍스트 출력을 다루므로 더 유연한 단정문 유형이 필요합니다: contains, matches_schema, satisfies_regex, llm_judge_score_above.

기본 어설션: contains 및 not_contains

가장 간단한 어설션은 키워드의 포함 여부를 확인합니다. 이러한 어설션은 분류 작업, 구조화된 출력, 안전성 확인에 효과적입니다.

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 호출입니다. 호출당 $0.005인 경우 테스트 사례 100개를 전체 실행하는 데 $0.50이 듭니다. 비용을 관리하는 전략은 다음과 같습니다.

  • 정적 테스트 입력의 응답을 캐시하고 CI에서는 캐시에서 실행합니다
  • 매일 밤 전체 테스트 모음을 실행하고, 각 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를 사용하십시오. CI에서는 --junitxml을 사용하여 GitHub Actions, GitLab CI, Jenkins와 호환되는 JUnit XML 보고서를 생성하십시오.

# 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 출력에 사용하는 주요 어설션 유형은 다음과 같습니다.

  • contains / not_contains: 키워드 포함 여부를 확인하며 레이블과 안전성 확인에 적합합니다
  • JSON 스키마 검증: 구조화된 출력 형식을 검증합니다
  • 정규식 일치: 특정 패턴(날짜, 코드)을 검증합니다
  • LLM 심사자: 자유 형식 설명의 품질을 평가합니다

깔끔하고 확장 가능한 테스트 모음에는 @pytest.mark.parametrize와 함께 pytest를 사용하십시오. 비용을 관리하려면 응답을 캐시하십시오. 각 PR에서는 스모크 테스트 하위 집합을 실행하고, 전체 테스트 모음은 매일 밤 실행하십시오. 다음 강의에서는 모델 업데이트 전반의 회귀 테스트를 다룹니다.

자주 묻는 질문

“단정문 기반 프롬프트 테스트” 강의는 무료인가요?

네 — “단정문 기반 프롬프트 테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Prompt Engineering 강의 전체를 잠금 해제할 수 있습니다. AI Prompt Engineering 강의에는 총 4개의 강의가 포함되어 있습니다.

“단정문 기반 프롬프트 테스트”에서 뭘 배우나요?

contains(), 정규식, JSON 스키마, LLM-as-judge로 출력을 확인합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Prompt Engineering을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Prompt Engineering을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Prompt Engineering은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“단정문 기반 프롬프트 테스트” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Prompt Engineering 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Prompt Engineering 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 프롬프트 테스트 사례 작성
  2. 단정문 기반 프롬프트 테스트
  3. 모델 업데이트에 따른 회귀 테스트
  4. 프롬프트 테스트 모음 만들기
← AI Prompt Engineering(으)로 돌아가기