☰
Semantic Kernel 自定义 Prompt 模板格式深度解析:从 IPromptTemplateEngine 到 IPromptTemplateFactory 的架构演进
2026/10/6 23:39:54 网站建设 项目流程

Semantic Kernel 自定义 Prompt 模板格式深度解析:从 IPromptTemplateEngine 到 IPromptTemplateFactory 的架构演进

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

本指南围绕 ADR 文档 0016-custom-prompt-template-formats.md 展开,系统讲解 Semantic Kernel 中自定义 Prompt 模板格式的设计脉络:先剖析旧版IPromptTemplateEngine引擎模型的三大缺陷与性能瓶颈,再完整还原IPromptTemplateFactory+IPromptTemplate工厂模型的决策过程、接口设计与最终落地实现(含 Handlebars、Liquid 官方模板格式),并对照当前仓库源码给出可直接运行的实战示例。读完你将掌握:如何让同一个 Kernel 同时支持多种模板格式、如何自定义模板工厂、以及模板编译性能优化背后的原理。

一、背景:为什么需要“自定义 Prompt 模板格式”

Semantic Kernel 内置了一套专属的 Prompt 模板语言,支持变量插值与函数执行,典型形态如:

Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?

这套语法解决了“把用户变量与原生函数结果拼进提示词”的基本诉求。但真实业务中,团队可能已经沉淀了其他模板生态(如前端团队常用的 Handlebars 语法、服务端模板 Liquid 语法),希望直接复用既有模板资产。因此,Semantic Kernel 需要一种机制,让自定义 Prompt 模板格式(例如 Handlebars 语法的模板)可以被无缝集成进来。本 ADR 正是这一能力的架构级设计文档。

二、旧版设计剖析:IPromptTemplateEngine的现状与问题

2.1 默认引擎与代码形态

默认情况下,Kernel使用BasicPromptTemplateEngine(即 Semantic Kernel 专属模板格式)。ADR 给出了“展开式”的完整示例,突出对kernel.PromptTemplateEngine的依赖:

IKernel kernel = Kernel.Builder .WithPromptTemplateEngine(new BasicPromptTemplateEngine()) .WithOpenAIChatCompletionService( modelId: openAIModelId, apiKey: openAIApiKey) .Build(); kernel.ImportFunctions(new TimePlugin(), "time"); string templateString = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?"; var promptTemplateConfig = new PromptTemplateConfig(); var promptTemplate = new PromptTemplate(templateString, promptTemplateConfig, kernel.PromptTemplateEngine); var kindOfDay = kernel.RegisterSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate); var result = await kernel.RunAsync(kindOfDay); Console.WriteLine(result.GetValue<string>());

其中kernel.CreateSemanticFunction(promptTemplate)扩展方法可以简化创建流程,但展开写法能清楚暴露kernel.PromptTemplateEngine这一硬依赖。ADR 同时指出:BasicPromptTemplateEngine是默认引擎,只要包可用且未显式指定其他引擎,它会被自动加载。

2.2 旧模型暴露的三个核心问题

ADR 明确列出旧设计的缺陷:

  1. 单引擎限制:Kernel只支持单个IPromptTemplateEngine,无法在同一时刻使用多种模板格式;
  2. 无状态导致重复解析:IPromptTemplateEngine是无状态的,每次渲染都必须重新解析模板;
  3. 状态纠缠:语义函数扩展方法依赖IPromptTemplate的具体实现PromptTemplate,它存储模板字符串、每次渲染都委托给引擎,且因为同时持有参数而“有状态”,职责不清。

2.3 性能数据:解析与渲染的代价分离

BasicPromptTemplateEngine先通过TemplateTokenizer解析模板(提取 Blocks),再渲染(插入变量、执行函数)。ADR 给出了实测时间(采样模板为"{{variable1}} {{variable2}} {{variable3}} {{variable4}} {{variable5}}"):

操作Ticks毫秒
提取 Blocks(Extract blocks)1044427103
渲染变量(Render variables)1680

同一用例下使用HandlebarsDotNet的对照数据:

操作Ticks毫秒
编译模板(Compile template)662776
渲染变量(Render variables)41730

