Edited at

Alexa の Unhandled を完全に勘違いしていたお話

More than 1 year has passed since last update.


はじめに

Alexa のスキル開発をやっていくなかで

「なんでこうなるの!?」

って感じで Alexa が腑に落ちない挙動をすることがありました。

そして、色々調べて行くうちに、大きな勘違いをしていたことに気がつきました。

本記事ではその大きな勘違いを共有する記事になります。

そして、細やかながら Alexa スキル開発を楽しむ皆様のお力になれたらいいなと思います。

※本記事では Alexa のスキル開発を行う際に登場する概念や用語を躊躇いなく使っていきます。

※また、本記事内ではそれらの用語の説明は行いませんのでご了承ください。


Unhandled ハンドラに入ってくれない!

まず、以下の対話モデルのスキーマと Lambda Function で定義されるスキルを考えます。

対話モデルのスキーマ

{

"interactionModel": {
"languageModel": {
"invocationName": "あんはんどる", //呼び出し名
"intents": [
{
"name": "GreetingIntent", //intent
"slots": [],
"samples": [ //サンプル発話
"こんにちは",
"おはよう"
]
}
],
"types": []
}
}
}

Lambda Function


index.js

// 挨拶をしたら、挨拶を返してくれるスキル

'use strict';

const Alexa = require('alexa-sdk');

const handler = {
'GreetingIntent' : function(){
this.emit(':tell','おはようございます!');
},
'Unhandled': function () {
this.emit(':tell', '何を仰ってるか分かりません');
}
};

exports.handler = function (event, context) {
const alexa = Alexa.handler(event, context);
alexa.appId = 'xxxxxxxxxxxxxxxxxx';
alexa.registerHandlers(handler);
alexa.execute();
};


「おはよう」or「こんにちは」というユーザーの発話に対して

「おはようございます!」と返してくれる、大変シンプルなスキルです。1


◆思い描いていた挙動


  • 「おはよう」or「こんにちは」 は GreetingIntent に対応する発話だから

    スキルは「おはようございます!」と返してくれる


  • 上記以外の発話は GreetingIntent に対応しない発話なので、 Unhandled として判断されて

    「何を仰ってるか分かりません」と返してくれる ← 大きな間違い!!



◆実際の挙動


  • 「こんばんは」or「ありがとう」or「ほげ」or「あいうえお」等々・・・

    ありとあらゆるユーザーの発話に対しても「おはようございます!」と返す

このような挙動に対して私は

「Unhandled ハンドラに入ってくれない!」「なんでこうなるの!?」

となったわけです。


どんなときに Unhandled ハンドラに入るのだろう?

今回のサンプルスキルに関して言えば、Unhandled ハンドラに入る方法の1つとして

Alexa, 「あんはんどる」を開いて

とユーザーが発話することが挙げられます。

この発話は Alexa の標準リクエストタイプ で定義されている LaunchRequest にあたる発話で、この発話に対しては、スキルは 「何を仰ってるか分かりません」と返してくれます。


◆LaunchRequest は Unhandled ハンドラに入る?

そんなことありません。

例えば、ここで、 Lambda Function の実装を以下のように変更してみましょう。


index.js

// 挨拶をしたら、挨拶を返してくれるスキル

'use strict';

const Alexa = require('alexa-sdk');

const handler = {
// LaunchRequest に対するハンドラを定義した!
'LaunchRequest' : function(){
this.emit(':ask','何か話してみてください');
},
'GreetingIntent' : function(){
this.emit(':tell','おはようございます!');
},
'Unhandled': function () {
this.emit(':tell', '何を仰ってるか分かりません');
}
};

exports.handler = function (event, context) {
const alexa = Alexa.handler(event, context);
alexa.appId = 'xxxxxxxxxxxxxxxxxx';
alexa.registerHandlers(handler);
alexa.execute();
};


実装を変更した後、再度

Alexa, 「あんはんどる」を開いて

と発話すると、スキルは「何か話してみてください」と返し、次のユーザーの発話を待ちます。

当然ですよね笑

そのように定義しましたから。

大事なのは Lambda Function でハンドラが定義されているかどうか です。


◆Unhandled ハンドラに入る本当の条件


  • ユーザーの発話から Alexa がスキルの呼び出し名を認識できる


    • Alexa がスキルを認識してくれなければ Lambda Function まで到達してくれません



  • ユーザーの発話から Alexa が LaunchRequest と対応する発話だと判断して

    なおかつ

    Lambda Function 内に LaunchRequest のハンドラが定義されていない

  • ユーザーの発話から Alexa が特定の Intent を持つ IntentRequest と対応する発話だと判断して

    なおかつ

    Lambda Function 内に対応する Intent のハンドラが定義されていない


