KI-Begriffe für .NET-Entwickler, Teil 1: Modelle, Prompts und Agenten

21. Oktober 2026 · aktualisiert 25. Oktober 2026

Agentic AI, Microsoft.Extensions.AI, Claude Code, MCP, .NET

Vor einigen Tagen habe ich die KI-Begriffe aus meinen Artikeln über CardsToLoop, das Testsystem für den Peakboard Designer und den Self-Healing-Loop gesammelt. In diesem ersten Teil werden die Begriffe zu Modellen, Prompts, Tools und Agenten erklärt, jeweils in wenigen Sätzen und mit einem Beispiel von 5 bis 25 Zeilen. Die meisten C#-Beispiele stammen aus dem CardRunner von CardsToLoop und verwenden Microsoft.Extensions.AI.

Jeder Eintrag beginnt mit einem Definitionssatz. Wo ein Prompt, eine Antwort oder ein Ablauf den Begriff besser zeigt als Code, besteht das Beispiel aus Text. Das Glossar ist kein Tutorial. Die Artikel über CardsToLoop mit Claude Code, über CardRunner, über das Testsystem für den Peakboard Designer sowie über den Self-Healing-Loop für eine neue Peakboard-Runtime zeigen die Begriffe im Einsatz. Im zweiten Teil werden Retrieval, Evaluierung und die Frameworks behandelt.

Die drei Gruppen dieses Teils und ihre Begriffe:

Die zwei Gruppen des zweiten Teils und ihre Begriffe:

Modelle

Large Language Model (LLM) und generative KI

Ein großes Sprachmodell (engl. Large Language Model, LLM) ist ein mit Text trainiertes neuronales Netz, das zu einem gegebenen Text das nächste Token vorhersagt. Eine Antwort ist eine lange Kette solcher Vorhersagen. Generative KI ist der Oberbegriff für Modelle, die statt einer Klassifizierung Text, Code oder Bilder erzeugen. Die folgenden Zeilen zeigen zwei Prompts und typische Antworten:

Prompt:  Which WPF class shows a modal dialog with Yes and No buttons?
Answer:  MessageBox. The call MessageBox.Show(text, caption, MessageBoxButton.YesNo)
         returns MessageBoxResult.Yes or MessageBoxResult.No.

Prompt:  Write the C# for the delete button.
Answer:  var result = MessageBox.Show($"Delete {item.Description}?", "Delete item",
             MessageBoxButton.YesNo, MessageBoxImage.Question);
         if (result != MessageBoxResult.Yes) return;

Code wird vom Modell auf dieselbe Weise geschrieben wie Prosa, Token für Token und ohne Compiler. Der Code oben ist richtig, da das Modell viele Dialoge in Windows Presentation Foundation (WPF) gesehen hat. Geprüft wurde dabei nichts. Zwischen zwei Aufrufen hat das Modell kein Gedächtnis. Der zweite Prompt funktioniert nur, da der erste Austausch mit ihm erneut gesendet wird (siehe den Eintrag zu System-Prompt und Benutzernachricht unten).

Token und Kontextfenster

Ein Token ist ein Textstück von etwa vier Zeichen, also ein kurzes Wort oder ein Teil eines längeren Worts. Das Modell liest und schreibt Tokens, die von der API abgerechnet werden. Das Kontextfenster ist die Zahl der Tokens, die ein Modell in einem Aufruf liest. Bei den aktuellen Claude-Modellen sind es 1 Million. Die folgenden Zeilen zeigen einen Satz als Tokens sowie die Zahlen eines Laufs von CardRunner:

"The Delete selected button removes the row immediately."
The | Delete | selected | button | removes | the | row | immediately | .
about 10 tokens, the exact split depends on the model

Run of the delete card:  round 1 reads 3,270 input tokens, round 20 reads 19,360
                         20 rounds together: 254,762 input tokens, 4,306 output tokens, 59 seconds
Claude Sonnet 5.5:       2 dollars per million input tokens, 10 dollars per million output tokens

Die Eingabe einer Runde ist die gesamte bisherige Konversation, dadurch wächst die Zahl mit jeder Runde. In zwanzig Runden werden die Karte und die früheren Tool-Ergebnisse 20-mal gelesen. Mit der folgenden Klasse von CardRunner, die unterhalb der Schleife läuft, werden die Tokens jeder Runde gezählt:

