Einen sicheren Kontext-Compiler für KI-Agenten in .NET 10 bauen
Modellkontext in .NET 10 kompilieren: mit Vertrauenslabels, Aktualitätsprüfungen, Schwärzung, Injection-Quarantäne, deterministischem Token-Budget und einem testbaren SHA-256-Manifest.
Wenn sich ein Agent in Produktion danebenbenimmt, wird meist das Modell beschuldigt. Oft ist der Schaden aber schon früher entstanden, als die Anwendung Anweisungen, Suchtreffer, Gedächtnis und nicht vertrauenswürdigen Webtext zu einem einzigen privilegierten String zusammengeklebt hat. Dieser String ist alles, was das Modell für die nächste Inferenz weiß. Wenn eine veraltete Richtlinie die aktuelle schlägt, eine abgerufene Seite Anweisungen enthält, Kundendaten eine Grenze überschreiten oder wertloser Verlauf das Kontextfenster frisst, repariert ein cleverer Prompt ganz oben die Architektur nicht.
Die Lösung: Diesen Zusammenbauschritt als echte Software behandeln. Jede Eingabe bekommt eine Identität und eine Vertrauensstufe. Richtlinien entscheiden, was aktuell genug ist und was verborgen werden muss. Ein festes Budget erzwingt explizite Entscheidungen. Die Ausgabe ist reproduzierbar und kommt mit Belegen dafür, was aufgenommen, gekürzt, geschwärzt, unter Quarantäne gestellt oder verworfen wurde. Im Folgenden wird diese Idee zu einem kleinen .NET-10-Dienst.
Warum einfache Verkettung nicht reicht
Die Engineering-Beiträge von Anthropic beschreiben Context Engineering als Kuratieren des gesamten Kontextzustands für jede Inferenz, was weit über das Feilen an der Formulierung eines einzelnen Prompts hinausgeht. Systemanweisungen, Tools, abgerufene Daten, Gedächtnis und Nachrichtenverlauf konkurrieren um eine einzige begrenzte Ressource, und größere Fenster beseitigen die Probleme mit Relevanz und Verschmutzung nicht.
Sicherheit verschärft das Auswahlproblem. Der Beitrag von OpenAI vom März 2026 über die Abwehr von Prompt Injection weist darauf hin, dass fähige Angriffe zunehmend wie Social Engineering aussehen, ein Eingabeklassifikator allein also keine vollständige Verteidigung ist. Er empfiehlt außerdem, in Quellen und Senken zu denken: wo nicht vertrauenswürdiger Einfluss hereinkommt und welche gefährliche Fähigkeit er erreichen kann. Die PlugMem-Arbeit von Microsoft Research ergänzt einen Zuverlässigkeitsaspekt und zeigt, dass rohes Abrufen aus dem Gedächtnis einen Agenten mit langem, wenig wertvollem Kontext überschwemmen kann.
Die praktische Lehre: Daten brauchen einen Aufnahmevertrag, Richtlinienprüfungen, ein Budget und eine Herkunft, bevor sie überhaupt ein Modell erreichen.
Die Form des Compilers
TrustedContextCompiler stellt einen einzigen Endpunkt bereit:
POST /v1/context/compile
Die Anfrage enthält die Aufgabe, die vertrauenswürdigen Systemanweisungen und eine Liste identifizierter Quellen, von denen jede ihre Art, Vertrauensstufe, Priorität und Abrufzeit angibt. Zurück kommen der zusammengesetzte Kontext, eine grobe Token-Schätzung, ein SHA-256-Fingerabdruck genau dieses Texts und ein Manifest mit einer Entscheidung pro Quelle. Die Verarbeitungspipeline sieht so aus:
flowchart LR
A[Retrieved sources] --> V[Validate ID and freshness]
V --> T[Apply trust policy]
T --> Q[Quarantine suspicious untrusted data]
T --> R[Redact sensitive values]
R --> B[Deterministic budget allocator]
B --> C[Compiled context]
B --> M[Decision manifest]
C --> F[SHA-256 fingerprint]
Im Projektaufbau ist die Richtlinie vom Transport getrennt:
TrustedContextCompiler/
├── src/TrustedContextCompiler.Core/
├── src/TrustedContextCompiler.Api/
├── tests/TrustedContextCompiler.Tests/
├── samples/compile-request.json
├── docs/verification.md
├── Dockerfile
└── TrustedContextCompiler.slnx
Die Kernbibliothek hängt von keinem Modell ab. Das ist Absicht, denn Kontextrichtlinien sollten deterministisch und testbar sein, ohne Token auszugeben oder auf einen Anbieter zu warten.
Identität und Vertrauen für jede Quelle
Anonyme Strings werden nicht akzeptiert. Jede Eingabe ist typisiert:
public enum TrustLevel
{
Trusted = 0,
Internal = 1,
User = 2,
Untrusted = 3
}
public sealed record ContextSource(
string Id,
string Kind,
string Content,
TrustLevel Trust,
int Priority,
DateTimeOffset RetrievedAt);
Id verbindet die Ausgabe mit den Abrufbelegen. Kind unterscheidet Richtlinien, Tickets, Gedächtnis, Webseiten und andere Quellarten. Priority ist eine begrenzte Eingabe für die Richtlinie, und RetrievedAt speist die Entscheidung über die Aktualität.
Das Vertrauenslabel muss aus dem Aufnahmecode stammen und niemals aus dem Dokument selbst, damit eine Webseite sich nicht selbst für vertrauenswürdig erklären kann. Auch eine Zeile aus einer internen Datenbank ist nicht automatisch vertrauenswürdig. Das Label sollte beschreiben, wer den Inhalt beeinflussen kann und welcher Validierungspfad ihn erzeugt hat.
Doppelte IDs werden abgelehnt, ebenso fehlende Felder und Prioritäten außerhalb von 0 bis 100. Frühes Scheitern ist hier wichtig, denn ein Manifest auf mehrdeutigen Identitäten erzeugt falsche Sicherheit.
Anweisungen bekommen ihr Budget zuerst
Bevor abgerufene Daten betrachtet werden, erhalten vertrauenswürdige Anweisungen und die Aufgabe einen eigenen Teil des Budgets:
var instructionBlock =
quot;<trusted-instructions>\n" +
quot;{request.SystemInstructions.Trim()}\n" +
quot;Task: {request.Task.Trim()}\n" +
quot;</trusted-instructions>";
var instructionTokens = EstimateTokens(instructionBlock);if (instructionTokens > options.ReservedInstructionTokens)
{
throw new ArgumentException(
"Instructions exceed the reserved instruction budget.",
nameof(request));
}
So verdrängt eine Flut an Suchergebnissen nicht still die Steuerungsebene. Außerdem kann ein übergroßer System-Prompt nicht das gesamte Datenbudget auffressen, ohne dass ein sichtbarer Konfigurationsfehler entsteht.
Token werden mit einer einfachen Schätzung von vier Zeichen pro Token gezählt:
public static int EstimateTokens(string value) =>
Math.Max(1, (value.Length + 3) / 4);
Das ist kein Tokenizer auf Abrechnungsniveau. In Produktion würdest du den Tokenizer des gewählten Modells verwenden und eine Sicherheitsmarge für Nachrichten-Wrapper und anbieterspezifische Kodierung einplanen. Eine deterministische Näherung taugt trotzdem zum Testen des Richtlinienverhaltens, sofern sie ehrlich als solche gekennzeichnet ist.
Veralteten Kontext verwerfen
Eine Quelle kann gültig und trotzdem für die Aufgabe falsch sein, weil sie alt ist. Jede wird mit einem konfigurierbaren Höchstalter verglichen:
if (now - source.RetrievedAt >
TimeSpan.FromMinutes(options.MaxSourceAgeMinutes))
{
manifest.Add(Record(
source,
ContextDecision.Stale,
0,
0,
hash,
"Source exceeded the freshness policy."));
continue;
}
In einem größeren System sollte die Aktualität je nach Quellart variieren. Eine Erstattungsrichtlinie braucht vielleicht ein kurzes Höchstalter, während ein Produkthandbuch monatelang stabil bleiben kann. Liefert das maßgebliche System eine Version oder ein Gültigkeitsintervall, nimm lieber das. Die Abrufzeit ist nur ein Notbehelf und beweist nicht, dass der Inhalt aktuell ist.
Die wichtige Designentscheidung: Veraltete Quellen bleiben im Manifest. Sie verschwinden aus dem Kontext des Modells, bleiben aber im Betriebsprotokoll erhalten.
Injection-Versuche in nicht vertrauenswürdigen Daten unter Quarantäne stellen
Nur Untrusted-Quellen durchlaufen eine Injection-Heuristik. Sie sucht nach engen Mustern: Versuchen, höher priorisierte Anweisungen zu überschreiben, Geheimnisse herauszuholen oder Markup im Rollenstil einzuschleusen.
if (source.Trust == TrustLevel.Untrusted &&
SafetyFilters.Injection().IsMatch(source.Content))
{
manifest.Add(Record(
source,
ContextDecision.Quarantined,
0,
0,
hash,
"Untrusted content matched an " +
"instruction-injection heuristic."));
continue;
}
Ein Treffer stellt die gesamte Quelle unter Quarantäne. Einen einzelnen Satz herauszuschneiden wäre fragil, also tauscht der Compiler etwas Recall gegen eine Entscheidung, die sich leicht erklären lässt.
Der reguläre Ausdruck ist keinesfalls eine "KI-Firewall".
Schwärzung, bevor das Modell etwas sieht
Quellen, die die Quarantäne überstehen, werden vor der Budgetierung transformiert. Das Beispiel erkennt E-Mail-Adressen, Bearer-Token und Werte, die wie Zahlungskartennummern aussehen:
public static (string Value, int Count) Redact(string value)
{
var count = 0;
value = Email().Replace(value, _ =>
{
count++;
return "[EMAIL]";
}); value = Bearer().Replace(value, _ =>
{
count++;
return "[SECRET]";
}); value = Card().Replace(value, _ =>
{
count++;
return "[CARD]";
}); return (value, count);
}
Weil die Schwärzung vor der Aufnahme passiert, landet der ursprüngliche Wert nie im kompilierten Kontext. Das Manifest behält nur die Anzahl der Schwärzungen und einen SHA-256-Hash der ursprünglichen Quelle.
Diese Muster sind Beispiele und kein vollständiges DLP-System. Internationale Kennungen, Kontonummern, firmenspezifische Geheimnisse und personenbezogene Daten in freier Form brauchen passende Detektoren. Ein Inhalts-Hash kann zudem einen Wert mit geringer Entropie verraten, wenn jemand ihn errät. Wo diese Bedrohung relevant ist, verwende einen HMAC mit geschütztem Schlüssel.
Ein deterministisches Budget
Kandidaten werden nach Priorität absteigend sortiert, dann nach stärkerem Vertrauen, dann nach Quell-ID:
foreach (var candidate in candidates
.OrderByDescending(item => item.Source.Priority)
.ThenBy(item => item.Source.Trust)
.ThenBy(item => item.Source.Id, StringComparer.Ordinal))
{
// Allocate the remaining budget.
}
Der Tie-Breaker über die ID ist keine Dekoration. Ohne ihn könnten zwei Suchergebnisse mit identischer Bewertung je nach Reihenfolge, in der Datenbank oder Netz sie zurückgeben, unterschiedliche Prompts, Cache-Schlüssel und unterschiedliches Modellverhalten erzeugen.
Jede Quelle hat eine Höchstzuteilung. Eine, die nicht passt, wird auf den Rest des Budgets gekürzt, und eine, bei der schon der Wrapper nicht passt, wird verworfen. Beide Ergebnisse erscheinen im Manifest. Jede Quelle wird mit ihrer ID, Art und ihren Vertrauensmetadaten umschlossen:
<source id="ticket-42" kind="ticket" trust="internal">
Contact [EMAIL]. Refund requested: $125.
</source>
Marker helfen bei Trennung und Beobachtbarkeit, sind im Modell aber keine kryptografische Grenze. Ein Modell kann Daten trotzdem falsch lesen oder ihnen folgen, deshalb müssen Tool-Kontrollen davon ausgehen, dass manche Kontextangriffe gelingen.
Das Artefakt mit Fingerabdruck versehen
Nach dem deterministischen Zusammenbau hasht der Compiler den exakten UTF-8-Text des Kontexts:
var context = string.Join("\n\n", sections);
var fingerprint = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(context)));
Der Fingerabdruck ermöglicht sichere Cache-Schlüssel, Regressionsvergleiche und die Korrelation mit Modellaufrufen, ohne den vollständigen Kontext zu loggen. Ein Cache-Schlüssel in Produktion sollte außerdem die Richtlinienversion des Compilers und die Modellkennung enthalten, denn derselbe Text kann unter geänderter Richtlinie oder anderem Tokenizer etwas anderes bedeuten.
Das Manifest ist nach Quell-ID sortiert, damit Vergleiche stabil bleiben. Für jede Quelle hält es Vertrauen, Entscheidung, geschätzte aufgenommene Token, Anzahl der Schwärzungen, Hash des Originalinhalts und den Grund fest.
Die ASP.NET-Core-Schicht
Eine Minimal API ergänzt, was ein Compiler-Dienst in Produktion braucht:
- Stark typisierte Optionen, beim Start validiert
- String-Enums im JSON-Vertrag für Aufrufer
- Ein Rate Limiter mit festem Zeitfenster
- Health Checks, Fehler werden als Problem Details zurückgegeben
- Per Source Generator erzeugte strukturierte Logs, die Zähler enthalten und niemals Kontext
- Abbruchunterstützung plus die Standard-Request-Limits von ASP.NET Core
Authentifizierung fehlt absichtlich. Bevor der Endpunkt für irgendwen erreichbar ist, ergänze Workload- oder Dienstidentität und Autorisierung für Mandanten, Quellarten und Richtlinienprofile.
Tests, die Entscheidungen prüfen
Fünf Tests decken die zentralen Aussagen ab:
- Eine nicht vertrauenswürdige Quelle, die wie eine Anweisung aussieht, kommt unter Quarantäne und fehlt im Kontext.
- E-Mail- und Kartenwerte werden vor der Aufnahme ersetzt.
- Die Eingaben in umgekehrter Reihenfolge ergeben denselben Kontext und Fingerabdruck.
- Wenn das Budget erschöpft ist, wird eine Quelle mit niedrigerer Priorität gekürzt oder verworfen.
- Eine veraltete Quelle steht im Manifest, aber nicht im Kontext.
Der Determinismustest ist kurz und sehr wichtig:
var left = compiler.Compile(Request(first, second));
var right = compiler.Compile(Request(second, first));
Assert.Equal(left.Fingerprint, right.Fingerprint);
Assert.Equal(left.Context, right.Context);
Die Lösung lief auf .NET 10 unter Windows. Restore war erfolgreich, der Release-Build erzeugte weder Warnungen noch Fehler, und alle fünf Tests bestanden. Ein Live-Check startete dann die API und schickte eine Anfrage mit drei Quellen: einer vertrauenswürdigen Richtlinie, einem internen Ticket mit E-Mail-Adresse und einer nicht vertrauenswürdigen Webnotiz mit Anweisungsangriff.
Fehler, die man vermeiden sollte
- Eine Quelle ihre eigene Vertrauensstufe wählen lassen
- Trenn-Tags als Sandbox betrachten
- Bequemlichkeitshalber den ganzen Kontext loggen
- Einen Suchscore als unbegrenzte Autorität verwenden
- Quellen still und ohne Manifest verwerfen
- Einen heuristischen Detektor als vollständige Injection-Abwehr bezeichnen
- Eine ungefähre Token-Zahl als Abrechnungswert ausgeben
Noch eine Regel: Kontext nicht losgelöst von Tools abstimmen. Ein nicht vertrauenswürdiger Satz ist nur so gefährlich wie die Fähigkeit, die er steuern kann, und das Abrufen eines öffentlichen Dokuments ist eine ganz andere Senke als das Versenden von Kundendaten, also braucht jede ihre eigene Autorisierung.
Was du davon hast
Kontext ist mehr als harmloser Text: Was darin landet, wird zur Welt, in der ein Agent denkt, und eine nicht vertrauenswürdige Idee kann steuern, was er als Nächstes tut. Ein solcher Compiler schafft Prompt Injection nicht aus der Welt. Er beseitigt aber die Unfälle. Quellen haben Identitäten, Vertrauen wird außerhalb des Inhalts festgelegt, alte Daten werden abgelehnt, sensible Werte transformiert, Budgets sind vorhersehbar, verdächtige Eingaben sitzen in Quarantäne, und jede Entscheidung lässt sich prüfen. Context Engineering ist dann kein Prompt-Handwerk mehr, sondern eine testbare Backend-Disziplin. Reviewer bekommen außerdem ein konkretes Artefakt, das sie untersuchen können, wenn sich das Agentenverhalten zwischen Releases ändert.