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
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.
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
- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
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
}
}
- 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
- 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
-
- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
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:
- **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.
- Kombinieren Sie mit OpenAPI-SpezifikationenVerwenden Sie für maximalen Realismus Kontexte mit OpenAPI-Spezifikationen:
- LeistungserwägungenSpeichernutzung
- **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