public sealed class RoundLogger(IChatClient inner, long maxTokens) : DelegatingChatClient(inner)
{
    public int Rounds { get; private set; }
    public long InputTokens { get; private set; }
    public long OutputTokens { get; private set; }

    public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        Rounds++;
        var response = await base.GetResponseAsync(messages, options, cancellationToken);
        InputTokens += response.Usage?.InputTokenCount ?? 0;
        OutputTokens += response.Usage?.OutputTokenCount ?? 0;
        if (InputTokens + OutputTokens > maxTokens)
        {
            throw new InvalidOperationException($"Token limit of {maxTokens} reached after round {Rounds}.");
        }
        return response;
    }
}

Jede Antwort enthält in der Eigenschaft Usage die Token-Zahlen ihres Aufrufs, die von der Klasse summiert werden. Bei 400.000 Tokens wird der Lauf gestoppt. Ein Lauf, der nie endet, hinterlässt damit eine fehlgeschlagene Karte. Zusätzlich werden lange Tool-Ergebnisse von CardRunner gekürzt (siehe den Eintrag zu Context Engineering).

Multimodal

Ein multimodales Modell nimmt neben Text auch Bilder und teilweise Audio oder Dokumente als Eingabe an. Für eine Desktop-Anwendung bedeutet das, dass das Modell einen Screenshot lesen kann. Im folgenden Code wird der Screenshot eines Dialogs zusammen mit einer Frage gesendet:

var image = await File.ReadAllBytesAsync("after.png");
var message = new ChatMessage(ChatRole.User,
[
    new TextContent("Does the dialog name the item to delete? Answer with yes or no and the name."),
    new DataContent(image, "image/png"),
]);
var response = await client.GetResponseAsync([message]);

Die Klasse DataContent enthält die Bytes und den Medientyp, und eine Nachricht kann mehrere Texte und Bilder aufnehmen. Ein Bild kostet ebenso Tokens wie ein Text. Die Dokumentation von Claude nennt dafür die Formel Breite mal Höhe geteilt durch 750, ein Screenshot mit 1.280 mal 800 Pixeln kostet damit etwa 1.400 Tokens.

Im Self-Healing-Loop für die neue Peakboard-Runtime werden Claude Code auf diese Weise je Board drei Bilder übergeben. Dazu zählen das Referenzbild, das Ist-Bild und das Diff-Bild. CardRunner erstellt Screenshots als Nachweis, liest Werte jedoch über Windows UI Automation. Eine aus einem Element gelesene Zahl ist exakt, eine aus einem Bild gelesene nicht.

Halluzination

Eine Halluzination ist eine Antwort, die richtig klingt und falsch ist, z. B. ein Parameter, den es nicht gibt. Vom Modell wird Text vorhergesagt, der richtig klingt. Ohne Quelle gilt das ebenso für eine Tatsache, die dem Modell fehlt. Die folgenden Zeilen zeigen das Muster:

Question:  Which parameter of MessageBox.Show in WPF sets the default button?
Answer:    The parameter defaultButton of type MessageBoxDefaultButton.
Check:     MessageBoxDefaultButton belongs to Windows Forms. WPF takes a
           MessageBoxResult as the fifth parameter, named defaultResult.

Die Lösung ist Kontext. Ein Tool, das die Dokumentation oder den Quellcode liest, liefert dem Modell die Tatsache, wie in CardRunner. Der passende Auszug im Prompt leistet dasselbe (siehe den Eintrag zu Retrieval in Teil 2). Der Rest wird von einem prüfenden Schritt gefunden, in CardsToLoop von der Live-Prüfung jedes Abnahmekriteriums.

Ein Agent halluziniert ebenso Tools, also Namen, die nicht in seiner Liste stehen. Ein solcher Aufruf wird von der Schleifenklasse in Microsoft.Extensions.AI standardmäßig als Fehlermeldung an das Modell zurückgegeben, dadurch korrigiert sich das Modell selbst. Mit der Option TerminateOnUnknownCalls wird der Lauf stattdessen beendet.

Deterministisch und nicht deterministisch

