Wenn die Katzenklappe zufriert…←▼→Der MG4 Elektrik-Trick

delphi,offsiteedit,web,werkzeuge

Programmieren mit KI

Erstellt: 17.01.2024 Lesedauer ~6:20 Min.

In einem „Selbstversuch“ wollte ich herausfinden, ob KI tatsächlich eine „Intelligenz“ ist oder aus Nachgeschlagenem versucht, etwas zu zimmern, das „intelligent“ aussieht. Mit etwas Geduld und den richtigen Fragen kann dabei durchaus etwas vernünftiges heraus kommen.

Mich beschäftigte ein konkreter Funktionswunsch für OffSiteEdit, als mir spontan in den Sinn kam: »Frag doch mal ChatGPT!«. Eine „Programmierversuche“ damit waren durchwachsen ausgefallen, was meinerseits auf die womöglich doch zu speziellen Anforderungen zurückgeführt wurde. Die konkrete Anforderung (s.u.) ließ sich in diesem Fall so weit reduzieren, dass keine speziellen Rahmenbedingungen für die Lösung erfüllt sein müssen. Womit einer „allgemeinen“ Lösung keine Hindernisse im Weg standen. Dachte ich zumindest.

User

Eine Delphi Funktion, mit der ich die Position einer öffnenden/schließenden Klammer finde, für die es kein Gegenstück gibt?

Die Antwort kam nach kurzer „Bedenkzeit“:

ChatGPT

Um die Position einer öffnenden oder schließenden Klammer zu finden, für die es kein entsprechendes Gegenstück gibt, könntest du eine Delphi-Funktion erstellen, die den String durchläuft und dabei den Zustand der Klammern überwacht. Hier ist eine einfache Implementierung:

function FindUnmatchedBracket(const input: string): Integer;
var
  i, count: Integer;
begin
  count := 0;
 
  for i := 1 to Length(input) do
  begin
    if input[i] = '(' then
      Inc(count)
    else if input[i] = ')' then
	 begin
      Dec(count);
      if count < 0 then
      begin
        Result := i; // Rückgabe der Position der schließenden Klammer ohne Öffnende
        Exit;
      end;
    end;
  end;
 
  if count > 0 then
    Result := -1 // Es gibt mehr öffnende Klammern als schließende
  else
    Result := 0; // Alle Klammern sind korrekt gepaart
end;

Sieht gut aus, doch bei genauerem Hinsehen wird klar: Das ist keine Lösung. Zumindest keine für das beschriebene Problem. Das zeigen die Ergebnisse1:

Teststring: () → openPos: 1 closePos: 0 Result: -1

Teststring: ()( → openPos: 4 closePos: 0 Result: 0

Teststring: (() → openPos: 4 closePos: 0 Result: 0

Teststring: ()) → openPos: 1 closePos: 3 Result: 0

Teststring: )() → openPos: 0 closePos: 1 Result: 0

Selbst wenn „Result“ gedreht wird, bleibt die Position für überschüssige öffnende Klammern falsch. Deshalb bekam ChatGPT eine zweite Chance, mit einem Fokus auf „richtige Fehlerstelle“ als Tipp:

User

Lässt sich die Funktion so erweitern, dass sowohl eine fehlende öffnende als auch fehlende Schließende zurück gegeben wird – die jeweils erste Positon, falls es mehrere sein sollten?

Nach etwas „grübeln“ kam das:

ChatGPT

Ja, die Funktion kann so erweitert werden, dass sie die Positionen der ersten fehlenden öffnenden und schließenden Klammer zurückgibt. Hier ist die modifizierte Funktion:

function FindUnmatchedBrackets(const input: string; var openPos, closePos: Integer): Boolean;
var
  i, count: Integer;
begin
  count := 0;
  openPos := 0;
  closePos := 0;
 
  for i := 1 to Length(input) do
  begin
    if input[i] = '(' then
    begin
      Inc(count);
      if openPos = 0 then
        openPos := i; // Die Position der ersten öffnenden Klammer merken
    end
    else if input[i] = ')' then
    begin
      Dec(count);
      if count < 0 then
      begin
        closePos := i; // Die Position der ersten schließenden Klammer ohne Öffnende merken
        Result := False; // Es gibt eine fehlende öffnende Klammer
        Exit;
      end;
    end;
  end;
 
  if count > 0 then
  begin
    openPos := i; // Die Position der ersten fehlenden schließenden Klammer merken
    Result := False; // Es gibt eine fehlende schließende Klammer
  end
  else
    Result := True; // Alle Klammern sind korrekt gepaart
