Back to "LLMApi: API-Kontexten: Konsistenz über Mock-API-Aufrufe erhalten"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI AI-Article API ASP.NET Core LLM LLMApi mockllmapi Nuget

LLMApi: API-Kontexten: Konsistenz über Mock-API-Aufrufe erhalten

Wednesday, 05 November 2025

HINWEIS: TSein Artikel wird in erster Linie als Teil meines Nuget-Pakets als Release-Dokumentation erzeugt.

Es ist ziemlich interessant, also habe ich es hier, aber wenn das ein Problem für Sie ist, ignorieren Sie es bitte.

Einleitung

**Beim Aufbau und Testen von Anwendungen ist eine der größten Herausforderungen mit traditionellen Mock-APIs ihre staatenlose Natur.**Jede Anfrage liefert völlig zufällige Daten ohne Beziehung zu früheren Anrufen.

Wenn Sie einen Benutzer mit der ID 123 holen, dann holen Sie ihre Bestellungen, gibt es keine Garantie, dass die Bestellung auf dieselbe Benutzer-ID verweist. API Kontexte

Lösen Sie dieses Problem, indem Sie Ihrer Mock API einen Speicher geben.NuGetNuGet

Sie können die

GitHub hier

sequenceDiagram
    participant Client
    participant MockAPI
    participant LLM

    Client->>MockAPI: GET /users/1
    MockAPI->>LLM: Generate user data
    LLM-->>MockAPI: {"id": 42, "name": "Alice"}
    MockAPI-->>Client: User data

    Client->>MockAPI: GET /orders?userId=42
    MockAPI->>LLM: Generate order data
    LLM-->>MockAPI: {"userId": 99, ...}
    MockAPI-->>Client: Order data (userId mismatch!)

für das Projekt, alle Public Domain etc...

Das Problem: Staatenloses Chaos

Traditionelle Mock-APIs erzeugen für jede Anforderung unabhängig Daten:

sequenceDiagram
    participant Client
    participant MockAPI
    participant Context as Context Manager
    participant LLM

    Client->>MockAPI: GET /users/1?context=session-1
    MockAPI->>Context: Get history for "session-1"
    Context-->>MockAPI: (empty - first call)
    MockAPI->>LLM: Generate user (no context)
    LLM-->>MockAPI: {"id": 42, "name": "Alice"}
    MockAPI->>Context: Store: GET /users/1 → {"id": 42, ...}
    MockAPI-->>Client: User data

    Client->>MockAPI: GET /orders?context=session-1
    MockAPI->>Context: Get history for "session-1"
    Context-->>MockAPI: Previous call: user with id=42, name=Alice
    MockAPI->>LLM: Generate order (with context history)
    LLM-->>MockAPI: {"userId": 42, "customerName": "Alice", ...}
    MockAPI->>Context: Store: GET /orders → {"userId": 42, ...}
    MockAPI-->>Client: Order data (consistent!)

Hast du das Problem bemerkt?

Der Benutzer hatte ID 42, aber die Bestellung kam mit userId 99 zurück.

Es gibt keine Konsistenz zwischen verwandten Anrufen.

Die Lösung: Kontextiges Gedächtnis

graph TD
    A[HTTP Request] --> B[ContextExtractor]
    B --> C{Context Name?}
    C -->|Yes| D[OpenApiContextManager]
    C -->|No| E[Generate without context]
    D --> F[Retrieve Context History]
    F --> G[PromptBuilder]
    E --> G
    G --> H[LLM]
    H --> I[Response]
    I --> J{Context Name?}
    J -->|Yes| K[Store in Context]
    J -->|No| L[Return Response]
    K --> L

**Mit API Contexts unterhält die mock API einen gemeinsamen Kontext über verwandte Anfragen hinweg:**Jetzt sieht der LLM den vorherigen Benutzeraufruf und generiert Aufträge, die auf dieselbe Benutzer-ID und denselben Namen verweisen. **Die Daten bilden eine zusammenhängende Geschichte.**Wie es funktioniert ArchitekturDas Kontextsystem besteht aus drei Hauptkomponenten:

1.

KontextExtraktorConcurrentDictionary:

public class OpenApiContextManager
{
    private readonly ConcurrentDictionary<string, ApiContext> _contexts;
    private const int MaxRecentCalls = 15;
    private const int SummarizeThreshold = 20;