Ein deterministisches Programm liefert für dieselbe Eingabe jedes Mal dieselbe Ausgabe. Ein Sprachmodell tut das nicht, derselbe Prompt erhält beim nächsten Aufruf eine andere Antwort. Die folgenden Zeilen zeigen zweimal denselben Prompt sowie einen Testschritt, der gleich bleibt:

Prompt, twice:  Name a WPF control for a list of rows with columns.
Answer 1:       DataGrid.
Answer 2:       The DataGrid control, or a ListView with a GridView as its view.

Script step, the same on every run:
{ "action": "click", "window": "Invoice Editor", "target": "DeleteItemButton" }

Beide Antworten sind richtig, ein Test, der sie mit einem festen String vergleicht, schlägt allerdings jedes zweite Mal fehl. Im Testsystem für den Peakboard Designer wird die Anwendung vom Modell nur einmal erkundet und dabei ein Skript in JavaScript Object Notation (JSON) geschrieben. Das Skript wird von einem Runner ohne Modell wiederholt. Die Tools, der Runner und das Skript sind deterministisch, nur das Modell ist es nicht. Frühere Modelle hatten einen Parameter temperature, um die Streuung zu verringern, von den Claude-5-Modellen wird er abgelehnt.

Prompts

System-Prompt und Benutzernachricht

Der System-Prompt ist die Anweisung, die für die gesamte Konversation gilt. Die Benutzernachricht (engl. User Message) ist die Aufgabe. Beides sind Nachrichten mit einer Rolle, und die Antwort kommt als Nachricht mit der Rolle assistant zurück. Im folgenden Code werden die zwei Nachrichten von CardRunner erzeugt, die Konversation fortgesetzt und die Rollen gelistet:

var messages = new List<ChatMessage>
{
    new(ChatRole.System, File.ReadAllText("instructions.md")),
    new(ChatRole.User, "THE CARD\n--------\n" + card.Text),
};
var response = await client.GetResponseAsync(messages);
messages.AddRange(response.Messages);

messages.Add(new ChatMessage(ChatRole.User, "Which file did you change?"));
response = await client.GetResponseAsync(messages);
messages.AddRange(response.Messages);

foreach (var message in messages)
{
    Console.WriteLine($"{message.Role,-9} {message.Text.Length,6} characters");
}

Die Ausgabe besteht aus fünf Zeilen mit den Rollen system, user, assistant, user und assistant. Die Liste ist das gesamte Gedächtnis der Konversation, und mit jedem Aufruf wird die vollständige Liste gesendet. Die zweite Frage funktioniert nur, da die erste Antwort in der Liste steht. Der System-Prompt von CardRunner liegt mit 44 Zeilen in einer Datei neben der EXE-Datei, die Karte wird als Benutzernachricht gesendet. Tool-Ergebnisse erhalten eine vierte Rolle namens tool (siehe die nächste Gruppe).

Prompt Engineering

Prompt Engineering ist das Schreiben von Anweisungen, mit denen das Modell eine Aufgabe zuverlässig erledigt. Dazu gehören Regeln, ein festes Antwortformat und Beispiele. Die folgenden Zeilen zeigen eine schwache Anweisung sowie die Zeilen des CardRunner-Prompts, die sie ersetzen:

Weak:  Implement the card and make sure it works.

Better:
4. Start the app again and verify every acceptance criterion of the card live
   through the ui_* tools: change values, click, read the results back.
   Take an after screenshot. A build that compiles proves nothing.

THE LAST LINE of your final message must be exactly one of:
RESULT: done
RESULT: failed: <one-line reason>

Die bessere Fassung nennt die Tools, die Reihenfolge der Schritte sowie den wichtigsten Satz „A build that compiles proves nothing“. Die letzte Zeile gibt dem Runner ein festes Format vor, aus dem er das Ergebnis liest. Gelesen wird sie von der folgenden Methode von CardRunner:

private static readonly Regex _resultLine = new(@"^RESULT: (?<outcome>done|failed)(: (?<reason>.*))?\s*$", RegexOptions.Multiline);

public static RunResult Parse(string text)
{
    var matches = _resultLine.Matches(text);
    if (matches.Count == 0)
    {
        return new RunResult(false, "no RESULT line in the final answer");
    }
    var last = matches[^1];
    var done = last.Groups["outcome"].Value == "done";
    return new RunResult(done, done ? "" : last.Groups["reason"].Value.Trim());
}

