0Pricing
AI Agents · Lekcja

Testowanie agentów oparte na asercjach

Sprawdzanie wywołań narzędzi, kroków pośrednich i struktury końcowego wyniku.

Testowanie agentów oparte na asercjach to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.

Odejście od dokładnego porównywania ciągów

Ponieważ wyniki LLM są niedeterministyczne, testowanie ich za pomocą assert response == 'exact text' jest podatne na błędy. Zamiast tego należy pisać asercje sprawdzające strukturę i intencję odpowiedzi, bez uzależniania testu od dokładnego brzmienia.

Sprawdzanie, czy wykonano wywołania narzędzi

W przypadku agentów korzystających z wywołań funkcji najbardziej niezawodną asercją jest sprawdzenie, czy agent wybrał właściwe narzędzie. Jest to sprawdzenie strukturalne — nie zależy od dokładnego sformułowania procesu rozumowania LLM.

import json
from unittest.mock import patch, MagicMock

@patch('myagent.client.chat.completions.create')
def test_agent_calls_search_tool(mock_create):
    # Mock: agent decides to call search_web
    tool_call = MagicMock()
    tool_call.function.name = 'search_web'
    tool_call.function.arguments = json.dumps({'query': 'Python tutorials'})
    mock_create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(tool_calls=[tool_call]))]
    )

    response = mock_create()  # simulating the agent call
    tc = response.choices[0].message.tool_calls

    assert tc is not None
    assert len(tc) > 0
    assert tc[0].function.name == 'search_web'

# --- demo: give unittest.mock.patch a real dotted path to patch ---
import sys
import types

_myagent = types.ModuleType('myagent')
_myagent.client = types.SimpleNamespace(
    chat=types.SimpleNamespace(completions=types.SimpleNamespace(create=lambda *a, **k: None))
)
sys.modules['myagent'] = _myagent

test_agent_calls_search_tool()
print('test_agent_calls_search_tool: PASS')

Sprawdzanie poprawnej nazwy narzędzia

Oprócz samego sprawdzenia, czy wywołania narzędzi istnieją, należy zweryfikować, czy konkretna nazwa narzędzia jest zgodna z oczekiwaniami. Pozwala to wykryć sytuacje, w których agent wybiera niewłaściwe narzędzie dla danego zapytania.

import json
from unittest.mock import MagicMock

def extract_tool_calls(response) -> list:
    message = response.choices[0].message
    if not message.tool_calls:
        return []
    return [
        {
            'name': tc.function.name,
            'args': json.loads(tc.function.arguments)
        }
        for tc in message.tool_calls
    ]

# In a test:
# calls = extract_tool_calls(mock_response)
# assert calls[0]['name'] == 'get_weather'
# assert calls[0]['args']['city'] == 'Paris'
print('Tool name and argument assertions are the most reliable agent tests')

Sprawdzanie argumentów narzędzia

Po zweryfikowaniu nazwy narzędzia należy sprawdzić, czy jego argumenty są poprawne. Agent musi nie tylko wybrać właściwe narzędzie, lecz także uzupełnić je właściwymi parametrami z zapytania użytkownika.

import json
from unittest.mock import patch, MagicMock

@patch('myagent.client.chat.completions.create')
def test_weather_tool_gets_correct_city(mock_create):
    tool_call = MagicMock()
    tool_call.function.name = 'get_weather'
    tool_call.function.arguments = json.dumps({'city': 'Tokyo', 'unit': 'celsius'})
    mock_create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(tool_calls=[tool_call]))]
    )

    response = mock_create()
    args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)

    assert args['city'] == 'Tokyo'
    assert args.get('unit') in ['celsius', 'fahrenheit', None]  # flexible

# --- demo: give unittest.mock.patch a real dotted path to patch ---
import sys
import types

_myagent = types.ModuleType('myagent')
_myagent.client = types.SimpleNamespace(
    chat=types.SimpleNamespace(completions=types.SimpleNamespace(create=lambda *a, **k: None))
)
sys.modules['myagent'] = _myagent

test_weather_tool_gets_correct_city()
print('test_weather_tool_gets_correct_city: PASS')

Walidacja wyników za pomocą JSON Schema

Gdy agent zwraca ustrukturyzowany JSON, należy zwalidować wynik względem JSON Schema, aby upewnić się, że wszystkie wymagane pola są obecne i mają prawidłowe typy. Biblioteka jsonschema znacznie to ułatwia.

