DotPrompt 是一个简单的库,它允许您使用基于配置的语法构建提示,而无需将它们嵌入到您的应用程序中。它支持通过 Fluid 模板语言对提示进行模板化,允许您重复使用相同的提示并在运行时传递不同的值。
提示文件就是任何以.prompt
扩展名结尾的文件。实际文件本身是一个 YAML 配置文件,扩展名允许库快速识别该文件以实现其预期目的。
.prompt
文件存在一个已知问题,会导致 Rider 和 IntelliJ 等工具出现异常行为。您可以通过禁用终端插件或使用不同的编辑器修改文件来解决此问题。
提示文件的内容包含一些顶级标识属性,后面是配置信息,最后是提示。
完整的提示文件如下所示。
名称:示例型号:gpt-4oconfig:输出格式:文本 温度:0.9 最大代币数:500 输入:参数:主题:字符串样式?:字符串默认:主题:社交媒体提示:系统:| 您是一位有用的研究助理,将为给定主题提供描述性答复以及它如何影响社会用户: | 解释一下 {{ topic }} 对我们作为一个社会如何参与技术的影响 {% if style -%} 你能用 {{ style }} {% endif -%}fewShots 的风格回答吗: - 用户:什么是蓝牙 回复:蓝牙是一种短距离无线技术标准,用于固定和移动设备之间短距离交换数据以及构建个人区域网络。 - 用户:机器学习与传统编程有何不同?回答:机器学习允许算法从数据中学习并随着时间的推移而改进,而无需明确编程。 - 用户:您能提供一个日常生活中人工智能的例子吗?回应:人工智能用于 Siri 和 Alexa 等虚拟助手,它们理解并响应语音命令。
该name
在配置中是可选的,如果未提供,则该名称取自文件名减去扩展名。因此,名为gen-lookup-code.prompt
的文件将获得名称gen-lookup-code
。这在提示本身的生成中不起作用(尽管将来的更新可能会起作用),但允许您在记录时识别提示源,并从提示管理器中选择提示。
如果使用此属性,则在加载文件时,名称将转换为小写,并且空格将替换为连字符。因此My cool Prompt
的名称将变为my-cool-prompt
。这样做是为了确保可以从代码中轻松访问该名称。
这是配置中的另一个可选项目,但它向提示文件的用户提供应使用哪个模型(或 Azure Open AI 的部署)的信息。由于如果未指定,该值可能为空,因此消费者应确保在使用前进行检查。例如:
var model =提示文件.Model ?? “我的默认”;
不过,使用此选项可以让提示工程师非常明确地了解他们打算使用哪种模型来提供最佳结果。
config
部分有一些顶级项目,供客户在其 LLM 调用中使用,以设置每个调用的选项。 outputFormat
属性采用text
或json
值,具体取决于 LLM 响应请求的方式。如果指定json
,则某些 LLM 需要系统或用户提示来声明预期输出也是 JSON。如果库在提示中没有检测到术语JSON
,那么它将在系统提示中附加一个小语句,请求响应采用 JSON 格式。
input
部分包含有关提供给提示的参数的详细信息。这些不是必需的,您可以创建根本不传递任何值的提示。但如果你这样做了,那么这些就是你所需要的。
input
下方是parameters
部分,其中包含键值对列表,其中键是参数的名称,值是参数的类型。如果您在参数名称后添加问号(例如style?
),则它被视为可选参数,并且如果您不为其提供值,则不会出错。
支持的类型有:
参数类型 | 点网类型 | C# 等效项 |
---|---|---|
细绳 | 系统字符串 | 细绳 |
布尔值 | 系统布尔值 | 布尔值 |
日期时间 | 系统日期时间偏移 | 系统日期时间偏移 |
数字 | 系统字节 系统.SByte 系统.UInt16 系统.Int16 系统.UInt32 系统.Int32 系统.UInt64 系统.Int64 系统.单一 系统.双 系统.十进制 | 字节 斯字节 超短 短的 单位 整数 乌龙 长的 漂浮 双倍的 小数 |
目的 | 系统对象 | 目的 |
前 4 个按提供的方式使用。传递给提示的对象将调用其ToString
方法以在提示中使用。
datetime
类型可以使用默认的ToString
表示形式显示,也可以使用 Fluid 的过滤器来指定其格式、更改时区等。
如果您为参数提供的值不符合指定的类型,则会引发错误。
input
中还有default
部分。此部分允许您指定任何参数的默认值。因此,如果您的应用程序中未提供该参数,则将使用默认值。
prompts
部分包含系统和用户提示的模板。虽然需要用户提示,但您不需要指定系统提示。
system
和user
提示都是字符串值,可以用 YAML 支持的任何方式定义。上面的示例使用多行字符串,其中保留回车符。
YAML 通过块标量对多行字符串值提供了很好的支持。有了这些,它支持文字字符串和折叠字符串。对于文字字符串,输入字符串中的新行字符将被保留,并且字符串将完全保持原样。折叠后,新行字符会折叠并替换为空格字符,从而允许您在多行上写入很长的字符串。使用折叠时,如果使用两个换行符,则会将换行符添加到字符串中。
# Foldedexample: > 船悬挂在天空中的方式与砖块的方式大致相同# Produces:# 船悬挂在天空中的方式与砖块的方式大致相同
# 文字示例: | 船悬挂在天空中的方式与砖块几乎相同#产生:#船悬挂在天空中的方式与砖块几乎相同#
提示的语法使用 Fluid 模板语言,该语言本身基于 Shopify 创建的 Liquid。这种模板语言允许我们定义用户提示,这些提示可以根据传递到模板解析器的值而变化。
在上面的示例中,您可以看到{{ topic }}
这是传入值的占位符,将直接替换到模板中。还有{% if style -%} ... {% endif -%}
部分,它告诉解析器仅在style
参数具有值时才包含此部分。标记末尾的-%}
包含连字符,它告诉解析器应该折叠空白行。
在线有一个关于使用 Fluid 编写模板的很棒的教程。
当您生成提示时,它不会替换模板,只会为您提供生成的输出。这意味着您可以使用不同的输入值多次生成提示。
fewShots
是一个允许提示编写者为解决方案提供少镜头提示技术的部分。在构建提示时,您将包括这些提示以及系统提示,然后是用户提示,这提供了有关 LLM 应如何响应用户提示的示例。如果您使用的是 OpenAI 或 Azure OpenAI,那么您可以使用扩展方法(见下文),这将为您创建所有消息。
可以直接访问提示文件。如果您只有几个文件或想要快速测试它们,那么这是一种相当简单的方法。
使用 DotPrompt;var PromptFile = PromptFile.FromFile("path/to/prompt-file.prompt");var systemPrompt = PromptFile.GetSystemPrompt(null);var userPrompt = PromptFile.GetUserPrompt(new Dictionary<string, object>{{ " topic", "蓝牙" },{ "风格", "二手车推销员" }});
如果提示文件包含上面的示例,那么它将生成以下内容。
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
这可能会导致法学硕士的回复如下(抱歉)
女士们先生们,聚集在一起,让我告诉你们现代技术的奇迹,它彻底改变了我们与电子设备的连接方式——我说的是蓝牙!蓝牙是无名英雄,是让我们的生活变得更加方便、更加互联、而且绝对更加高科技的秘密武器。想象一下:您喜爱的设备之间的无缝、无线通信。不再有缠结的电线,不再有混乱。这就像拥有未来前排的 VIP 通行证!
...
提示管理器是处理提示文件的首选方法。它允许您从某个位置加载它们,然后按名称访问,然后在您的应用程序中使用它们。
提示管理器的默认设置是访问本地prompts
夹中的文件,但您可以根据需要指定不同的路径。
// 从 `prompts` 目录的默认位置加载 varpromptManager = new PromptManager();varpromptFile =promptManager.GetPromptFile("example");// 使用不同的文件夹varpromptManager = new PromptManager("another-location"); var PromptFile = PromptManager.GetPromptFile("example");// 列出所有加载的提示var PromptNames = PromptManager.ListPromptFileNames();
提示管理器实现了IPromptManager
接口,因此如果您想通过 DI 容器或 IoC 模式使用它,那么您可以轻松提供模拟版本进行测试。
提示管理器还可以采用IPromptStore
实例,该实例允许您构建可能不是基于文件的自定义存储(请参阅创建自定义提示存储)。这还允许提供模拟接口,以便您可以编写不依赖于存储机制的单元测试。
使用提示管理器读取提示,然后在对 Azure OpenAI 终结点的调用中使用它。
注意:此示例假设有一个包含可用提示文件的prompts
目录。
使用 System.ClientModel;使用 Azure.AI.OpenAI;使用 DotPrompt;var openAiClient = new(new Uri("https://endpoint"), new ApiKeyCredential("abc123"));var PromptManager = new PromptManager();var PromptFile = PromptManager.GetPromptFile("example");// 系统提示和用户提示方法采用包含模板所需值的字典。如果不需要,您可以简单地传入 null.var systemPrompt = PromptFile.GetSystemPrompt(null);var userPrompt = PromptFile.GetUserPrompt(new Dictionary<string, object>{{ "topic", "bluetooth" },{ "style" , "二手车推销员" }});var client = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var finish = wait 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));
或者,使用 OpenAI 提供的扩展方法。
使用 System.ClientModel;使用 Azure.AI.OpenAI;使用 DotPrompt;使用 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", "二手车推销员" }};var client = openAiClient.GetChatClient(promptFile.Model ?? "default-model");var finish = wait client .CompleteChatAsync(promptFile.ToOpenAiChatMessages(promptValues),promptFile.ToOpenAiChatCompletionOptions());var response =完成.Value;Console.WriteLine(response.Content[0].Text);
现在,如果我们需要修改提示符,我们可以简单地更改提示符文件并保留我们的代码(假设参数不变)。
上面显示了如何使用 DotPrompt 从磁盘读取提示文件。但是,如果您希望提示位于更中心的位置(例如云存储服务或数据库),该怎么办?那么提示管理器可以将IPromptStore
实例作为参数。在上面的所有示例中,它都使用包含的FilePromptStore
,但您也可以构建自己的。只需要实现该接口即可。
举个例子,下面是一个简单的实现,它使用 Azure 存储表存储来保存提示详细信息。
/// <summary>/// Azure 存储表的 IPromptStore 实现/// </summary>public class AzureTablePromptStore : IPromptStore{/// <summary>/// 从表存储加载提示/// < /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 >/// 获取表客户端/// </summary>private static TableClient GetTableClient(){// 将此处的配置项替换为您的值或切换到using// 基于 Entra 的身份验证var client = new TableServiceClient(new Uri($"https://{Configuration.StorageAccountName}.table.core.windows.net/"),new TableSharedKeyCredential(Configuration.StorageAccountName, Configuration.StorageAccountKey)); var tableClient = client.GetTableClient("提示");tableClient.CreateIfNotExists();return tableClient;}}/// <summary>/// 表示存储表中保存的一条记录/// </summary>public class PromptEntity : ITableEntity{/// <summary>/// 获取、设置记录的分区键/// </摘要>公共字符串PartitionKey { 获取;放; } = string.Empty;/// <summary>/// 获取、设置记录的行键/// </summary>public string RowKey { get;放; } = string.Empty;/// <summary>/// 获取、设置条目的时间戳/// </summary>public DateTimeOffset?时间戳{获取;放; }/// <summary>/// 获取、设置记录ETag值/// </summary>public ETag ETag { get;放; }/// <summary>/// 获取、设置要使用的模型/// </summary>公共字符串?模型{得到;放; }/// <summary>/// 获取、设置输出格式/// </summary>public string OutputFormat { get;放; } = string.Empty;/// <summary>/// 获取、设置最大token数量/// </summary>public int MaxTokens { get;放; }/// <summary>/// 获取、设置以 JSON 字符串值保存的参数信息/// </summary>public stringParameters { get;放; } = string.Empty;/// <summary>/// 获取、设置以 JSON 字符串值形式保存的默认值/// </summary>public string Default { get;放; } = string.Empty;/// <summary>/// 获取、设置系统提示模板/// </summary>public string SystemPrompt { get;放; } = string.Empty;/// <summary>/// 获取、设置用户提示模板/// </summary>public string UserPrompt { get;放; } = string.Empty;/// <summary>/// 将提示实体记录返回到 <see cref="PromptFile"/> 实例中/// </summary>/// <returns></returns>public PromptFile ToPromptFile(){varparameters = new Dictionary<string, string>();var defaults = new Dictionary<string, object>();//如果有参数值则将其转换为字典if (!string.IsNullOrEmpty(Parameters)){varEntityParameters = (JsonObject)JsonNode.Parse(Parameters)!;foreach(var(prop,propType)inentityParameters){parameters.Add(prop,propType?.AsValue().ToString () ?? string.Empty);}}// 如果有默认值则将其转换为字典if (!string.IsNullOrEmpty(Default)){varEntityDefaults = (JsonObject)JsonNode.Parse(Default)!;foreach(var(prop,defaultValue)inentityDefaults){defaults.Add(prop,defaultValue?.AsValue().GetValue <object>() ?? string.Empty);}}// 生成新的提示文件varpromptFile = new PromptFile{Name = RowKey,Model = Model,Config = new PromptConfig{OutputFormat = Enum.Parse<OutputFormat>(OutputFormat,true),MaxTokens = MaxTokens,Input = new InputSchema{Parameters = 参数,Default = 默认值}},Prompts = new Prompts{System = SystemPrompt, User = UserPrompt}};返回提示文件;}}
然后要使用它,我们将执行以下操作
varpromptManager=newPromptManager(newAzureTablePromptStore());varpromptFile=promptManager.GetPromptFile(“示例”);
这里仍有工作空间,我们正在研究的一些项目包括
附加配置选项
额外的提示技巧
开放反馈。你有什么想看的吗?让我们知道