DotPrompt é uma biblioteca simples que permite criar prompts usando uma sintaxe baseada em configuração, sem a necessidade de incorporá-los ao seu aplicativo. Ele oferece suporte à modelagem de prompts por meio da linguagem de modelagem Fluid, permitindo reutilizar o mesmo prompt e transmitir valores diferentes em tempo de execução.
Um arquivo de prompt é simplesmente qualquer arquivo que termine com a extensão .prompt
. O arquivo em si é um arquivo de configuração YAML e a extensão permite que a biblioteca identifique rapidamente o arquivo para a finalidade pretendida.
Há um problema conhecido com arquivos .prompt
que causa comportamento incomum em ferramentas como Rider e IntelliJ. Você pode contornar isso desabilitando o plug-in Terminal ou usando um editor diferente para modificar os arquivos.
O conteúdo de um arquivo de prompt contém algumas propriedades de identificação de nível superior, seguidas por informações de configuração e, finalmente, pelos prompts.
Um arquivo de prompt completo ficaria assim.
nome: Exemplo de modelo: gpt-4oconfig: outputFormat: texto temperatura: 0,9 máximo de tokens: 500 input:parameters: topic: string style?: stringdefault: topic: social mediaprompts: system: | Você é um assistente de pesquisa útil que fornecerá respostas descritivas para um determinado tópico e como ele impacta a sociedade usuária: | Explique o impacto de {{ topic }} em como nos envolvemos com a tecnologia como sociedade {% if style -%} Você pode responder no estilo de {{ style }} {% endif -%}fewShots: - usuário: O que é Bluetoothresposta: Bluetooth é um padrão de tecnologia sem fio de curto alcance usado para troca de dados entre dispositivos fixos e móveis em distâncias curtas e construção de redes de área pessoal. - usuário: Como o aprendizado de máquina difere da programação tradicional?resposta: O aprendizado de máquina permite que algoritmos aprendam com os dados e melhorem ao longo do tempo sem serem explicitamente programados. - usuário: Você pode dar um exemplo de IA na vida cotidiana?resposta: A IA é usada em assistentes virtuais como Siri e Alexa, que entendem e respondem a comandos de voz.
O name
é opcional na configuração, se não for fornecido o nome será retirado do nome do arquivo menos a extensão. Portanto, um arquivo chamado gen-lookup-code.prompt
receberia o nome gen-lookup-code
. Isso não desempenha um papel na geração dos prompts em si (embora atualizações futuras possam fazê-lo), mas permite identificar a origem do prompt durante o registro e selecionar o prompt no gerenciador de prompts.
Se você usar esta propriedade, quando o arquivo for carregado, o nome será convertido para letras minúsculas e os espaços serão substituídos por hifens. Portanto, o nome My cool Prompt
se tornaria my-cool-prompt
. Isso é feito para garantir que o nome seja facilmente acessível a partir do código.
Este é outro item opcional na configuração, mas fornece informações ao usuário do arquivo de prompt sobre qual modelo (ou implantação para Azure Open AI) ele deve usar. Como isso pode ser nulo se não for especificado, o consumidor deve verificar antes do uso. Por exemplo:
var modelo = promptFile.Model ?? "meu padrão";
Usar esta opção permite que o engenheiro imediato seja muito explícito sobre qual modelo pretende usar para fornecer os melhores resultados.
A seção config
possui alguns itens de nível superior que são fornecidos para o cliente usar em suas chamadas LLM para definir opções em cada chamada. A propriedade outputFormat
assume um valor de text
ou json
dependendo de como o LLM pretende responder à solicitação. Se especificar json
, alguns LLMs exigirão que o sistema ou o prompt do usuário indiquem que a saída esperada também é JSON. Se a biblioteca não detectar o termo JSON
no prompt, ela anexará uma pequena instrução ao prompt do sistema solicitando que a resposta esteja no formato JSON.
A seção input
contém detalhes sobre os parâmetros fornecidos aos prompts. Eles não são obrigatórios e você pode criar prompts que não possuem nenhum valor sendo passado. Mas se você fizer isso, é disso que você precisa.
Abaixo input
está a seção parameters
que contém uma lista de pares chave-valor onde a chave é o nome do parâmetro e o valor é seu tipo. Se você adicionar um ponto de interrogação ao nome do parâmetro (por exemplo, style?
), ele será considerado um parâmetro opcional e não causará erro se você não fornecer um valor para ele.
Os tipos suportados são:
Tipo de parâmetro | Tipo Dotnet | Equivalente em C# |
---|---|---|
corda | System.String | corda |
bool | Sistema.Booleano | bool |
datahora | System.DateTimeOffset | System.DateTimeOffset |
número | Sistema.Byte Sistema.SByte Sistema.UInt16 Sistema.Int16 Sistema.UInt32 Sistema.Int32 Sistema.UInt64 Sistema.Int64 Sistema.Single Sistema.Duplo Sistema.Decimal | byte sbyte ucurto curto unint interno longe longo flutuador dobro decimal |
objeto | Sistema.Object | objeto |
Os primeiros 4 são usados conforme fornecido. Os objetos que são passados para o prompt terão seu método ToString
chamado para ser usado no prompt.
O tipo datetime
pode ser exibido com sua representação ToString
padrão ou você pode usar os filtros do Fluid para especificar seu formato, alterar o fuso horário e muito mais.
Se você fornecer um valor para um parâmetro que não esteja em conformidade com o tipo especificado, um erro será gerado.
Também na input
está a seção default
. Esta seção permite especificar valores padrão para qualquer um dos parâmetros. Portanto, se o parâmetro não for fornecido em seu aplicativo, o valor padrão será usado.
A seção prompts
contém os modelos para o sistema e os prompts do usuário. Embora o prompt do usuário seja obrigatório, não é necessário especificar um prompt do sistema.
Os prompts system
e user
são valores de string e podem ser definidos de qualquer forma compatível com YAML. O exemplo acima usa uma string multilinha onde os retornos de carro são preservados.
YAML tem ótimo suporte para valores de string multilinha por meio de Block Scalars. Com estes, ele suporta strings literais e dobradas . Com strings literais, os novos caracteres de linha na string de entrada são mantidos e a string permanece exatamente como escrita. Com dobrado, os caracteres da nova linha são recolhidos e substituídos por um caractere de espaço, permitindo escrever strings muito longas em várias linhas. Usando dobrado, se você usar dois caracteres de nova linha, uma nova linha será adicionada à string.
# Exemplo dobrado: > Os navios ficaram pendurados no céu da mesma maneira que os tijolos não# Produz:# Os navios ficaram pendurados no céu da mesma maneira que os tijolos não
#Exemplo literal: | Os navios pairavam no céu da mesma maneira que os tijolos não# Produz:# Os navios pairavam# no céu da mesma maneira# que os tijolos não
A sintaxe dos prompts usa a linguagem de modelagem Fluid, que é baseada no Liquid criado pelo Shopify. Essa linguagem de modelagem nos permite definir prompts do usuário que podem mudar dependendo dos valores passados para o analisador de modelo.
No exemplo acima você pode ver {{ topic }}
que é um espaço reservado para o valor que está sendo passado e será substituído diretamente no modelo. Há também a seção {% if style -%} ... {% endif -%}
que informa ao analisador para incluir esta seção apenas se o parâmetro style
tiver um valor. O -%}
no final do marcador contém o símbolo de hífen que informa ao analisador que ele deve recolher as linhas em branco.
Há um ótimo tutorial sobre como escrever modelos com Fluid disponível online.
Quando você gera o prompt, ele não substitui o modelo, apenas fornece a saída gerada. Isso significa que você pode gerar o prompt quantas vezes quiser com diferentes valores de entrada.
fewShots
é uma seção que permite ao redator do prompt fornecer técnicas de prompt de poucos disparos para a solução. Ao construir um prompt, você os incluiria, junto com o prompt do sistema e, em seguida, o prompt do usuário. Isso fornece exemplos de como o LLM deve responder ao prompt do usuário. Se estiver usando OpenAI ou Azure OpenAI, você poderá usar os métodos de extensão (veja mais tarde) que criarão todas as mensagens para você.
Os arquivos de prompt podem ser acessados diretamente. Se você tiver apenas alguns arquivos ou quiser testá-los rapidamente, esta é uma maneira bastante simples de fazer isso.
usando DotPrompt;var promptFile = PromptFile.FromFile("caminho/para/prompt-file.prompt");var systemPrompt = promptFile.GetSystemPrompt(null);var userPrompt = promptFile.GetUserPrompt(new Dicionário<string, objeto>{{ " topic", "bluetooth" },{ "style", "vendedor de carros usados" }});
Se o arquivo de prompt contivesse o exemplo acima, produziria o seguinte.
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
Isso pode resultar em uma resposta do LLM semelhante a esta (desculpe)
Senhoras e senhores, reúnam-se e deixem-me contar-lhes sobre o milagre da tecnologia moderna que revolucionou a forma como nos conectamos com nossos gadgets – estou falando do Bluetooth! Bluetooth é o herói anônimo, o ingrediente secreto que tem tornado nossas vidas mais convenientes, mais conectadas e, definitivamente, mais de alta tecnologia. Imagine isto: comunicação perfeita e sem fio entre seus dispositivos favoritos. Chega de cabos emaranhados, chega de bagunça. É como ter um passe VIP para a primeira fila do futuro!
...
O gerenciador de prompt é o método preferido para lidar com seus arquivos de prompt. Ele permite carregá-los de um local, acessá-los por nome e usá-los em seu aplicativo.
O padrão para o gerenciador de prompts é acessar arquivos na pasta local prompts
, embora você possa especificar um caminho diferente, se desejar.
// Carrega a partir do local padrão do diretório `prompts`var promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("example");// Use uma pasta diferentevar promptManager = new PromptManager("another-location"); var promptFile = promptManager.GetPromptFile("example");// Lista todos os prompts carregadosvar promptNames = promptManager.ListPromptFileNames();
O gerenciador de prompt implementa uma interface IPromptManager
e, portanto, se você quiser usá-la por meio de um contêiner DI ou padrão IoC, poderá fornecer facilmente uma versão simulada para teste.
O gerenciador de prompt também pode usar uma instância IPromptStore
que permite construir um armazenamento personalizado que pode não ser baseado em arquivo (consulte Criando um armazenamento de prompt personalizado). Isso também permite fornecer uma interface simulada para que você possa escrever testes de unidade que não dependam do mecanismo de armazenamento.
Usar o gerenciador de prompt para ler um prompt e usá-lo em uma chamada para um ponto de extremidade do Azure OpenAI.
NB Este exemplo assume que existe um diretório prompts
com o arquivo de prompt disponível.
usando System.ClientModel;usando Azure.AI.OpenAI;usando DotPrompt;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("example");// Os métodos de prompt do sistema e de prompt do usuário usam dicionários contendo os valores necessários para o // modelo. Se nenhum for necessário, você pode simplesmente passar null.var systemPrompt = promptFile.GetSystemPrompt(null);var userPrompt = promptFile.GetUserPrompt(new Dictionary<string, object>{{ "topic", "bluetooth" },{ "style" , "vendedor de carros usados" }});var cliente = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var conclusão = aguarde 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));
Ou usando os métodos de extensão fornecidos pela OpenAI.
usando System.ClientModel;usando Azure.AI.OpenAI;usando DotPrompt;usando DotPrompt.Extensions.OpenAi;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var promptManager = new PromptManager();var promptFile = promptManager.GetPromptFile("exemplo");var promptValues = novo Dicionário<string, objeto>{{ "topic", "bluetooth" },{ "style", "vendedor de carros usados" }};var client = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var conclusão = aguardar client.CompleteChatAsync(promptFile.ToOpenAiChatMessages(promptValues),promptFile.ToOpenAiChatCompletionOptions());var resposta = conclusão.Value;Console.WriteLine(response.Content[0].Text);
E agora, se precisarmos modificar nosso prompt, podemos simplesmente alterar o arquivo de prompt e deixar nosso código em paz (assumindo que os parâmetros não mudam).
A descrição acima mostra como você pode usar o DotPrompt para ler arquivos de prompt do disco. Mas e se você tiver uma situação em que deseja que seus prompts estejam em algum lugar mais central, como um serviço de armazenamento em nuvem ou um banco de dados? Bem, o gerenciador de prompt pode usar uma instância IPromptStore
como argumento. Em todos os exemplos acima está usando o FilePromptStore
que está incluído, mas você também pode construir o seu próprio. Basta implementar a interface e pronto.
Para dar um exemplo, aqui está uma implementação simples que usa um Armazenamento de Tabelas do Azure para armazenar os detalhes do prompt.
/// <summary>/// Implementação do IPromptStore para tabelas de armazenamento do Azure /// </summary>public class AzureTablePromptStore : IPromptStore{/// <summary>/// Carrega os prompts do armazenamento de tabelas /// < /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;}/// <resumo >/// Obtém um cliente de tabela /// </summary>private static TableClient GetTableClient(){// Substitua os itens de configuração aqui pelo seu valor ou mude para usar // autenticação baseada em Entraclientvar = 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>/// Representa um registro mantido na tabela de armazenamento /// </summary>public class PromptEntity : ITableEntity{/// <summary>/// Obtém, define a chave de partição para o registro /// </summary>public string PartitionKey { get; definir; } = string.Empty;/// <summary>/// Obtém, define a chave de linha para o registro /// </summary>public string RowKey { get; definir; } = string.Empty;/// <summary>/// Obtém, define o carimbo de data/hora da entrada /// </summary>public DateTimeOffset? Carimbo de data e hora {obter; definir; }/// <summary>/// Obtém, define o valor da ETag dos registros /// </summary>public ETag ETag { get; definir; }/// <summary>/// Obtém, define o modelo para usar /// </summary>public string? Modelo {obter; definir; }/// <summary>/// Obtém, define o formato de saída /// </summary>public string OutputFormat { get; definir; } = string.Empty;/// <summary>/// Obtém, define o número máximo de tokens /// </summary>public int MaxTokens { get; definir; }/// <summary>/// Obtém, define as informações do parâmetro que são mantidas como um valor de string JSON /// </summary>public string Parameters { get; definir; } = string.Empty;/// <summary>/// Obtém, define os valores padrão que são mantidos como um valor de string JSON /// </summary>public string Default { get; definir; } = string.Empty;/// <summary>/// Obtém, define o modelo de prompt do sistema /// </summary>public string SystemPrompt { get; definir; } = string.Empty;/// <summary>/// Obtém, define o modelo de prompt do usuário /// </summary>public string UserPrompt { get; definir; } = string.Empty;/// <summary>/// Retorna o registro da entidade de prompt em uma instância <see cref="PromptFile"/> /// </summary>/// <returns></returns>public PromptFile ToPromptFile(){var parâmetros = new Dictionary<string, string>();var defaults = new Dictionary<string, object>();// Se houver valores de parâmetros, converta-os em um dicionárioif (!) () ?? string.Empty);}}// Se houver valores padrão, converta-os em um dicionário se (!string.IsNullOrEmpty(Default)){var entidadeDefaults = (JsonObject)JsonNode.Parse(Default)!;foreach (var (prop, defaultValue) em entidadeDefaults){defaults.Add(prop, defaultValue?.AsValue().GetValue <object>() ?? string.Empty);}}// Gera o novo prompt filevar promptFile = novo PromptFile{Nome = RowKey,Model = Modelo,Config = novo PromptConfig{OutputFormat = Enum.Parse<OutputFormat>(OutputFormat, true),MaxTokens = MaxTokens,Input = novo InputSchema{Parâmetros = parâmetros,Default = padrões}}, Prompts = novos prompts{System = SystemPrompt,User = UserPrompt}};return promptFile;}}
E então para usar isso faríamos o seguinte
var promptManager = novo PromptManager(new AzureTablePromptStore());var promptFile = promptManager.GetPromptFile("exemplo");
Ainda há espaço para trabalho a ser feito aqui e alguns dos itens que estamos analisando incluem
Opções de configuração adicionais
Técnicas adicionais de solicitação
Aberto a comentários. Há algo que você gostaria de ver? Deixe-nos saber