两组数据揭示的核心洞察是:“解析/编译模板”与“渲染变量”是两个可以彻底分离的阶段。旧引擎每次渲染都重新走完整流程,而把编译与渲染分离后,模板只需编译一次即可反复渲染,从而获得数量级上的性能收益。这一结论直接催生了本 ADR 的决策方向。文档还注明,将用该示例实现来支持 f-string 模板格式。

2.4 旧版两个核心接口

public interface IPromptTemplateEngine { Task<string> RenderAsync(string templateText, SKContext context, CancellationToken cancellationToken = default); } public interface IPromptTemplate { IReadOnlyList<ParameterView> Parameters { get; } public Task<string> RenderAsync(SKContext executionContext, CancellationToken cancellationToken = default); }

ADR 还给出了一个 Handlebars 引擎的原型实现(明确标注仅用于演示):

public class HandlebarsTemplateEngine : IPromptTemplateEngine { private readonly ILoggerFactory _loggerFactory; public HandlebarsTemplateEngine(ILoggerFactory? loggerFactory = null) { this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; } public async Task<string> RenderAsync(string templateText, SKContext context, CancellationToken cancellationToken = default) { var handlebars = HandlebarsDotNet.Handlebars.Create(); var functionViews = context.Functions.GetFunctionViews(); foreach (FunctionView functionView in functionViews) { var skfunction = context.Functions.GetFunction(functionView.PluginName, functionView.Name); handlebars.RegisterHelper($"{functionView.PluginName}_{functionView.Name}", async (writer, hcontext, parameters) => { var result = await skfunction.InvokeAsync(context).ConfigureAwait(true); writer.WriteSafeString(result.GetValue<string>()); }); } var template = handlebars.Compile(templateText); var prompt = template(context.Variables); return await Task.FromResult(prompt).ConfigureAwait(true); } }

该原型暴露了两个问题:IPromptTemplate接口未被使用、造成困惑;且仍无法支持“同一时刻多种模板格式”。

2.5 Handlebars 的动态 Helper 绑定考量

ADR 用一段代码说明 Handlebars 的一个关键特性——Helper 的注册时机非常灵活:

HandlebarsHelper link_to = (writer, context, parameters) => { writer.WriteSafeString($"<a href='{context["url"]}'>{context["text"]}</a>"); }; string source = @"Click here: {{link_to}}"; var data = new { url = "https://github.com/rexm/handlebars.net", text = "Handlebars.Net" }; // Act var handlebars = HandlebarsDotNet.Handlebars.Create(); handlebars.RegisterHelper("link_to", link_to); var template = handlebars1.Compile(source); // handlebars.RegisterHelper("link_to", link_to); This also works var result = template1(data);

即 Helper 既可以在编译前注册,也可以在编译后注册。设计启示是:理想方案是让某个函数集合共享一个Handlebars实例、只注册一次 Helper;但如果 Kernel 的函数集合在运行期发生了变更(增删函数),则被迫在渲染时重建实例并重新注册 Helper,从而无法享受“编译一次、多次渲染”的性能优化。这一权衡被完整记录在 ADR 中,直接影响了后续“模板对象持有解析结果”的最终设计。

三、决策驱动因素与备选方案

3.1 五项决策驱动因素

ADR 明确列出(无先后顺序):

  • 支持不依赖IKernel实例直接创建语义函数;
  • 支持函数晚绑定,即函数在 Prompt 渲染时才被解析;
  • 支持模板只解析(编译)一次以优化性能;
  • 支持单个Kernel实例同时使用多种模板格式;
  • 提供简单抽象,让第三方可以轻松实现自定义模板格式。

3.2 备选方案

ADR 记录了两个候选方向:

  • 方案 A(最终采纳):废弃IPromptTemplateEngine,用IPromptTemplateFactory取代;
  • 方案 B:仅作为占位列出,未展开细节。

四、最终决策:IPromptTemplateFactory工厂模型

决策结果:采纳方案 A——“废弃IPromptTemplateEngine并替换为IPromptTemplateFactory”,理由是该方案完整覆盖了上述需求,并为未来演进保留了良好灵活性。

4.1 新模型的核心变化

