Alexa Best Practices
Lessons Learned: Halbwegs intelligente Skills zu entwickeln ist nicht trivial
Alexa Intelligenz einzuhauchen ist nicht ganz einfach. Nach mehreren PoCs mit unterschiedlichen Komplexitäten haben sich jedoch einige Best Practices herausgebildet. Im Artikel Alexa Skill: Die ersten Schritte haben wir bereits gesehen, wie man für Alexa sogenannte Skills entwickeln kann und worauf dabei zu achten ist.
Alexa ist in aller Munde, und Amazon ist noch immer damit beschäftigt, den vielen Vorbestellungen nachzukommen. Es ist also jetzt schon abzusehen, dass der nett aussehende Sprachassistent bald in sehr vielen Wohnzimmern zu finden sein wird. Grund genug, uns eingehender mit einigen Erfahrungen aus der Praxis zu beschäftigen. Beim letzten Mal haben wir einen einfachen Würfel-Skill entwickelt, um den Workflow zu veranschaulichen. Dieses Mal haben wir uns über ein hypothetisches Supermarktsystem Gedanken gemacht und schauen uns mithilfe der Alexa Best Practices die Herangehensweisen dafür an.
Im letzten Artikel haben wir bereits gesehen, wie man für Alexa sogenannte Skills entwickeln kann und worauf zu achten ist. Nach mehreren PoCs mit unterschiedlichen Komplexitäten haben sich einige „Best Practices“ herausgebildet, die unser Kollege Marcel Naujeck im Folgenden erläutert.
Einkaufen mit Alexa? Aber wie?
Wie wir bereits wissen versteht Alexa nur Dinge die man im Vorfeld in einem sogenannten Custom-Slot definiert. Es ist natürlich illusorisch eine Produktdatenbank in einen Custom-Slot unterbringen zu wollen. Zum einen sind wir durch das zu nutzende Webinterface stark eingeschränkt, zum anderen muss man sich vor Augen halten, dass der Nutzer das gewünschte Produkt auch genauso aussprechen müsste, wie es im Slot definiert ist. Nur welcher Nutzer würden sagen: „Bestelle mir den Sony xyz1023 28“ Widescreen.“, wenn er ihn vorher nicht kennt? Richtig, niemand. Er würde eher dazu neigen zu sagen: “Ich möchte einen Fernseher von Sony bestellen.“
Einstiegspunkte
Wenn wir uns den letzten Satz anschauen, haben wir drei Anhaltspunkte was zu tun ist. „Ich möchte einen {Kategorie} von {Hersteller} bestellen.“ Der Nutzer möchte einen Fernseher einer bestimmten Marke bestellen. Wir haben einen Intent für den Vorgang der Bestelleinleitung und zwei Slots die uns eine Kategorie und einen Hersteller zurückgeben. So haben wir bereits einen guten Ansatzpunkt, um über einen geführten Dialog weiter eingrenzen zu können was, der Nutzer möchte.
Expertensystem
Dazu wäre es natürlich hilfreich wenn unsere Datenstruktur genau diese Entscheidungsbäume schon abbildet. Relationale Datenbanken mit flachen Tabellen sind hier nicht wirklich praktikabel. Besser wäre eine dokumentenorientierte Datenbank wie z.B. MongoDB. Wie könnte nun also solch ein Schema für Fernseher aussehen? (Beispiel in Abb. 1)
Haben wir diesen Einstiegspunkt und dieses Dokumentenschema, gestaltet sich die weitere Dialognavigation sehr einfach, da unser Backend entsprechend der Nutzerwahl nur noch in die Tiefe iterieren muss.
Als nächstes würde Alexa also fragen: „Möchtest du einen 24 Zoll oder 28 Zoll Fernseher?“, dann „Widescreen oder normal?“ und letztlich „Ich habe einen Artikel gefunden, der zu deinen Wünschen passt: Sony xzy1023 Widescreen 28 Zoll zum Preis von 1023,99 Euro. Möchtest du ihn in deinen Warenkorb legen?“
Der Einstieg bestimmt das Schema
Hierarchische Schemata können natürlich beliebige Formen haben. Daher kann man sagen, dass Einstieg und Schema unmittelbar voneinander abhängen. Würde ich sagen „Ich möchte einen Fernseher mit 28 Zoll bestellen.“ würde der Hersteller unterhalb der Zollangabe eingehängt sein. Ich bräuchte also eine andere oder weitere Struktur in meiner Datenbank.
Das sieht zwar wenig flexibel aus, doch von der Illusion der „freien Gesprächsführung“ mit Alexa kommt man ohnehin schnell ab und muss sich eher dem Pragmatismus hingeben. Hier machen dokumentenorientierte Datenbanken wieder Sinn, denn sehr viel Programmlogik wird einfach in die Datenbank verlagert, so dass mein Skill schlank bleibt und ich mich auf das Wesentliche, nämlich auf die Dialogführung, konzentrieren kann.
Habe ich ohnehin schon ein PIM welches z.B. auf relationalen Datenbanken basiert, kann ich mir natürlich gewünschte und gebrauchte Strukturen mit etwas Aufwand erzeugen lassen, was einen Hauch von CQRS hat. Lasse ich meine Strukturen ohnehin erzeugen und achte nicht auf Redundanz, lassen sich die vielfältigsten Hierarchien aufbauen und damit auch verschiedene Wege der Produktnavigation.
Dynamische Daten, statische Slots. Was nun?
Wie wir gelernt haben, müssen Dinge, die Alexa verstehen soll, in sogenannten Custom-Slots definiert werden. Unsere Gesprächsführung ist aber weitgehend dynamisch genauso wie unser PIM. Wir können nicht bei jeder Änderung in der Struktur oder den Stammdaten auch unsere Slots und deren Inhalte ändern. Das wäre nicht praktikabel. Was wir allerdings tun können, ist die Custom-Slots zu zweckentfremden. Die Intent- und Slot-Analyse von Amazon hat die Eigenart auch Dinge zu übermitteln, die nicht als Value in einem Custom-Slot vordefiniert sind. Dies machen wir uns zu nutze.
Custom-Slots zweckentfremden
Haben wir Beispielsweise einen Slot „Hersteller“ mit den Value „Test“ und sagen „Sony“ wird Amazon als Value des Slots höchstwahrscheinlich auch Sony übergeben. Sagen wir jedoch „Sony Corporation“ wird als Value das Slots null zurückgegeben. Das liegt daran, dass Amazon offensichtlich die Anzahl der Wörter eines Values berücksichtigt. Würden wir also zusätzlich ein Custom-Slot-Value „Testeins Testzwei“ definieren, würde Amazon auch „Sony Corporation“ zurückgeben können.
Es gilt also die Regel, die Anzahl der Worte die erkannt werden sollen, müssten sich auch in einem Value des entsprechenden Custom-Slots wiederspiegeln. Dokumentationen dazu sucht man vergebens, jedoch kann ich aus der Praxis sagen, dass es genau so funktioniert.
Status persistieren
Wenn wir nun allerdings Intents so allgemein halten, dass wir sie für jede benötigte Entscheidung einsetzen können haben wir das Problem, dass wir nie wissen auf was sich die Antwort gerade bezieht. „Ich möchte 24 Zoll.“ oder „Ich möchte Widescreen.“ sind dann nämlich der gleiche Intent. Wir müssen uns also im Dialog merken an welcher Stelle wir uns gerade befinden.
Hierfür könnten wir die Session nutzen, da es hier die Möglichkeit gibt Werte ins Session-Objekt zu schreiben. Der Nachteil ist, dass wir permanent alle Daten im Session-Objekt mit jedem Request/Response mitsenden, ein Entwickler aber bekanntlich gerne wenig Overhead erzeugt. Ein weiteres Problem ist, dass Objekte nicht serialisiert werden, sondern in JSON umgewandelt. Nutzen wir also Pojos, müssen wir uns mit einem JSON-Mapper erst wieder das Pojo aus dem JSON erzeugen.
UserID der Freund und Helfer
Eine andere Möglichkeit an dieser Stelle ist die UserID zu nutzen. Jeder Nutzer bekommt beim Aktivieren eines Skills für genau diesen Skill eine eindeutige UserID, die solange bestehen bleibt, bis der Nutzer den Skill wieder deaktiviert. „Aktiveren“ und „Deaktivieren“ sind in diesem Fall als äquivalent zu „Installieren“ und „Deinstallieren“ zu sehen. Diese UserID können wir der Session entnehmen, die bei jedem Callback mitgeschickt wird. Eine einfache Implementation im Speechlet könnte z.B. wie folgt aussehen. (Beispiel in Abb. 2)
Zum langfristigerem Persistieren würde man den User natürlich in irgendeine Form von nichtflüchtiger Datenbank schreiben. Für kurzfristiger Dinge oder als Session-Ersatz reicht ein Class Member.
Was wir nun allerdings haben ist ein Objekt, was wir eindeutig einer Person zuordnen können, mit beliebigen Daten befüllen können, welches die Session nicht belastet und dessen Lifecycle nicht mit dem Session-Ende endet.
Warenkorb, Merkzettel, Historie und Vorschläge
Wollen wir uns z.B. Produkte merken, egal ob für einen Warenkorb oder eine Merkliste, wäre es fatal, wenn jedes Mal nach Session-Ende entsprechende Listen wieder leer sein würden. Dieses Problem ist über das User-Objekt gelöst.
Definieren wir z.B. im User-Objekt eine ArrayList<Product> basket mit entsprechendem Getter und Setter, haben wir die Basket-Funktionalität quasi schon verwirklicht. Gleiches gilt für eine Merkliste.
Wie wir bereits gesehen haben, kann es sehr umständlich sein, Produkte auszuwählen. Haben wir zum Beispiel ein Supermarktsortiment werden sich unsere Käufe oft wiederholen. Wenn man eine Milch kaufen möchte wäre es kaum hinnehmbar jedes Mal durch ein Expertensystem zu iterieren bis man irgendwann auf die gewünschte Milch trifft. Habe ich allerdings schon einmal eine Milch bestellt oder vielleicht auf die Merkliste gesetzt, könnte man die Produktauswahl natürlich etwas cleverer gestalten.
Wenn ich also sage: “Ich möchte Milch kaufen.“, iteriert unser Backend in unserem User-Objekt über die Merkliste und Bestellhistorie und prüft ob es „Milch“ findet. Ist das der Fall, könnte Alexa fragen „Meinst du die Biomilch 1,5% von xyz?“ und mit einem weiteren „Ja.“ hätten wir gegebenenfalls die richtige Milch im Warenkorb.
Status merken
Allein an diesem kleinen Beispiel zeigt sich, dass es oft mehr Aufwand gibt, als im ersten Moment zu erkennen ist. Wir wollen eine Milch kaufen. Das Backend sieht aber nun, dass es eventuell schon eine Milch gibt, für die wir uns entscheiden könnten. Jedoch könnte es auch sein, dass wir uns gegen diese Milch entscheiden. Dann müsste das Backend im nächsten Schritt doch wieder durch das Expertensystem iterieren.
Wir müssen uns an solchen Stellen also definitiv den Status, in dem wir uns gerade befinden, merken. Dies können wir tun, indem wir uns im User-Objekt einfach Properties für Intent und Slot anlegen oder indem wir uns irgendeine Form von Statuscode selber definieren. Die Milch muss jedoch auf jeden Fall irgendwo gespeichert werden. Entscheidet sich der User nun die vorgeschlagene Milch nicht zu kaufen, weiß unser Backend trotzdem noch, an welcher Stelle es im Expertensystem wieder einsteigen muss.
Gleiches gilt bei der Iteration durch das Expertensystem. Wir müssen uns permanent die Position merken, an der wir uns befinden, um im nächsten Schritt von dieser Position und mit Hilfe der getroffenen Entscheidung, tiefer zu iterieren.
Ja? Aber auf was bezieht sich das?
Der Status ist auch besonders bei Defaultintents wie „Ja“ ,“Nein“, “Hilfe“ oder „Stop“ wichtig. Gehen wir nach den Amazon Beispielen bekommen wir ständig von Alexa dieselben Floskeln vorgebetet, wenn wir um Hilfe bitten. In der Praxis sieht es doch aber meist so aus, dass wir in unterschiedlichen Situationen auch unterschiedliche Hilfe benötigen.
Wenn ich gerade in meinem Warenkorb bin will ich wissen wie mein Warenkorb funktioniert und nicht wie man Produkte sucht. Ein „Ja.“ bei der Frage „Möchtest du deinen Warenkorb löschen?“ hat eine völlig andere Bewandtnis als ein „Ja.“ auf die Frage „Meinst du dieses Produkt?“
Sich den Status des Dialogs zu merken ist also eine essenzielle Sache. Zumindest bei etwas komplexerer Logik, die in Bereiche und Sequenzen untergliedert ist. So könnte der Intent „Ja.“ an einer Stelle, an der er nicht benutzt werden kann, auch eine Hilfe auslösen.
In der Praxis hat sich gezeigt, dass Testpersonen völlig andere Sprachphrasen benutzt haben, als ich vermutete. Hilfe wird also „immer“ bitter nötig sein.
Last but not least...
Wie schon im vorherigen Artikel erwähnt, lässt Alexas Verständnis oft zu wünschen übrig. Manches versteht sie allerdings auch so gut, dass man sich fragen muss, gegen was für Daten Amazon die Worterkennung laufen lässt. „Kim Kardashian“ oder „Kim Jong Un“ versteht sie nicht nur, sie gibt die Namen sogar völlig korrekt geschrieben zurück. Bei anderen Worten könnte man mit gutem Willen eine gewisse Ähnlichkeit zu dem erkennen was man sagte.
Das Sprachverständnis ist also sehr unterschiedlich. Um mit in Slots undefinierten Datenbeständen arbeiten zu können, bleibt uns nichts Anderes übrig, als Fuzzylogic oder phonetische Suchalgorithmen zu nutzen.
Die einfachste Möglichkeit ist, StringUtil von Apache Common zu nutzen, welche z.B. die Funktionen „getFuzzyDistance“, „getJaroWinklerDistance“ und „getLevenshteinDistance“ bieten. Alle drei Funktionen geben über unterschiedliche Algorithmen die Distanz zwischen zwei Strings an. Über einen Threshold kann man dann letztlich entscheiden, welche der Strings in eine Liste von Vorschlägen kommen sollen.
Möchte man es etwas ausgeklügelter haben, kann man natürlich auch Apache Lucene nutzen. Lucene unterstützt phonetische Suchalgorithmen, die das Ergebnis teils verbessern können. Letztlich muss die Frage der Balance jeder für sich selbst klären. Der Algorithmenvielfalt wegen, haben wir in unseren PoCs Lucene eingesetzt.
Abschließend
Beherzt man all diese Dinge ist es durchaus möglich Skills zu entwickeln die praktikabel eingesetzt werden können und sogar ansatzweise intelligent sein. Wichtig ist vor allem, dass der Nutzer immer das Gefühl hat, dass Alexa weiß in welchem Kontext er sich gerade befindet. Ist er im Warenkorb eines Skills, dann sollte er dort solange bleiben bis er ihn explizit verlässt und nicht durch eine andere Phase plötzlich ganz woanders sein. Das würde den Eindruck von Inkonsistenz vermitteln. Der Nutzer muss an die Hand genommen werden und darf nicht zu viele Möglichkeiten bekommen, etwas falsch zu machen. Das würde ihn nur frustrieren.
Den Startschuss für GUIlose Interfaces hat es gerade erst gegeben und wie bei GUIs auch, wird es hier sicherlich auch Entwicklungen und Trends geben. Ich für meinen Teil bin sehr gespannt, wohin uns der Weg führen wird.
Dieser Artikel wurde auf entwickler.de veröffentlicht.