2個目と3個目の条件はいずれかが成り立つときと考えてください。

これで、Unhandled ハンドラに入る基本的な条件を整理することができました。

実は、これらの条件は厳密には前提が足りていなかったりします。それは後ほど説明します。


◆Alexa は必ずいずれかの Intent に当てはめてしまう

ところで、上記の条件の中に敢えて


  • ユーザーの発話から Alexa が特定の Intent を持つ IntentRequest と対応する発話だと判断できること

を入れていません。

それは、この条件は必ずクリアされるということを明示的に表現したかったためです。

私がこの勘違いに気付くきっかけになった alexa-skills-kit-sdk-for-nodejs の Github の The unhandled intent is never called #43 という Issue 上で多くの方が、Alexa の intent の判断に関して、下記のようにコメントしています。


Same here, I put in some gibberish and it still called one of my customized intent


  とか


Same issue. I think it has nothing to do with this SDK however, as the utterance tester in the dev console consistently resolves gibberish test utterances to valid defined intents (somewhat randomly).


  とか


So from my testing 'Unhandled' is only called when some intent is received that is not handled by your handler or state handler. This IS useful, as it means that you can have a really large intent-schema with a bunch of different intents, and you don't need to have every intent covered in every state handler. The problem lies in the NLP itself, which (at least in the skills test environment online) always returns one of your intents, even if you write complete gibberish. This is pretty much resolvable through @pheintzelman solution above, but that is kinda sad...


詳しい内容や議論の様子は実際に Issue を見ていただいた方が良いのですが、

どうやら、Alexa はユーザーの gibbereish(ちんぷんかんぷんな[訳の分からない]話[おしゃべり・文章]、でたらめ) に対してもいずれかの intent を判断して Lambda Function で処理をハンドルするようなのです。

これは NLP(自然言語処理)上の問題で、スキルの実装とは直接関係のない問題で、

「スキルの設計や実装上の工夫で上手く回避する方法知ってる方いませんかー?」な状態のようです。

つまり、ユーザーがどんな無茶苦茶な発話をしても Alexa は intent schema の中から最も近い(と Alexa が判断した) intent に当てはめて判断してしまうのです。

この仕様を正しく理解していなかったため、元々の Unhandled に関する勘違いも生まれてしまったのです。


Unhandled ハンドラの正しい使い道

この記事を書くことになった発端は Unhandled ハンドラに入る条件を完全に勘違いしていたことでした。

そして、この勘違いはその正しい使い道の勘違いにも繋がっていました。

今や Unhandled ハンドラに入る正しい条件を理解できたので、その使い道も違って見えてくるはずです。

Unhandled ハンドラに入る条件をざっくり言うと、「intent schema に intent を定義したけど、ハンドラを実装しなかった場合」です。

スキルの実装がこのような状態になるのはどういったパターンが考えられるでしょうか。


パターン A : うっかり!ハンドラの実装忘れ

単純にハンドラの実装漏れです。

intent schema に不要なインテントが定義されていないかを確認して、不要なものは削除していきましょう。



  • LaunchRequest に対するハンドラは基本的に必須ですね 2

  • 使わないカスタムインテントやビルトインインテントは対話モデルに定義しない!


パターン B : スキルがステートを持つ場合

スキルの対話モデルの設計等をちゃんとやった上で、Unhandled がちゃんと必要になるのはほとんどこのパターンだと思います。

例として、以下の対話モデルのスキーマと Lambda Function で定義されるスキルを考えます。

対話モデルのスキーマ

{

"interactionModel": {
"languageModel": {
"invocationName": "あんはんどる",
"intents": [
{
"name": "GreetingIntent",
"slots": [],
"samples": [
"こんにちは",
"おはよう"
]
},
{
"name": "GoodbyeIntent",
"slots": [],
"samples": [
"またね",
"ばいばい"
]
}
],
"types": []
}
}
}

Lambda Function


index.js

// 挨拶をしたら、挨拶を返してくれるスキル(ステートを導入したバージョン)

'use strict';

const Alexa = require('alexa-sdk');

const STATUS = {
NEXTMODE: '_NEXTMODE'
};

// 始まりの挨拶を受け付けるステート
const startHandler = {
'LaunchRequest' : function(){
this.emit(':ask','何か話してみてください');
},
'GreetingIntent' : function(){
this.handler.state = STATUS.NEXTMODE;
this.emit(':ask','おはようございます!');
},
'Unhandled': function () {
this.emit(':ask', 'まだ始まりの挨拶もしてませんよね');
}
};