决策后的设计(对应 ADR 中配图 prompt-template-factory.png)把职责一分为二:

  • IPromptTemplateFactory(工厂):根据PromptTemplateConfig.TemplateFormat选择并创建对应的IPromptTemplate实例;
  • IPromptTemplate(模板):持有模板字符串与配置,负责(且仅负责)渲染,并在内部缓存一次性的解析/编译结果。

语义函数可以脱离 Kernel 先创建,这是与旧模型最显著的区别。ADR 给出了新流程的完整示例:

// Semantic function can be created once var promptTemplateFactory = new BasicPromptTemplateFactory(); string templateString = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?"; var promptTemplateConfig = new PromptTemplateConfig(); // Line below will replace the commented out code var promptTemplate = promptTemplateFactory.CreatePromptTemplate(templateString, promptTemplateConfig); var kindOfDay = ISKFunction.CreateSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate) // var promptTemplate = new PromptTemplate(promptTemplate, promptTemplateConfig, kernel.PromptTemplateEngine); // var kindOfDay = kernel.RegisterSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate); // Create Kernel after creating the semantic function // Later we will support passing a function collection to the KernelBuilder IKernel kernel = Kernel.Builder .WithOpenAIChatCompletionService( modelId: openAIModelId, apiKey: openAIApiKey) .Build(); kernel.ImportFunctions(new TimePlugin(), "time"); // Optionally register the semantic function with the Kernel kernel.RegisterCustomFunction(kindOfDay); var result = await kernel.RunAsync(kindOfDay); Console.WriteLine(result.GetValue<string>());

关键注解:BasicPromptTemplateFactory是默认实现,会在KernelSemanticFunctionExtensions中自动提供,开发者也可提供自定义实现;工厂依据新的PromptTemplateConfig.TemplateFormat字段选择正确的IPromptTemplate;从CreateSemanticFunction中移除promptTemplateConfig参数属于本 ADR 范围之外的后续工作。

4.2 决策草案中的默认工厂与模板实现

ADR 给出了BasicPromptTemplateFactory与BasicPromptTemplate的参考实现,其核心逻辑(判断TemplateFormat是否等于PromptTemplateConfig.SEMANTICKERNEL,否则委托给内部工厂或抛出SKException)后来演化为当前仓库中的KernelPromptTemplateFactory与AggregatorPromptTemplateFactory(见下文)。参考实现中BasicPromptTemplate的亮点在于:ExtractBlocks通过Lazy<>惰性初始化、每个模板只解析一次,RenderAsync不再重复解析,这正是 ADR 性能考量从设计到代码的落地体现:

public sealed class BasicPromptTemplate : IPromptTemplate { public BasicPromptTemplate(string templateString, PromptTemplateConfig promptTemplateConfig, ILoggerFactory? loggerFactory = null) { this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; this._logger = this._loggerFactory.CreateLogger(typeof(BasicPromptTemplate)); this._templateString = templateString; this._promptTemplateConfig = promptTemplateConfig; this._parameters = new(() => this.InitParameters()); this._blocks = new(() => this.ExtractBlocks(this._templateString)); this._tokenizer = new TemplateTokenizer(this._loggerFactory); } public IReadOnlyList<ParameterView> Parameters => this._parameters.Value; public async Task<string> RenderAsync(SKContext executionContext, CancellationToken cancellationToken = default) { return await this.RenderAsync(this._blocks.Value, executionContext, cancellationToken).ConfigureAwait(false); } // Not showing the implementation details }

五、最终落地的接口与实现(当前仓库源码佐证)

ADR 的方案在后续版本中正式落地,当前仓库dotnet工程中的实现与 ADR 一脉相承,可以作为理解该决策的最佳“验收证据”。

5.1 稳定版抽象接口

在 SemanticKernel.Abstractions/PromptTemplate 目录下,抽象层已收敛为两个稳定接口:

