アサーションベースのエージェントテスト
ツール呼び出し、中間ステップ、最終出力の構造を検証します。
「アサーションベースのエージェントテスト」はCoddyKit上の無料AI Agentsレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Agents学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Agentsコースには全4レッスンが含まれています。
完全な文字列一致を超えて
LLMの出力は非決定的であるため、assert response == 'exact text'のようにテストすると壊れやすくなります。その代わりに、正確な表現に依存せず、レスポンスの構造と意図をチェックするアサーションを記述します。
ツール呼び出しが行われたことをアサートする
関数呼び出しを行うエージェントでは、エージェントが正しいツールを選んで呼び出したことを確認するのが、最も信頼性の高いアサーションです。これは構造に基づくチェックであり、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')
正しいツール名をアサートする
ツール呼び出しが存在することを確認するだけでなく、具体的なツール名が期待どおりであることも検証します。これにより、エージェントがクエリに対して誤ったツールを選ぶケースを検出できます。
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')ツールの引数をアサートする
ツール名を検証した後、引数が正しいことを確認します。エージェントは正しいツールを選ぶだけでなく、ユーザーのリクエストから正しいパラメーターを設定する必要があります。
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')
出力のJSON Schema検証
エージェントが構造化されたJSONを返す場合は、JSON Schemaに対して出力を検証し、必須フィールドがすべて存在し、正しい型になっていることを確認します。jsonschemaライブラリを使うと簡単に実行できます。
# 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}')キーワードの存在をアサートする
正確な表現が変化するテキストレスポンスでは、重要な概念や単語が出力に含まれていることを確認します。柔軟でありながら意味のあるチェックで、エージェントの回答に少なくとも関連する用語が含まれていることを検証できます。
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レスポンス形式をアサートする:型チェック
型のアサーションは高速で信頼性が高い方法です。エージェントがNoneではなくdictを返すこと、リスト型のフィールドがリストであること、数値フィールドが有効な範囲に収まっていることを確認します。
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')finish_reasonをアサートする
finish_reasonフィールドは、モデルが生成を停止した理由を示します。これをアサートすると、問題を検出しやすくなります。'stop'は正常に回答が完了したこと、'tool_calls'はエージェントがツールを呼び出そうとしていること、'length'は出力が途中で切り詰められたことを意味します。
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')ループ内のステップ数をアサートする
ループで実行されるエージェントは、妥当なステップ数で完了する必要があります。エージェントが最大反復回数以内に終了することをアサートします。これにより、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')複数の入力に対してテストをパラメーター化する
pytestの@pytest.mark.parametrizeを使うと、同じテストをさまざまな入力に対して実行できます。異なる種類のクエリを、エージェントが正しいツールに振り分けられることのテストに最適です。
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カスタムアサーションヘルパーを作成する
エージェントのテストスイートが大きくなったら、共通するアサーションのパターンをヘルパーに切り出します。これにより、テストが短く読みやすくなり、エージェントのレスポンス形式が変わった場合も保守しやすくなります。
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')
理解度チェック:アサーションに基づくエージェントテスト
エージェントのテストで使うアサーション戦略について、理解度を確認しましょう。
復習:アサーションに基づくエージェントテスト
これで、エージェントのテストに必要なアサーションツールが一通りそろいました。
- エージェントがツールを使うべき場合に、
tool_callsが空でないことを確認します tc.function.name == 'expected_tool'を使って、正しいツール名をアサートしますtc.function.argumentsをJSONとして解析し、ツールの引数を検証します- 構造化された出力の検証には、
jsonschema.validate()を使います - 柔軟なテキストアサーションには、キーワードの存在チェックを使います
- ループで動作するエージェントでは、
finish_reasonとステップ数を確認します - 複数の入力シナリオには、
@pytest.mark.parametrizeを使います
よくある質問
「アサーションベースのエージェントテスト」レッスンは無料ですか?
はい。「アサーションベースのエージェントテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Agentsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Agentsコースには全4レッスンが含まれています。
「アサーションベースのエージェントテスト」で何を学びますか?
ツール呼び出し、中間ステップ、最終出力の構造を検証します。 ブラウザで直接実行するハンズオンコードでAI Agentsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Agentsを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Agentsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「アサーションベースのエージェントテスト」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Agentsレッスンでコードを書いて実行できますか?
はい。すべてのAI Agentsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- エージェントのテストが異なる理由
- テストにおけるLLM呼び出しのモック
- アサーションベースのエージェントテスト
- エージェントパイプラインの統合テスト