Im Oktober 2026 habe ich die KI-Begriffe aus meinen Artikeln über CardsToLoop, das Testsystem für den Peakboard Designer und den Self-Healing-Loop gesammelt. Im ersten Teil wurden Modelle, Prompts, Tools und Agenten behandelt. In diesem zweiten Teil werden die Begriffe zu Retrieval, Evaluierung und den Frameworks erklärt, jeweils mit einem Beispiel von 5 bis 25 Zeilen in C# oder Text.
Jeder Eintrag beginnt mit einem Definitionssatz, darauf folgen das Beispiel und eine Erklärung, was es zeigt. Das Glossar ist kein Tutorial. Die Beispiele zum Judge stammen aus dem Self-Healing-Loop für eine neue Peakboard-Runtime. In den Beispielen zu den Frameworks wird die Karte von CardRunner ausgeführt. Im ersten Teil werden Token, Tool Calling, Agent Loop und die übrigen Begriffe erklärt, auf denen diese Einträge aufbauen.
Die zwei Gruppen dieses Teils und ihre Begriffe:
- Retrieval und Evaluierung
- Frameworks und Werkzeuge
Die drei Gruppen des ersten Teils und ihre Begriffe:
- Modelle
- Prompts
- Tools und Agenten
Retrieval und Evaluierung
Embeddings
Ein Embedding ist ein von einem Embedding-Modell berechneter Vektor aus Zahlen, der die Bedeutung eines Texts codiert. Zwei Texte mit ähnlicher Bedeutung erhalten Vektoren, die in eine ähnliche Richtung zeigen, gemessen wird das mit der Kosinus-Ähnlichkeit (engl. Cosine Similarity). Im folgenden Code werden drei Sätze verglichen:
IEmbeddingGenerator<string, Embedding<float>> generator =
new OpenAIClient(openAiKey).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
var embeddings = await generator.GenerateAsync(
[
"The total ignores the discount.",
"The sum does not apply the rebate.",
"The window title is wrong.",
]);
Console.WriteLine($"{Similarity(embeddings[0], embeddings[1]):F2} {Similarity(embeddings[0], embeddings[2]):F2}");
static float Similarity(Embedding<float> a, Embedding<float> b)
{
return TensorPrimitives.CosineSimilarity(a.Vector.Span, b.Vector.Span);
}
Das Programm gibt etwa 0,55 und 0,18 aus. Die ersten beiden Sätze haben außer „the“ kein Wort gemeinsam und erhalten trotzdem den deutlich höheren Wert, der dritte erhält gegenüber beiden einen niedrigen, 0,18 und 0,21.
Ein Embedding-Modell liest einen Text wie ein Sprachmodell: Der Text wird in Tokens zerlegt, und für jedes Token wird ein Vektor berechnet, der die umgebenden Wörter berücksichtigt. Statt das nächste Token vorherzusagen, fasst das Modell diese Vektoren zu einem einzigen Vektor fester Länge zusammen. Im Training werden dem Modell Millionen zusammengehöriger Textpaare gezeigt, etwa eine Frage und ihre Antwort. Es lernt, deren Vektoren eine ähnliche Richtung zu geben und den Vektoren unabhängiger Texte verschiedene. Eine einzelne Zahl des Vektors hat keine eigene Bedeutung, es zählt nur die Richtung des ganzen Vektors.
Die Schnittstelle IEmbeddingGenerator ist neben IChatClient die zweite Schnittstelle von Microsoft.Extensions.AI. Da Anthropic kein Embedding-Modell anbietet, wird im Beispiel „text-embedding-3-small“ von OpenAI verwendet. OpenAI hat das Modell im Januar 2024 zusammen mit dem größeren „text-embedding-3-large“ veröffentlicht. Es liest bis zu 8.191 Tokens pro Text und liefert einen Vektor aus 1.536 Zahlen. Laut Preisliste berechnet OpenAI 0,02 Dollar je Million Tokens, eine Hilfedatei mit 100.000 Wörtern kostet damit etwa einen Viertelcent. Zwei Vektoren werden von der Methode CosineSimilarity der Klasse TensorPrimitives ohne Schleife verglichen.
Retrieval-Augmented Generation (RAG) und Agentic Retrieval
Retrieval-Augmented Generation (RAG) fügt die Auszüge einer Dokumentensammlung, die der Frage am ähnlichsten sind, in den Prompt ein, bevor das Modell antwortet. Dazu werden die Dokumente in Teilstücke (engl. Chunks) von einigen hundert Wörtern zerlegt, und jeder Chunk erhält ein Embedding. Eine Frage findet ihre Chunks anschließend über die Ähnlichkeit. Im folgenden Code werden die Chunks einer Hilfedatei einmalig erzeugt und eine Frage aus den drei besten beantwortet:
// Once: one chunk per paragraph of the help, each with its embedding
var chunks = File.ReadAllText("help.md").Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
var embeddings = await generator.GenerateAsync(chunks);
// Per question: the three most similar chunks go into the prompt
var questionEmbedding = await generator.GenerateAsync(question);
var excerpts = chunks.Zip(embeddings)
.OrderByDescending(pair => Similarity(pair.Second, questionEmbedding))
.Take(3)
.Select(pair => pair.First);
var prompt = "Answer only from the following excerpts of the help.\n\n"
+ string.Join("\n---\n", excerpts) + "\n\nQuestion: " + question;
var response = await chatClient.GetResponseAsync(prompt);
Das Modell antwortet aus den Auszügen, dadurch kann sich die Hilfe ändern, ohne dass das Modell geändert wird. Über die Qualität entscheidet die Aufteilung in Chunks. Ein Absatz ist die einfachste Einheit, ein Abschnitt mit seiner Überschrift ist oft die bessere. Bei einer großen Sammlung werden die Liste und die Sortierung durch eine Vektordatenbank ersetzt.
Eine kleine Sammlung braucht keine Embeddings. Eine Hilfedatei mit 100.000 Wörtern hat etwa 130.000 Tokens und passt als Ganzes in das Kontextfenster aktueller Modelle. Beim Prompt Caching wird der verarbeitete Anfang eines Prompts beim Anbieter gespeichert, und die nächste Anfrage mit demselben Anfang muss ihn nicht neu berechnen. Anthropic hält diesen Cache fünf Minuten nach dem letzten Treffer und berechnet ihn mit einem Zehntel des normalen Input-Preises. Trotzdem steht der ganze Text in jedem Prompt, die Antwort dauert länger als mit drei Auszügen, und in einem sehr langen Kontext übersieht das Modell Details leichter.
Embeddings lohnen sich, wenn die Sammlung nicht mehr in das Kontextfenster passt oder wenn viele Fragen kommen und sich die Tokens summieren. Mit RAG enthält eine Frage drei Auszüge mit etwa 2.000 Tokens statt 130.000, und die Embeddings der Hilfe werden nur einmal berechnet.
Agentic Retrieval braucht weder Chunks noch Embeddings. Das Modell entscheidet selbst, wonach gesucht wird, und ruft dafür Tools auf, bei Claude Code etwa Glob zum Finden von Dateien, Grep zum Durchsuchen ihres Inhalts und Read zum Öffnen. In der einfachsten Form wählt das Modell Abschnitte aus dem Inhaltsverzeichnis, und nur diese kommen in den Prompt. Nach jedem Ergebnis kann die Suche verfeinert und wiederholt werden. Bei Code und Notizen findet das oft mehr als RAG. Eine Suche nach dem Klassennamen InvoiceCalculator liefert genau die Dateien, die ihn enthalten, ein Embedding dagegen nur Texte mit ähnlicher Bedeutung.
LLM as a Judge
LLM as a Judge bedeutet, dass ein großes Sprachmodell (engl. Large Language Model, LLM) eine Ausgabe anhand schriftlicher Regeln bewertet. Eine feste Prüfung kann das nicht. Eine Pixelzählung kann Anti-Aliasing nicht von einer falschen Statusfarbe unterscheiden, ein Modell mit den Regeln schon. Die folgenden Zeilen zeigen die Regeln des Judges im Self-Healing-Loop, gekürzt und mit einer JSON-Antwort statt der RESULT-Zeile:
You compare two screenshots of the same board, reference.png from the WPF runtime
and actual.png from the web runtime. diff.png marks the differing pixels.
Acceptable: anti-aliasing, a slightly different font, an offset of one or two pixels.
A bug: wrong colors, wrong or missing texts and values, missing elements,
a clearly wrong position or size, even on a small area.
Charts: type, values and colors have to match, labels may move.
Answer with one JSON object: {"bug": true or false, "reason": "<one sentence>"}
In den Regeln wird festgelegt, was akzeptabel und was ein Fehler ist, ebenso die Reihenfolge der Bilder und ein festes Antwortformat. Mit der folgenden Klasse werden die Regeln und die drei Bilder gesendet und das Urteil gelesen:
public sealed class Judge(IChatClient client)
{
private static readonly string _rules = File.ReadAllText("judge-rules.md");
public async Task<Verdict> JudgeAsync(string referencePath, string actualPath, string diffPath)
{
var message = new ChatMessage(ChatRole.User,
[
new TextContent(_rules),
new DataContent(await File.ReadAllBytesAsync(referencePath), "image/png"),
new DataContent(await File.ReadAllBytesAsync(actualPath), "image/png"),
new DataContent(await File.ReadAllBytesAsync(diffPath), "image/png"),
]);
var response = await client.GetResponseAsync<Verdict>([message]);
return response.Result;
}
}
Der Record Verdict mit den Feldern Bug und Reason stammt aus dem Eintrag zu Structured Output in Teil 1. Der Judge erhält einen eigenen Prompt und einen eigenen Aufruf, getrennt vom Agenten, der den Code schreibt. Dadurch vermischen sich die Regeln des Judges nicht mit den Anweisungen für die Korrektur. Im Self-Healing-Loop ist Claude Code der Judge, die drei Bilder werden dabei mit dem eigenen Read-Tool gelesen.
Visual Regression
Visual Regression Testing vergleicht einen Screenshot mit einem Referenzbild und meldet die abweichenden Pixel. Im Self-Healing-Loop stammt das Referenzbild aus der Runtime für Windows Presentation Foundation (WPF) und das Ist-Bild aus der Web-Runtime. Im folgenden Code werden die abweichenden Pixel markiert und gezählt sowie das Diff-Bild gespeichert:
var diff = new Bitmap(reference.Width, reference.Height);
var differing = 0;
for (var y = 0; y < reference.Height; y++)
{
for (var x = 0; x < reference.Width; x++)
{
if (Distance(reference.GetPixel(x, y), actual.GetPixel(x, y)) > Tolerance)
{
diff.SetPixel(x, y, Color.Red);
differing++;
}
}
}
diff.Save("diff.png");
Console.WriteLine($"{100.0 * differing / (reference.Width * reference.Height):F2} percent of the pixels differ");
static double Distance(Color a, Color b)
{
return Math.Sqrt(Math.Pow(a.R - b.R, 2) + Math.Pow(a.G - b.G, 2) + Math.Pow(a.B - b.B, 2));
}
Die Methode Distance berechnet die Länge des Vektors zwischen den beiden Farben, mit Rot, Grün und Blau als den drei Achsen. Oberhalb einer Toleranz wird ein Pixel als abweichend gezählt. Der Prozentwert wird ins Log geschrieben und entscheidet nichts. Entschieden wird vom Judge, der das Diff-Bild als dritte Eingabe erhält. Ein Fehler kann aus wenigen hundert falschen Pixeln bestehen, und ein Diagramm mit vielen tausend abweichenden Pixeln kann in Ordnung sein.
Evals
Evals messen, wie oft ein Modell eine Aufgabe richtig löst, etwa wie oft der Judge richtig urteilt. Ein Unit-Test erwartet bei jedem Lauf dasselbe Ergebnis. Ein Modell kann auf dieselbe Eingabe unterschiedlich antworten, deshalb werden in einem Eval viele Fälle mit bekannter Antwort durchlaufen und die richtigen gezählt. Das Ergebnis ist eine Erfolgsquote statt eines einfachen Bestanden oder Nicht bestanden. Das Eval für den Judge oben verwendet Bildpaare, die ein Tester bereits von Hand geprüft hat. Jeder Fall ist eine JSON-Zeile mit den drei Bildpfaden und dem Urteil des Testers:
{"reference":"cases/status-bar/reference.png","actual":"cases/status-bar/actual.png","diff":"cases/status-bar/diff.png","expectedBug":true}
Das Flag expectedBug enthält das Urteil des Testers. Eine Zeile mit true kennzeichnet einen Fehler. Eine Zeile mit false ist ein Fall, in dem sich die Bilder unterscheiden und der Tester den Unterschied akzeptiert hat, etwa beim Anti-Aliasing. Im folgenden Test mit xUnit, einem Test-Framework für .NET, werden 20 solcher Fälle durch den Judge geschickt und die Fälle gezählt, in denen der Judge mit dem Tester übereinstimmt:
public sealed record EvalCase(string Reference, string Actual, string Diff, bool ExpectedBug);
[Fact]
public async Task The_judge_agrees_with_the_tester_in_at_least_18_of_20_cases()
{
var judge = new Judge(new AnthropicClient().AsIChatClient("claude-sonnet-5-5"));
var cases = File.ReadLines("cases.jsonl").Select(line => JsonSerializer.Deserialize<EvalCase>(line)!).ToList();
var agreed = 0;
foreach (var evalCase in cases)
{
var verdict = await judge.JudgeAsync(evalCase.Reference, evalCase.Actual, evalCase.Diff);
agreed += verdict.Bug == evalCase.ExpectedBug ? 1 : 0;
}
Assert.True(agreed >= 18, $"{agreed} of {cases.Count} cases agreed with the tester.");
}
Der Test erzeugt die Klasse Judge aus dem Eintrag oben mit einem Claude-Client. Der Test gilt ab 18 von 20 als bestanden und nicht erst bei 20 von 20. Das Modell ist nicht deterministisch, und manche Fälle liegen an der Grenze, etwa ein Versatz von drei Pixeln, daher sind ein oder zwei abweichende Urteile normal. Seinen Nutzen zeigt das Eval, wenn der Prompt des Judges geändert wird. Der neue Prompt wird gegen dieselben 20 Fälle getestet, und ein schlechterer Prompt wird als niedrigere Zahl sichtbar, etwa 15 von 20, bevor der Self-Healing-Loop ihn verwendet. Jedes Urteil des Judges, das ein Tester korrigiert, wird zu einer neuen Zeile in cases.jsonl.
Ein Eval lohnt sich für jeden Schritt, in dem ein Modell entscheidet und keine feste Prüfung richtig von falsch unterscheiden kann. Beispiele sind das Urteil des Judges, die Kategorie eines Support-Tickets oder ein Wert, der aus einer Rechnung gelesen wird. Es läuft vor jeder Änderung am Prompt und vor dem Wechsel zu einem anderen Modell oder einer neuen Modellversion. In einem Loop oder einer Pipeline, in der niemand die einzelne Antwort liest, läuft es zusätzlich regelmäßig. Ein einmaliger Prompt oder eine Ausgabe, die jedes Mal ein Mensch prüft, braucht kein Eval, und für deterministischen Code genügt ein normaler Unit-Test.
Frameworks und Werkzeuge
Claude Code
Claude Code ist das Kommandozeilenwerkzeug von Anthropic, das Claude als Agent in einem Repository ausführt. Es bringt Tools für Dateien und eine Shell mit und bindet Server des Model Context Protocol (MCP) ein. Im Headless-Modus läuft es ohne den Dialog im Terminal, bis die Aufgabe erledigt ist. Die folgenden Zeilen zeigen, wie der PowerShell-Runner von CardsToLoop Claude Code startet und das Ergebnis liest:
$output = claude -p $prompt --mcp-config .mcp.json --strict-mcp-config 2>&1
$output | Set-Content $logPath -Encoding UTF8
$resultLine = @($output | Where-Object { $_ -match "^RESULT:" }) | Select-Object -Last 1
if ($resultLine -match "^RESULT: done") { Set-CardState "Done" } else { Set-CardState "Failed" }
Mit dem Flag -p wird der Prompt übergeben und die letzte Nachricht ausgegeben. Mit dem Strict-Flag werden nur die Server aus der angegebenen Datei geladen. Die folgende Datei nennt den UiMcpServer:
{
"mcpServers": {
"ui": {
"command": "src/UiMcpServer/bin/Debug/net8.0-windows/UiMcpServer.exe",
"args": []
}
}
}
Claude Code startet den Server als Kindprozess und listet seine Tools, auf dieselbe Weise wie CardRunner mit dem MCP-Client. Mit dem Flag –output-format stream-json kommt die Ausgabe als ein JSON-Objekt je Zeile, einschließlich der Session-ID. Über diese ID wird ein Lauf vom Slack-Bot und vom Kanban-Board fortgesetzt (siehe den Eintrag zu ChatOps in Teil 1).
Microsoft.Extensions.AI
Microsoft.Extensions.AI ist eine Bibliothek von Microsoft mit der Schnittstelle IChatClient für Chat-Modelle, der Klasse AIFunction für Tools sowie der Schleife. Der Anbieter wird in einer einzigen Zeile festgelegt. Im folgenden Code wird ein Client desselben Typs für Claude und für ein OpenAI-Modell erzeugt und anschließend der Client von CardRunner mit eigener Middleware gebaut:
IChatClient claude = new AnthropicClient().AsIChatClient("claude-sonnet-5-5");
IChatClient openAi = new OpenAIClient(openAiKey).GetChatClient(openAiModel).AsIChatClient();
var client = new RoundLogger(claude, maxTokens: 400_000).AsBuilder()
.UseFunctionInvocation(configure: loop => loop.MaximumIterationsPerRequest = 60)
.Build();
var response = await client.GetResponseAsync("What is a token?");
Console.WriteLine(response.Text);
Die Methode AsIChatClient ist Teil des Software Development Kits (SDK) des Anbieters, hier des Anthropic-SDK für C# und des Pakets Microsoft.Extensions.AI.OpenAI. Alles nach der Zeile, die den Client erzeugt, ist für beide gleich.
Die Klasse RoundLogger aus dem Eintrag zu den Tokens in Teil 1 ist von DelegatingChatClient abgeleitet, der Basisklasse für eigene Middleware. Die Schleife wird vom Builder über den Logger gesetzt, dadurch sieht der Logger jede Runde. Die Bibliothek bringt weitere Middleware für Logging, Caching und OpenTelemetry mit. CardRunner verwendet Version 10.11.0.
Microsoft Agent Framework
Das Microsoft Agent Framework ist eine Bibliothek von Microsoft für Agenten in .NET und Python. Es baut auf Microsoft.Extensions.AI auf, das den Client und die Schleife liefert, und ergänzt drei Dinge. Ein Agent ist ein Objekt, das die Anweisungen und die Tools enthält. Eine Session enthält die Konversation und lässt sich speichern und wieder laden. Ein Tool kann vor der Ausführung auf die Freigabe durch einen Menschen warten. Microsoft bezeichnet das Framework als Nachfolger von Semantic Kernel und AutoGen. Im folgenden Code wird der CardRunner-Prompt als Agent ausgeführt, vor dem Commit eine Bestätigung eingeholt und die Session gespeichert:
IChatClient chatClient = new AnthropicClient().AsIChatClient("claude-sonnet-5-5");
var commit = new ApprovalRequiredAIFunction(AIFunctionFactory.Create(localTools.GitCommitAsync, "git_commit"));
AIAgent agent = new ChatClientAgent(chatClient, instructions: File.ReadAllText("instructions.md"), tools: [.. uiTools, commit]);
AgentSession session = await agent.CreateSessionAsync();
var response = await agent.RunAsync("THE CARD\n--------\n" + card.Text, session);
var request = response.Messages.SelectMany(message => message.Contents).OfType<ToolApprovalRequestContent>().FirstOrDefault();
if (request is not null)
{
var call = (FunctionCallContent)request.ToolCall;
Console.Write($"{call.Name} {call.Arguments?["message"]}\nRun it? [y/n] ");
var approval = request.CreateResponse(approved: Console.ReadLine() == "y");
response = await agent.RunAsync(new ChatMessage(ChatRole.User, [approval]), session);
}
var checkpoint = await agent.SerializeSessionAsync(session);
File.WriteAllText("session.json", checkpoint.GetRawText());
Die Klasse ChatClientAgent ist der Agent. Sie enthält den System-Prompt aus der Datei instructions.md und die Tools. Die Session enthält die Nachrichten der Konversation, daher setzt der zweite Aufruf von RunAsync dort fort, wo der erste aufgehört hat. Der Wrapper ApprovalRequiredAIFunction kennzeichnet das Commit-Tool als freigabepflichtig. Will das Modell committen, wird das Tool vom Framework nicht ausgeführt. Stattdessen endet der erste Lauf mit einem ToolApprovalRequestContent, der den geplanten Aufruf mit der Commit-Nachricht enthält. Der Code zeigt die Nachricht an, liest y oder n von der Konsole und schickt die Antwort als nächste Nachricht in dieselbe Session. Bei y wird der Commit vom Framework ausgeführt, bei n erhält das Modell die Ablehnung und arbeitet ohne Commit weiter.
Mit SerializeSessionAsync wird die Session in JSON umgewandelt, die Datei session.json ist der Checkpoint. Ein späterer Prozess, auch nach einem Neustart, lädt die Datei und setzt die Karte an derselben Stelle fort. Die selbst geschriebene Schleife von CardRunner hält die Konversation nur im Speicher und kann das nicht. Das Beispiel verwendet Version 1.25.0 des Pakets Microsoft.Agents.AI.
Semantic Kernel
Semantic Kernel ist das ältere SDK von Microsoft für LLM-Anwendungen in .NET und Python. Ein Kernel enthält das Modell und die Plugins, und ein Plugin ist eine Klasse, deren Methoden das Modell aufrufen kann. Der folgende Code registriert ein Plugin und lässt das Modell es verwenden:
public sealed class InvoicePlugin
{
[KernelFunction("read_items")]
[Description("Returns the items of the current invoice, one per line: name, quantity, price.")]
public string ReadItems()
{
return "Gadget, 0, 100.00\nWidget, 2, 25.00";
}
}
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(chatClient.AsChatCompletionService());
builder.Plugins.AddFromType<InvoicePlugin>("invoice");
var kernel = builder.Build();
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
var result = await kernel.InvokePromptAsync("Which items have a quantity of zero?", new KernelArguments(settings));
Das Attribut KernelFunction kennzeichnet eine Methode des Plugins als Tool, ähnlich wie das Attribut Description in Microsoft.Extensions.AI. Das Modell sieht die Methode als invoice-read_items. Mit der Einstellung FunctionChoiceBehavior.Auto entscheidet das Modell, ob es das Plugin aufruft, die Schleife wird vom Kernel ausgeführt. Die Methode AsChatCompletionService umhüllt einen beliebigen IChatClient, dadurch funktioniert hier ebenfalls der Anthropic-Client. Das Beispiel verwendet Version 1.81.0.
Microsoft Foundry
Microsoft Foundry, bis November 2025 Azure AI Foundry, ist die Azure-Plattform, die Modelle mehrerer Anbieter hinter einem Endpunkt bereitstellt. Abrechnung und Zugriffskontrolle erfolgen über das Azure-Abonnement, Claude steht dort ebenfalls zur Verfügung. Im folgenden Code wird der Anthropic-Client für Foundry erzeugt, einmal mit einem API-Schlüssel und einmal mit einem Token:
var credentials = new AnthropicFoundryApiKeyCredentials(apiKey, "my-resource");
IChatClient chatClient = new AnthropicFoundryClient(credentials).AsIChatClient("claude-sonnet-5-5");
var response = await chatClient.GetResponseAsync("What is a token?");
Console.WriteLine(response.Text);
// The same client with a token from Microsoft Entra ID instead of the key
var entraCredentials = new AnthropicFoundryIdentityTokenCredentials(new DefaultAzureCredential(), "my-resource");
IChatClient entraClient = new AnthropicFoundryClient(entraCredentials).AsIChatClient("claude-sonnet-5-5");
Der Client wird durch das Paket Anthropic.Foundry ersetzt, der übrige Code bleibt gleich. Die Klasse DefaultAzureCredential aus dem Paket Azure.Identity bezieht das Token vom angemeldeten Entwickler, von einer Managed Identity oder aus einer Umgebungsvariablen. Dadurch liegt weder im Code noch in der Konfiguration ein Schlüssel. Der Ressourcenname ist Teil des Endpunkts der Foundry-Ressource.
Der Foundry-Client ist ein IChatClient wie jeder andere und funktioniert daher auch als Modell des Agenten aus dem Eintrag zum Microsoft Agent Framework oben. Zusätzlich bietet Foundry den Foundry Agent Service, der den Agenten und seine Konversation in Azure speichert statt in einer lokalen session.json.
Foundry passt zu einem Unternehmen, das bereits mit Azure arbeitet. Claude wird dann über das Azure-Abonnement abgerechnet, der Zugriff läuft über Entra ID und Azure-Rollen, und die Modelle mehrerer Anbieter liegen in einem Konto. Für ein kleines Projekt oder ein Unternehmen ohne Azure ist die Anthropic API einfacher, und neue Modelle und Beta-Funktionen erscheinen dort meist zuerst.
Git-Workflow
Ein Agent ändert Dateien. Ein Git-Workflow sorgt dafür, dass jede dieser Änderungen sichtbar ist und sich leicht zurücknehmen lässt. Nach jeder Aufgabe committet der Agent seine Änderung. Die Commit-Nachricht sagt, was er geändert und wie er die Änderung in der laufenden Anwendung geprüft hat, etwa dass der Löschdialog erscheint und der Button No die Summe bei 88,80 lässt. Ein Entwickler sieht die Änderung danach im Log und kann sie mit git revert zurücknehmen. In CardRunner kann das Modell Git nicht direkt aufrufen. Es hat keine Shell, sondern ein einziges Tool für den Commit, und dieses Tool führt feste Befehle aus:
[Description("Stages the changed files under src/InvoiceApp and commits them. The first line of the message is 'card: <title>', the body says what changed and how it was verified. Returns the commit hash.")]
public async Task<string> GitCommitAsync([Description("The complete commit message, first line and body")] string message)
{
var messageFile = Path.GetTempFileName();
File.WriteAllText(messageFile, message);
try
{
await RunAsync("git", $"add {AppFolder}", GitTimeoutMs);
await RunAsync("git", $"commit -F \"{messageFile}\"", GitTimeoutMs);
var (_, hash) = await RunAsync("git", "rev-parse --short HEAD", GitTimeoutMs);
return $"committed {hash.Trim()}";
}
finally
{
File.Delete(messageFile);
}
}
Das Tool erledigt drei Dinge. Es nimmt nur die Dateien in src/InvoiceApp in den Commit auf, damit keine Logdatei und keine andere Datei hineingerät. Die Nachricht wird über eine temporäre Datei übergeben, weil Anführungszeichen und Zeilenumbrüche auf der Kommandozeile Fehler verursachen. Zuletzt gibt es den kurzen Commit-Hash an das Modell zurück. Die Nachricht selbst schreibt das Modell. Laut Beschreibung des Tools beginnt sie mit „card:“ und dem Titel der Karte. Die folgenden Zeilen zeigen das Log von CardsToLoop nach zwei Läufen, der neueste Commit steht oben:
17b20d5 Revert the input checks of the .NET run so main keeps all cards open
dc9c0cb Add the log of the validate card run from 20.10.2026 with the .NET runner
495c995 (tag: validate-card-dotnet-done) card: Validate quantity and price before adding an item
bdd5c43 Revert the delete dialog of the .NET run so main keeps all cards open
696299a Add the log of the delete card run from 20.10.2026 with the .NET runner
a8d6528 (tag: delete-card-dotnet-done) card: Deleting an item should ask first
Jeder Lauf hinterlässt drei Commits. Von unten gelesen ist der erste die Korrektur des Modells, mit dem Titel der Karte und einem Tag. Der zweite fügt das Log des Laufs hinzu. Der dritte ist ein Revert, der die Korrektur wieder aus main herausnimmt. Damit bleibt main beim alten Stand, und dieselbe Karte kann im nächsten Test erneut laufen. Die Korrektur bleibt über das Tag erhalten, git checkout delete-card-dotnet-done zeigt sie jederzeit. Der Self-Healing-Loop committet nur auf einem eigenen Test-Branch und pusht nie.
UI Automation und Playwright
Windows UI Automation und Playwright sind die Schnittstellen, über die die Tools eines Agenten klicken und lesen. UI Automation meldet die Steuerelemente eines Fensters an andere Programme. Playwright ist eine Bibliothek von Microsoft für die Browser-Automatisierung. Im folgenden Code wird mit beiden jeweils ein Button angeklickt und ein Feld gelesen:
// Windows UI Automation: click a button of a WPF window and read a text box, both by AutomationId
var window = AutomationElement.RootElement.FindFirst(TreeScope.Children,
new PropertyCondition(AutomationElement.NameProperty, "Invoice Editor"));
var button = window.FindFirst(TreeScope.Descendants,
new PropertyCondition(AutomationElement.AutomationIdProperty, "DeleteItemButton"));
((InvokePattern)button.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
var discount = window.FindFirst(TreeScope.Descendants,
new PropertyCondition(AutomationElement.AutomationIdProperty, "DiscountTextBox"));
var discountText = ((ValuePattern)discount.GetCurrentPattern(ValuePattern.Pattern)).Current.Value;
// Playwright: the same in the browser, the button by role and name, the field by its test id
var page = await browser.NewPageAsync();
await page.GotoAsync("http://localhost:5000/board/1");
await page.GetByRole(AriaRole.Button, new() { Name = "Delete selected" }).ClickAsync();
var discountInBrowser = await page.GetByTestId("discount").InputValueAsync();
Eine AutomationId ist ein fester Name, der vom Entwickler an einem Steuerelement gesetzt wird, eine Test-ID ist dasselbe in HTML. Playwright findet ein Element ebenfalls über Rolle und Namen. Ein Pattern, hier Invoke und Value, ist die Menge der Aktionen, die ein Steuerelement unterstützt. Ein Button hat kein Value-Pattern und ein Textfeld kein Invoke-Pattern.
Beide Schnittstellen arbeiten ohne Screenshot und lesen Werte exakt. UI Automation wird in den MCP-Servern von CardsToLoop und vom Testsystem für den Peakboard Designer gekapselt. Für die Control-Tests des Self-Healing-Loops wird Playwright verwendet.
Das war’s.
Bei Fragen gerne melden.
Links:
CardsToLoop auf GitHub →
Microsoft.Extensions.AI →
Microsoft Agent Framework →
Semantic Kernel →
Claude auf Microsoft Foundry →
Playwright für .NET →