end;

Das führt zu diesen Ergebnissen:

Teststring: () → openPos: 1 closePos: 0 Result: true ✔

Teststring: ()( → openPos: 4 closePos: 0 Result: false ✔

Teststring: (() → openPos: 4 closePos: 0 Result: false ✔

Teststring: ()) → openPos: 1 closePos: 3 ✔ Result: false ✔

Teststring: )() → openPos: 0 closePos: 1 ✔ Result: false ✔

Schon besser, doch keineswegs überzeugend. Ich hatte konkret nach der Position der ersten fehlerhaften Klammer gefragt. Welcher Rückgabewert das letztendlich ist, bleibt unbeantwortet, bei öffnenden Klammern sind die ausgegebenen Positionen in den Teststrings weiterhin falsch.

Statt über „Wie muss gefragt werden, damit die Maschine das Problem begreift?“, war der konzeptionell in die korrekte Richtung führende Code Ausgangspunkt meiner selbst gestrickten Lösung.

Zwischenstand

Eine (eigentlich) schlichte Programmieranforderung lässt ChatGPT an Grenzen stoßen, wenn die Fragen „zu unscharf“ sind. Aber: „Intelligenz“ bedeutet ebenfalls, gegebenenfalls per Nachfragen für eine Entwicklungsanforderung die konkrete Ergebniserwartung abzuklären. In einer leidlich intelligenten Kommunikation reicht dafür oft schon ein knackig kurzes »Hä?«.

ChatGPT gehen offenbar davon aus, dass es alles richtig verstanden hat und legt los. Auf Nachfrage wird ein Missverständnis eingeräumt, doch fast immer wird der erste Ansatz nach weiteren Nachfragen mit Iterationen modifiziert. Diese können bei mehrfachen Nachfragen im Kreis laufen: Der explizite Hinweis „das ist falsch“, wird ein paar Korrekturfragen später geflissentlich ignoriert und eine bereits als unbrauchbar ausgewiesene Variante erneut angeboten2.

Programmcode ist diesbezüglich sehr entlarvend: Die Ergebnisse machen die Qualität der Aussage direkt messbar. Nachdem „meine“ Lösung geschrieben war, kam mir in den Sinn, ChatGPT ein Feedback zu seinen Entwicklungsqualitäten zu geben. Wobei ich vorangegangene Versuch summierend erwähne:

User

Diese Funktion übersieht Klammern. Ich habe mir selbst geholfen. Bei Delphi / Pascal ist der vorgeschlagene Code fast immer fehlerhaft oder unzureichend.

Darauf kam eine überraschende Antwort3:

ChatGPT

Es tut mir leid, wenn der vorgeschlagene Code nicht Ihren Anforderungen entspricht. Delphi/Pascal-Code kann in der Tat spezifisch sein, und ich schätze Ihr Verständnis.

Es wäre hilfreich, wenn Sie Ihre eigene Lösung teilen könnten, damit andere von Ihrer Erfahrung profitieren können. Falls Sie weitere Unterstützung benötigen oder Fragen haben, stehe ich Ihnen gerne zur Verfügung.

Womit indirekt verraten wurde, wie „intelligent“ ChatGPT ist: Was sich nirgends klauen lässt, …

Auf die Frage, wo ich denn meine Erfahrungen teilen solle, kam diese Liste (mit hier gesparten Erläuterungen): Delphi-Foren, GitHub, Blogs, Social Media. Mit der Anmerkung:

ChatGPT

Teilen Sie Ihre Lösung am besten dort, wo Sie denken, dass die Delphi-Community am meisten davon profitieren könnte. Wenn Sie eine spezifische Plattform im Sinn haben oder eine Empfehlung benötigen, lassen Sie es mich wissen.

Sprich: Ich soll bitte noch verraten, wo am besten geklaut werden kann.

