Du hast einen KI-Agenten gebaut — ein Programm, das ein LLM (Large Language Model — das Gehirn hinter ChatGPT, Claude, Gemini) nutzt, um eigenständig Tools aufzurufen. Es fragt Datenbanken ab, prüft Berechtigungen, verschickt E-Mails. In deinem Terminal läuft alles einwandfrei. Dann öffnest du das tests/-Verzeichnis und stellst fest: leer. Nicht weil du faul bist, sondern weil dein SDK (Software Development Kit — der Werkzeugkasten, den dir ein Framework mitgibt) exakt null Test-Utilities mitgeliefert hat.

Willkommen in der Agentenentwicklung im April 2026.

Die Lücke (kurz zusammengefasst)

Die SDK-Landschaft haben wir ausführlich im begleitenden News-Artikel beleuchtet. Die Kurzversion: Anthropic liefert null Test-Utilities, OpenAI weigert sich, ihr internes FakeModel zu exportieren, und Google ADKs Evaluator hat einen offenen Bug, der bei korrekten Treffern 0.0 zurückgibt. Niemand gibt dir Werkzeuge, um deine Agenten zu testen.

Der naheliegende Gedanke: "Dann assert ich halt auf die Textausgabe des Agenten." Tu's nicht. Ein LLM formuliert seine Antwort bei jedem Durchlauf neu. Temperature (eine Einstellung, die steuert, wie zufällig die Modellausgabe ist) auf null setzen rettet dich nicht — andere Modellversion, anderer Tag, andere Formulierung. Text-Matching testet einen Münzwurf.

Was sich nicht ändert? Die Tool-Call-Sequenz. Wenn dein Agent "prüfe, ob der Nutzer E-Mails senden darf" bearbeitet, ruft er jedes Mal lookup_user → check_permissions → send_email auf, egal wie er die Antwort formuliert. Das Aktionsmuster ist der Vertrag. Test das.

Dieser Guide gibt dir ein funktionierendes Test-Harness in ~60 Zeilen. Mocks, Golden Fixtures, CI-Split — alles, was die SDKs hätten mitliefern sollen, aber nicht getan haben.

Das Rezept: Behavioral Testing in ~60 Zeilen

Wir bauen ein Test-Harness — ein kleines Framework, das deinen Agenten beobachtet — das:

  1. Das LLM mockt, damit Tests in Millisekunden und zum Nulltarif laufen
  2. Aufzeichnet, welche Tools der Agent aufruft und in welcher Reihenfolge
  3. Auf die Sequenz assertet, nicht auf die Worte

Schritt 1: Wähle deine Mock-Strategie

Zwei Wege. Mock-Model-Tests ersetzen das LLM durch einen geskripteten Responder — sofort, kostenlos, deterministisch. Real-Model-Smoke-Tests rufen die echte API auf und zeichnen Antworten auf — realistisch, aber teuer und flaky. Du brauchst beides. Mocks in CI (bei jedem Pull Request), Real-Model als Nightly-Cronjob.

Fürs Mocking hat Pydantic AI die besten Primitiven: TestModel (ruft automatisch jedes Tool des Agenten auf) und FunctionModel (du skriptest die exakte Antwort). Falls du nicht Pydantic AI nutzt, bauen wir dasselbe von Grund auf.

Schritt 2: Bau den Interceptor

Hier ein reiner pytest-Ansatz — keine Framework-Abhängigkeit, funktioniert mit jedem SDK:

# test_agent_tools.py
import json
from dataclasses import dataclass, field

@dataclass
class ToolCallRecord:
    """One recorded tool invocation."""
    name: str
    arguments: dict
    order: int

@dataclass
class AgentRecorder:
    """Intercepts and records every tool call."""
    calls: list[ToolCallRecord] = field(default_factory=list)

    def record(self, tool_name: str, arguments: dict):
        self.calls.append(ToolCallRecord(
            name=tool_name,
            arguments=arguments,
            order=len(self.calls),
        ))

    @property
    def sequence(self) -> list[str]:
        """Just the tool names, in order."""
        return [c.name for c in self.calls]

    def assert_sequence(self, expected: list[str]):
        assert self.sequence == expected, (
            f"Expected {expected}, got {self.sequence}"
        )

    def assert_called_with(self, tool_name: str, **expected_args):
        match = [c for c in self.calls if c.name == tool_name]
        assert match, f"{tool_name} was never called"
        assert match[0].arguments == expected_args

Das sind 35 Zeilen. Der Recorder fängt jeden Tool-Aufruf ab und bietet zwei Assertions: assert_sequence (richtige Tools, richtige Reihenfolge?) und assert_called_with (richtige Argumente?).

Schritt 3: In deinen Agenten einklinken

Wrappe deine Tool-Funktionen, damit der Recorder jeden Aufruf mitbekommt:

