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!') # passesSprawdzanie 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_toolPisanie 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_callsnie 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.argumentsjako 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_reasoni liczbę kroków w agentach działających w pętli - Należy używać
@pytest.mark.parametrizedla 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
- Dlaczego testowanie agentów jest inne
- Mockowanie wywołań LLM w testach
- Testowanie agentów oparte na asercjach
- Testy integracyjne potoków agentów