Das Ergebnis wird von einem regulären Ausdruck gelesen, dadurch muss kein Code Prosa verstehen. Es zählt der letzte Treffer, da das Modell das Format manchmal zitiert, bevor es antwortet. Eine abschließende Antwort ohne diese Zeile wird als fehlgeschlagen gewertet. Der Prompt und diese Methode bilden einen Vertrag, und eine Änderung auf der einen Seite erfordert eine Änderung auf der anderen.

Context Engineering

Context Engineering ist die Entscheidung, was das Modell in einem Aufruf sieht, wie viel davon und in welcher Reihenfolge. Ein Tool-Ergebnis bleibt im Kontext jeder späteren Runde, ein langes Ergebnis kostet damit Tokens bis zum Ende des Laufs. Die folgenden Zeilen zeigen, wie das Build-Tool von CardRunner antwortet:

var relevant = lines.Where(line => line.Contains("error", StringComparison.OrdinalIgnoreCase)
    || line.Contains("warning", StringComparison.OrdinalIgnoreCase));
var summary = exitCode == 0 ? "Build succeeded." : $"Build failed with exit code {exitCode}.";
return summary + "\n" + string.Join("\n", relevant.Distinct().Take(BuildOutputLines));

Zurückgegeben werden eine Zusammenfassungszeile sowie die Fehler und Warnungen, nicht die vollständige Ausgabe des Builds. Die vollständige Ausgabe würde in jeder späteren Runde Tokens kosten, und das Modell benötigt nur die Zeilen, die ein Problem nennen. Mit der folgenden Klasse, einer gekürzten Fassung der Klasse LoggedFunction von CardRunner, wird jedes Tool umhüllt und jedes Ergebnis gekürzt:

public sealed class CutFunction(AIFunction inner) : DelegatingAIFunction(inner)
{
    private const int MaxResultLength = 4000;

    protected override async ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
    {
        var text = (await base.InvokeCoreAsync(arguments, cancellationToken))?.ToString() ?? "";
        if (text.Length > MaxResultLength)
        {
            text = text[..MaxResultLength] + $"\n[cut after {MaxResultLength} of {text.Length} characters]";
        }
        return text;
    }
}

Von der Klasse DelegatingAIFunction wird jeder Aufruf an das innere Tool weitergegeben, dadurch umhüllt eine Zeile im Runner die gesamte Toolliste. Die Kürzung wird im Ergebnis genannt, dadurch kann das Modell beim nächsten Mal weniger anfordern, z. B. die Elemente eines Fensters statt aller Fenster. Im Testsystem für den Peakboard Designer liefert ein Klick ein Diff des UI-Baums statt des ganzen Baums.

Structured Output

Structured Output ist eine Antwort in einem festen JSON-Format statt in Prosa, damit Code sie ohne Textanalyse lesen kann. Von der Bibliothek wird aus einem C#-Typ ein JSON-Schema erzeugt, das das Modell füllen soll. Der folgende Code fordert ein Urteil mit zwei Feldern an:

public sealed record Verdict(
    [property: Description("true when the difference between the images is a bug")] bool Bug,
    [property: Description("One sentence that names the difference")] string Reason);

var response = await client.GetResponseAsync<Verdict>(
    "Compare reference.png and actual.png. Is the difference a bug?");
if (!response.TryGetResult(out var verdict))
{
    throw new InvalidOperationException("The answer was not the expected JSON: " + response.Text);
}
Console.WriteLine($"{verdict.Bug}: {verdict.Reason}");

Aus dem Record wird ein Schema mit den zwei Beschreibungen erzeugt und mit der Anfrage gesendet. Das Modell antwortet mit einem JSON-Objekt mit den Feldern bug und reason, das von der Bibliothek in den Record deserialisiert wird. Die Methode TryGetResult gibt false zurück, wenn die Antwort kein gültiges JSON für den Typ ist, die Eigenschaft Result wirft stattdessen eine Exception. Die Zeile RESULT von CardRunner ist die einfachere Form derselben Idee, eine feste Zeile am Ende eines freien Texts.

Tools und Agenten

Tool Calling

