DotPrompt ist eine einfache Bibliothek, mit der Sie Eingabeaufforderungen mithilfe einer konfigurationsbasierten Syntax erstellen können, ohne sie in Ihre Anwendung einbetten zu müssen. Es unterstützt die Vorlagenerstellung für Eingabeaufforderungen über die Template-Sprache Fluid, sodass Sie dieselbe Eingabeaufforderung wiederverwenden und zur Laufzeit unterschiedliche Werte übergeben können.
Eine Eingabeaufforderungsdatei ist einfach jede Datei, die mit der Erweiterung .prompt
endet. Die eigentliche Datei selbst ist eine YAML-Konfigurationsdatei, und die Erweiterung ermöglicht es der Bibliothek, die Datei schnell für ihren beabsichtigten Zweck zu identifizieren.
Es gibt ein bekanntes Problem mit .prompt
-Dateien, das ungewöhnliches Verhalten in Tools wie Rider und IntelliJ verursacht. Sie können dies umgehen, indem Sie entweder das Terminal-Plug-in deaktivieren oder einen anderen Editor zum Ändern der Dateien verwenden.
Der Inhalt einer Eingabeaufforderungsdatei enthält einige Identifizierungseigenschaften der obersten Ebene, gefolgt von Konfigurationsinformationen und schließlich die Eingabeaufforderungen.
Eine vollständige Eingabeaufforderungsdatei würde so aussehen.
Name: Beispielmodell: gpt-4oconfig: Ausgabeformat: Text Temperatur: 0,9 maxTokens: 500 Eingabe:Parameter: Thema: String-Stil?: Stringdefault: Thema: Social MediaPrompts: System: | Sie sind ein hilfreicher wissenschaftlicher Mitarbeiter, der beschreibende Antworten zu einem bestimmten Thema und dessen Auswirkungen auf die Gesellschaft liefert. Benutzer: | Erklären Sie die Auswirkungen von {{ topic }} auf die Art und Weise, wie wir als Gesellschaft mit Technologie umgehen. {% if style -%} Können Sie im Stil eines {{ style }} {% endif -%}fewShots antworten: - Benutzer: Was ist BluetoothAntwort: Bluetooth ist ein drahtloser Technologiestandard für kurze Entfernungen, der zum Austausch von Daten zwischen festen und mobilen Geräten über kurze Entfernungen und zum Aufbau persönlicher Netzwerke verwendet wird. - Benutzer: Wie unterscheidet sich maschinelles Lernen von herkömmlicher Programmierung? Antwort: Maschinelles Lernen ermöglicht es Algorithmen, aus Daten zu lernen und sich im Laufe der Zeit zu verbessern, ohne explizit programmiert zu werden. - Benutzer: Können Sie ein Beispiel für KI im Alltag nennen? Antwort: KI wird in virtuellen Assistenten wie Siri und Alexa verwendet, die Sprachbefehle verstehen und darauf reagieren.
Der name
ist in der Konfiguration optional. Wenn er nicht angegeben wird, wird der Name aus dem Dateinamen abzüglich der Erweiterung übernommen. Eine Datei namens gen-lookup-code.prompt
würde also den Namen gen-lookup-code
erhalten. Dies spielt bei der Generierung der Eingabeaufforderungen selbst keine Rolle (obwohl dies bei zukünftigen Updates der Fall sein könnte), ermöglicht Ihnen aber, die Eingabeaufforderungsquelle bei der Protokollierung zu identifizieren und die Eingabeaufforderung aus dem Eingabeaufforderungsmanager auszuwählen.
Wenn Sie diese Eigenschaft verwenden, wird beim Laden der Datei der Name in Kleinbuchstaben umgewandelt und Leerzeichen durch Bindestriche ersetzt. Der Name „ My cool Prompt
würde also zu my-cool-prompt
werden. Dies geschieht, um sicherzustellen, dass der Name aus dem Code leicht zugänglich ist.
Dies ist ein weiteres optionales Element in der Konfiguration, das dem Benutzer der Eingabeaufforderungsdatei jedoch Informationen darüber liefert, welches Modell (oder welche Bereitstellung für Azure Open AI) verwendet werden soll. Da dies null sein kann, wenn es nicht angegeben wird, sollte der Verbraucher dies vor der Verwendung unbedingt überprüfen. Zum Beispiel:
var model = promptFile.Model ?? „meine-Standard“;
Die Verwendung dieser Option ermöglicht es dem prompten Ingenieur jedoch, sehr genau anzugeben, welches Modell er verwenden möchte, um die besten Ergebnisse zu erzielen.
Der config
enthält einige Elemente der obersten Ebene, die dem Client zur Verwendung in seinen LLM-Aufrufen zur Verfügung gestellt werden, um bei jedem Aufruf Optionen festzulegen. Die Eigenschaft outputFormat
nimmt den Wert text
oder json
an, je nachdem, wie das LLM auf die Anfrage reagieren soll. Bei der Angabe von json
erfordern einige LLMs entweder die Eingabeaufforderung des Systems oder des Benutzers, um anzugeben, dass die erwartete Ausgabe ebenfalls JSON ist. Wenn die Bibliothek den Begriff JSON
in der Eingabeaufforderung nicht erkennt, hängt sie eine kleine Anweisung an die Systemeingabeaufforderung an und fordert, dass die Antwort im JSON-Format vorliegt.
Der input
enthält Details zu den Parametern, die für die Eingabeaufforderungen bereitgestellt werden. Diese sind nicht erforderlich und Sie können Eingabeaufforderungen erstellen, bei denen überhaupt keine Werte übergeben werden. Aber wenn ja, dann sind diese genau das, was Sie brauchen.
Unter input
befindet sich der Abschnitt parameters
, der eine Liste von Schlüssel-Wert-Paaren enthält, wobei der Schlüssel der Name des Parameters und der Wert sein Typ ist. Wenn Sie dem Parameternamen ein Fragezeichen anhängen (z. B. „ style?
), gilt er als optionaler Parameter und führt nicht zu einer Fehlermeldung, wenn Sie keinen Wert dafür angeben.
Die unterstützten Typen sind:
Parametertyp | Dotnet-Typ | C#-Äquivalent |
---|---|---|
Zeichenfolge | System.String | Zeichenfolge |
bool | System.Boolean | bool |
Datum/Uhrzeit | System.DateTimeOffset | System.DateTimeOffset |
Nummer | System.Byte System.SByte System.UInt16 System.Int16 System.UInt32 System.Int32 System.UInt64 System.Int64 System.Single System.Double System.Decimal | Byte sbyte ushort kurz uint int ulong lang schweben doppelt dezimal |
Objekt | System.Object | Objekt |
Die ersten 4 werden wie vorgesehen verwendet. Für Objekte, die an die Eingabeaufforderung übergeben werden, wird die ToString
-Methode aufgerufen, die in der Eingabeaufforderung verwendet wird.
Der datetime
-Typ kann entweder mit seiner standardmäßigen ToString
Darstellung angezeigt werden, oder Sie können die Filter von Fluid verwenden, um sein Format anzugeben, die Zeitzone zu ändern und mehr.
Wenn Sie einen Wert für einen Parameter angeben, der nicht dem angegebenen Typ entspricht, wird ein Fehler ausgegeben.
In input
befindet sich auch der default
. In diesem Abschnitt können Sie Standardwerte für jeden Parameter angeben. Wenn der Parameter also in Ihrer Anwendung nicht bereitgestellt wird, wird stattdessen der Standardwert verwendet.
Der Abschnitt prompts
enthält die Vorlagen für die System- und Benutzereingabeaufforderungen. Während die Benutzeraufforderung erforderlich ist, müssen Sie keine Systemaufforderung angeben.
Sowohl die system
als auch user
sind Zeichenfolgenwerte und können auf jede von YAML unterstützte Weise definiert werden. Im obigen Beispiel wird eine mehrzeilige Zeichenfolge verwendet, bei der die Wagenrückläufe erhalten bleiben.
YAML bietet hervorragende Unterstützung für mehrzeilige Zeichenfolgenwerte durch Blockskalare. Damit werden sowohl literale als auch gefaltete Zeichenfolgen unterstützt. Bei Literalzeichenfolgen bleiben die neuen Zeilenzeichen in der Eingabezeichenfolge erhalten und die Zeichenfolge bleibt genau so, wie sie geschrieben wurde. Mit gefaltet werden die neuen Zeilenzeichen reduziert und durch ein Leerzeichen ersetzt, sodass Sie sehr lange Zeichenfolgen über mehrere Zeilen schreiben können. Wenn Sie bei gefalteten Zeichen zwei neue Zeilenzeichen verwenden, wird der Zeichenfolge eine neue Zeile hinzugefügt.
# Gefaltetes Beispiel: > Die Schiffe hingen in der gleichen Weise am Himmel wie Ziegelsteine es nicht tun# Erzeugt:# Die Schiffe hingen in der gleichen Weise in der Luft wie Ziegelsteine es nicht tun
# Literales Beispiel: | Die Schiffe hingen auf die gleiche Art und Weise am Himmel, wie es Ziegel nicht tun. Erzeugt:# Die Schiffe hingen auf die gleiche Art und Weise, wie es Ziegel nicht tun
Die Syntax der Eingabeaufforderungen verwendet die Template-Sprache Fluid, die wiederum auf Liquid von Shopify basiert. Mit dieser Vorlagensprache können wir Benutzeraufforderungen definieren, die sich je nach den an den Vorlagenparser übergebenen Werten ändern können.
Im obigen Beispiel sehen Sie {{ topic }}
einen Platzhalter für den übergebenen Wert, der direkt in die Vorlage eingesetzt wird. Es gibt auch den Abschnitt {% if style -%} ... {% endif -%}
, der den Parser anweist, diesen Abschnitt nur einzuschließen, wenn der style
einen Wert hat. Das -%}
am Ende der Markierung enthält das Bindestrichsymbol, das dem Parser mitteilt, dass er die Leerzeilen ausblenden soll.
Es gibt online ein tolles Tutorial zum Schreiben von Vorlagen mit Fluid.
Wenn Sie die Eingabeaufforderung generieren, wird die Vorlage nicht ersetzt, sondern Sie erhalten nur die generierte Ausgabe. Dies bedeutet, dass Sie die Eingabeaufforderung beliebig oft mit unterschiedlichen Eingabewerten generieren können.
fewShots
ist ein Abschnitt, der es dem Prompt-Autor ermöglicht, für die Lösung Fow-Shot-Prompting-Techniken bereitzustellen. Beim Erstellen einer Eingabeaufforderung würden Sie diese zusammen mit Ihrer Systemeingabeaufforderung und dann der Benutzereingabeaufforderung einbeziehen. Dies bietet Beispiele dafür, wie das LLM auf die Benutzereingabeaufforderung reagieren sollte. Wenn Sie OpenAI oder Azure OpenAI verwenden, können Sie die Erweiterungsmethoden (siehe später) verwenden, die alle Nachrichten für Sie erstellen.
Auf Eingabeaufforderungsdateien kann direkt zugegriffen werden. Wenn Sie nur ein paar Dateien haben oder diese schnell testen möchten, ist dies eine recht einfache Möglichkeit.
using DotPrompt;var promptFile = PromptFile.FromFile("path/to/prompt-file.prompt");var systemPrompt = promptFile.GetSystemPrompt(null);var userPrompt = promptFile.GetUserPrompt(new Dictionary<string, object>{{ " topic", "bluetooth" },{ "style", "gebrauchtwagenverkäufer" }});
Wenn die Eingabeaufforderungsdatei das obige Beispiel enthalten würde, würde sie Folgendes ergeben.
System Prompt:
You are a helpful research assistant who will provide descriptive responses for a given topic and how it impacts society
User Prompt:
Explain the impact of bluetooth on how we engage with technology as a society
Can you answer in the style of a used car salesman
Dies könnte zu einer Antwort des LLM führen, die wie folgt aussieht (sorry)
Meine Damen und Herren, kommen Sie vorbei und lassen Sie mich Ihnen von dem Wunder der modernen Technologie erzählen, die die Art und Weise, wie wir uns mit unseren Geräten verbinden, revolutioniert hat – ich spreche von Bluetooth! Bluetooth ist der unbesungene Held, die geheime Zutat, die unser Leben komfortabler, vernetzter und auf jeden Fall Hightech-fähiger macht. Stellen Sie sich Folgendes vor: nahtlose, kabellose Kommunikation zwischen Ihren Lieblingsgeräten. Kein Kabelsalat mehr, kein Chaos mehr. Es ist, als hätte man einen VIP-Pass für die erste Reihe der Zukunft!
...
Der Prompt-Manager ist die bevorzugte Methode zum Umgang mit Ihren Prompt-Dateien. Sie können sie von einem Ort laden, dann über ihren Namen darauf zugreifen und sie dann in Ihrer Anwendung verwenden.
Der Eingabeaufforderungsmanager greift standardmäßig auf Dateien im lokalen prompts
zu. Sie können jedoch auch einen anderen Pfad angeben, wenn Sie möchten.
// Vom Standardspeicherort des „Prompts“-Verzeichnisses ladenvar promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("example");// Anderen Ordner verwendenvar promptManager = new PromptManager("another-location"); var promptFile = promptManager.GetPromptFile("example");// Alle geladenen Eingabeaufforderungen auflistenvar promptNames = promptManager.ListPromptFileNames();
Der Prompt Manager implementiert eine IPromptManager
Schnittstelle. Wenn Sie diese also über einen DI-Container oder ein IoC-Muster verwenden möchten, können Sie ganz einfach eine simulierte Version zum Testen bereitstellen.
Der Eingabeaufforderungsmanager kann auch eine IPromptStore
Instanz verwenden, mit der Sie einen benutzerdefinierten Speicher erstellen können, der möglicherweise nicht dateibasiert ist (siehe Erstellen eines benutzerdefinierten Eingabeaufforderungsspeichers). Dies ermöglicht auch die Bereitstellung einer simulierten Schnittstelle, sodass Sie Komponententests schreiben können, die nicht vom Speichermechanismus abhängig sind.
Verwenden des Prompt-Managers zum Lesen einer Eingabeaufforderung und deren anschließende Verwendung in einem Aufruf an einen Azure OpenAI-Endpunkt.
Hinweis: In diesem Beispiel wird davon ausgegangen, dass ein prompts
Verzeichnis mit der verfügbaren Prompt-Datei vorhanden ist.
using System.ClientModel;using Azure.AI.OpenAI;using DotPrompt;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("example");// Die System-Eingabeaufforderungs- und Benutzer-Eingabeaufforderungsmethoden verwenden Wörterbücher, die die für die Vorlage benötigten Werte enthalten. Wenn keine benötigt werden, können Sie einfach null.var systemPrompt = promptFile.GetSystemPrompt(null);var userPrompt = promptFile.GetUserPrompt(new Dictionary<string, object>{{ "topic", "bluetooth" },{ "style" übergeben , „Gebrauchtwagenverkäufer“ }});var client = openAiClient.GetChatClient(promptFile.Model ?? „default-model“);var summary = waiting client.CompleteChatAsync([new SystemChatMessage(systemPrompt),new UserChatMessage(userPrompt)],new ChatCompletionOptions(ResponseFormat = promptFile.OutputFormat == OutputFormat.Json ? ChatResponseFormat.JsonObject : ChatResponseFormat.Text,Temperature = promptFile.Config.Temperature,MaxTokens = promptFile.Config.MaxTokens));
Oder verwenden Sie die von OpenAI bereitgestellten Erweiterungsmethoden.
using System.ClientModel;using Azure.AI.OpenAI;using DotPrompt;using DotPrompt.Extensions.OpenAi;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("example");var promptValues = new Dictionary<string, object>{{ "topic", "bluetooth" },{ "style", "gebrauchtwagenverkäufer" }};var client = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var abgeschlossen = client erwarten .CompleteChatAsync(promptFile.ToOpenAiChatMessages(promptValues),promptFile.ToOpenAiChatCompletionOptions());var Response = Vervollständigung.Value;Console.WriteLine(response.Content[0].Text);
Und wenn wir jetzt unsere Eingabeaufforderung ändern müssen, können wir einfach die Eingabeaufforderungsdatei ändern und unseren Code in Ruhe lassen (vorausgesetzt, die Parameter ändern sich nicht).
Das Obige zeigt, wie Sie DotPrompt verwenden können, um Eingabeaufforderungsdateien von der Festplatte zu lesen. Aber was ist, wenn Sie eine Situation haben, in der Sie Ihre Eingabeaufforderungen an einem zentraleren Ort haben möchten, beispielsweise in einem Cloud-Speicherdienst oder in einer Datenbank? Nun, der Prompt-Manager kann eine IPromptStore
Instanz als Argument verwenden. In allen obigen Beispielen wird der mitgelieferte FilePromptStore
verwendet, Sie können aber auch Ihren eigenen erstellen. Es muss nur noch die Schnittstelle implementiert werden und schon sind Sie fertig.
Um Ihnen ein Beispiel zu geben, hier ist eine einfache Implementierung, die einen Azure Storage-Tabellenspeicher verwendet, um die Eingabeaufforderungsdetails zu speichern.
/// <summary>/// Implementierung des IPromptStore für Azure Storage Tables/// </summary>public class AzureTablePromptStore : IPromptStore{/// <summary>/// Lädt die Eingabeaufforderungen aus dem Tabellenspeicher/// < /summary>public IEnumerable<PromptFile> Load(){var tableClient = GetTableClient();var promptEntities = tableClient.Query<PromptEntity>(e => e.PartitionKey == "DotPromptTest");var promptFiles = promptEntities.Select(pe => pe.ToPromptFile()).ToList();return promptFiles;}/// <summary >/// Ruft einen Tabellenclient ab/// </summary>private static TableClient GetTableClient(){// Ersetzen Sie die Konfigurationselemente hier durch Ihren Wert oder wechseln Sie zu using// Entra-basierte Authentifizierungvar client = new TableServiceClient(new Uri($"https://{Configuration.StorageAccountName}.table.core.windows.net/"),new TableSharedKeyCredential(Configuration.StorageAccountName, Configuration.StorageAccountKey));var tableClient = client.GetTableClient("prompts");tableClient.CreateIfNotExists();return tableClient;}}/// <summary>/// Stellt einen in der Speichertabelle enthaltenen Datensatz dar/// </summary>public class PromptEntity : ITableEntity{/// <summary>/// Ruft den Partitionsschlüssel für den Datensatz ab und legt ihn fest/// </ Zusammenfassung>öffentliche Zeichenfolge PartitionKey { get; Satz; } = string.Empty;/// <summary>/// Ruft den Zeilenschlüssel für den Datensatz ab und legt ihn fest/// </summary>public string RowKey { get; Satz; } = string.Empty;/// <summary>/// Ruft den Zeitstempel des Eintrags ab und legt ihn fest/// </summary>public DateTimeOffset? Zeitstempel { get; Satz; }/// <summary>/// Ruft den ETag-Wert der Datensätze ab und legt ihn fest/// </summary>public ETag ETag { get; Satz; }/// <summary>/// Ruft das zu verwendende Modell ab und legt es fest/// </summary>public string? Modell { get; Satz; }/// <summary>/// Ruft das Ausgabeformat ab und legt es fest/// </summary>public string OutputFormat { get; Satz; } = string.Empty;/// <summary>/// Ruft ab und legt die maximale Anzahl an Token fest/// </summary>public int MaxTokens { get; Satz; }/// <summary>/// Ruft ab und legt die Parameterinformationen fest, die als JSON-Stringwert gespeichert sind/// </summary>public string Parameters { get; Satz; } = string.Empty;/// <summary>/// Ruft die Standardwerte ab, die als JSON-Stringwert gespeichert werden, und legt diese fest/// </summary>public string Default { get; Satz; } = string.Empty;/// <summary>/// Ruft die Systemeingabeaufforderungsvorlage ab und legt sie fest/// </summary>public string SystemPrompt { get; Satz; } = string.Empty;/// <summary>/// Ruft die Benutzeraufforderungsvorlage ab und legt sie fest/// </summary>public string UserPrompt { get; Satz; } = string.Empty;/// <summary>/// Gibt den Prompt-Entitätsdatensatz in eine <see cref="PromptFile"/>-Instanz/// </summary>/// <returns></returns>public zurück PromptFile ToPromptFile(){varparameter = new Dictionary<string, string>();var defaults = new Dictionary<string, object>();// Wenn Parameterwerte vorhanden sind, dann konvertieren Sie sie in ein Dictionaryif (!string.IsNullOrEmpty(Parameters)){var entityParameters = (JsonObject)JsonNode.Parse(Parameters)!;foreach (var (prop, propType) in entityParameters){parameters.Add(prop, propType?.AsValue().ToString () ?? string.Empty);}}// Wenn es Standardwerte gibt, dann konvertieren Sie sie in ein Wörterbuch (!string.IsNullOrEmpty(Default)){var entityDefaults = (JsonObject)JsonNode.Parse(Default)!;foreach (var (prop, defaultValue) in entityDefaults){defaults.Add(prop, defaultValue?.AsValue().GetValue <object>() ?? string.Empty);}}// Erzeuge die neue Eingabeaufforderungsdateivar promptFile = new PromptFile{Name = RowKey,Model = Model,Config = new PromptConfig{OutputFormat = Enum.Parse<OutputFormat>(OutputFormat, true),MaxTokens = MaxTokens,Input = new InputSchema{Parameters =parameter,Default = defaults}},Prompts = neue Eingabeaufforderungen{System = SystemPrompt,User = UserPrompt}};return promptFile;}}
Und um dies zu nutzen, würden wir Folgendes tun
var promptManager = new PromptManager(new AzureTablePromptStore());var promptFile = promptManager.GetPromptFile("example");
Hier gibt es noch viel zu tun, und einige der Punkte, die wir uns ansehen, sind:
Zusätzliche Konfigurationsmöglichkeiten
Zusätzliche Aufforderungstechniken
Offen für Feedback. Gibt es etwas, das Sie gerne sehen würden? Lassen Sie es uns wissen