  • IPromptTemplate.cs:渲染接口。签名已从旧版的RenderAsync(SKContext, ...)演化为面向新 Kernel 模型的RenderAsync(Kernel kernel, KernelArguments? arguments = null, CancellationToken cancellationToken = default),即“根据 Kernel 与参数渲染出提示词字符串”;
  • IPromptTemplateFactory.cs:工厂接口。核心方法是bool TryCreate(PromptTemplateConfig templateConfig, out IPromptTemplate? result),返回布尔值表示是否支持该格式,不支持时返回false而非抛异常。

配套的 PromptTemplateFactoryExtensions.cs 提供了Create扩展方法:当所有工厂都不支持某格式时抛出KernelException("Prompt template format ... is not supported.")。这种TryCreate+Create的组合,正是 ADR 中“工厂按格式分发、失败有明确反馈”思路的工程化。

5.2 格式标识符:PromptTemplateConfig.TemplateFormat

PromptTemplateConfig.cs 中定义了格式的“身份证”:

  • SemanticKernelTemplateFormat => "semantic-kernel":内置格式标识符,也是TemplateFormat属性为空时的默认值(见该文件TemplateFormat属性的AllowNull与回退逻辑);
  • 对应的 JSON 字段名为template_format。

PromptTemplateConfig还聚合了模板元数据:template(模板字符串)、input_variables(输入变量列表,见 InputVariable.cs)、output_variable(输出变量,见 OutputVariable.cs)、execution_settings(按 service ID 键控的执行设置)以及allow_dangerously_set_content(默认false,用于防范提示词注入)。工厂正是读取这些配置来构建IPromptTemplate。

5.3 默认工厂与聚合工厂

SemanticKernel.Core/PromptTemplate 目录下有两个关键实现:

  • KernelPromptTemplateFactory.cs:内置semantic-kernel格式的默认工厂。当TemplateFormat等于PromptTemplateConfig.SemanticKernelTemplateFormat时创建KernelPromptTemplate,否则返回false。其 XML 注释明确写道“当未提供其他工厂时,用作默认IPromptTemplateFactory”——与 ADR 中“默认实现自动提供”的约定完全一致;
  • AggregatorPromptTemplateFactory.cs:聚合工厂,接收多个IPromptTemplateFactory,TryCreate时按顺序遍历、返回第一个成功创建的模板。这正是 ADR 所追求“单个 Kernel 同时支持多种模板格式”的机制核心——将多个格式工厂聚合为一个。

5.4 官方扩展模板格式:Handlebars 与 Liquid

ADR 中以 Handlebars 为例,当前仓库已将其与 Liquid 一起做成官方扩展包:

Handlebars 格式(template_format: "handlebars"):

  • HandlebarsPromptTemplateFactory.cs:HandlebarsTemplateFormat => "handlebars",格式匹配时创建HandlebarsPromptTemplate;同时提供NameDelimiter(默认分隔符,来源于HandlebarsPromptTemplateOptions.PrefixSeparator)与AllowDangerouslySetContent(默认false,防提示词注入)配置项;
  • HandlebarsPromptTemplate.cs:渲染时创建HandlebarsDotNet.Handlebars.Create()实例,依次注册 Kernel 系统 Helper(KernelSystemHelpers)、HandlebarsDotNet.Helpers内置 Helper 与自定义 Helper,然后Compile+ 执行,最后按EnableHtmlDecoder选项决定是否做 HTML 解码。注意其渲染流程仍然“每次渲染都 Compile”,这印证了 ADR 中关于“Kernel 函数集合可能被变更、无法共享编译实例”的权衡讨论;
  • 配套的 HandlebarsPromptTemplateOptions.cs 控制分隔符、HTML 解码等行为。

Liquid 格式(template_format: "liquid"):