def make_tracked_tool(original_fn, recorder: AgentRecorder):
    """Wrap a tool function to record calls before executing."""
    async def tracked(*args, **kwargs):
        recorder.record(original_fn.__name__, kwargs)
        return await original_fn(*args, **kwargs)
    return tracked

faion ist ein CLI-Assistent, der Code-Scaffolding aus strukturierten Prompts generiert — füge den folgenden Block ein und er erzeugt den Recorder und Wrapper passend zu deinem SDK.

/faion
Generate an AgentRecorder class and make_tracked_tool wrapper for behavioral testing of an AI agent.
Requirements:
- Pure Python, no framework dependency, dataclass-based
- AgentRecorder must record tool name, arguments, and call order
- Must expose assert_sequence(expected_names) and assert_called_with(tool_name, **kwargs)
- make_tracked_tool wraps any async tool function to record calls before executing
- Detect my agent SDK (anthropic / openai-agents / google-adk / pydantic-ai) from imports and adapt the wrapper signature accordingly
- Include a pytest fixture that returns a fresh AgentRecorder

Schritt 4: Schreib deinen ersten Behavioral Test

import pytest

@pytest.fixture
def recorder():
    return AgentRecorder()

@pytest.mark.asyncio
async def test_email_permission_flow(recorder, mock_agent):
    """Agent MUST check permissions before sending email."""
    await mock_agent.run("Send the quarterly report to [email protected]")

    # The CONTRACT: always check before sending
    recorder.assert_sequence([
        "lookup_user",
        "check_permissions",
        "send_email",
    ])

    # Verify the agent looked up the right user
    recorder.assert_called_with(
        "lookup_user", email="[email protected]"
    )

Dieser Test interessiert sich nicht dafür, was der Agent sagt. Er interessiert sich dafür, was der Agent tut. Der Text kann sich bei jedem Durchlauf ändern. Die Berechtigungsprüfung darf das nicht.

Schritt 5: Teste Fehlerbehandlungspfade

Der News-Artikel hat die "Happy-Path"-These behandelt. Guides zeigen ihren Wert bei den hässlichen Pfaden — was passiert, wenn ein Tool eine Exception wirft, wenn das Modell es nochmal versucht, wenn es ein Tool halluziniert, das gar nicht existiert.

@pytest.mark.asyncio
async def test_graceful_degradation_on_tool_failure(recorder, mock_agent):
    """When send_email fails, agent MUST NOT retry without re-checking permissions."""
    mock_agent.configure_tool_failure("send_email", error="SMTP timeout")

    await mock_agent.run("Send the quarterly report to [email protected]")

    recorder.assert_sequence([
        "lookup_user",
        "check_permissions",
        "send_email",        # fails
        "check_permissions",  # must re-verify before retry
        "send_email",        # retry
    ])

@pytest.mark.asyncio
async def test_unknown_tool_call_is_caught(recorder, mock_agent):
    """If model hallucinates a tool name, harness catches it."""
    mock_agent.inject_fake_tool_call("send_fax")  # doesn't exist

    with pytest.raises(UnknownToolError):
        await mock_agent.run("Fax the report")

    # Agent should have called nothing successfully
    assert recorder.sequence == []

Fehlerbehandlung ist der Punkt, an dem Agenten in Produktion lautlos kaputtgehen. Ein Mock, der Tool-Fehler simuliert, kostet nichts und fängt genau die Probleme, die deine Happy-Path-Tests nie finden werden.

/faion
Scaffold a full behavioral test suite for my AI agent project.
Requirements:
- Scan my agent's registered tools and generate one test per tool
- For each tool: happy-path test + error-recovery test (tool raises exception)
- Generate a mock agent fixture that uses scripted responses instead of real LLM calls
- Include conftest.py with AgentRecorder fixture and make_tracked_tool helper
- Add golden fixture save/load utilities (JSON snapshots in tests/fixtures/)
- Mark real-model tests with @pytest.mark.smoke
- Detect my SDK from imports and adapt accordingly

Schritt 6: Golden Fixtures für Regressionserkennung

Ein Golden Fixture ist ein Snapshot des erwarteten Verhaltens, den du in der Versionskontrolle speicherst — deine gesegnete Baseline. Speichere die Recorder-Ausgabe als JSON:

# conftest.py
import json
from pathlib import Path

FIXTURE_DIR = Path("tests/fixtures")

def save_golden(recorder: AgentRecorder, name: str):
    """Save current tool-call sequence as the blessed baseline."""
    fixture = {
        "sequence": recorder.sequence,
        "calls": [
            {"name": c.name, "arguments": c.arguments}
            for c in recorder.calls
        ],
    }
    (FIXTURE_DIR / f"{name}.json").write_text(json.dumps(fixture, indent=2))

def assert_matches_golden(recorder: AgentRecorder, name: str):
    """Assert current behavior matches the saved baseline."""
    fixture = json.loads((FIXTURE_DIR / f"{name}.json").read_text())
    assert recorder.sequence == fixture["sequence"], (
        f"Behavioral drift detected!\n"
        f"  Golden: {fixture['sequence']}\n"
        f"  Actual: {recorder.sequence}"
    )

Wenn du dein Modell von Claude Sonnet 4 auf das nächste Ding upgradest, lässt du die Suite laufen. Falls der Agent anfängt, check_permissions zu überspringen, weißt du es bevor deine Nutzer es merken.

Schritt 7: CI-Integration

Deine CI-Config teilt sich in zwei Jobs. Block Engineerings Testing-Pyramide bringt das Prinzip auf den Punkt: "We don't run live LLM tests in CI. It's too expensive, too slow, and too flaky."

# .github/workflows/agent-tests.yml
name: Agent Tests
on: [pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/agents/ -m "not smoke" --tb=short
# .github/workflows/agent-smoke.yml
name: Agent Smoke Tests (Nightly)
on:
  schedule:
    - cron: '0 6 * * *'  # 2am ET
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/agents/ -m smoke --tb=long

Markiere Real-Model-Tests mit @pytest.mark.smoke. Sie laufen nachts, alarmieren bei Drift und blockieren nie einen PR.

/faion
Generate GitHub Actions CI config for agent behavioral tests.
Requirements:
- Two workflow files: agent-tests.yml (on PR) and agent-smoke.yml (nightly cron at 2am ET)
- PR workflow runs pytest with -m "not smoke", fails fast
- Nightly workflow runs pytest with -m smoke, uses secrets for API keys
- Add a third workflow: agent-golden-update.yml (manual trigger) that runs tests with --update-golden flag and opens a PR with fixture diffs
- Include pip caching and Python 3.12 setup

Stolperfallen

1. Anthropic sagt, mach das nicht. Ihr Engineering-Blog (9. Januar 2026) warnt explizit: "Checking that agents followed very specific steps like a sequence of tool calls results in overly brittle tests." Sie haben recht — bei explorativen Multi-Step-Flows, wo mehrere valide Pfade existieren. Sie liegen falsch bei kritischen Business-Pfaden, wo ein übersprungener Schritt eine Sicherheitslücke bedeutet. Nutze Sequenz-Assertions für Verträge (Berechtigungsprüfungen, Datenvalidierung), Outcome-Assertions für alles andere.

2. Golden Fixtures veralten. Du upgradest dein Modell und jede Fixture bricht — nicht weil der Agent falsch liegt, sondern weil er einen legitimerweise besseren Pfad gefunden hat. Lösung: Diffs reviewen wie Code-Reviews. Wenn die neue Sequenz valide ist, Fixture aktualisieren. Wenn nicht, hast du gerade eine Regression erwischt.

3. Mock-Tests belügen dich. Ein FakeModel, das immer check_permissions aufruft, sagt dir, dass deine Routing-Logik funktioniert. Es sagt dir nichts darüber, ob Claude oder GPT das Tool tatsächlich aufrufen werden. Mock-Tests decken grob 70% der testbaren Oberfläche ab — Routing, Argument-Parsing, Fehlerbehandlung. Die restlichen 30% brauchen Real-Model-Smoke-Tests im Nightly-Schedule.

4. Nicht-Determinismus ist strukturell, kein Bug. Selbst bei gleichem Input kann ein Modell Tools in unterschiedlicher Reihenfolge aufrufen. Für unkritische Pfade nutze AgentEvals mit unordered Trajectory Mode — es prüft das Set der aufgerufenen Tools, nicht die Sequenz. Strikte Reihenfolge nur für Pfade, wo die Reihenfolge IS der Vertrag ist.

5. Teste Multi-Agent-Handoffs separat. Wenn dein Agent an Sub-Agenten delegiert, ist jeder Handoff eine Grenze. Zeichne den Delegierungsaufruf als Tool-Call auf (delegate_to_research_agent), dann teste die Tool-Sequenz des Sub-Agenten unabhängig. Agent-Grenzen in einer einzigen Assertion zu kreuzen macht alles fragil und nichts debuggbar.

Was du jetzt tun kannst

Du hast ein tests/agents/-Verzeichnis mit Behavioral Tests, Golden Fixtures in der Versionskontrolle und einen CI-Split, der schnelle Mocks bei jedem PR laufen lässt und Real-Model-Smoke-Tests nachts. Dein Agent shippt mit dem gleichen Vertrauen wie der Rest deiner Codebase — nicht weil er deterministisch geworden ist, sondern weil du aufgehört hast, das Falsche zu testen. Der Text ändert sich bei jedem Durchlauf. Die Aktionen sollten es nicht.