// 終わりの挨拶を受け付けるステート
const nextHandler = Alexa.CreateStateHandler(STATUS.NEXTMODE, {
'GoodbyeIntent' : function(){
this.emit(':tell','さようなら、またお話ししましょうね');
},
'Unhandled': function () {
this.emit(':ask', 'おはようございます!');
}
});

exports.handler = function (event, context) {
const alexa = Alexa.handler(event, context);
alexa.appId = 'xxxxxxxxxxxxxxxxxx';
alexa.registerHandlers(startHandler, nextHandler);
alexa.execute();
};


今回のスキルでは「おはよう」or「こんにちは」みたいな始まりの挨拶を受け付けるステートと

「またね」or「ばいばい」みたいな終わりの挨拶を受け付けるステートを持っています。

実は、この例では


  • 始まりの挨拶を受け付けるステートで「またね」or「ばいばい」とユーザーが発話する

  • 終わりの挨拶を受け付けるステートで「おはよう」or「こんにちは」とユーザーが発話する

の2パターンの対話で Unhandled ハンドラに入ってくれます。


◆Unhandled ハンドラに入る本当の本当の条件

先ほど Unhandled ハンドラに入る本当の条件を定義した際に

「厳密には前提が足りていない」と書きました。

ここで前提を追記します(赤字部分)。


  • ユーザーの発話から Alexa がスキルの呼び出し名を認識できる


    • Alexa がスキルを認識してくれなければ Lambda Function まで到達してくれません



  • ユーザーの発話から Alexa が LaunchRequest と対応する発話だと判断して

    なおかつ

    Lambda Function 内の現在のステートに LaunchRequest のハンドラが定義されていない

  • ユーザーの発話から Alexa が特定の Intent を持つ IntentRequest と対応する発話だと判断して

    なおかつ

    Lambda Function 内の現在のステートに対応する Intent のハンドラが定義されていない

このように複数のステートを定義して、各ステートごとに提供してくれる機能や受け付けるユーザーの発話が区別されているようなスキルでは Unhandled ハンドラはバリバリと効力を発揮するのです。


◆Unhandled ハンドラの正しい使い道の話に戻すと

ここで、ようやく Unhandled ハンドラの正しい使い道に話を戻します。


  • ステート管理が不要なシンプルなスキルでは、不要なインテントは intent schema で定義しなくていいはず

  • どんなスキルでも基本的に必要なインテントのハンドラは Lambda Function で実装すべき

  • ステート管理が必要なスキルではステートによっては intent schema に定義したインテントに対応するハンドラを実装しない場合がある

これらの前提とスキルにおけるステートがユーザーと Alexa の対話における文脈を表すと考えると

Unhandled ハンドラは文脈に合わないユーザーの発話を拾う機能

として使うのが正しいのだと思います。

これは Alexa が「ユーザーが何を言ってるのか、どういう意図で言ってるのかは理解できるけど、今その文脈じゃないんだよなー」と感じたというような感覚でしょうか。

そして、現状ではUnhandled ハンドラはユーザーの理解できない発話を拾う機能

として使うのは難しいようです。

これは Alexa が「ユーザーが何を言ってるのか、どういう意図で言ってるのかは分からない」と感じたというような感覚でしょうか。

この勘違いが Unhandled ハンドラに対する最大の勘違いでした。


おわりに

Unhandled ハンドラにまつわる勘違いに関して

いろいろ調べて整理してみたので、記事にして共有しようと思いました。

Unhandled ハンドラにまつわる新たな認識



  • Unhandled ハンドラは訳の分からないユーザー発話を拾う機能じゃない


  • Unhandled ハンドラは文脈に合わないユーザーの発話を拾う機能

  • Alexa は訳の分からないユーザーの発話に対しては必ず intent schema 内の intent の中で最も近いものを判断してしまう

Unhandled ハンドラを使って、正しいエラーハンドリングや場合によっては複数のインテントのハンドラをまとめるみたいな使い方もできそうに思えてきました。

最後に、この記事が私と同様の勘違いをしていた人の助けになれば幸いです。





  1. 実際は「あんはんどるでこんにちは」のように、ややトリッキーな発話をしなければ挨拶を返してくれない対話モデルのスキーマになっていますが、説明の都合上ご容赦ください 



  2. スキルが提供する機能が単一で、あらゆるリクエストに対して同一のレスポンスを返したい場合などは Unhandled ハンドラだけを実装するという手もあるなと思います