    public void AddToContext(
        string contextName,
        string method,
        string path,
        string? requestBody,
        string responseBody)
    {
        var context = _contexts.GetOrAdd(contextName, _ => new ApiContext
        {
            Name = contextName,
            CreatedAt = DateTimeOffset.UtcNow,
            RecentCalls = new List<RequestSummary>(),
            SharedData = new Dictionary<string, string>(),
            TotalCalls = 0
        });

        context.RecentCalls.Add(new RequestSummary
        {
            Timestamp = DateTimeOffset.UtcNow,
            Method = method,
            Path = path,
            RequestBody = requestBody,
            ResponseBody = responseBody
        });

        ExtractSharedData(context, responseBody);

        if (context.RecentCalls.Count > MaxRecentCalls)
        {
            SummarizeOldCalls(context);
        }
    }
}

- Extrahiert den Kontextnamen aus der Anfrage

  1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
graph LR
    A[20+ Calls] --> B[Keep 15 Most Recent]
    A --> C[Summarize Older Calls]
    B --> D[Full Request/Response]
    C --> E[Summary: 'GET /users - called 5 times']
    D --> F[Included in LLM Prompt]
    E --> F
private void SummarizeOldCalls(ApiContext context)
{
    var toSummarize = context.RecentCalls
        .Take(context.RecentCalls.Count - MaxRecentCalls)
        .ToList();

    var summary = new StringBuilder();
    summary.AppendLine($"Earlier calls ({toSummarize.Count}):");

    var groupedByPath = toSummarize
        .GroupBy(c => $"{c.Method} {c.Path.Split('?')[0]}");

    foreach (var group in groupedByPath)
    {
        summary.AppendLine($"  {group.Key} - called {group.Count()} time(s)");
    }

    context.ContextSummary = summary.ToString();
    context.RecentCalls.RemoveRange(0, toSummarize.Count);
}

OpenApiContextManager

  • Verwaltet Kontextspeicher und Abruf
private void ExtractSharedData(ApiContext context, string responseBody)
{
    using var doc = JsonDocument.Parse(responseBody);
    var root = doc.RootElement;

    if (root.ValueKind == JsonValueKind.Array && root.GetArrayLength() > 0)
    {
        var firstItem = root[0];
        ExtractValueIfExists(context, firstItem, "id", "lastId");
        ExtractValueIfExists(context, firstItem, "userId", "lastUserId");
        ExtractValueIfExists(context, firstItem, "name", "lastName");
        ExtractValueIfExists(context, firstItem, "email", "lastEmail");
    }
    else if (root.ValueKind == JsonValueKind.Object)
    {
        ExtractValueIfExists(context, root, "id", "lastId");
        ExtractValueIfExists(context, root, "userId", "lastUserId");
        ExtractValueIfExists(context, root, "name", "lastName");
        // ... more common patterns
    }
}
  1. Der Präsident. — Das Wort hat die Fraktion der Europäischen Volkspartei (Christlich-Demokratische Fraktion).

PromptBuilder

- Beinhaltet Kontextverlauf in LLM-Prompten

Kontextspeicherung

Kontexte werden in-Memory mit einem Thread-Safe gespeichertAutomatische Zusammenfassung

GET /api/mock/users?context=my-session
GET /api/mock/users?api-context=my-session

Um zu verhindern, dass der Kontext unbestimmt wächst und LLM-Token-Grenzen überschreitet, fasst das System alte Anrufe automatisch zusammen, wenn die Anzahl 15 übersteigt:

GET /api/mock/users
X-Api-Context: my-session

Gemeinsame Datenextraktion

POST /api/mock/orders
Content-Type: application/json

{
  "context": "my-session",
  "shape": {"orderId": 0, "userId": 0}
}

Der Kontextmanager extrahiert automatisch gemeinsame Identifikatoren aus Antworten, um sie leicht zugänglich zu machen:

Dies ermöglicht es dem System, die neueste Benutzer-ID, Bestell-ID, etc. zu verfolgen, so dass sie in der Kontext-Historie zur Verfügung.Kontexte verwendenDrei Möglichkeiten, Kontext zu spezifizieren

Sie können den Kontextnamen auf drei verschiedene Weisen übergeben, mit dieser Rangfolge:

GET /api/mock/users/123?context=session-1

1.

GET /api/mock/stream/stock-prices?context=trading-session
Accept: text/event-stream

Abfrageparameter

POST /graphql?context=my-app
Content-Type: application/json

{
  "query": "{ users { id name } }"
}

(höchste Priorität)

