Von Code zu Code : KI-gestütze Generierung
Bisher haben wir unseren ASA SQL Code ausschließlich analytisch migriert, durch das Definieren einer Grammatik, das Parsen in einen AST und diverse Visitoren, die den Baum in eine neue Variante überführt haben. Jetzt wollen wir mal schauen, was eine KI zu diesem Prozess beitragen kann. Dafür braucht es zunächst ein paar Vorbereitungen. Wir brauchen einen API-Key für OpenAI und müssen diesen natürlich unseren Tools zur Verfügung stellen. Ich gehe mal nicht auf die Details ein, dafür gibt es im Internet reichlich Informationsquellen. Es gibt viele Wege, um mit einer KI zu kommunizieren. In meinem Beispiel fange ich üblicherweise mit einer PowerShell-gestützten Anwendung an und gehe dann zur Verwendung des Semantic Kernel von Microsoft über. Ich präsentiere Ihnen hier ein kurzes Beispiel als Startpunkt:
$prompt = @"
Your an expert for Sql Anywhere Sql and Microsoft SQL Server. You are asked to translate the following trigger definition from Sql Anywhere to Microsoft SQL Server, be aware that this is a row level trigger:
create trigger "Kabine_AfterUpdate" after update order 1 on
"DBA"."Kabine"
referencing old as "o" new as "n"
for each row
begin
if "o"."AnzahlKojen" <> "n"."AnzahlKojen" then
call "Schiff_NeueKoje"("n"."KabinenNummer")
end if
end
"@
Wir haben hier in der PowerShell eine Variable mit dem Prompt definiert, den wir an ChatGPT weitergeben wollen. Mit folgendem Code führen wir den Aufruf durch:
Invoke-OAIChat $prompt
Das Cmd-let Invoke-OAIChat wir von einem PowerShell-Modul namens PSAI von Doug Fink zur Verfügung gestellt. Genaueres gibt es hier: PSAI auf Github.
Schauen wir uns mal das Ergebnis von ChatGPT an:
To translate the provided Adaptive Server Anywhere (also known as Sybase SQL Anywhere) trigger definition to Microsoft SQL Server, we need to consider the differences in syntax and the execution context of triggers in SQL Server.
In SQL Server, triggers are defined similarly, but we do not use `referencing old as "o" new as "n"`; instead, `INSERTED` and `DELETED` tables are used to reference the new and old values, respectively. The equivalent logic will be encapsulated in the `IF` statement using those special tables.
Here is the equivalent SQL Server trigger definition:
CREATE TRIGGER Kabine_AfterUpdate
ON DBA.Kabine
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM DELETED o
JOIN INSERTED n ON o.KabinenNummer = n.KabinenNummer
WHERE o.AnzahlKojen <> n.AnzahlKojen
)
BEGIN
-- Assuming BTP_NewBett is a stored procedure:
EXEC Schiff_NeueKoje((SELECT n.LaufendKabinenNummerezimmernummer
FROM INSERTED n
WHERE n.KabinenNummer = o.KabinenNummer));
END
END;
### Explanation:
1. **Trigger Definition**: The trigger is created with the `CREATE TRIGGER` statement.
2. **Table References**: We use `INSERTED` table for new values and `DELETED` for old values. Each row that is updated will have a corresponding row in both these tables.
3. **IF EXISTS**: This checks for any rows where the `Bettanz` value has changed.
4. **Stored Procedure Execution**: The `EXEC` statement is used to call the stored procedure `BTP_NewBett`. The `SELECT` statement within is used to pass the appropriate `Laufendezimmernummer` from the `INSERTED` table.
Make sure that the trigger also includes appropriate error handling and performance considerations depending on the use case. Note that you might need to adjust the trigger logic based on your specific business rules or SQL Server environment settings.
Ein erster Schritt und schneller Erfolg!
Doch Vorsicht, es ist nicht alles Gold was glänzt! Man kommt niemals umhin, das Ergebnis zu prüfen. Dabei stehen erst einmal zwei Fragen im Vordergrund:
- Lässt sich der Code auf dem Zielsystem ausführen?
- Leistet der generierte Code das, was er soll?
Der erste Punkt lässt sich recht einfach testen: Man nehme den generierten Code und führe ihn auf der Ziel-DB aus. Auftretende Syntaxfehler sind relativ einfach zu entdecken und ihre Ursache ist schnell gefunden – jedenfalls meistens. Mit dem zweiten Punkt wird es komplizierter. Ein geschultes Auge stellt schnell fest, dass im obigen Code etwas nicht stimmt. Er kann gar nicht das leisten, was er soll. Wir müssen uns also parallel zu unserer Migration immer überlegen, wie wir die Qualitätssicherung betreiben. Wir werden später noch darauf zurückkommen, inwiefern uns die KI beim Erzeugen von Tests und Dokumentation behilflich sein kann. Mit anderen Worten: Der Umgang mit der KI gehört in die Hand von Experten, die beurteilen können, ob das Ergebnis den Erwartungen entspricht.
Semantic Kernel
Ok, so far so good (oder auch nicht). Wir sehen, dass wir mehr Informationen brauchen, um ein wiederholbares, vernünftiges Ergebnis zu erzielen. Nutzen wir dazu wiederum ein NuGet-Paket aus unserem Fundus, basierend auf dem Semantic Kernel von Microsoft: Einführung in den Semantic Kernel
Semantic Kernel vorbereiten
Um mit dem Semantic Kernel zu arbeiten, braucht es ein paar Dinge: den API-Key und das Sprachmodell, mit dem wir arbeiten möchten.
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
builder.Services.AddOpenAIChatCompletion("gpt-4o", apiKey);
var kernel = builder.Build();
Ab hier steht uns jetzt der Kernel zur Verfügung. Gehen wir nun her, definieren einen Prompt und lassen ihn arbeiten:
using Microsoft.SemanticKernel;
var statement = "CALL proc_getUserData('001232');";
var prompt = $"Translate the following SqlAnywhere statement into MSSQL: \r\n{statement}";
var result = await kernel.InvokePromptAsync(prompt);
Ich spare mir die Ausgabe, da diese ausgesprochen geschwätzig ist.
Spezielle Syntax eines Prompts
Prinzipiell sind wir jetzt genauso weit, wie wir es oben mit dem Aufruf über PSAI schon einmal waren. Vertiefen wir jetzt das Wissen bzw. verfeinern die Anforderungen durch die Erweiterung des Prompts.
using Microsoft.SemanticKernel;
var statement = "CALL proc_getUserData('001232');";
var additionalContext = "Please only return the translated code without any explanations.\r\n" +
"Here are some valid examples in MSSQL when calling a procedure:\r\n" +
"- EXEC dbo.proc_test1;\r\n" +
"- EXEC dbo.proc_test2 3, 'name'; \r\n" +
"- EXEC dbo.proc_test3 'xyz', NULL; \r\n" +
"Be sure your translation follows the syntax of the above examples.";
var prompt = $"Translate the following SqlAnywhere statement into MSSQL: \r\n{statement}\r\n{additionalContext}";
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result);
Hier geben wir dem Aufruf zusätzlich Informationen mit, was wir von ChatGPT erfüllt wissen wollen. Insbesondere der Hinweis auf die Tatsache, dass wir keine Erklärungen wünschen, ist interessant. Schauen wir mal, wie das Ergebnis jetzt aussieht:
EXEC dbo.proc_getUserData '001232';
Prinzipiell ist damit klar, wie man auch komplexere Szenarien löst. Das ganze Vorgehen ist ein iterativer Prozess: Anfrage stellen, Ergebnis betrachten, ggf. Rückschlüsse ziehen, diese in den Prompt einfließen lassen und dann wieder von vorne beginnen. Schauen wir uns zum Abschluss eine Geschichte an, die mir sehr geholfen hat.
Recherche in komplexen Datenmengen
Oft kommt es vor, dass man zwar über eine Dokumentation verfügt, aber das Suchen nach speziellen Informationen recht mühsam ist. Die Suche nach Schlüsselwörtern ist zwar meistens möglich, aber man hat dann unter Umständen sehr viele Fundstellen, die man manuell durcharbeiten muss, um das Gewünschte zu finden. Wäre es nicht geradezu ideal, einen Chatbot gezielt zu befragen? Kein Problem, mit Semantic Kernel ist es möglich, Informationen aus lokal abgelegten Dateien oder Web-URLs herauszufiltern. Folgendes Beispiel lädt die ASA SQL Reference PDF:
using Microsoft.SemanticKernel;
using Microsoft.KernelMemory;
// create kernel memory
var memory = new KernelMemoryBuilder()
.WithOpenAIDefaults(apiKey)
.Build<MemoryServerless>();
// import document into memory
await memory.ImportDocumentAsync(new Document("sqlanywhere_grammar_documentation")
.AddFile("C:\\Users\\meinh\\Documents\\SQL-Anywhere-Server-SQL-Reference-en.pdf")
.AddTag("dialect", "SqlAnywhere")
.AddTag("collection", "documentation")
.AddTag("collection", "bnf-grammar"));
Nachdem wir das Dokument geladen haben (das dauert natürlich einige Zeit, denn klein ist es nicht), befragen wir es gezielt nach einer Information:
using Microsoft.SemanticKernel;
using Microsoft.KernelMemory;
// ask question to memory
await memory.AskAsync("Pleas give me a list of all possible datatypes in sqlanywhere.")
...
Mit dem Ergebnis:
The possible data types in SQL Anywhere are as follows:
- Character Data Types:
- CHAR
- VARCHAR
- LONG VARCHAR
- NCHAR
- NVARCHAR
- LONG NVARCHAR
- TEXT
- UNIQUEIDENTIFIERSTR
- Numeric Data Types:
- BIGINT
- INT
- SMALLINT
- TINYINT
- DECIMAL
- NUMERIC
- FLOAT
- REAL
- DOUBLE
- Date and Time Data Types:
- DATE
- TIME
- TIMESTAMP
- TIMESTAMP WITH TIME ZONE
- Binary Data Types:
- BINARY
- VARBINARY
- LONG BINARY
- Bit Array Data Types:
- BIT
- BIT VARYING
- ROW and ARRAY Composite Data Types
- TABLE REF Data Type
- Spatial Data Types
- Domains
Hinzufügen von Kontext und mehr Wissen für das KI-Modell
Das Besondere an KI-Modellen ist, dass sie aus bisherigen Prompts lernen können. Durch den Kontext und das Training der vergangenen Prompts werden zukünftig ausgeführte Prompts aussagekräftiger und korrekter. In Semantic Kernel gibt es das Konzept der ChatHistory. Dies ist eine Liste, die alle Nachrichten einer Konversation zwischen User und KI speichert. Es ist ebenfalls möglich, im Programmablauf programmatisch User-Nachrichten in die ChatHistory zu schreiben. So kann man der KI im Voraus die richtigen Daten geben, indem man ihr gute Beispiele und Kontext liefert. Der Vorteil der Benutzung der ChatHistory gegenüber dem Aneinanderketten von Prompts im String-Format ist, dass man bei dieser Methode keine Probleme mit dem Überschreiten der Token-Größe einer Nachricht erhält, da jede Kontextinformation einem Prompt in der Historie einer Chat-Konversation entspricht.
Das folgende Statement kann auf der ASA fehlerfrei ausgeführt werden, scheitert aber auf dem MSSQL:
Select Name, Geburtsdatum, Geburtsdatum-10 as Vorwarnung from Person
Schauen wir mal, was es braucht, um der KI die nötigen Informationen zu liefern.
using Microsoft.SemanticKernel;
using Microsoft.KernelMemory;
using Microsoft.SemanticKernel.ChatCompletion;
private ChatHistory history = [];
history.AddUserMessage("some additional context information to improve generation ...");
var statement = "Select Name, Geburtsdatum, Geburtsdatum-10 as Vorwarnung, GetDate()+1 as Morgen from Person;";
var additionalContext = "Please only return the translated code without any explanations.\r\n" ;
additionalContext += $"Use the following table definition for typing information\r\n";
additionalContext += "CREATE TABLE PERSON (NAME VARCHAR(50), Geburtsdatum DATE);";
var prompt = $"Translate the following SqlAnywhere statement into MSSQL: \r\n{statement}\r\n{additionalContext}";
history.AddUserMessage(prompt);
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var promptSetting = new PromptExecutionSettings();
var result = await chatCompletionService.GetChatMessageContentAsync(history, promptSetting);
result.ToString().DisplayAs("text/markdown")
Mit dem Resultat :
SELECT Name, Geburtsdatum, DATEADD(day, -10, Geburtsdatum) AS Vorwarnung, DATEADD(day, 1, GETDATE()) AS Morgen
FROM Person;
Fazit
Wir haben verschiedene Wege kennengelernt, wie wir ASA SQL Code nach MSSQL migrieren können: den analytischen Weg über Grammatik, Parser und Visitoren, die zum Teil inspiriert sind von der Art und Weise, wie KI die Generierung machen würde. Beide Wege bedeuten einen gewissen Aufwand, da das Ergebnis immer sowohl syntaktisch als auch semantisch geprüft werden muss. Beim analytischen Weg haben wir im Wesentlichen folgende Vorteile:
- Das Ergebnis ist determinitisch
- Die notwendigen Tools existieren in unseren Bibliotheken und müssen nicht mehr entwickelt werden
- Performance & Ressourcen: Hier ist der analytische Ansatz der KI um Größenordnungen überlegen
Der KI-Weg beinhaltet ebenfalls einen nicht unerheblichen Aufwand. Denn:
- Der erzeugte Code ist oft nicht im ersten Wurf zu gebrauchen, das wird im Laufe der Zeit besser, je mehr man die KI trainiert hat.
- Das Ergebnis ist nicht immer deterministisch. Wird ein Prompt ein zweites Mal ausgeführt, muss das Ergebnis nicht unbedingt dasselbe sein.
- Auch wenn man den Code gegen den MSSQL Server testet und die Fehlermeldungen wieder als Feedback in die Generierung gibt, kommt man unter Umständen nicht umhin, der KI zusätzliche Informationen zur Verfügung zu stellen.
- Performance & Ressourcen: Zum heutigen Zeitpunkt sind die von den einschlägigen Diensten (hier OpenAI) bereitgestellten Ressourcen beschränkt. Das kann dazu führen, dass man die Übersetzung von Entitäten aufbrechen muss, weil die Anzahl der zu nutzenden Tokens überschritten wird. Es gibt zwar Wege, das zu optimieren, dies bedeutet aber wiederum zusätzlichen Aufwand. Wie wir bereits gesehen haben, verbrauchen einige Anfragen auch reichlich Zeit. Das mag bei einzelnen Fällen wenig erscheinen, aber es summiert sich ganz schön auf, gerade weil man mit der KI eine eher agile Vorgehensweise hat.
- Kosten: Die hier demonstrierten Dienste sind natürlich nicht umsonst. Es braucht auf jeden Fall ein Abo bei OpenAI, um auch auf die neuesten Modelle zurückgreifen zu können. Es stehen auch andere Dienste zur Verfügung (deshalb auch der Semantic Kernel, damit kann man auch andere Plattformen bedienen), aber wir haben uns hier nicht die Mühe gemacht, denn es geht uns nicht um den Vergleich, sondern um die Technik.
- KIs sind notorische Lügner. Die Ergebnisse sehen oft auf den ersten Blick logisch aus, aber auf den zweiten Blick stellen sie kompletten Unsinn dar. Sie binden damit auf jeden Fall Expertenwissen, denn man muss ihnen permanent auf die Finger schauen.
Meine empfohlene Vorgehensweise daher (und so sind unsere Bibliotheken auch entstanden):
- Nutzen Sie die analytische Vorgehensweise
- Ziehen Sie die KI zu Hilfe, wenn es ungelöste Probleme gibt, und arbeiten Sie die Erkenntnisse in den analytischen Prozess ein
Eine letzte Sache noch: Es kann Fälle geben, wo Sie weder mit der einen noch mit der anderen Vorgehensweise Erfolg haben werden. Scheuen Sie sich nicht, manuell einzugreifen! Gerade wenn es nur ein Ausnahmefall ist, wird es manchmal einfacher und weniger aufwendig sein, die entsprechende Entität von Hand zu migrieren. Sorgen Sie nur dafür, dass im gesamten Prozess auch der manuelle Fall berücksichtigt ist (zum Beispiel durch Hinzufügen von SQL-Skripten). Kein Tool wird Ihnen eine 100%ige Umsetzung garantieren.
Das Ergebnis der vorstehend dargestellten Arbeitsweise ist ein CLI namens dbmAIgrate aus unserem Hause. Es verfügt über eine reichhaltige Kommandostruktur, mit der alle ausgeführten Schritte abgearbeitet werden können. Es eignet sich daher hervorragend für ein automatisiertes Vorgehen.
Das war’s an dieser Stelle. Ich wünsche allen Kollegen einen schönen Sommer, und wir sehen uns hoffentlich auf der Konferenz!