# pip install jsonschema
import jsonschema

AGENT_RESPONSE_SCHEMA = {
    'type': 'object',
    'required': ['answer', 'sources', 'confidence'],
    'properties': {
        'answer': {'type': 'string', 'minLength': 1},
        'sources': {
            'type': 'array',
            'items': {'type': 'string', 'format': 'uri'}
        },
        'confidence': {'type': 'number', 'minimum': 0, 'maximum': 1}
    }
}

def test_agent_output_schema(agent_output: dict):
    try:
        jsonschema.validate(instance=agent_output, schema=AGENT_RESPONSE_SCHEMA)
        print('Schema validation passed')
    except jsonschema.ValidationError as e:
        raise AssertionError(f'Invalid agent output: {e.message}')

Sprawdzanie obecności słów kluczowych

W przypadku odpowiedzi tekstowych, w których dokładne sformułowanie może się różnić, należy sprawdzić, czy wynik zawiera kluczowe pojęcia lub słowa. Jest to elastyczne, a zarazem nadal miarodajne — odpowiedź agenta musi przynajmniej wspominać o istotnych terminach.

def assert_keywords_present(text: str, keywords: list, require_all: bool = True):
    lower_text = text.lower()
    found = [kw.lower() in lower_text for kw in keywords]

    if require_all:
        missing = [kw for kw, f in zip(keywords, found) if not f]
        assert not missing, f'Missing keywords: {missing}'
    else:
        assert any(found), f'None of {keywords} found in: {text[:100]}'

# Tests
response = 'The capital city of France is Paris, located in western Europe.'
assert_keywords_present(response, ['paris', 'france', 'capital'])
print('All keywords present!')  # passes

assert_keywords_present(response, ['spain', 'france'], require_all=False)
print('At least one keyword present!')  # passes

Sprawdzanie formatu odpowiedzi: typy

Asercje typów są szybkie i niezawodne. Należy sprawdzić, czy agent zwraca dict (a nie None), czy pola list są listami oraz czy pola liczbowe mieszczą się w prawidłowych zakresach.

def test_agent_returns_valid_structure(agent_result):
    # Type checks
    assert isinstance(agent_result, dict), 'Result must be a dict'
    assert isinstance(agent_result.get('answer'), str), 'answer must be a string'
    assert isinstance(agent_result.get('steps'), list), 'steps must be a list'

    # Non-empty checks
    assert len(agent_result['answer']) > 0, 'answer must not be empty'
    assert len(agent_result['steps']) >= 1, 'must have at least one step'

    # Range checks
    confidence = agent_result.get('confidence', 0)
    assert 0.0 <= confidence <= 1.0, 'confidence must be 0-1'

print('Structural assertions are fast and reliable')

Sprawdzanie finish_reason

Pole finish_reason informuje, dlaczego model zakończył generowanie. Sprawdzanie jego wartości pomaga wykrywać problemy: 'stop' oznacza poprawną odpowiedź, 'tool_calls' oznacza, że agent chce wywołać narzędzie, a 'length' oznacza obcięcie wyniku.

from unittest.mock import MagicMock

def test_agent_stops_cleanly(mock_response):
    finish_reason = mock_response.choices[0].finish_reason
    assert finish_reason in ('stop', 'tool_calls'), \
        f'Unexpected finish_reason: {finish_reason}'

def test_no_truncation(mock_response):
    finish_reason = mock_response.choices[0].finish_reason
    assert finish_reason != 'length', \
        'Response was truncated — increase max_tokens'

# Example mock for a clean stop
mock = MagicMock()
mock.choices = [MagicMock(finish_reason='stop')]
test_agent_stops_cleanly(mock)
print('finish_reason: stop — clean termination')

Sprawdzanie liczby kroków w pętli

Agent działający w pętli powinien kończyć pracę w rozsądnej liczbie kroków. Należy sprawdzić, czy agent kończy działanie w granicach maksymalnej liczby iteracji — pozwala to wykryć nieskończone pętle, którym ma zapobiegać zabezpieczenie max_iterations.

def test_agent_completes_in_bounded_steps(mock_agent):
    result = mock_agent.run('Search for the weather in Paris')

    # Agent should complete within 5 steps
    assert result['steps_taken'] <= 5, \
        f'Agent took too many steps: {result["steps_taken"]}'

    # Agent should produce a final answer, not exit on timeout
    assert result['status'] == 'completed', \
        f'Agent did not complete: {result["status"]}'

    assert result['answer'] is not None