{
  "mostlylucid.mockllmapi": {
    "HubContexts": [
      {
        "Name": "stock-ticker",
        "Description": "Real-time stock prices",
        "ApiContextName": "stocks-session",
        "Shape": "{\"symbol\":\"string\",\"price\":0}"
      }
    ]
  }
}

2. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

HTTP-Header

  1. Der Präsident. — Das Wort hat die Fraktion der Europäischen Volkspartei (Christlich-Demokratische Fraktion).
### 1. Create user
POST /api/mock/users?context=checkout-flow
{
  "shape": {
    "userId": 0,
    "name": "string",
    "email": "string",
    "address": {"street": "string", "city": "string"}
  }
}

### Response: {"userId": 42, "name": "Alice", ...}

### 2. Create cart (will reference same user)
POST /api/mock/cart?context=checkout-flow
{
  "shape": {
    "cartId": 0,
    "userId": 0,
    "items": [{"productId": 0, "quantity": 0}]
  }
}

### Response: {"cartId": 123, "userId": 42, ...}

### 3. Create order (consistent user and cart)
POST /api/mock/orders?context=checkout-flow
{
  "shape": {
    "orderId": 0,
    "userId": 0,
    "cartId": 0,
    "total": 0
  }
}

### Response: {"orderId": 789, "userId": 42, "cartId": 123, ...}

Antragsstelle

Unterstützte Endpoint-Typen

### First call - establishes baseline
GET /api/mock/stocks?context=market-data
    &shape={"symbol":"string","price":0,"volume":0}

### Response: {"symbol": "ACME", "price": 145.50, "volume": 10000}

### Second call - price changes realistically
GET /api/mock/stocks?context=market-data
    &shape={"symbol":"string","price":0,"volume":0}

### Response: {"symbol": "ACME", "price": 146.20, "volume": 12000}
### Notice: Same symbol, price increased by $0.70 (realistic)

### Third call - continues the trend
GET /api/mock/stocks?context=market-data
    &shape={"symbol":"string","price":0,"volume":0}

### Response: {"symbol": "ACME", "price": 145.80, "volume": 11500}
### Notice: Price fluctuates but stays in realistic range

Kontexte arbeiten über

alle

Endpunkttypen:

### Start game
POST /api/mock/game/start?context=game-session-123
{
  "shape": {
    "playerId": 0,
    "level": 0,
    "health": 0,
    "score": 0,
    "inventory": []
  }
}

### Response: {"playerId": 42, "level": 1, "health": 100, "score": 0}

### Complete quest
POST /api/mock/game/quest?context=game-session-123
{
  "shape": {
    "playerId": 0,
    "level": 0,
    "score": 0,
    "reward": {"item": "string", "value": 0}
  }
}

### Response: {"playerId": 42, "level": 2, "score": 500,
###           "reward": {"item": "Sword", "value": 100}}
### Notice: Same player, level increased, score increased

### Get stats
GET /api/mock/game/player?context=game-session-123
    &shape={"playerId":0,"level":0,"health":0,"score":0}

### Response: {"playerId": 42, "level": 2, "health": 100, "score": 500}
### Notice: Consistent with quest completion

REST-APIs

Streaming-APIs

GET /api/openapi/contexts

### Response:
{
  "contexts": [
    {
      "name": "session-1",
      "totalCalls": 5,
      "recentCallCount": 5,
      "sharedDataCount": 3,
      "createdAt": "2025-01-15T10:00:00Z",
      "lastUsedAt": "2025-01-15T10:05:00Z",
      "hasSummary": false
    }
  ],
  "count": 1
}

SchaubildQL

GET /api/openapi/contexts/session-1

### Response shows full context including:
### - All recent calls with timestamps
### - Extracted shared data (IDs, names, emails)
### - Summary of older calls (if any)

SignalR (über Konfiguration)

DELETE /api/openapi/contexts/session-1

Fälle der realen Verwendung

DELETE /api/openapi/contexts

Anwendungsfall 1: E-Commerce Flow

Simulieren Sie ein komplettes Einkaufserlebnis mit konsistenten Benutzer- und Bestelldaten:

Anwendungsfall 2: Simulation des Aktienkurses