Tool Calling, auch Function Calling genannt, erlaubt dem Modell, das Programm um die Ausführung einer Funktion zu bitten. Ein Tool hat einen Namen, eine Beschreibung und typisierte Parameter, und das Modell sieht alle drei. Im folgenden Code wird aus einer C#-Methode ein Tool erzeugt und ausgegeben, was das Modell sieht:

public sealed class FileTools(string repoRoot)
{
    [Description("Reads a text file. The path is relative to the repository root and must lie under src/.")]
    public string ReadFile([Description("File path relative to the repository root")] string path)
    {
        return File.ReadAllText(Path.GetFullPath(path, repoRoot));
    }
}

var readFile = AIFunctionFactory.Create(new FileTools(repoRoot).ReadFile, "read_file");
Console.WriteLine($"{readFile.Name}: {readFile.Description}\n{readFile.JsonSchema}");
var options = new ChatOptions { Tools = [readFile] };

Ausgegeben werden der Name, die Beschreibung und das folgende Schema:

{
  "type": "object",
  "properties": {
    "path": {
      "description": "File path relative to the repository root",
      "type": "string"
    }
  },
  "required": ["path"]
}

Ausgeführt wird die Funktion nicht vom Modell. Es antwortet mit einem Aufruf, der aus dem Namen read_file, einer Call-ID und dem Argument path als JSON besteht. Die Methode wird vom Programm ausgeführt und das Ergebnis als Nachricht mit der Rolle tool und derselben Call-ID zurückgesendet.

Nur aus den zwei Description-Attributen erfährt das Modell, wann es das Tool aufrufen soll und was der Parameter bedeutet. Tool Integration ist der Oberbegriff für die Anbindung von Funktionen und Systemen an ein Modell auf diese Weise.

Model Context Protocol (MCP)

Das Model Context Protocol (MCP) ist ein offener Standard für Tool Calling über Prozessgrenzen hinweg. Ein MCP-Server stellt Tools bereit, die von einem Client mit JSON-RPC-Nachrichten über die Standardein- und -ausgabe oder über HTTP gelistet und aufgerufen werden. Der folgende Code zeigt die Serverseite mit einem der acht Tools des UiMcpServers von CardsToLoop sowie dem Host, der sie bereitstellt:

[McpServerToolType]
public sealed class UiTools
{
    [McpServerTool(Name = "ui_get_text")]
    [Description("Reads an element's text: Value pattern, then Text pattern, then the Name property.")]
    public static string GetText(
        [Description("Title fragment of the window")] string windowTitle,
        [Description("AutomationId or Name of the element")] string target)
    {
        var element = FindTarget(FindWindow(windowTitle), target);
        if (element.TryGetCurrentPattern(ValuePattern.Pattern, out var value))
        {
            return ((ValuePattern)value).Current.Value;
        }
        return element.Current.Name;
    }
}

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Services.AddMcpServer().WithStdioServerTransport().WithTools<UiTools>();
await builder.Build().RunAsync();

Über zwei Attribute werden eine Klasse und eine Methode als Tools gekennzeichnet, die Description-Attribute sind dieselben wie bei einem lokalen Tool. Da die Standardausgabe den JSON-RPC-Stream trägt, darf der Server dort nichts anderes schreiben, auch keine Logzeile. Der folgende Code verbindet CardRunner mit diesem Server:

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Name = "ui",
    Command = "src/UiMcpServer/bin/Debug/net8.0-windows/UiMcpServer.exe",
});
await using var mcpClient = await McpClient.CreateAsync(transport);
var tools = await mcpClient.ListToolsAsync();

Der Server wird vom Client als Kindprozess gestartet. Die acht Tools kommen als AIFunction-Objekte an und stehen in derselben Liste wie die lokalen Tools, für das Modell gibt es dadurch keinen Unterschied. Claude Code verwendet denselben Server über dasselbe Protokoll (siehe den Eintrag zu Claude Code in Teil 2).

Agent und Agent Loop

Ein Agent ist ein Sprachmodell, das in einer Schleife Tools aufruft, bis die Aufgabe erledigt ist. Agentic AI und Agentic Coding bezeichnen dasselbe. Der zweite Begriff wird für Änderungen am Code verwendet. Die Schleife ist kurz. Der folgende Code zeigt eine einfache Fassung:

while (true)
{
    var response = await client.GetResponseAsync(messages, options);
    messages.AddRange(response.Messages);
    var calls = response.Messages.SelectMany(message => message.Contents).OfType<FunctionCallContent>().ToList();
    if (calls.Count == 0)
    {
        return response.Text;
    }
    foreach (var call in calls)
    {
        var result = await tools[call.Name].InvokeAsync(new AIFunctionArguments(call.Arguments));
        messages.Add(new ChatMessage(ChatRole.Tool, [new FunctionResultContent(call.CallId, result)]));
    }
}

Ein Durchlauf der Schleife ist eine Runde. Das Modell fordert Tools an, die vom Programm ausgeführt werden, und entscheidet nach dem Anhängen der Ergebnisse über den nächsten Schritt. Microsoft.Extensions.AI enthält diese Schleife, mit den folgenden drei Zeilen wird sie einem Client hinzugefügt:

var client = chatClient.AsBuilder()
    .UseFunctionInvocation(configure: loop => loop.MaximumIterationsPerRequest = 60)
    .Build();
var response = await client.GetResponseAsync(messages, new ChatOptions { Tools = tools });

Anschließend läuft der Aufruf GetResponseAsync, bis das Modell mit Text antwortet oder die Grenze der Runden erreicht ist, CardRunner verwendet 60. Wirft ein Tool eine Exception, wird die Fehlermeldung als Ergebnis an das Modell gesendet, damit es einen neuen Versuch machen kann. Claude Code ist dieselbe Schleife mit eigenen Tools.

Guardrails

Guardrails sind die festen Grenzen um einen Agenten, im Code und nicht im Prompt. Der Prompt sagt, was das Modell tun soll, die Guardrail entscheidet, was es tun kann. Im folgenden Code wird jeder Pfad geprüft, den das Modell an ein Datei-Tool von CardRunner übergibt:

private string Resolve(string path, string[] roots)
{
    var fullPath = Path.GetFullPath(path, repoRoot);
    var allowed = roots.Select(root => Path.GetFullPath(root, repoRoot))
        .Any(rootPath => fullPath.StartsWith(rootPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase));
    if (!allowed)
    {
        throw new ArgumentException($"{path} is outside the allowed folders ({string.Join(", ", roots)}).");
    }
    return fullPath;
}

Der Pfad wird zunächst aufgelöst, dadurch scheitert ein Pfad mit zwei Punkten, der das Repository verlässt, auf dieselbe Weise. Die Exception geht als Tool-Ergebnis an das Modell, das daraufhin den Grund liest und einen anderen Pfad wählt.

CardRunner hat vier solche Grenzen. Die Pfade bleiben unter src, die Toolliste enthält keine Shell, und ein Lauf endet nach 60 Runden und nach 400.000 Tokens. Im Testsystem für den Peakboard Designer wird zusätzlich nach jedem Schritt eines Skripts der Zustand der Anwendung geprüft. Diese Prüfungen heißen Invarianten oder Testorakel (engl. Test Oracles). Beispiele sind kein neues Fenster und kein Fehlerdialog.

Human in the Loop

Human in the Loop bedeutet, dass ein Mensch an einer festgelegten Stelle eines automatisierten Laufs entscheidet, z. B. durch eine Freigabe, eine Antwort oder eine Korrektur. Die folgende Klasse fragt nach, bevor das Commit-Tool von CardRunner ausgeführt wird:

public sealed class ApprovedFunction(AIFunction inner) : DelegatingAIFunction(inner)
{
    protected override async ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
    {
        Console.Write($"{Name} {arguments["message"]}\nRun it? [y/n] ");
        if (Console.ReadLine() != "y")
        {
            return "The user declined the commit. Say what is missing in the RESULT line.";
        }
        return await base.InvokeCoreAsync(arguments, cancellationToken);
    }
}

Ein abgelehnter Aufruf liefert einen Text, den das Modell wie jedes andere Tool-Ergebnis liest. CardRunner hat diese Klasse nicht und committet selbstständig. Für dieselbe Idee bietet Microsoft.Extensions.AI die Klasse ApprovalRequiredAIFunction. Der Aufruf wird damit als Anfrage zurückgegeben statt ausgeführt, und das Programm antwortet im nächsten Aufruf mit einer Freigabe. Der Eintrag zum Microsoft Agent Framework in Teil 2 zeigt das.

Im Self-Healing-Loop ergänzt ein Tester einen Kommentar zu einem Board und startet es erneut, der Kommentar wird dabei in den Prompt übernommen. Im Testsystem für den Peakboard Designer werden Regeln von Testern in Slack entgegengenommen und bei jedem Lauf angewendet.

Agent Memory

Agent Memory ist das, was ein Agent zwischen zwei Läufen behält, in Form von Dateien, die er am Anfang liest und am Ende schreibt. Ohne Agent Memory beginnt jeder Lauf ohne Wissen und findet jedes bekannte Problem erneut. Die folgenden Zeilen zeigen eine Router-Datei sowie eine Themendatei:

MEMORY.md
- [Dialogs](dialogs.md): Enter in a combo box, default buttons, dialogs of the main window
- [Grid](grid.md): row selection, cell values, the German number format

dialogs.md
## Enter closes the dialog
Symptom:  the dialog closed on Enter in the combo box, the value was lost
Cause:    WPF sends Enter to the default button of the dialog
Works:    press Down to accept the autocompletion, then click the OK button

Der Agent liest den Router, öffnet ein oder zwei Themen und folgt den Links, die er benötigt. Ein Learning Loop ist der Schritt am Ende eines Laufs, in dem die neuen Notizen geschrieben werden.

Im Testsystem für den Peakboard Designer bestehen die Notizen aus rund tausend Markdown-Dateien im Repository. Gefunden werden sie vom Agenten über den Router und grep, ohne Embeddings und ohne Datenbank. Auf dieselbe Weise werden dort zwei weitere Dateien verwendet. Dazu zählen das Feedback mit den Regeln der Tester sowie ein Glossar der erlaubten Begriffe.

Agent-Orchestrierung, Subagenten und Session Continuation

Agent-Orchestrierung ist die Koordination mehrerer Läufe über einen Prozess hinweg. Dazu gehören eine Reihenfolge, Abhängigkeiten sowie die Stellen, an denen ein Mensch entscheidet. Ein Subagent ist ein zweiter Agent, den der erste mit einer Aufgabe startet. Mit Session Continuation wird ein Lauf mit seinem vollständigen Kontext fortgesetzt. Die folgenden Zeilen zeigen die Läufe für eine Karte auf einem Kanban-Board:

card-17  concept   run 1   claude -p "/concept card-17"                concept.md on the card
card-17  develop   run 2   claude -p "/develop card-17"                commit on branch card-17
card-17  test      run 3   claude -p "/test card-17"                   report with one finding
card-17  question  "Is the discount a percentage or an amount?"        waits for an answer
card-17  develop   run 4   claude --resume <session of run 2> "..."    continues with the answer

Jeder Lauf ist ein Start von Claude Code mit einer Rolle, d. h. einer Markdown-Datei mit Anweisungen. Die Läufe werden vom Board nacheinander gestartet und die Ergebnisse auf der Karte angezeigt. Das Resume-Flag setzt die Session von Lauf 2 fort, dadurch weiß Lauf 4, was er geändert hat. Für fehlende AutomationIds wird im Testsystem für den Peakboard Designer ein Subagent gestartet, der nur Datei-Tools erhält, dadurch bleibt der Kontext des Hauptlaufs klein.

ChatOps

ChatOps bedeutet, dass ein Chat-Werkzeug, hier Slack, die Benutzeroberfläche der Automatisierung ist. Eine Nachricht startet einen Lauf, der Bot postet den Fortschritt und das Ergebnis, und eine spätere Nachricht im selben Channel setzt die Session fort. Die folgenden Zeilen zeigen einen solchen Austausch:

tester   @testbot test the connection dialog of the new data source
testbot  run started, session 7c1e
testbot  ui_click MainMenu_File, ui_find_elements ...   (the new tool calls, every 4 seconds)
testbot  run finished, 1 finding, report.html and run.gif attached
tester   @testbot what did step 12 check?
testbot  Step 12 reopened the dialog and read the port back, it showed 1883 as entered.
tester   @testbot remember: [test] never navigate with Tab, fix the AutomationId instead

