Die Einbindung von Large Language Models (LLMs) in reguläre Software wird immer populärer. LLMs werden also nicht mehr nur als Unterstützung für Chatbots genutzt, sondern werden als Ersatz für Software-Logik eingesetzt. Auch bei Aclue arbeiten wir in verschiedenen Projekten mit generativer KI (GenAI) und optimieren die Verwendung von LLMs, um das beste Ergebnis zu erzielen. Präzise formulierte Prompts spielen dabei eine entscheidende Rolle, da sie bestimmen, wie das Modell auf Anfragen reagiert. Prompts sind hier aber nicht nur einfache Strings sondern werden oft mit Zusatzinformationen über Platzhalter angereichert, wie z.B. “Region = Skandinavien”.
Das Erstellen und Optimieren von Prompts erfordert initiales Experimentieren und Anpassen. Hierbei ist es wichtig, systematisch vorzugehen und die Auswirkungen jeder Änderung genau zu dokumentieren, um die besten Ergebnisse zu erzielen. Denn ohne Struktur kann es schwer sein, den Überblick zu behalten.
Dadurch stehen wir vor insgesamt drei Herausforderungen:
Hier kommt unser Konzept des Prompt-Templating ins Spiel.
Die ersten beiden Herausforderungen können wir über das Auslagern der Prompts lösen. Anstatt Prompts nur über Version-Control-Systems (z.B. Git) zu verwalten, lagern wir sie in Dateien aus, und laden die Prompts zur Laufzeit. Dies hat den Vorteil, dass wir nicht jedes Mal neu deployen müssen, um einen neuen Prompt auszuprobieren. Stattdessen wird der genutzte Prompt über eine Konfiguration angegeben. Dadurch können wir schneller andere Prompts ausprobieren. Zusätzlich kann die Prompt-Datei über ein definiertes Namensschema versioniert werden, was die Übersichtlichkeit verbessert.
Um auch die dritte Herausforderung – die dynamische Definition von Prompts durch Platzhalter – anzugehen, setzen wir auf bewährte Technologien. Templating-Engines werden seit Jahrzehnten in der Webentwicklung eingesetzt, um dynamische HTML-Websiten zu erstellen. Wir haben bei unserem aktuellen Projekt erkannt, dass deren Funktionalität auf Prompting projiziert werden kann. Denn auch beim Prompting wollen wir nach Bedarf Platzhalter ersetzen.
Templating-Engines werden in den verschiedenen Programmiersprachen über unterschiedliche Libraries mit oft ähnlicher Syntax bereitgestellt. Zu den populären Template-Sprachen gehören Handlebars und Mustache. Zusätzlich gibt es für jede Programmiersprache verschiedene Libraries, mit leicht abgewandelter Templating-Syntax. Da wir im Projekt in Typescript arbeiten und bereits positive Erfahrungen mit liquid.js
gesammelt hatten, entschieden wir uns hierfür.
Die Handhabung wird im folgenden Schaubild und Beispielcode (mit AWS & OpenAI) gezeigt. Über ein Input-Event oder eine Einstellung wird die Prompt-Template-Datei referenziert. Zur Laufzeit wird die Template-Datei geladen. Vor der Ausführung des Prompts wird das Template mit vorher gesammelten Platzhalterwerten gefüllt. Hierbei sollte sichergestellt werden, dass ein Fehlerhandling für eventuell fehlende Platzhalterwerte existiert. Abschließend wird das LLM mit den aufgelösten Template als Prompt ausgeführt.
Hinweis: Zur Unterstützung von neuen Platzhaltern in Templates empfiehlt es sich, den Code so zu gestallten, dass neue Platzhalter als Input an das Programm mitgegeben und dann auch stets beim Templating verwendet werden.
import { Liquid } from 'liquidjs'
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import OpenAI from 'openai';
type IncomingEvent = {
promptFilename: string
placeholderX: number
placeholderY: string
placeholderZ: string[]
}
// 1. Accept event
const event = ...
// 2. load placeholder values
const placeholders = {
...event,
dateString: new Date().toISOString()
}
// 3. load prompt Template
const client = new S3Client({})
const getTemplateCommand = new GetObjectCommand({
Bucket: 'template-bucket',
Key: event.promptFilename,
})
const promptResponse = await client.send(getTemplateCommand);
const promptTemplate = await promptResponse.Body.transformToString();
const engine = new Liquid()
const template = engine.parse(promptTemplate)
// 4. fill Template
const prompt = await engine.render(template, placeholders)
// 5. execute LLM call
const client = new OpenAI({});
const response = await openai.chat.completions.create({
messages: [{ role: 'user', content: prompt }],
model: 'gpt-4o',
});
Bei Projekten mit LLM-Einsatz muss man mit Prompts experimentieren, um ein optimales Ergebnis zu erzielen. In unserem Projekt haben wir über 50 verschiedene Experimente mit verschiedenen Ansätzen und Prompts durchgeführt. Das Auslagern der Prompts in Templates hat uns dabei sehr geholfen, da wir so schneller die Prompts anpassen beziehungsweise austauschen konnten.
Nachdem ein passender Prompt gefunden wurde, ist es nicht mehr unbedingt nötig, eine Versionierung der Prompts über Dateinamen durchzuführen. Gleichzeitig wird die Möglichkeit Prompts schnell anzupassen nicht mehr benötigt. Die Prompts können dann auch über Git verwaltet werden. Templating kann dann dennoch weiterhin sinnvoll sein, da es ermöglicht, Prompts sehr dynamisch zu gestalten und zum Beispiel ganze Sätze fallabhängig in den Prompt aufzunehmen. Für unseren Use-Case haben wir dies noch nicht genutzt. Wir betrachten es dennoch als interessante Option.
Insgesamt hat sich das Auslagern der Prompts in Templates als äußerst nützlich erwiesen und wir sind gespannt auf weitere Optimierungen bei der Arbeit mit LLMs.