使用 SIML(合成智能标记语言)为 C# 应用程序添加聊天机器人 - 第二部分






4.95/5 (12投票s)
延续文章《使用 SIML(合成智能标记语言)将聊天机器人集成到 C# 应用程序中》。
引言
之前,我介绍了 SIML 及其一些基本功能。在本文中,我们将深入探讨。
- 请先阅读“前一篇文章 - 第一部分”,然后再继续阅读本文。
通过递归进行缩减
递归的核心思想是帮助开发人员尽可能少地使用模式来匹配尽可能多的用户输入。
考虑这三个句子:
- “beatboxing”这个词是什么意思?
- “beatboxing”是什么意思?
- 定义“beatboxing”
尽管在 SIML 中,我可以将这三个模式都写在同一个 Model 中,但我可能会遇到一种情况,即两名或多名开发人员可能正在处理同一个 SIML 知识库,而我唯一的权限是添加一个新 Model 在一个单独的文件中,并保持主知识库不变。
这里的技巧是将用户输入分解为最简单的形式,然后重试模式搜索。考虑到上述句子,最简单的句子将是“定义 beatboxing”。
所以一个 SIML 代码示例是:
<Model>
<Pattern>DEFINE *</Pattern>
<Response>Let me search the dictionary for the word <Match />...</Response>
</Model>
<Model>
<Pattern>
<Item>WHAT IS THE MEANING OF THE WORD *</Item>
<Item>WHAT DOES * MEAN</Item>
</Pattern>
<Response>
<Goto>Define <Match /></Goto>
</Response>
</Model>
第一个 SIML Model 处理与词义查询相关的最简单的可能模式。然而,第二个 Model 中没有任何响应,它所做的只是将模式 “单词 * 的含义是什么” 和 “* 的意思是什么” 重定向到 “定义 *”。我们在第二个 Model 中使用的元素是 <Goto> SIML 元素。这是重定向元素,它没有任何属性,其唯一功能是将模式搜索重定向到新值。
所以现在,如果您在控制台中键入 “单词 beatboxing 的含义是什么”,第一个 Model 将被激活,响应将是 “让我为您查找 beatboxing 这个词的字典”。
但是,如果您的模式中有两个或多个通配符怎么办?您将如何重定向模式搜索?
您可以通过在 <Match/>
元素中指定索引来做到这一点。假设您没有使用 “单词 * 的含义是什么”,而是使用了模式 “* 的含义是什么 *”。在这种情况下,我们的重定向应指定要使用的通配符的索引。
SIML 代码示例
<Model>
<Pattern>* meaning of the word *</Pattern>
<Response>
<Goto>Define <Match Index="2"/></Goto>
</Response>
</Model>
在上面的示例中,在 <Match/>
元素内,我添加了一个名为 Index
的属性,该属性的值设置为“2
”。因此,现在如果 <Match />
元素的值被计算,它将返回我们模式中第二个通配符捕获的值。同样,您可以使用 3、4、5 等来指定模式中相应通配符的索引。
<Goto>
元素的另一个派生是 <GotoMatch />
元素,它是...的快捷标签。
<Goto><Match/></Goto>
但这并没有结束。就像 <Match />
元素一样,<GotoMatch />
元素也有一个 Index
属性。这就是为什么我称它为 <Goto>
元素的派生。因此,如果您指定一个像 <GotoMatch Index="2"/>
这样的 Index
,快捷标签将被解释为...
<Goto><Match Index="2"/></Goto>
<GotoMatch />
元素的一个简单用例
<Model>
<Pattern>CAN YOU *</Pattern>
<Response>Yes I can. <GotoMatch /></Response>
</Model>
假设您正在处理本文附加的 SIML 项目,如果您在控制台中键入 “你能播放音乐吗?”,输出将是 “是的,我可以。正在播放音乐。”。
- 在考虑重定向时,请始终尝试指定最简单的可能模式。
- 避免元重定向。不要将模式搜索重定向到可能再次尝试将新模式重定向到更新模式的新模式。
- 如果可能,请坚持在同一个 Model 中使用多个模式,只有在您非常确定您重定向到的新模式将为指定模式提供响应时,才使用重定向。
- 如果 2 个或更多重定向指向彼此。则解释器会自动终止或超时聊天请求。
请求与输入
每当用户尝试与机器人互动时,他或她都会创建一个 ChatRequest
。Chat Request
是发送到机器人的完整用户消息。所以,如果您键入 “你好吗?你在做什么?”。Chat Request
将以原始格式保存整个消息。
SIML 解释器不处理 ChatRequest
;它只处理 Input
。Input
是由 SIML 解释器处理的单个句子。因此,上面的聊天请求 “你好吗?你在做什么?” 将被分解为两个输入:“你好吗?” 和 “你在做什么?”。然后将分别处理这两个输入,并将合并结果发送回给用户。
因此,如果用户在某个时候尝试测试机器人以检查它是否记得之前的请求,您可以使用 <Request>
或 <Input>
元素。
检索整个用户消息的 SIML 代码示例
<Model>
<Pattern>What did I say</Pattern>
<Response>You said "<Request />"</Response>
</Model>
在用户输入 “我说了什么?” 后,上面的 SIML 代码将返回 “你好吗?你在做什么?”。
检索最后一个用户句子的 SIML 代码示例
<Model>
<Pattern>What did I just say</Pattern>
<Response>You said "<Input />"</Response>
</Model>
在用户输入 “我刚才说了什么?” 后,上面的 SIML 代码将返回 “你在做什么?”。 这是因为 <Input> 元素返回机器人处理的最后一个用户句子,而不是发送给机器人的整个消息(聊天请求)。
重要的是要注意,<Request>
和 <Input>
元素都提供 Index
属性。Index
属性使您能够使用索引号向后引用之前的聊天请求或之前的用户输入。
例如,在上面的 <Input>
示例中,如果您选择返回 “你好吗?” (第一个句子而不是第二个句子),而不是 “你在做什么?”。
<Model>
<Pattern>What was the first thing I said</Pattern>
<Response>You wrote "<Input Index="2"/>"</Response>
</Model>
上面的 SIML 代码中的 Input
元素内的 Index
属性现在引用用户输入中的第一个句子。要向后引用用户输入,开发人员需要牢记用户输入是以降序获取的。
- “你在做什么?” - 第二个或最后一个句子(索引 1 - 如果未指定 Index 属性,则为默认索引值)
- “你好吗?” - 第一个句子(索引 2)
结果与输出
就像聊天 Request
代表用户的整个消息一样,SIML 中的 Result
代表机器人的整个先前话语。同样,就像 <Input>
元素表示前一个用户输入的最后一个句子一样,<Output>
元素表示前一个机器人话语的最后一个句子。
提取机器人最后一个话语的 SIML 代码示例。
<Model>
<Pattern>WHAT DID YOU SAY</Pattern>
<Response>I said "<Result />"</Response>
</Model>
从机器人前一个话语中提取最后一个句子的 SIML 代码示例
<Model>
<Pattern>WHAT DID YOU JUST SAY</Pattern>
<Response>I said "<Output />"</Response>
</Model>
现在,如果用户说 “你好吗?你在做什么?” 并且机器人回复 “我很好。只是在和用户聊天。”,我们可以分别使用 <Result>
或 <Output>
元素来检索机器人的整个先前话语或仅检索最后一个句子。
不出所料(就像 <Request>
和 <Input>
一样),<Result>
和 <Output>
元素都有一个 Index
属性,可用于向后引用机器人的先前话语。
变量和运算符
在 SIML 中(如前所述),开发人员可以创建、操作和使用与用户或机器人相关的属性/变量。
以下元素用于控制 SIML 响应的流程。
<If>
、<ElseIf>
和<Else>
<Switch>
(连同<Case>
和<Default>
)<While>
以上所有元素都可以使用 SIML 运算符的数量来控制响应的流程。
值
大于
大于或等于
小于
小于或等于
Exists
已定义
非
Contains
现在我们将详细讨论上述元素。
<If>、<ElseIf> 和 <Else>
让我为您举一个 <If>
、<ElseIf>
和 <Else>
元素的例子。
以下 SIML 代码已在上一篇文章中讨论过:
<Model>
<Pattern>I AM @NUMBER YEARS OLD</Pattern>
<Response>
<Match /> years is good enough. <Think><User Set="age"><Match />
</User></Think></Response>
</Model>
现在,在上面的 SIML 代码之后,我们将添加一个新的 Model:
<Model>
<Pattern>AM I OLD ENOUGH</Pattern>
<Response>
<If User="age" Value="18">Yes you are!</If>
<ElseIf User="age" GreaterThan="18">You are more than 18 years old.
So why not?</ElseIf>
<Else>I am not sure enough.</Else>
</Response>
</Model>
上面的代码与 C# 的 if
、elseif
和 else
语句非常相似。输出取决于用户的年龄。Value
属性(在 <If>
元素中)是一个相等比较,它检查用户的年龄是否为 18
。在 <ElseIf>
元素中,我使用了 GreaterThan
运算符,如果用户的年龄大于 18 岁,将返回该运算符的值。只有当前面 If
和 ElseIf
元素中指定的条件不满足时,才会返回 <Else>
元素的值。
- 我 18 岁 -> 我够成熟了吗? -> 是的,你够了!
- 我 35 岁 ->我够成熟了吗? -> 你超过 18 岁了。那为什么不行?
- 我 5 岁 -> 我够成熟了吗? -> 我不太确定。
<Switch>(连同 <Case> 和 <Default>)
上面的 SIML 代码可以使用 <Switch>
元素重写。这个 switch
元素的功能与 C#(以及许多知名编程语言)中的 Switch
关键字非常相似。
<Model>
<Pattern>AM I OLD ENOUGH</Pattern>
<Response>
<Switch User="age">
<Case Value="18">Yes you are!</Case>
<Case GreaterThan="18">You are more than 18 years old. So why not.</Case>
<Default>I am not sure enough.</Default>
</Switch>
</Response>
</Model>
Switch
元素的使用语法非常简单。在 Switch
元素中,您指定变量及其所有者。在上面的代码中,变量的所有者是 User
,并且正在考虑的变量是用户的 age
。
每个 <Case>
元素连同运算符一起用于创建唯一条件。如果任何条件得到满足,则该元素的内部值将被接受为响应的一部分。
但是,如果祖先 case 元素中没有一个条件得到满足,则接受 <Default>
元素的值。Switch
元素内不一定需要 <Default>
元素,但每个 Switch
元素至少应包含 1 个 Case
元素。
<While>
我个人从未用过 SIML While
元素,恕我直言,如果您不确定循环何时结束,应该使用 JavaScript。
SIML 代码示例
<Model>
<Pattern>REPEAT * * TIMES</Pattern>
<Response>
<Think>
<Var Set="num">
<Match Index="2" />
</Var>
</Think>
<While Var="num" GreaterThan="0">
<Match />
<Bot Get="space" />
<Think>
<Var Set="num">
<Math Get="decrement">
<Var Get="num" />
</Math>
</Var>
</Think>
</While>
</Response>
</Model>
上面的 SIML 代码为我们提供了重复用户指定次数的句子或单词的功能。因此,如果用户说 “重复 bla 4 次”,响应将是 “bla bla bla bla”。
上面示例中的新元素 Var
是变量的临时所有者,其范围是它们所在的 Model
。使用 <Var>
元素,我创建了一个新变量 num
来存储用户希望机器人重复单词或句子的次数。
<While>
循环应包含一个变量和一个比较运算符。代码将一直循环,直到运算符指定的条件为 True
。在上面的示例中,While
循环一直运行,直到变量 num
的值大于 0
。
记忆与学习
开发人员在使用 SIML <Remember>
和 <Learn>
元素之前,应了解它们之间的区别。
记忆某事涉及将 SIML Models 存储在分配给特定用户的单独知识库中。此区别可用于保存仅对帮助创建它们的用户的有效事实/知识。
记忆
假设我尝试教我的机器人“番茄是水果”,而其他用户试图教机器人“番茄是蔬菜”。
<Model>
<Pattern>A TOMATO IS A *</Pattern>
<Response>
<Remember>
<Model>
<Pattern>What is a tomato</Pattern>
<Response>A tomato is a <Process><Match /></Process></Response>
</Model>
</Remember>
Alright I'll remember that a tomato is a <Match />.
</Response>
</Model>
在上面的 SIML 代码中,我创建了一个模式 “一个番茄是 *”,并在我的 <Response>
元素中使用了 <Remember>
元素。在 <Remember>
元素内,我添加了一个具有原子模式(“什么是番茄”)和响应的模型,该响应将输出“一个番茄是”后跟用户指定的精确值。
上面的 Model
仅为激活 Model
的用户保存。因此,如果我教我的机器人番茄是水果。每当我问我的机器人番茄是什么时,答案都是 水果
。然而,如果其他用户教机器人番茄是蔬菜。机器人仍然会记住对于 Dave
来说,番茄是 水果
,而对于其他用户来说,番茄是 蔬菜
。
请注意上述代码中的 <Process>
元素。此元素强制解释器在将 Model 存储到用户的图节点之前评估内部或子元素。
使用 <Process>
元素会将 Response
元素的内部值更改为...
<Response>A tomato is a fruit</Response>
学习
另一方面,学习涉及将 SIML Models 存储在机器人全局使用的主要知识图中。它非常像 <Remember>
元素,但底层 Model 被存储到机器人的主知识图中。
<Model>
<Pattern>THE SUN IS A *</Pattern>
<Response>
<Learn>
<Model>
<Pattern>What is the sun</Pattern>
<Response>The sun is a <Process><Match /></Process></Response>
</Model>
</Learn>
Alright I have now learnt that a Sun is a <Match />.
</Response>
</Model>
使用 <Learn>
元素保存的模型是全局可访问的,即,它们的响应对所有用户都是通用的(与 <Remember>
元素不同)。
因此,如果任何用户教机器人“太阳是恒星”。无论谁问“太阳是什么?”。答案将始终是“太阳是恒星”。当然,如果其他用户教机器人太阳是巨大的火球。那么所有用户的响应都将改变。
属性中的元素
如果您需要在另一个元素的属性中使用一个元素的值怎么办?
SIML 提供了一种可行的变通方法,即使用 <Define>
元素。Define
元素将一个 Key
分配给一个元素的值。此 Key
的值以后可以在属性和响应中使用。
例如,以下示例:
<Model>
<Pattern>CHANGE TEXT TO *</Pattern>
<Response>
<Define Key="{0}"><Match /></Define>
"<Text Get="{0}"><Match /></Text>"
</Response>
</Model>
在上面的 SIML 代码中,通配符 *
被存储为键 {0}
,并且在 <Text>
元素的 Get
属性内,我使用了键。因此,在聊天会话期间,键 {0}
将被 <Match/>
元素的值替换。
- 整个
Define
元素不返回任何文本值,消除了将其包含在<Think>
元素内的需要。 - 每个
Define
元素都应有一个Key
属性。其值应为唯一标识符。
一旦定义了 Key
,就可以在任何地方使用它(作为属性值或任意文本)。因此,我可以用上面的代码中的 <Text>
元素内的键 {0}
替换 <Match />
元素,并且输出仍然相同。
停止重复
大多数机器人架构的一个主要问题是它们会重复输出相同的模式。SIML 为重复的用户输入提供了一个简单的变通方法。关于重复有三种可能性:
- 用户不重复输入 - 消息是唯一的。
- 用户重复整个消息 - 用户输入中的每个句子都已处理过。
- 用户重复一个或多个句子 - 聊天请求中至少有一个重复的、零星的句子。
应对以上任何一种情况的成功方法是实现一个重复管理系统,使用 <Repeat>
和 <Echo>
元素。
为每个 SIML Model 管理重复将是非常不足和冗余的。因此,每个 SIML Concept 都可以有一个设置为 True
或 False
的 Repeat
属性。这告诉 SIML 解释器相应地处理子 Models
。
为了测试此功能,您必须先在 Chatbot Studio 中创建一个新的 Concept 文件。
在本教程中,请将新的 Concept 文件命名为“不要重复”。
对于我的示例 SIML 代码,我创建了一个名为 Don't Repeat 的新 Concept,并将 Repeat 属性的值设置为 False。
<Concept Name="Don't Repeat" Type="Public" Repeat="False">
<Model>
<Pattern>WHERE IS THE MILLION DOLLAR HIGHWAY</Pattern>
<Response>It's in Colorado.</Response>
</Model>
</Concept>
保存新的 Concept 文件后,单击 Settings -> Repetition。此(Repetition.sml)文件默认包含一些重复管理代码。在我们深入研究重复管理之前,我们应该熟悉以下用户变量及其值:
- Partial-Repeat - 如果用户消息中存在至少一个重复的输入,则此用户变量设置为
True
。 - Repeat-Count - 此变量的值是机器人处理的重复输入的数量。
重复管理涉及以下步骤:
- 在文档根元素(
<Siml>
)内创建一个<Repeat>
元素。 - 在上述
Repeat
元素内添加一个新的<Response>
元素。 - 通过检查
Partial-Repeat
变量的值来处理Partial
和Complete
重复。 - 通过检查
Repeat-Count
变量的值来处理用户重复的每个输入。
<Repeat>
<Response>
<If User="partial-repeat">
<Switch User="repeat-count">
<Case Value="1">And I have already mentioned that <Echo Index="1" /></Case>
<Case Value="2">And I think you already know that <Echo Index="1" />
and <Echo Index="2" /></Case>
<Default>And I do not like to repeat myself.</Default>
</Switch>
</If>
<Else>
<Switch User="repeat-count">
<Case Value="1">I have already mentioned that <Echo Index="1" /></Case>
<Case Value="2">I think you already know that <Echo Index="1" />
and <Echo Index="2" /></Case>
<Default>I do not like to repeat myself.</Default>
</Switch>
</Else>
</Response>
</Repeat>
在上面的 SIML 代码中,根据上述步骤,设置有一个 Response
元素,其中我使用了一个 <If>
元素来检查用户变量 partial-repeat
的值是否为 true
。如果是 partial-repeat
,我可以确定在我的响应之前将有一些其他的响应用于零星的输入。因此,我以单词 And
开头我的响应。If
和 Else
元素之后是 Switch
元素,其子元素根据用户重复的句子数量进行激活。
上面示例中的 <Echo>
元素用于检索与模式匹配重复用户输入的 Model 的响应。对于重复的输入,每个重复的输出都存储在 Echo Stack 中,并带有索引值。因此,第一个重复的输出存储在索引 1,第二个存储在索引 2,依此类推。
最佳实践
磨刀不误砍柴工。以下实践将帮助您实现更好的聊天机器人。
从基本模式开始
始终尝试先使用原子模式,然后迁移到更宽松的模式。因此,与其使用 “* 你的名字”,不如先创建一个模式 “你的名字是什么”。
模式迁移是一个分步过程,推荐的迁移如下:
- 原子模式(你的名字是什么)
- 带有集合的模式(我的眼睛是 [颜色])
- 带有正则表达式的模式(我出生在 @Date)
- 带有通配符的模式(我叫 *)
- 关键字({你的名字})
不要忘记您过滤掉的内容
在上一篇文章中,我们学习了如何过滤掉单词或将其规范化为最简单的形式。因此,如果您创建了一个将单词 Whats 转换为 What is 的过滤器。以下模式永远不会匹配。
<Pattern>WHATS YOUR NAME</Pattern>
始终检查您的规范化文件,以了解您已过滤掉的内容。
避免基于关键字的模式
像 {你的名字} 这样的模式非常模糊。它可以匹配 “你的名字是什么?”、“我不知道你的名字”、“我不喜欢你的名字” 等等。
但是,它们的模糊性并没有使它们变得无用。我建议在可以为用户提供替代模式的场景中使用它们。
例如,用户说,“我不知道你的名字”。如果它匹配基于关键字的模式,则提供类似 “你的意思是‘你的名字是什么?’” 的输出,如果用户回答 “是”,那么(使用 Goto 元素)将模式搜索重定向到 “你的名字是什么?”。
使用集合而不是正则表达式
Set
的唯一目的是匹配单词。它们已针对此类任务进行了高度优化。使用正则表达式而不是 Set
会非常麻烦。
所以,而不是
<Regex Name="Color" Pattern="\b(red|green|blue|...)\b"/>
坚持使用
<Set Name="Color">
<Item>Red</Item>
<Item>Green</Item>
<Item>Blue</Item>
......
</Set>
是的,我明白后者看起来更冗长,但随着时间的推移,您会体会到它的好处。
谨慎使用重定向
<Goto>
元素将模式搜索重定向到新值。必须本能地决定何时使用 Gotos 以及何时在 Models 中使用多个模式。更重要的是,要确保 2 个 <Goto>
元素不会指向彼此。
我鼓励开发人员在 Models 中使用多个模式,而不是使用冗余的 Gotos。
<Model>
<Pattern>
<Item>HELLO THERE</Item>
<Item>HI</Item>
<Item>HOLA</Item>
<Item>HI</Item>
</Pattern>
<Response>Hi there!</Response>
</Model>
相信可重用性
在 SIML 中,您可以重用随机响应、短语、JavaScript、集合、正则表达式、映射、模型甚至情感。
假设您创建了一个新的 JavaScript 来查找数字的阶乘值。
<Model>
<Pattern>WHAT IS THE FACTORIAL OF *</Pattern>
<Response>
<Js>function fact(x) {
if(x==0) { return 1;}
return x * fact(x-1);
}
fact(<Match />);
</Js>
</Response>
</Model>
您可以通过使 JavaScript 函数全局可访问来使其可重用。为此,您可以将 JavaScript 函数存储在 Script 文件中。
<Script Type="JavaScript">
function fact(x) {
if(x==0) { return 1;}
return x * fact(x-1);
}
</Script>
然后重用您的函数……
<Model>
<Pattern>WHAT IS THE FACTORIAL OF *</Pattern>
<Response>
<Js>fact(<Match />);</Js>
</Response>
</Model>
相同的理念可以用于随机响应、模型(使用 <Goto>
)和情感。
代码可读性
可读性是编写聊天机器人代码的一个重要方面。可读性不仅使您的代码更具吸引力且易于理解,而且还减少了维护代码所需的劳动量。
以下几点应该能帮助您入门:
- 通过重用代码避免冗余。
- 嵌套 Models 时,不要超过 2 或 3 个级别。
- 将大型 Concept 文件分成两个或更多独立的文件以方便维护。
- 在复杂 Models 的顶部添加注释(在 Chatbot Studio 中按 Alt+C)以帮助其他开发人员理解您的代码。
- 为 Sets、Maps、Regular Expressions 和 JavaScript 函数使用简单但具体的标识符。
关注点
在本文中,我试图详细介绍 SIML 元素,并设置更多示例来演示其用法。第一部分和第二部分应为任何开发人员提供足够的 SIML 背景。在未来的教程中,我计划与该库进行更多合作并探索其功能。
历史
- 2015 年 1 月 26 日星期一 - 初始发布(第二部分)