public async Task<string> HandleRequestAsync(
    string method,
    string fullPathWithQuery,
    string? body,
    HttpRequest request,
    HttpContext context,
    CancellationToken cancellationToken = default)
{
    // 1. Extract context name from request
    var contextName = _contextExtractor.ExtractContextName(request, body);

    // 2. Get context history if context specified
    var contextHistory = !string.IsNullOrWhiteSpace(contextName)
        ? _contextManager.GetContextForPrompt(contextName)
        : null;

    // 3. Build prompt with context history
    var prompt = _promptBuilder.BuildPrompt(
        method, fullPathWithQuery, body, shapeInfo,
        streaming: false, contextHistory: contextHistory);

    // 4. Get response from LLM
    var response = await _llmClient.GetCompletionAsync(prompt, cancellationToken);

    // 5. Store in context if context name provided
    if (!string.IsNullOrWhiteSpace(contextName))
    {
        _contextManager.AddToContext(
            contextName, method, fullPathWithQuery, body, response);
    }

    return response;
}

Realistische Kursbewegungen anstelle von Zufallswerten generieren:

Ohne Kontext würde jeder Anruf ein völlig zufälliges Symbol und Preis zurückgeben.

TASK: Generate a varied mock API response.
RULES: Output ONLY valid JSON. No markdown, no comments.

API Context: session-1
Total calls in session: 3

Shared data to maintain consistency:
  lastId: 42
  lastName: Alice
  lastEmail: [email protected]

Recent API calls:
  [10:00:05] GET /users/42
    Response: {"id": 42, "name": "Alice", "email": "[email protected]"}
  [10:00:12] GET /orders?userId=42
    Response: {"orderId": 123, "userId": 42, "items": [...]}

Generate a response that maintains consistency with the above context.

Method: POST
Path: /shipping/123
Body: {"orderId": 123}

Im Kontext behält die LLM den gleichen Bestand und passt die Preise realistisch an.

Anwendungsfall 3: Spielzustand-Progression

Verfolgen Sie den Fortschritt des Spielers durch Spielsitzungen:

 Bad:  ?context=test1
 Good: ?context=user-checkout-flow-jan15

API für Kontextverwaltung

Alle Kontexte auflisten

### After completing your test scenario
DELETE /api/openapi/contexts/user-checkout-flow-jan15

Kontextdetails abrufen

Einen spezifischen Kontext löschen

GET /api/mock/users?context=demo-session
GET /api/mock/orders?context=demo-session
GET /api/mock/shipping?context=demo-session

Alle Kontexte löschen

Einzelheiten zur Umsetzung

GET /api/openapi/contexts/demo-session

Integration in Request Handler

Jeder Request-Handler (REST, Streaming, GraphQL, SignalR) folgt dem gleichen Muster:

Kontext in LLM-Prompts

### Load spec with context
POST /api/openapi/specs
{
  "name": "petstore",
  "source": "https://petstore3.swagger.io/api/v3/openapi.json",
  "basePath": "/petstore",
  "contextName": "petstore-session"
}

### All petstore endpoints will share the same context

Wenn ein Kontext existiert, ist seine Geschichte in der LLM-Prompt enthalten:

Das LLM sieht alle vorherigen Anrufe und generiert Antworten, die auf dieselben IDs, Namen und andere Daten verweisen, wobei die Konsistenz gewahrt bleibt.

Best Practices

  • Deskriptive Kontextnamen verwenden
    1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Kontexte löschen, wenn erledigtKontexte bestehen im Speicher, bis explizit gelöscht oder der Server neu gestartet wird:

3. Der Präsident. — Das Wort hat die Fraktion der Europäischen Volkspartei (Christlich-Demokratische Fraktion).

OpenApiContextManagerKontexte über verwandte Endpunkte teilenConcurrentDictionaryVerwenden Sie den gleichen Kontextnamen für alle verwandten Anrufe:

4. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Kontextgröße überwachen

Überprüfen Sie die Kontextdetails, um zu sehen, wie viele Anrufe gespeichert sind:

  1. **Wenn Sie viele Anrufe (>100) haben, erwägen Sie das Clearing und starten Sie neu, um schnelle Längenprobleme zu vermeiden.**5. Verordnung (EWG) Nr. 1408/71 des Rates vom 14. Juni 1971 zur Anwendung der Systeme der sozialen Sicherheit auf Arbeitnehmer und Selbständige (ABl.
  2. Kombinieren Sie mit OpenAPI-SpezifikationenVerwenden Sie für maximalen Realismus Kontexte mit OpenAPI-Spezifikationen:
  3. LeistungserwägungenSpeichernutzung
  4. **Jeder Kontext speichert:**Bis zu 15 aktuelle Anrufe (vollständige Anfrage/Antwort)

Zusammenfassung älterer Anrufe (komprimiert)

Geteilte Daten extrahiert (kleines Wörterbuch)

Typischer Speicher pro Kontext

: ~50-200 KB je nach Ansprechgrößen

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.