Semantic Kernel推广
提示词工程通过具体化、格式化、提供示例、设定拒答策略及上下文等策略提升大模型输出可预测性。SemanticKernel支持内联提示、变量定义及txt/yaml文件模板,并具备变量嵌入、外部函数调用等模板特性。
提示词工程:让大模型输出更可预测的实操指南
想让大模型的输出更可靠、更可控?提示词工程是最直接、最有效的方法。不过,方法虽多,真正落地时仍有不少注意事项。下面结合 Semantic Kernel 的实践,介绍几个关键策略和具体实现方式。
提示词工程的核心方法
为了让大模型的结果更可预测,最有效的做法就是运用提示工程。常见且有效的方法包括:

- 使提示更具体
- 使用格式化输出为结果添加结构
- 提供带有少量示例的提示
- 告诉AI该做什么、避免什么(设定拒答策略)
- 为AI提供上下文
- 在聊天完成提示中使用角色
- 鼓励AI运用思维链
- 使用更优质的模型
详细内容可参考 Prompt Engineering 相关文档。值得注意的是,在 Semantic Kernel 中定义角色的写法如下:
测试准备工作
// 根据上面的文章获取IChatCompletionService
var chatCompletionService = kernel.GetRequiredService();
内联提示词
内联提示词,即提示词直接写在代码中。对于简单提示词,直接内联在代码中无可厚非;但对于复杂提示词,尤其是涉及参数配置时,代码管理变得不便。下面是一个“将文案改写为小红书风格的提示词”的内联示例:
using Microsoft.SemanticKernel.Connectors.OpenAI;
var article = """9月6日16时20分,今年第11号超强台风“摩羯”在海南文昌沿海登陆。超强台风“摩羯”风力有多大?记者只能“抱团”出镜。""";
var prompt = $"""请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。
{article}""";
// 参数配置
PromptExecutionSettings settings = new OpenAIPromptExecutionSettings()
{
Temperature = 1,
TopP = 1,
FrequencyPenalty = 0,
PresencePenalty = 0
};
settings.Display();
var response = await chatCompletionService.GetChatMessageContentAsync(prompt, settings);
response.Content.Display();
运行结果示例:
超强台风“摩羯”来袭!
9月6日16时20分,超强台风“摩羯”(今年第11号)猛烈袭击了海南文昌沿海。风力到底有多强?记者们只能“抱团”出镜报道,现场形势严峻!
#台风摩羯 #海南天气 #记者报道
这里用到的几个关键参数说明如下:
| 参数名称 | 类型 | 是否必需 | 默认值 | 描述 |
|---|---|---|---|---|
| max_tokens | 数字 | 可选 | null | 生成的最大标记数。提示的标记数加上max_tokens不能超过模型的上下文长度。 |
| temperature | 数字 | 可选 | 1 | 采样温度。值越高,模型越大胆。适合创意写作或需要明确答案的应用。 |
| top_p | 数字 | 可选 | 1 | 核采样方法,仅考虑累积概率质量大于或等于top_p的最小标记集。 |
| presence_penalty | 数字 | 可选 | 0 | 介于-2到2之间的值,惩罚新标记是否出现在文本中,减少其出现的可能性。 |
| frequency_penalty | 数字 | 可选 | 0 | 介于-2到2之间的值,惩罚新标记在文本中的现有频率,减少其重复的可能性。 |
定义变量
上述内联提示词使用的是字符串拼接方式。实际上,Semantic Kernel 提供了更优雅的语法 {{$variableName}} 来定义变量。上面的例子可以改写为:
var prompt = """请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。
{{ $article }}""";
// 参数配置
PromptExecutionSettings settings = new OpenAIPromptExecutionSettings()
{
Temperature = 1,
TopP = 1,
FrequencyPenalty = 0,
PresencePenalty = 0
};
var rewriteFunction = kernel.CreateFunctionFromPrompt(prompt, settings);
var article = """9月6日16时20分,今年第11号超强台风“摩羯”在海南文昌沿海登陆。超强台风“摩羯”风力有多大?记者只能“抱团”出镜。""";
var rewriteResult = await kernel.InvokeAsync(rewriteFunction, new() { ["article"] = article });
如果不需要额外指定参数,还可以进一步简化:
var prompt = """请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。
{{$article}} """;
var article = """9月6日16时20分,今年第11号超强台风“摩羯”在海南文昌沿海登陆。超强台风“摩羯”风力有多大?记者只能“抱团”出镜。""";
var result = await kernel.InvokePromptAsync(prompt, new() { ["article"] = article });
Console.WriteLine(result);
运行结果:
**超强台风“摩羯”登陆海南!**
9月6日16时20分,今年的第11号超强台风“摩羯”在海南文昌沿海地区强势登陆。 风力强劲到何种程度?记者们只能紧紧“抱团”出现在镜头前。
#台风#海南#强风
文件模板提示词
以上演示了如何创建和运行内联提示,并使用了变量。但在实际项目中,更常见的做法是将提示词放在单独的文件里,以便跨项目复用和共享。
txt 格式提示模板 (skprompt.txt & config.json)
首先来看如何用 txt 格式将提示词模板化并保存到文件中。创建如下目录结构:
Prompts
│
└─── RewriteRedBookStyle
|└─── config.json
└─── skprompt.txt
RewriteRedBookStyle 代表一个 Plugin,而 skprompt.txt 则是一个语义函数(Semantic Function),具体细节在后续介绍 Plugin 时会展开。
skprompt.txt 用于定义提示词内容:
请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。
{{$article}}
config.json 用于配置:
{
"schema": 1,
"type": "completion",
"description": "A function that rewrite with Emoji style",
"execution_settings": {
"default": {
"max_tokens": 1000,
"temperature": 0
}
},
"input_variables": [
{
"name": "article",
"description": "The user's request.",
"required": true
}
]
}
各配置项含义如下:
| 英文术语 | 中文翻译 | 描述 |
|---|---|---|
| type | 类型 | 提示的类型。这里使用的是 completion 类型。 |
| description | 描述 | 提示的作用描述,用于自动编排计划与功能。 |
| completion | 完成设置 | 完成模型的设置。对于 OpenAI 模型,包括最大令牌数和温度属性。 |
| max_tokens | 最大令牌数 | 模型生成的最大令牌数。 |
| temperature | 温度 | 控制生成文本的随机性。 |
| input | 输入 | 定义提示中使用的变量(例如,输入)。 |
接下来通过以下方式使用文件中定义的提示词模板:
var kernelPlugins = kernel.CreatePluginFromPromptDirectory("Prompts");
var article = @"9月6日16时20分,今年第11号超强台风“摩羯”在海南文昌沿海登陆。超强台风“摩羯”风力有多大?记者只能“抱团”出镜。";
var result = await kernel.InvokeAsync(kernelPlugins["RewriteRedBookStyle"], new() { ["article"] = article });
result.Display();
yaml 格式提示模板
txt 格式将提示词和配置拆分为两个文件,这是较为主流的方案。但 Semantic Kernel 也支持将二者合并为一个 yaml 文件,不过需要额外引入 NuGet 包:Microsoft.SemanticKernel.Yaml。
#r "nuget:Microsoft.SemanticKernel.Yaml"
仍以上述例子为基础进行改写。在 Prompts 文件夹下定义一个 RewriteEmojiStyle 文件夹,里面放入一个 rewritwithemoji.yaml 文件。
Prompts
│
└─── RewriteEmojiStyle
| └─── rewritwithemoji.yaml
rewritewithemoji.yaml 的内容如下:
name: RewriteEmojiStyle
template: |
请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。
{{$article}}
template_format: semantic-kernel
description: A function that rewrite with Emoji style.
input_variables:
- name: article
description: The user's request.
is_required: true
output_variable:
description: The rewrite emoji content.
execution_settings:
default:
temperature: 0.6
然后用以下方式调用 yaml 格式的提示词模板:
var prompt = await File.ReadAllTextAsync("Prompts/RewriteEmojiStyle/rewirtewithemoji.yaml");
var rewriteFunction = kernel.CreateFunctionFromPromptYaml(prompt);
var article = @"9月6日16时20分,今年第11号超强台风“摩羯”在海南文昌沿海登陆。超强台风“摩羯”风力有多大?记者只能“抱团”出镜。";
var result = await kernel.InvokeAsync(rewriteFunction, new() { ["article"] = article });
result.Display();
SK 提示词模板
Semantic Kernel 自带的提示词模板是一种使用纯文本定义和组合 AI 函数的简单方法。你可以用它来创建自然语言提示、生成响应、提取信息、调用其他提示,或者执行任何其他可以用文本表示的任务。该模板支持三个基本特性:
- 包含变量
- 调用外部函数
- 将参数传递给函数
无需编写任何代码或导入外部库,只需使用双花括号 {{…}} 在提示词中嵌入表达式。语义内核会解析模板并执行其背后的逻辑。这样一来,你可以轻松地将 AI 集成到产品中。
Variables(变量)
要在提示符中包含变量值,使用 {{$VariableName}} 语法。
例如,如果你有一个名为 name 的变量来保存用户名,可以这样写:
Hello {{$name}}, welcome to Semantic Kernel!
花括号中的空格可被忽略,也可以为提高可读性而适当添加空格:
Hello {{ $name }}, welcome to Semantic Kernel!
Function calls(函数调用)
要调用外部函数并将结果嵌入提示符中,使用 {{namespace.FunctionName}} 语法。例如,如果有一个名为 weather.getForecast(位于 weather 命名空间下)的函数,它返回给定位置的天气预报。
namespace weather;
[KernelFunction("getForecast")]
[Description("获取给定位置的天气预报")]
[return: Description("指定位置的天气预报")]
public Weather GetForecast(string localtion)
{
//....
}
可以这样编写提示词:
The weather today is {{weather.getForecast}}.
上面的提示词等价于:
The weather today is {{weather.getForecast $localtion}}
因为 SK 调用函数时会自动根据函数定义的参数名来传递函数参数。
Function Parameters(函数参数)
调用外部函数并向其传递参数,使用 {{namespace.functionName $varName}} 和 {{namespace.functionName "value"}} 语法。
例如,如果要向天气预报函数传递不同的输入,可以编写:
The weather today in {{city}} is {{weather.getForecast city}}.
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class WeatherPlugin
{
[KernelFunction("GetForecast")]
[Description("获取给定位置的天气预报")]
[return: Description("指定位置的天气预报")]
public static string GetForecast( stringlocation )
{
return $"Sunny, 23℃";
}
}
var prompt = "The weather today in {{ $location }} is {{GetForecast}}.";
kernel.Plugins.Clear();
kernel.Plugins.AddFromType();
var res = await kernel.InvokePromptAsync(prompt, new (){ ["location"]= "Shenzhen" });
res.Display();
The weather in Shenzhen today is sunny with a temperature of 23 degrees Celsius.
var prompt = "The weather today in {{$city}} is {{GetForecast $city}}.";
kernel.Plugins.Clear();
kernel.Plugins.AddFromType();
var res = await kernel.InvokePromptAsync(prompt, new (){ ["city"]= "Shenzhen" });
res.Display();
The weather in Shenzhen today is sunny with a temperature of 23 degrees Celsius.
注意事项
关于特殊字符的注意事项:语义函数模板是文本文件,因此无需转义换行符和制表符等特殊字符。但有两种情况需要特殊语法处理。
提示模板中需要包含双花括号
双花括号有一个特殊的用途——它们被用于注入变量、值和函数。如果你需要在提示中包含 {{ 和 }} 序列,这可能会触发特殊的渲染逻辑。最好的解决方案是用引号括起来:
{{ "{{" }} and {{ "}}" }}
举例:
{{ "{{" }} and {{ "}}" }} are special SK sequences.
渲染结果:
{{ and }} are special SK sequences.
var prompt = """{{ "{{" }} and {{ "}}" }} are special SK sequences.""";
var promptTemplateFactory = new KernelPromptTemplateFactory();
promptTemplateFactory.TryCreate(new PromptTemplateConfig(prompt), out var template);
var renderedPrompt = await template.RenderAsync(kernel);
renderedPrompt.Display();
{{ and }} are special SK sequences.
包含引号
值可以用单引号和双引号括起来。为了避免混淆,可以这样处理:
当使用包含单引号的值时,建议用双引号括起来。
...text... {{ functionName "one 'quoted' word" }} ...text...
当使用包含双引号的值时,用单引号括起来。
...text... {{ functionName 'one "quoted" word' }} ...text...
简而言之,用双引号括住单引号,用单引号括住双引号。
var prompt = """{{ "'shengjie'"}} {{'"shengjie"' }} """;
var promptTemplateFactory = new KernelPromptTemplateFactory();
var config = new PromptTemplateConfig(prompt);
config.AllowDangerouslySetContent = true;
promptTemplateFactory.TryCreate(config, out var template);
var renderedPrompt = await template.RenderAsync(kernel);
renderedPrompt.Display();
'shengjie' "shengjie"
使用转义符
对于同时包含单引号和双引号的值,需要使用 \ 进行转义。
- 需要双引号时,使用
":... {{ "quotes' "escaping" example" }} ... - 需要单引号时,使用
':... {{ 'quotes' "escaping" example' }} ... - 需要反斜杠时,使用
\\:... {{ 'c:documentsai' }} ...
前两个示例都会渲染为:... quotes' "escaping" example ...
注意:' 总是渲染为 '," 总是渲染为 "。如果需要渲染 ' 或 ",则需要使用 ' 或 "。
var prompt = """{{ 'quotes' "escaping" example' }}""";
var promptTemplateFactory = new KernelPromptTemplateFactory();
var config = new PromptTemplateConfig(prompt);
config.AllowDangerouslySetContent = true;
promptTemplateFactory.TryCreate(config, out var template);
var renderedPrompt = await template.RenderAsync(kernel);
renderedPrompt.Display();
quotes' "escaping" example
