Semantic Kernel避坑:如何处理{{}}特殊字符防报错
2025-04-17 13:16:35
处理 Semantic Kernel 提示中的特殊字符:告别 KernelException
写 Semantic Kernel 应用的时候,InvokePromptAsync
是个常用操作,用起来挺顺手。但有时候,要是你的提示(Prompt)里包含了一些特殊字符组合,比如 {{
和 }}
,麻烦就来了。
问题来了:恼人的 KernelException
看这段 C# 代码:
var result = await kernel.InvokePromptAsync(prompt, new(executionSettings));
通常情况下,它工作得好好的。可一旦 prompt
字符串里包含了像下面这样的内容:
{ bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}
程序就可能直接给你抛出一个 Unhandled exception. Microsoft.SemanticKernel.KernelException: The function identifier is empty
的错误。
瞅着挺吓人,对吧?
刨根问底:为什么会出错?
这事儿得从 Semantic Kernel 的提示模板语法说起。
Semantic Kernel 提供了一套模板语法,让提示变得更动态、更强大。这套语法里,{{...}}
这对双大括号有着特殊的含义。它们通常用来:
- 插入变量值: 比如
{{$input}}
会把名为input
的变量值填到这个位置。 - 调用函数: 比如
{{MyPlugin.MyFunction}}
会执行MyPlugin
里的MyFunction
并把结果插入。
当你提供的 prompt
字符串里直接包含了 {{ bla }}
或者 {{request}}
这样的文本时,Semantic Kernel 的模板引擎会尝试去解析它们。它看到 {{
和 }}
,就以为这里应该是一个变量或者一个函数调用。
问题就出在 {{ bla }}
或 {{request}}
这些可能并非你想调用的函数或变量名。模板引擎试着把括号里的内容(比如 bla
或者 request
)当作一个函数标识符来处理。如果 bla
或 request
并非一个注册到 Kernel 里的有效函数,或者像 {{ bla }}
这样在 {{
和 }}
之间还混杂了其他字符导致无法识别,引擎就懵了,因为它找不到对应的函数或者无法解析这个标识符。特别是像 {{}}
紧挨着的情况,里面的标识符直接就是空的,引擎自然就报怨“The function identifier is empty”(函数标识符是空的)。
简单说,就是你字符串里的 {{
和 }}
跟 Semantic Kernel 的模板语法规则“撞车”了。你想让它们作为普通文本显示,但 Semantic Kernel 却把它们当成了需要特殊处理的指令。
官方文档说了啥?
你可能也翻了官方文档,比如:
这些文档确实提到了如何处理需要包含双大括号本身的情况,以及引号的转义。它们建议了一些方法,比如使用 {{ '{{' }}
这样的方式来输出字面意义上的 {{
。
但这引出了你的核心问题:有没有一个内置的、通用的 方法来自动搞定这些特殊字符的转义,省得我们自己手动处理,还可能出错?
寻找答案:有现成的转义工具吗?
咱们直接说结论:截至目前(基于 Semantic Kernel .NET SDK 的常见版本),并没有提供一个像 SemanticKernel.Util.EscapePromptString(string prompt)
这样的单一、现成的 C# 工具方法,能自动帮你扫描整个 prompt
字符串并转义所有可能引起冲突的 {{
和 }}
序列。
Semantic Kernel 的转义机制更多是体现在模板解析 的过程中,特别是处理变量值 的时候,而不是提供一个预处理工具来“净化”整个原始模板字符串。
那么,面对这种情况,难道只能自己动手写正则替换吗?别急,有更好的办法,而且可以说是 Semantic Kernel 设计上更推荐的方式。
解决方案:如何绕过这个坑?
既然没有一键转义的魔法棒,咱们就得换个思路来解决问题。主要有两种策略:
方案一:手动转义(如果你能控制提示模板内容)
如果你的提示模板是相对固定的,或者你在构造它的时候完全有控制权,可以按照官方文档的指引进行手动转义。
原理和作用:
直接修改提示模板字符串,用 Semantic Kernel 能理解的转义语法来表示你想要输出的字面量 {{
和 }}
。文档提到过几种方式,核心思想是利用模板引擎自身的逻辑。比如,如果你想输出 {{hello}}
这段文本,而不是调用一个叫 hello
的函数或变量,你可以这样写:
{{ '{{hello}}' }}
这里利用了字符串字面量 '{{hello}}'
,然后外层的 {{ ... }}
告诉引擎把这个字符串字面量的值插入进来。
操作步骤:
假设你的原始问题提示是:
string problematicPrompt = "This is a test: { bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}";
你需要把它改成类似这样(具体转义方式可能需要根据确切需求调整,以下是一种可能的改法,侧重于转义 {{...}}
部分):
// 注意:这种手动修改可能很繁琐且容易出错
string manuallyEscapedPrompt = "This is a test: { bla } {{ '{{ bla }}' }} { $bla } {{ '{{ bla }}' }} {{ '{{request}}' }} {{ '{{choices.[0]}}' }}";
// 然后调用
var result = await kernel.InvokePromptAsync(manuallyEscapedPrompt, new(executionSettings));
提醒:
- 这种方法只适用于你能完全预知并控制提示模板内容的场景。
- 如果提示内容是动态生成或者来自用户输入,手动查找并替换
{{
和}}
会非常复杂、易错,而且维护起来很头疼。遇到嵌套或者更复杂的组合时,简直是灾难。 - 这种方法不推荐 用于动态或不可信来源的提示内容。
方案二:利用变量注入机制(推荐)
这是处理此类问题的首选 方法,也更符合 Semantic Kernel 的设计理念。
原理和作用:
核心思想是:不要把包含特殊字符的、你想作为纯文本处理的内容,直接硬编码在提示模板的主体里。 而是将这部分内容作为变量的值 传递给 InvokePromptAsync
。
当 Semantic Kernel 模板引擎遇到 {{$variableName}}
时,它会查找名为 variableName
的变量,并将其值 原样插入。关键在于,引擎在插入变量值时,不会 对这个值本身再次进行模板语法的解析。也就是说,就算变量的值里包含了 {{
或 }}
,它们也会被当作普通文本插入,而不会触发函数调用或进一步的变量替换逻辑。
操作步骤:
-
修改提示模板: 将原来包含特殊字符的部分替换成一个变量占位符。
// 原来的问题提示,我们把它拆分 // string originalPrompt = "This is a test: { bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}"; // 修改后的模板,用一个变量占位符替换掉 problematic part string promptTemplate = "This is a test: {{$problematicContent}}";
-
准备 KernelArguments: 创建一个
KernelArguments
对象(或者用字典等其他方式),把那段包含特殊字符的文本作为变量的值存进去。// 这是那段包含特殊字符的实际内容 string contentWithBraces = "{ bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}"; // 创建参数,变量名要和模板里的占位符匹配 var arguments = new KernelArguments { { "problematicContent", contentWithBraces } // 注意,这里的 "problematicContent" 对应模板里的 {{$problematicContent}} // 如果你的 executionSettings 也在这里设置,一并加入 }; arguments.ExecutionSettings = executionSettings; // 假设 executionSettings 已定义
-
调用 InvokePromptAsync: 使用修改后的模板和包含变量的参数对象来调用。
var result = await kernel.InvokePromptAsync(promptTemplate, arguments);
这样做的效果:
模板引擎解析 promptTemplate
时,看到 {{$problematicContent}}
。它会查找 arguments
里名为 problematicContent
的变量,找到它的值是 " { bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}} "
。然后,引擎会把这个完整的字符串 插入到占位符的位置。最终传递给底层模型(比如 OpenAI)的提示内容就会是:
This is a test: { bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}
在这个过程中,原始字符串里的 {{ bla }}
等内容被完整保留,没有被 Semantic Kernel 错误地解析成函数调用,自然也就避免了 KernelException
。
进阶使用技巧:
- 复杂内容结构化: 如果你有多个片段需要类似处理,可以用多个变量。甚至可以将复杂的结构(如 JSON 字符串)作为变量值传入,然后在提示模板中简单地引用这个变量,让大模型自己去理解和处理这段结构化数据。这让提示模板保持简洁,把复杂性封装在变量里。
- 动态内容生成: 这种方法对于动态生成的内容特别友好。无论程序在运行时构造出什么样包含特殊字符的字符串,只要把它赋值给一个变量,然后通过参数传入,就不用担心转义问题。
重点:为什么变量注入是更好的方法?
对比手动转义,变量注入的方式有几个显著的好处:
- 关注点分离: 它清晰地分开了提示的结构(模板) 和提示的内容(变量) 。模板负责流程和框架,变量负责填充具体数据。这让模板更易读、易维护。
- 利用框架能力: 这是 Semantic Kernel 设计用来处理动态内容的方式。你是在利用框架提供的机制,而不是试图“对抗”它的解析器。
- 降低风险: 避免了手动查找替换引入的错误。正则表达式虽然强大,但也容易写错,尤其是在处理嵌套或边缘情况时。
- 通用性: 不管你的内容字符串里包含什么(只要它是合法的字符串),这种方法都适用。
虽然它不是一个简单的 Escape(string)
函数调用,但通过参数化传递内容,实质上达到了让框架安全处理包含特殊字符文本的目的,可以说是 Semantic Kernel 处理这类问题的“内置方案”。
下次再遇到 {{}}
引发的 KernelException
时,先别急着写转义函数,试试看把那部分内容变成变量传进去,问题很可能就迎刃而解了。
相关资源
- Semantic Kernel Prompt template syntax - 官方文档,详细解释了模板语法,包括变量和函数调用。