Im Testsystem für den Peakboard Designer ist der Bot ein .NET-Dienst ohne Modell. Er prüft den Absender gegen eine Liste, startet Claude Code im Headless-Modus und liest dessen Ausgabe als JSON-Stream. Der folgende Code zeigt den Kern eines solchen Bots:

var startInfo = new ProcessStartInfo("claude", $"-p \"{request}\" --output-format stream-json --verbose")
{
    RedirectStandardOutput = true,
    UseShellExecute = false,
};
using var process = Process.Start(startInfo)!;

while (await process.StandardOutput.ReadLineAsync() is { } line)
{
    var json = JsonDocument.Parse(line).RootElement;
    switch (json.GetProperty("type").GetString())
    {
        case "system":
            await PostAsync(channel, $"run started, session {json.GetProperty("session_id").GetString()}");
            break;
        case "result":
            await PostAsync(channel, json.GetProperty("result").GetString() ?? "");
            break;
    }
}

Jede Zeile des Streams ist ein JSON-Objekt mit einem Typ. Die erste Zeile enthält die Session-ID. Dazwischen stehen die Nachrichten des Modells und die Tool-Ergebnisse, die letzte Zeile ist die abschließende Antwort.

Die Session-ID wird vom Bot je Channel gespeichert, und eine spätere Nachricht setzt den Lauf mit dem Resume-Flag fort. Mit einer Nachricht, die mit „remember“ beginnt, wird einer Feedback-Datei eine Regel hinzugefügt, und jeder spätere Lauf wendet die Regeln dieser Datei an.

Self-Healing-Loop

Ein Self-Healing-Loop ist ein Test-Loop, in dem der Agent einen Fehler nicht nur findet, sondern auch selbst behebt. Anschließend wird bis zu einer festen Zahl von Versuchen erneut getestet. Im Loop für die neue Web-Runtime von Peakboard vergleicht der Test zwei Screenshots desselben Boards. Das sind das Referenzbild aus der produktiven Runtime auf WPF und das Ist-Bild aus der Web-Runtime. Claude Code beurteilt die Bilder und behebt den Fehler im Quellcode der Web-Runtime, wenn es einen findet. Die folgende Methode testet ein Board:

enum Verdict { Ok, Fixed }
enum BoardResult { Passed, PassedAfterFix, Failed }

async Task<BoardResult> TestBoardAsync(string board, string sessionId)
{
    var repaired = false;
    for (var attempt = 1; attempt <= 3; attempt++)
    {
        BuildWebRuntime();
        var reference = RenderWithWpf(board);
        var actual = RenderWithWeb(board);
        var diff = DiffImage(reference, actual);

        // Claude Code reads the three images. It answers Ok, or it fixes the source and answers Fixed.
        var verdict = await AskClaudeCodeAsync(sessionId, reference, actual, diff);
        if (verdict == Verdict.Ok)
        {
            return repaired ? BoardResult.PassedAfterFix : BoardResult.Passed;
        }
        repaired = true;
    }
    return BoardResult.Failed;
}

Die Methode liefert eines von drei Ergebnissen. Passed bedeutet, dass der erste Vergleich in Ordnung war. PassedAfterFix bedeutet, dass vor dem Ok mindestens ein Fixed kam, der geänderte Quellcode wird daraufhin vom Aufrufer auf dem Test-Branch committet. Failed bedeutet drei Versuche ohne Ok, der Quellcode bleibt dann so, wie Claude Code ihn hinterlassen hat.

In jedem Fall wird das Ergebnis vom Aufrufer neben das Board geschrieben, und die Seite für die Tester zeigt das Board mit beiden Bildern. Ein Tester kann einen Kommentar ergänzen und das Board erneut starten. Über die Session-ID bleibt jeder Versuch in derselben Konversation, dadurch muss Claude Code die Codebasis nicht erneut lesen. Der Loop selbst ruft kein Modell auf, der einzige Aufruf erfolgt in der Methode AskClaudeCodeAsync.

Das war’s.

Bei Fragen gerne melden.

CardsToLoop auf GitHub →
Microsoft.Extensions.AI →
Model Context Protocol →
Anthropic C# SDK →
Claude Code programmatisch ausführen →