User

Ich werde die Lösung in meinem Technik-Blog veröffentlichen.

ChatGPT

Das klingt nach einer ausgezeichneten Idee! Das Teilen von Lösungen und Erfahrungen in einem Technik-Blog ist eine großartige Möglichkeit, Ihr Wissen mit anderen zu teilen und gleichzeitig Ihre Präsenz in der Entwicklergemeinschaft zu stärken. Wenn Sie Ihren Blogartikel veröffentlichen, könnte dies anderen Entwicklern, die auf ähnliche Herausforderungen stoßen, wirklich helfen.

Großartig findet ChatGPT sicher ebenfalls, dass es die Ideen anderer, ohne deren Urheberschaft zu erwähnen, in seinen Antworten verwenden kann. Allerdings: Mit Blick auf das Gelieferte, brächte das die beklauten Entwickler bei Namensnennung möglicherweise völlig unberechtigt in Verruf.

Was ich „eigentlich“ haben wollte

Eine Prozedur, die in der Editor-Komponente von OffSiteEdit wahlweise überschüssige runde, eckige oder geschwungene Klammern findet und die erste gefundene „überschüssige“ Klammer als Auswahl markiert.

Die eigene Prozedur wurde an den Entwurf von ChatGPT angelehnt, in den relevanten Teilen so modifiziert, dass sie lieferte, was bestellt war. Das funktionierte, ich war zufrieden mit mir und bis hier sogar einen Artikel dazu geschrieben und veröffentlicht.

Zweiter Anlauf

Beim „Online-Kontrolllesen“ des Artikels fiel mir auf, dass ich (ehrlicherweise) ziemlich unscharf gefragt hatte, die Antworten für beide Positionen durchaus im Antwortrahmen waren – von den falschen Ergebnissen abgesehen. Also wurde ChatGPT von mir mit einer wesentlich differenzierten Frage beschäftigt:

Ich benötige eine Delphi-Prozedur, die in einem Editor („sEdit“) mit den Eigenschaften „.SelStart“ und „.SelEnd“ Blöcke markieren kann, eine Suchhilfe für fehlerhafte Klammerstrukturen. Dafür soll der Editor-Inhalt („sEdit.Text“) vom Anfang durchsucht werden. Werden nur paarige Klammern gefunden, wird eine Meldung „Alles in Ordnung“ ausgegeben, wird eine überzählige Klammer gefunden, wird die erste Klammer ohne öffnendes oder schließendes Gegenstück im Editor mit den genannten Befehlen als Block markiert. Die Suche soll für geschwungene, eckige und geschwungene Klammern gleichermaßen gelten.

🔍 Die Testanwendung für beide Varianten. Das Memofeld arbeitet zwar anders als ausgedacht, doch die Lösung ist womöglich eleganter.
Die Testanwendung für beide Varianten. Das Memofeld arbeitet zwar anders als ausgedacht, doch die Lösung ist womöglich eleganter.
In der ersten Antwort wurde durch den Prozedurnamen schon deutlich: Hier soll etwas markiert werden, die Vorgaben wurden adaptiert. Allerdings wurde die Definitionsreihenfolge von Funktionen ignoriert (erst definieren, dann verwenden) und ein „TStringStack“ erfunden.

In der zweiten Antwort auf den Hinweis, dass es kein „TStringStack“ gibt, wurde der mit „TStack“ ersetzt, was wenig zielführend war – fehlt ebenso.

in der dritten Antwort auf den Hinweis, dass „TStack“ keine Abhilfe ist, wurde unterstellt »dass in Delphi möglicherweise keine generische Stapelklasse standardmäßig verfügbar ist.« Die wurde deshalb kurzerhand definiert, was zu einem grundsätzlich funktionierenden Code führte, der Fehlermeldungen ausgibt. Allerdings blieb der Code die gewünschte Markierungen im Text schuldig.

Das wurde durch weitere Iterationen und erstmals seit meinen Versuchen mit einer grundlegenden Code-Änderung behoben. Die Lösung geht zwar ein bisschen an der ursprünglichen Anforderung vorbei, denn es wird die fehlerhafte Struktur markiert, statt nur die überschüssige Klammer. Was durchaus einen gewissen Charme hat.