  • LiquidPromptTemplateFactory.cs:LiquidTemplateFormat => "liquid",同样遵循TryCreate模式,创建LiquidPromptTemplate,并提供AllowDangerouslySetContent配置。

两个扩展工厂的实现模式与 ADR 草案中的BasicPromptTemplateFactory高度一致:判断TemplateFormat→ 匹配则创建模板 → 不匹配则返回false。由此可见,ADR 提出的“单一工厂接口 + 按格式分发”抽象,为第三方实现自定义格式提供了极低的上手门槛。

六、实战:在同一个 Kernel 中使用多种模板格式

结合AggregatorPromptTemplateFactory与官方扩展包,可以按 ADR 设定的目标——“单个 Kernel 支持多种模板格式”——组织代码(以下代码体现当前仓库的 API 形态,可根据实际引用的包调整 using):

using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; using Microsoft.SemanticKernel.PromptTemplates.Liquid; // 1. 聚合多个格式工厂:内置 semantic-kernel 格式 + Handlebars + Liquid var factory = new AggregatorPromptTemplateFactory( new KernelPromptTemplateFactory(), new HandlebarsPromptTemplateFactory(), new LiquidPromptTemplateFactory()); var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion(modelId: openAIModelId, apiKey: openAIApiKey) .Build(); // 2. 内置格式:template_format 默认即 semantic-kernel var skConfig = new PromptTemplateConfig { Template = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?", }; var skTemplate = factory.Create(skConfig); // 3. Handlebars 格式:显式指定 template_format var hbConfig = new PromptTemplateConfig { TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat, Template = "Today is: {{time.Date}}", InputVariables = [ new() { Name = "time", Description = "Time plugin" } ], }; var hbTemplate = factory.Create(hbConfig); // 4. Liquid 格式 var liquidConfig = new PromptTemplateConfig { TemplateFormat = LiquidPromptTemplateFactory.LiquidTemplateFormat, Template = "Today is: {{ time.Date }}", }; var liquidTemplate = factory.Create(liquidConfig); // 5. 统一渲染:同一 Kernel、同一组参数,多种格式各取所需 var arguments = new KernelArguments(); var skPrompt = await skTemplate.RenderAsync(kernel, arguments); var hbPrompt = await hbTemplate.RenderAsync(kernel, arguments); var liquidPrompt = await liquidTemplate.RenderAsync(kernel, arguments);

要点总结:

  • 格式由配置驱动:同一个PromptTemplateConfig.TemplateFormat字段决定工厂分发到哪个实现,无需更换 Kernel;
  • 工厂可聚合:AggregatorPromptTemplateFactory按传入顺序逐个尝试,第一个成功者胜出;
  • 安全默认值:各工厂的AllowDangerouslySetContent默认均为false,在面向聊天补全服务时应保持关闭以防提示词注入;面向 Text-To-Image 等场景可显式开启以获得更复杂的提示词表达能力(这是 ADR 之后各实现类注释共同说明的约束)。

如需将模板进一步封装为 KernelFunction,可借助 Kernel 的CreateFunctionFromPrompt系扩展方法(内部同样走IPromptTemplateFactory创建模板),并结合 PromptTemplateConfig.FromJson 从 JSON 配置直接反序列化(注意该方法在无JsonSerializerOptions时依赖反射,不适用于 AOT 场景)。

七、设计启示与演进脉络

回顾整个 ADR,其价值不止于一次接口替换:

  1. 职责分离是主线:IPromptTemplateEngine同时承担“解析”与“渲染”,被拆解为“工厂选型” + “模板持有并缓存解析结果”两个清晰角色;
  2. 性能优化靠生命周期管理:解析/编译是一次性代价,渲染是重复性代价,只有把前者从渲染循环中剥离,才能实现“编译一次、渲染多次”;ADR 的时序数据(103ms 解析 vs 0ms 渲染、6ms 编译 vs 0ms 渲染)为这一判断提供了量化依据;
  3. 可扩展性来自格式分发:TemplateFormat作为配置开关 + 工厂按格式分发 + 聚合工厂组合,使第三方自定义格式成为“实现一个接口、注册一个工厂”的低成本动作;
  4. 权衡被显式记录:ADR 毫不回避 Handlebars 动态 Helper 注册与“函数集合可能变更”之间的矛盾——当函数集合在渲染期变化时,被迫牺牲编译缓存。这种诚实的权衡记录是架构决策文档最有价值的部分之一。

当前仓库中 KernelPromptTemplateFactory.cs、AggregatorPromptTemplateFactory.cs 以及 Handlebars/Liquid 两个扩展包,就是这份 ADR 从“决策”走向“实现”的完整答卷。开发者若想进一步验证其行为,可参考 dotnet 侧的单元测试目录(如 SemanticKernel.UnitTests/PromptTemplate)中围绕PromptTemplateConfig、模板渲染的测试用例。

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询