Lessons Learned: Halbwegs intelligente Skills zu entwickeln ist nicht trivial

Alexa Best Practices
Kommentare

Alexa Skills 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 Amazons Sprachassistentin Alexa sogenannte Skills entwickeln kann und worauf dabei zu achten ist. Nach mehreren PoCs mit unterschiedlichen Komplexitäten haben sich für mich einige „Alexa Best Practices“ herausgebildet, die ich im Folgenden erläutern möchte.

Alexa Best Practices

Die unter anderem auf dem Echo beheimatete Sprachassistentin 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 Echo oder Echo Dot mit der Sprachassistentin bald in sehr vielen Wohnzimmern zu finden sein wird. Grund genug, uns eingehender mit einigen Erfahrungen aus der Praxis zu beschäftigen.

Im letzten Artikel haben wir einen einfachen Würfel-Skill entwickelt, um den Entwicklungs-Workflow zu veranschaulichen. In diesem Artikel wollen wir über ein hypothetisches Supermarktsystem nachdenken und uns mithilfe der Alexa Best Practices, die ich im Alltag sammeln konnte, Herangehensweisen dafür anschauen.


In diesem Beitrag werden wir uns mit fortgeschrittenen Techniken und Überlegungen vertraut machen. Den Einstieg in die Entwicklung von Alexa Skills finden Sie in unserem How-to: So entwickelt man einen Skill für Amazons Alexa.

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 Zoll 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? Ein Beispiel dafür findet man im folgenden Listing 1.

{
  "Sony" : {
    "24 Zoll" : { … },
    "28 Zoll" : {
      "Normal" : { … },
      "Widescreen" : {
        "XYZ1023" : {
          "Details" : { … },
          "Preis in Euro" : 1023,99
        }
      }
    }
  },
  "Grundig" : { … }
}

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.

Von der Illusion der „freien Gesprächsführung“ mit Alexa kommt man schnell ab.

Das sieht zwar wenig flexibel aus, doch von der Illusion der „freien Gesprächsführung“ mit Alexa kommt man ohnehin schnell ab und lernt, dass man sich eher dem Pragmatismus hingeben muss. Hier ergeben 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.

Die Lösung findet man in der Zweckentfremdung der Custom-Slots.

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. Genau das machen wir uns zunutze.

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 es, 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 in Listing 2 zu sehen aussehen:

public class DeinSpeechlet implements Speechlet {
  private static HashMap<String, User> userMap;
  
  static {
    userMap = new HashMap<>();
  }

  @Override
  public SpeechletResponse onIntent(final IntentRequest request, final Session session) throws SpeechletException {
    User user = userMap.get(session.getUser().getUserId());
    if (user == null) {
      user = new User(session.getUser().getUserId());
      userMap.put(user.getUserId(), user);
    }
  }
}

Zum langfristigerem Persistieren würde man den User natürlich in irgendeine Form von nichtflüchtiger Datenbank schreiben. Für kurzfristige Dinge oder als Session-Ersatz reicht ein Class-Member.

Was wir nun allerdings haben, ist ein Objekt, das wir eindeutig einer Person zuordnen und 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 die entsprechenden Listen wieder leer wären. 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 Default-Intents 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 jeder für sich selbst die Frage der Balance klären. Der Algorithmenvielfalt wegen haben wir in unseren PoCs Lucene eingesetzt.

Die bekanntesten deutschen Skills

Natürlich bietet Amazon bereits zahlreiche Skills für seine Sprachassistentin. Im Bereich der Reiseauskunft ist da natürlich der Skill der Deutschen Bahn zu erwähnen, der Auskunft über Abfahrtszeiten und Zugverbindungen gibt. Mit dem Skill von MyTaxi ist es möglich, sich mit einem simplen „Alexa, rufe mir ein Taxi“ ein MyTaxi an die Haustür zu ordern.
Zur sprachgesteuerten Bedienung im Smarthome gibt es Skills für die intelligente Heizungssteuerung tado, ebenso wie für Hue, ein WLAN-gesteuertes Beleuchtungssystem von Philips. Wer seine HomeMatic Devices mit Alexa steuern möchte, muss indes einiges an (Entwickler-)Arbeit investieren.
Erfreulich ist hingegen, dass man zum Radiohören gar keinen Skill benötigt: Das integrierte TuneIn findet zahllose Sender – egal ob Internetradio oder reguläre Sender.

Abschließend

Beherzt man diese Alexa Best Practices, ist es durchaus möglich, Skills zu entwickeln die praktikabel eingesetzt werden können – und sogar ansatzweise intelligent sind. Wichtig ist vor allem, dass der Nutzer immer das Gefühl hat, dass die Sprachassistentin 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 landen; 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 GUI-lose Interfaces hat es gerade erst gegeben – unter anderem mit Amazons Echo und Echo Dot, 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.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -