Hinweis: Dieses Video und dieser Podcast wurden mithilfe von KI erstellt auf Grundlage der ursprünglichen Inhalten sowie den technischen Erkenntnissen des Autors des Blogartikels.
Das betrifft vor allem zwei Bereiche. Erstens Anwendungen, die in kritischen Umgebungen reproduzierbar dieselben Ergebnisse liefern müssen. Zweitens Testumgebungen, in denen zufällige Antworten die Stabilität und Aussagekraft der Tests untergraben. Und genau darum geht es hier: Wie umgehen wir diese Nichtdeterministik, ohne unsere Architektur zu verbiegen?
Bevor wir zur Lösung kommen, lohnt sich ein Blick auf das eigentliche Problem. LLM-Schnittstellen direkt zu testen, führt zu instabilen Tests, weil das Modell bei jedem Lauf leicht unterschiedliche Antworten erzeugt. Hinzu kommt die Latenz der Kommunikation mit dem externen Modell. Und falls Sie ein kommerzielles Modell von OpenAI, Anthropic, Google oder einem anderen Anbieter nutzen, entstehen zudem Kosten für jede Anfrage.
Um das zu vermeiden, können Sie das Paket mocken, das die Kommunikation mit dem Modell übernimmt. Bei klassischen Unit-Tests ist das gängige Praxis: Nutzen Sie beispielsweise das openai-Paket und ersetzen Sie es durch einen Mock, der die Anfragen beantwortet. Bei Integrationstests hat das jedoch einen Nachteil: Sie entfernen eine potenzielle Fehlerquelle aus dem System und testen nicht, ob das Paket korrekt verwendet wird. Gerade bei Updates ist das ein relevanter Punkt. Eine bessere Lösung ist es daher, direkt an der HTTP-Kommunikation zum Modell anzusetzen.
Die meisten LLM-APIs kommunizieren über HTTP. Das ist von Vorteil, denn für HTTP-Mocking existieren etablierte Lösungen. Eine der populärsten ist msw. Die Idee ist einfach: msw fängt die HTTP-Anfrage an das Modell ab, bevor sie das System verlässt, und liefert eine vorbereitete Antwort zurück. Das Ergebnis ist absolute Reproduzierbarkeit ohne Dynamik oder Überraschungen.
Allerdings gibt es eine Einschränkung: msw fängt nur Anfragen mit fetch und XMLHttpRequest ab, nicht jedoch den nativen HTTP-Client von Node, den das OpenAI-Paket verwendet. Um msw dennoch nutzen zu können, übergeben Sie dem OpenAI-Client bei der Initialisierung eine fetch-Implementierung, sodass alle Anfragen über die fetch-Funktion laufen. In Listing 1 sehen Sie ein Beispiel dafür.
Praktische Fullstack-Architektur: von der API bis zur BenutzeroberflächeErlebe das neue Bootcamp vom 15. – 16. Oktober 2026 |
Listing 1: LLM-Kommunikation mit msw testen
import { afterAll, afterEach, beforeAll, describe, expect, it } from ‘vitest’;
import { http, HttpResponse } from ‘msw’;
import { setupServer } from ‘msw/node’;
import { ask } from ‘./chat.js’;
const handlers = [
http.post(‘https://api.openai.com/v1/responses’, () => {
return HttpResponse.json({
output_text: ‘Hamburg’,
});
}),
];
const server = setupServer(…handlers);
describe(‘ask function’, () => {
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
it(‘ask returns the correct answer’, async () => {
const question = ‘What is the capital of Germany?’;
const expectedAnswer = ‘Hamburg’;
const answer = await ask(question);
expect(answer).toBe(expectedAnswer);
});
});
Allgemeine HTTP-Mocking-Bibliotheken wie msw bieten eine solide Grundlage für den Umgang mit LLMs, stoßen bei umfangreicheren KI-Anwendungen jedoch schnell an ihre Grenzen. Die Kommunikation mit LLMs unterscheidet sich strukturell von der klassischen Client-Server-Interaktion. Das liegt unter anderem an Features wie Streaming, Tool-Calling, strukturierten Antworten, Agent-Kommunikation oder MCP. All das erzeugt Anforderungen, die sich zwar mit den klassischen Lösungen umsetzen lassen, bei denen es aber schnell unkomfortabel wird.
Genau hier setzt aimock an. Das Open-Source-Projekt aus dem CopilotKit-Ökosystem stellt spezialisierte Mocking-Werkzeuge bereit, die explizit für KI-Workflows entwickelt wurden. Statt nur HTTP-Requests abzufangen, versteht aimock den Hintergrund typischer LLM-Interaktionen und kann sie realistisch simulieren. Das Paket ist modular aufgebaut und deckt zahlreiche Features ab:
- LLM-Mocks: für verschiedene Anbieter wie OpenAI, Anthropic, Google, Microsoft oder Ollama inklusive Chat-, Completion- und Embedding-APIs
- Realistisches Streaming-Mocking: simuliert tokenweise Antworten
- Tool-Calling-Mocks: bilden Funktionsaufrufe, Parameter und Rückgabewerte vollständig nach
- Proxy- und Recording-Modus: zeichnet echte LLM-Antworten einmal auf und gibt sie dann deterministisch wieder
- Mock-Server für Entwicklung und Offlinearbeit: KI-Features werden lokal verfügbar, auch wenn kein Modell verfügbar ist
Damit schließt aimock die Lücke zwischen klassischen Testwerkzeugen und den Anforderungen moderner KI-Systeme. Es ermöglicht reproduzierbare Tests, ohne die Architektur der Anwendung zu verändern oder die Kommunikation mit dem LLM-Client künstlich zu abstrahieren. Gleichzeitig bleibt die Testumgebung realistisch genug, um Integrationsfehler frühzeitig zu erkennen.
Set-up und erster Test mit aimock
Bevor wir tiefer in Features wie Streaming, Tool-Calling oder Recording einsteigen, werfen wir einen Blick auf das grundlegende Set-up. Denn einer der größten Vorteile von aimock ist, dass nur wenig Infrastruktur benötigt wird, um einen ersten Test lauffähig zu machen. Die Vorbereitungsmaßnahmen erinnern etwas an msw, mit dem Unterschied, dass aimock speziell auf die Arbeit mit KI-Plattformen ausgerichtet ist.
Als Erstes wird das Paket installiert mit dem Kommando npm install -D @copilotkit/aimock. Der Einstiegspunkt in die Arbeit mit aimock ist die Klasse LLMock, die einen lokalen Mock-Server startet und ein OpenAI-kompatibles API bereitstellt. In einem Test mit Vitest sieht das Grundgerüst wie in Listing 2 aus.
Listing 2: Test mit vitest und aimock
import { LLMock } from ‘@copilotkit/aimock’;
import { describe, it, expect, beforeAll, afterAll, vi } from ‘vitest’;
import OpenAI from ‘openai’;
describe(‘ask function’, () => {
let mock;
let ask;
beforeAll(async () => {
mock = new LLMock();
await mock.start();
vi.stubEnv(‘OpenAIBaseURL’, mock.url);
vi.resetModules();
ask = (await import(‘./chat.js’)).ask;
});
afterAll(async () => {
await mock.stop();
vi.unstubAllEnvs();
});
it(‘tests the ask function with aimock’, async () => {
mock.on(
{ userMessage: ‘What is the capital of France?’ },
{ content: ‘Lyon’ },
);
const result = await ask(‘What is the capital of France?’);
expect(result).toBe(‘Lyon’);
});
});
Beim Start des Tests wird der Mock-Server hochgefahren, seine URL in einer Umgebungsvariablen hinterlegt und anschließend die zu testende Funktion geladen, die mit dem LLM kommuniziert. Das ist wichtig, da der OpenAI-Client eine konfigurierbare baseURL akzeptieren muss, damit die Anfragen nicht an die echten Endpunkte, sondern an den lokalen Mock-Server gesendet werden. Da die URL des Servers jedoch erst nach der Initialisierung zur Verfügung steht, ist ein dynamischer Import erforderlich.
Der eigentliche Kern des Tests ist die Definition der erwarteten Modellantwort. Hier kommt mock.on ins Spiel. Mit dieser Funktion verknüpfen Sie eine erwartete Nutzereingabe mit einer vorher festgelegten Antwort. Sie müssen weder HTTP-Strukturen nachbauen noch JSON-Formate imitieren. Sie beschreiben lediglich die Interaktion in Form eines Anfrage-Antwort-Paars. aimock übernimmt alles Weitere: das Routing, die OpenAI-kompatible Response-Struktur, die Statuscodes und die korrekte Form des Antwortobjekts. Ihre Anwendung glaubt weiterhin, mit einem echten Modell zu sprechen. Tatsächlich kommuniziert sie jedoch mit einem lokalen Server, der die definierte Antwort liefert.
Realistisches Streaming mit aimock
Ein zentrales Merkmal von KI-Plattformen und Sprachmodellen ist ihr Streaming-Verhalten. Anstatt eine vollständige Antwort auf einmal zurückzugeben, liefern Modelle die Antwort Token für Token, ähnlich wie ein Mensch, der einen Satz formuliert, während er ihn spricht. Dieses inkrementelle Ausgeben von Text ist nicht nur ein UX-Feature, sondern ein struktureller Bestandteil der LLM-Interaktion: Es reduziert die Latenz, ermöglicht reaktive Oberflächen und erlaubt es Anwendungen, schon während der Antwort weiterzuarbeiten.
Für Tests stellt genau dieses Verhalten eine Herausforderung dar. Klassische Mocking-Bibliotheken geben Antworten synchron zurück und können die zeitliche Dynamik eines echten Modells nur schwer nachbilden. Aimock geht hier einen Schritt weiter. Das Paket erlaubt das asynchrone Streamen von Tokens, wie es sich etwa mit einer Generatorfunktion simulieren ließe. Zusätzlich kann es die für LLMs typische Zeitverzögerung realistisch nachbilden.
Im Beispiel für einen Test mit vitest und aimock aus Listing 3 streamen Sie die Antwort tokenweise. Zusätzlich definieren Sie ein Streaming-Profil, das die zeitliche Charakteristik der Kommunikation beschreibt:
- ttft (Time to First Token): die Verzögerung, bevor das erste Token gesendet wird
- tps (Tokens per Second): die Frequenz, mit der die weiteren Tokens gesendet werden
- jitter: Varianz, mit der natürliche Schwankungen simuliert werden
Listing 3: Streaming-Test mit aimock
it(‘tests the streamOpenai function with aimock’, async () => {
mock.on(
{ userMessage: ‘What is the capital of France?’ },
{ content: ‘The capital of France is Vienna.’ },
{
streamingProfile: {
ttft: 800,
tps: 50,
jitter: 0.2,
},
},
);
let result = ”;
const stream = await streamOpenai([
{ role: ‘user’, content: ‘What is the capital of France?’ },
]);
for await (const chunk of stream) {
if (chunk.choices[0].delta.content) {
result += chunk.choices[0].delta.content;
}
}
expect(result).toBe(‘The capital of France is Vienna.’);
});
Der Test zeigt zwei Dinge: Sie können die gestreamte Antwort von aimock genauso mit einer asynchronen for-await-of-Schleife konsumieren wie bei einem echten Modell und müssen Ihre Anwendung nicht anpassen. Außerdem können Sie die zeitliche Dynamik steuern. Allerdings ist zu beachten, dass dies zu einer deutlich verlängerten Laufzeit des Tests führt. Dank dieses realistischen Verhaltens können Sie UI-Komponenten, Ladeindikatoren, Tokenanimationen und Race Conditions jedoch realistisch testen.
Streaming wird damit zu einem vollwertigen Bestandteil Ihrer Teststrategie und bleibt nicht länger ein Sonderfall.
Tool-Calling mit aimock
Moderne Sprachmodelle können nicht nur Text erzeugen, sondern auch indirekt Funktionen aufrufen. Mit Hilfe dieses sogenannten Tool-Calling kann das Modell der Hostapplikation signalisieren, dass es ein Werkzeug verwenden möchte. Das kann ein Datenbankzugriff, ein API-Call oder eine Berechnung sein. Damit das funktioniert, registriert die Hostapplikation die verfügbaren Tools beim LLM. Anschließend entscheidet das Modell selbst, wann ein Tool sinnvoll ist, welche Parameter es übergibt und wie das Ergebnis in die Antwort einfließt. So wird aus einem reinen Textgenerator ein steuernder Agent.
Frameworks wie LangChain abstrahieren diesen Prozess und vereinfachen ihn deutlich. Mit der Funktion createAgent() können Sie einen Agenten erzeugen, der Tools erkennt, sie für das Modell aufruft und die Ergebnisse an das Modell zurückspielt. Die eigentliche Logik des Tools bleibt dabei vollständig in der Anwendung. Das Modell übernimmt lediglich die Entscheidung, wann und wie das Tool genutzt werden soll.
Für Tests entsteht dadurch eine neue Herausforderung: Sie müssen nicht nur die Modellantworten mocken, sondern auch mit Tools umgehen. Mit Aimock können Anfragen für Tools in den Test integriert und der Workflow zwischen LLM, Tool und Ergebnis simuliert werden.
Für den Test in Listing 4 gehen wir von einem Agenten aus, der über ein Tool namens get_weather_for_city verfügt, das das Wetter und die Temperatur für eine Stadt zurückgibt. Modell und Tool werden über LangChain miteinander verknüpft. Der Test selbst folgt derselben Struktur wie beim Streaming. Zunächst wird der Mock-Server gestartet, dann die baseURL gesetzt und anschließend der Agent geladen. Der entscheidende Teil ist hier jedoch die Definition der Tool-Interaktion. Diese läuft in zwei Phasen ab:
- Das Modell entscheidet, ein Tool aufzurufen. on() beschreibt die erwartete Eingabe und den daraus resultierenden Tool-Call.
- Das Tool liefert ein Ergebnis zurück. onToolResult() definiert die Antwort des LLMs, die das Resultat des Tools beinhaltet.
Listing 4: aimock mit Tool-Calling
it(‘tests the tool calling agent with aimock’, async () => {
const weatherSpy = vi.spyOn(weatherFns, ‘getWeatherForCity’);
mock.on(
{
userMessage: ‘What is the weather like in Berlin?’,
hasToolResult: false,
},
{
toolCalls: [
{
id: ‘call_123’,
name: ‘get_weather_for_city’,
arguments: { city: ‘Berlin’ },
},
],
},
);
mock.onToolResult(‘call_123’, {
content: ‘The current weather in Berlin is Sunny 10°C.’,
});
const result = await agent.invoke({
messages: [
{ role: ‘user’, content: ‘What is the weather like in Berlin?’ },
],
});
const finalContent = result.messages[result.messages.length – 1].content;
expect(finalContent).toBe(‘The current weather in Berlin is Sunny 10°C.’);
expect(weatherSpy).toHaveBeenCalledTimes(1);
expect(weatherSpy).toHaveBeenCalledWith({ city: ‘Berlin’ });
});
Eine weitere Besonderheit ist, dass der Agent die echte Toolfunktion weiterhin ausführt. Der Spy im Test ermöglicht, zu überprüfen, ob der Agent korrekt mit dem Tool interagiert.
Record und Replay mit dem Proxy- und Recording-Modus
Neben den klassischen Mocks, die Sie direkt in Ihre Tests integrieren können, bietet Ihnen aimock einen Record- und Replay-Modus, bei dem ein eigenständiger Mock-Server als Proxy vor dem echten LLM läuft. Ihre Anwendung spricht weiterhin das gleiche API an, nur dass dieses von aimock bereitgestellt wird. Je nach Modus werden die Anfragen entweder an das echte Modell durchgereicht und aufgezeichnet oder aus bestehenden Aufzeichnungen beantwortet.
Der Grund für einen solchen Modus ist, dass sich nicht jede LLM-Interaktion sinnvoll von Hand mocken lässt. Gerade bei umfangreicheren Chats, Agenten, Tool-Chains oder längeren Kontextverläufen möchte man lieber echte Modellantworten verwenden, ohne jedes Mal Kosten, Latenz und Nichtdeterminismus in Kauf nehmen zu müssen. Der Record- und Replay-Modus eignet sich besonders für
- Integrations- und End-to-End-Tests
- Regressionstests
- lokale Entwicklung ohne Internet oder API-Budget
- CI/CD-Pipelines
Der Record- und Replay-Modus funktioniert komplett über die Kommandozeile und teilt sich in zwei Schritte auf.
Schritt 1: Start des Recording-Servers
Im Aufnahme-Modus fungiert aimock als Proxy: Alle eingehenden Anfragen werden an das echte Modell weitergeleitet, die Antworten werden lokal gespeichert. Mit dem Kommando
$ npx -p @copilotkit/aimock llmock -f ./fixtures \
–record \
–provider-openai https://api.openai.com \
starten Sie den Mock-Server mit dem OpenAI-Provider. Weitere Provider sind beispielsweise Gemini oder Ollama. In Ihrer Anwendung müssen Sie lediglich den baseURL so anpassen, dass sie auf den aimock-Server zeigt. Die entsprechende Portnummer erhalten Sie als Ausgabe beim Start des aimock-Servers. Im Anschluss können Sie in Ihrer Anwendung mit dem Modell wie gewohnt interagieren. Die Kommunikation mit dem Modell wird dann als Datei im fixtures-Verzeichnis gespeichert.
Schritt 2: Start des Replay-Servers
Nachdem Sie eine Session aufgezeichnet haben, können Sie die Inhalte nutzen und sie mit dem Replay-Modus zur Verfügung stellen. In diesem Fall verlässt keine Anfrage Ihre Infrastruktur in Richtung des LLM-Providers. Stattdessen beantwortet der aimock-Server die Anfrage direkt. Stellen Sie auch hier sicher, dass der baseURL Ihrer Anwendung auf den aimock-Server zeigt. Mit dem folgenden Kommando starten Sie den Replay-Modus:
npx -p @copilotkit/aimock llmock -f ./fixtures
Bleib informiert
Brancheninsights im Newsletter erhalten:
Der aimock-Server überprüft bei jeder Anfrage, ob eine passende Fixture existiert, und antwortet mit der aufgezeichneten Antwort. Da die Fixtures als JSON-Daten vorliegen, können Sie diese beliebig modifizieren und erweitern.
Was lernen wir daraus?
Wenn wir KI-Funktionen in unsere Anwendungen integrieren, bewegen wir uns immer auf dünnem Eis, denn Sprachmodelle arbeiten probabilistisch und dynamisch. Genau das macht sie so leistungsfähig, aber stabile Tests und reproduzierbare Abläufe nahezu unmöglich. Aimock löst dieses Problem auf elegante Weise, ohne dass Sie die Architektur verbiegen oder unnötige Abstraktionsschichten einfügen müssen.
Statt gegen den Charakter von LLMs anzukämpfen, kapselt aimock sie so, dass Sie weiterhin realistisch entwickeln können, während gleichzeitig die Stabilität erhalten bleibt, die Sie für die Absicherung benötigen. Dabei müssen Sie sich dank Record und Replay Ihre Testdaten noch nicht einmal selbst ausdenken, sondern können auf echte Daten zurückgreifen.
Mit solchen Tools werden einst schwer testbare KI-Features zu ganz normalen Bestandteilen Ihrer Codebasis. Es lohnt sich also, aimock auszuprobieren. Außerdem bietet das aimock-Paket über die hier vorgestellten Features hinaus noch vieles mehr.
Author
🔍 Frequently Asked Questions (FAQ)
1. Was ist aimock?
aimock ist ein Open-Source-Tool zum Testen von KI-Anwendungen. Es simuliert die Kommunikation mit LLMs wie OpenAI, Anthropic oder Google und ermöglicht reproduzierbare Tests.
2. Warum sind Tests mit LLMs oft schwierig?
Sprachmodelle arbeiten probabilistisch und können bei identischen Eingaben unterschiedliche Antworten erzeugen. Das erschwert stabile und reproduzierbare Testabläufe.
3. Welche Nachteile haben direkte Tests gegen LLM-APIs?
Direkte Tests können instabile Ergebnisse liefern und verursachen zusätzliche Latenz sowie Kosten durch API-Aufrufe. Zudem hängen sie von der Verfügbarkeit externer Dienste ab.
4. Worin unterscheidet sich aimock von klassischen HTTP-Mocking-Lösungen wie msw?
Während msw HTTP-Anfragen abfängt, bietet aimock zusätzliche Funktionen für typische LLM-Workflows. Dazu gehören unter anderem Streaming, Tool Calling sowie Record- und Replay-Funktionen.
5. Wie funktioniert das Testen mit aimock?
aimock startet einen lokalen Mock-Server, an den die Anwendung ihre Anfragen sendet. Erwartete Eingaben werden mit definierten Antworten verknüpft, sodass die Anwendung mit einer realistischen LLM-Schnittstelle kommuniziert.
6. Kann aimock Streaming und Tool Calling simulieren?
Ja. Das Tool kann tokenweises Streaming mit konfigurierbaren Zeitprofilen sowie Tool-Calling-Workflows inklusive Tool-Ergebnissen realitätsnah nachbilden.
7. Wofür eignet sich der Record- und Replay-Modus?
Der Modus zeichnet echte LLM-Antworten auf und stellt sie später deterministisch wieder bereit. Das ist besonders für Integrations-, End-to-End- und Regressionstests sowie für die Offline-Entwicklung geeignet.
8. Welchen Vorteil bietet aimock für die Entwicklung von KI-Anwendungen?
aimock ermöglicht reproduzierbare Tests, ohne die Architektur der Anwendung grundlegend zu verändern. Gleichzeitig bleibt die Kommunikation mit dem LLM realitätsnah, sodass Integrationsfehler frühzeitig erkannt werden.