print('Bounding step count prevents runaway agents from passing tests')

Parametryzowanie testów dla wielu danych wejściowych

pytest umożliwia za pomocą @pytest.mark.parametrize uruchamianie tego samego testu dla wielu różnych danych wejściowych. Jest to idealne rozwiązanie do sprawdzania, czy agent kieruje różne typy zapytań do właściwych narzędzi.

import pytest
from unittest.mock import patch, MagicMock
import json

@pytest.mark.parametrize('query,expected_tool', [
    ('What is the weather in Tokyo?', 'get_weather'),
    ('Calculate 15% of 200', 'calculator'),
    ('Search for Python books', 'web_search'),
    ('What time is it in Berlin?', 'get_time'),
])
@patch('myagent.client.chat.completions.create')
def test_agent_tool_routing(mock_create, query, expected_tool):
    tool_call = MagicMock()
    tool_call.function.name = expected_tool
    tool_call.function.arguments = json.dumps({'input': query})
    mock_create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(tool_calls=[tool_call]))]
    )
    response = mock_create()
    actual = response.choices[0].message.tool_calls[0].function.name
    assert actual == expected_tool

Pisanie niestandardowych pomocników do asercji

W miarę rozrastania się zestawu testów agenta należy przenosić powtarzające się wzorce asercji do pomocników. Dzięki temu testy są krótsze, czytelniejsze i łatwiejsze w utrzymaniu, gdy zmieni się format odpowiedzi agenta.

import json

def assert_tool_called(response, tool_name: str, required_args: dict = None):
    message = response.choices[0].message
    assert message.tool_calls, 'Expected tool call but got plain text'
    names = [tc.function.name for tc in message.tool_calls]
    assert tool_name in names, f'Expected {tool_name}, got {names}'

    if required_args:
        for tc in message.tool_calls:
            if tc.function.name == tool_name:
                args = json.loads(tc.function.arguments)
                for key, val in required_args.items():
                    assert args.get(key) == val, \
                        f'Arg {key}: expected {val}, got {args.get(key)}'

# Clean test using the helper:
# assert_tool_called(response, 'get_weather', {'city': 'Paris'})

# --- demo ---
from unittest.mock import MagicMock

tool_call = MagicMock()
tool_call.function.name = 'get_weather'
tool_call.function.arguments = json.dumps({'city': 'Paris'})
response = MagicMock(choices=[MagicMock(message=MagicMock(tool_calls=[tool_call]))])

assert_tool_called(response, 'get_weather', {'city': 'Paris'})
print('assert_tool_called passed: agent called get_weather with city=Paris')

Sprawdzenie wiedzy: Testowanie agentów za pomocą asercji

Sprawdź swoją wiedzę na temat strategii asercji stosowanych w testach agentów.

Podsumowanie: Testowanie agentów za pomocą asercji

Można już korzystać z kompletnego zestawu asercji do testów agentów:

  • Należy sprawdzać, czy tool_calls nie jest puste, gdy agent powinien użyć narzędzia
  • Należy sprawdzać poprawną nazwę narzędzia za pomocą tc.function.name == 'expected_tool'
  • Należy walidować argumenty narzędzia, parsując tc.function.arguments jako JSON
  • Należy używać jsonschema.validate() do walidacji ustrukturyzowanych wyników
  • Należy sprawdzać obecność słów kluczowych w elastycznych asercjach tekstowych
  • Należy sprawdzać finish_reason i liczbę kroków w agentach działających w pętli
  • Należy używać @pytest.mark.parametrize dla wielu scenariuszy danych wejściowych

Często zadawane pytania

Czy lekcja „Testowanie agentów oparte na asercjach” jest bezpłatna?

Tak — pełny tekst „Testowanie agentów oparte na asercjach” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.

Co nauczysz się w „Testowanie agentów oparte na asercjach”?

Sprawdzanie wywołań narzędzi, kroków pośrednich i struktury końcowego wyniku. Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Agents?

Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Testowanie agentów oparte na asercjach”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?

Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego testowanie agentów jest inne
  2. Mockowanie wywołań LLM w testach
  3. Testowanie agentów oparte na asercjach
  4. Testy integracyjne potoków agentów
← Powrót do AI Agents