Erkenntnis

Wie im richtigen Leben hängt bei KIs die Antwort von den richtigen Fragen, Nachfragen und Geduld bei der Erarbeitung von Antworten ab.

Die finale Lösung von ChatGPT – so sortiert, dass der Compiler es ohne Murren genommen hat – sieht so aus (für ein TMEmo):

function GetMatchingBracket(Bracket: Char): Char;
begin
  case Bracket of
    ')': Result := '(';
	 ']': Result := '[';
    '}': Result := '{';
    else Result := #0;
  end;
end;
 
type
  TBracketInfo = record
    Position: Integer;
    Bracket: Char;
  end;
 
type
  TCharStack = class
  private
    FItems: array of TBracketInfo;
  public
    procedure Push(Position: Integer; Bracket: Char);
    function Pop: Char;
    function Peek: Char;
    function PeekPos: Integer;
    function Count: Integer;
    function IsEmpty: Boolean;
  end;
 
procedure TCharStack.Push(Position: Integer; Bracket: Char);
begin
  SetLength(FItems, Length(FItems) + 1);
  FItems[High(FItems)].Position := Position;
  FItems[High(FItems)].Bracket := Bracket;
end;
 
function TCharStack.Pop: Char;
begin
  if not IsEmpty then
  begin
    Result := FItems[High(FItems)].Bracket;
    SetLength(FItems, Length(FItems) - 1);
  end
  else
    Result := #0;
end;
 
function TCharStack.Peek: Char;
begin
  if not IsEmpty then
    Result := FItems[High(FItems)].Bracket
  else
    Result := #0;
end;
 
function TCharStack.PeekPos: Integer;
begin
  if not IsEmpty then
    Result := FItems[High(FItems)].Position
  else
    Result := 0;
end;
 
function TCharStack.Count: Integer;
begin
  Result := Length(FItems);
end;
 
function TCharStack.IsEmpty: Boolean;
begin
  Result := Count = 0;
end;
 
procedure MarkMismatchedBrackets(sEdit: TMemo);
var
  Stack: TCharStack;
  i, StartPos, EndPos: Integer;
  CurrentChar, MatchingChar: Char;
begin
  Stack := TCharStack.Create;
 
  try
    for i := 1 to Length(sEdit.Text) do
    begin
      CurrentChar := sEdit.Text[i];
 
      case CurrentChar of
        '(', '[', '{':
          Stack.Push(i, CurrentChar);
 
        ')', ']', '}':
          begin
            if not Stack.IsEmpty then
            begin
              MatchingChar := GetMatchingBracket(CurrentChar);
              if Stack.Peek = MatchingChar then
                Stack.Pop
              else
              begin
                // Mismatched closing bracket found
                StartPos := Stack.PeekPos;
                EndPos := i;
                sEdit.SelStart := StartPos - 1;
                sEdit.SelLength := EndPos - StartPos + 1;
                ShowMessage('Fehlerhafte Klammerstruktur gefunden!');
                Exit;
              end;
            end
            else
            begin
              // Mismatched closing bracket found
              StartPos := i;
              EndPos := i;
              sEdit.SelStart := StartPos - 1;
              sEdit.SelLength := 1;
              ShowMessage('Fehlerhafte Klammerstruktur gefunden!');
              Exit;
            end;
          end;
      end;
    end;
 
    if Stack.IsEmpty then
      ShowMessage('Alles in Ordnung')
    else
    begin
      // Mismatched opening bracket found
      StartPos := Stack.PeekPos;
      EndPos := Length(sEdit.Text);
      sEdit.SelStart := StartPos - 1;
      sEdit.SelLength := EndPos - StartPos + 1;
      ShowMessage('Fehlerhafte Klammerstruktur gefunden!');
    end;
  finally
    Stack.Free;
  end;
end;

1Die Ergebnisse wurden mit einem damit gefütterten Testprogramm generiert.

2Das war das Ergebnis einer sehr exzessiven ChatGPT-Sitzung, die hier jeden Rahmen sprengen würde.

3Aus dem anfänglichen „du“ wird jetzt „Sie“ – das ist interessant!