返回

Semantic Kernel避坑:如何处理{{}}特殊字符防报错

Ai

处理 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 提供了一套模板语法,让提示变得更动态、更强大。这套语法里,{{...}} 这对双大括号有着特殊的含义。它们通常用来:

  1. 插入变量值: 比如 {{$input}} 会把名为 input 的变量值填到这个位置。
  2. 调用函数: 比如 {{MyPlugin.MyFunction}} 会执行 MyPlugin 里的 MyFunction 并把结果插入。

当你提供的 prompt 字符串里直接包含了 {{ bla }} 或者 {{request}} 这样的文本时,Semantic Kernel 的模板引擎会尝试去解析它们。它看到 {{}},就以为这里应该是一个变量或者一个函数调用。

问题就出在 {{ bla }}{{request}} 这些可能并非你想调用的函数或变量名。模板引擎试着把括号里的内容(比如 bla 或者 request)当作一个函数标识符来处理。如果 blarequest 并非一个注册到 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 的变量,并将其 原样插入。关键在于,引擎在插入变量值时,不会 对这个值本身再次进行模板语法的解析。也就是说,就算变量的值里包含了 {{}},它们也会被当作普通文本插入,而不会触发函数调用或进一步的变量替换逻辑。

操作步骤:

  1. 修改提示模板: 将原来包含特殊字符的部分替换成一个变量占位符。

    // 原来的问题提示,我们把它拆分
    // string originalPrompt = "This is a test: { bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}";
    
    // 修改后的模板,用一个变量占位符替换掉 problematic part
    string promptTemplate = "This is a test: {{$problematicContent}}";
    
  2. 准备 KernelArguments: 创建一个 KernelArguments 对象(或者用字典等其他方式),把那段包含特殊字符的文本作为变量的值存进去。

    // 这是那段包含特殊字符的实际内容
    string contentWithBraces = "{ bla } {{ bla }} { $bla } {{ bla }} {{request}} {{choices.[0]}}";
    
    // 创建参数,变量名要和模板里的占位符匹配
    var arguments = new KernelArguments
    {
        { "problematicContent", contentWithBraces } // 注意,这里的 "problematicContent" 对应模板里的 {{$problematicContent}}
        // 如果你的 executionSettings 也在这里设置,一并加入
    };
     arguments.ExecutionSettings = executionSettings; // 假设 executionSettings 已定义
    
    
  3. 调用 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 字符串)作为变量值传入,然后在提示模板中简单地引用这个变量,让大模型自己去理解和处理这段结构化数据。这让提示模板保持简洁,把复杂性封装在变量里。
  • 动态内容生成: 这种方法对于动态生成的内容特别友好。无论程序在运行时构造出什么样包含特殊字符的字符串,只要把它赋值给一个变量,然后通过参数传入,就不用担心转义问题。

重点:为什么变量注入是更好的方法?

对比手动转义,变量注入的方式有几个显著的好处:

  1. 关注点分离: 它清晰地分开了提示的结构(模板)提示的内容(变量) 。模板负责流程和框架,变量负责填充具体数据。这让模板更易读、易维护。
  2. 利用框架能力: 这是 Semantic Kernel 设计用来处理动态内容的方式。你是在利用框架提供的机制,而不是试图“对抗”它的解析器。
  3. 降低风险: 避免了手动查找替换引入的错误。正则表达式虽然强大,但也容易写错,尤其是在处理嵌套或边缘情况时。
  4. 通用性: 不管你的内容字符串里包含什么(只要它是合法的字符串),这种方法都适用。

虽然它不是一个简单的 Escape(string) 函数调用,但通过参数化传递内容,实质上达到了让框架安全处理包含特殊字符文本的目的,可以说是 Semantic Kernel 处理这类问题的“内置方案”。

下次再遇到 {{}} 引发的 KernelException 时,先别急着写转义函数,试试看把那部分内容变成变量传进去,问题很可能就迎刃而解了。

相关资源