一種在智慧對話中實現上下文功能的方法

原文地址:http://blog.csdn.net/speeds3/article/details/78302774

智慧對話中有一個經典的場景:

Q:上海的天氣
A:上海的天氣是……
Q:那北京的呢
A:北京的天氣是……

第二個問句是一個特殊的問句,它的語義和前一句關聯,但單獨說它沒有明確的意圖。目前的olami開放平臺提供的IDS模組(應用管理->配置模組->對話系統模組)自身可支援上下文,但對平臺使用者自己開發的NLI模組卻沒有提供直接的支援。不過我們可以通過一些辦法實現這種功能。下面就介紹一下解決方案。

分析

這種上下文的語句有兩種型別,一種是對前文的語義進行一部分的修改,另一種是補充前文語義缺失的部分。修改前文語義的語句一般都是問更多的資訊,所以這裡把處理這種語句的grammar稱為more grammar;補充前文語義,一般都是補充osl語言描述的“slot”資訊,所以處理它的grammar在本文叫做slot grammar。前面的例子就是修改前文語義,下例是補充前文語義的情況:

Q:查天氣
A:你要查哪裡的天氣
Q:上海
A:上海的天氣是……

大多數時候more grammar可以很好的解決問題,但有些情況就不適用。比如:

Q:買機票
A:請問你要從哪裡出發
Q:上海                    ⑴
A:請問你要去哪裡
Q:北京                    ⑵
A:上海到北京的機票

⑴和⑵兩句的型別完全相同,但slot名稱不同,只能用同一句grammar來抓取。如果用more grammar來處理,拿到語義後還要糾正slot的名稱,這和一般的more grammar有一定的區別。所以將其設定為特殊的more grammar —— slot grammar,在這當中進行設定slot的操作。

利用前面定好的規則,我們希望實現如下場景:

Q:查天氣
A:你要查哪裡的天氣
Q:上海
A:上海的天氣是……
Q:那北京的呢?
A:北京的天氣是……
Q:南京的呢
A:南京的天氣是……

另外,如果有多個應用,不應該出現一個模組的more處理了另一個模組上文的情況:

Q:導航去南京
A:到南京的路線……
Q:那北京的呢?    // 這是天氣的more
A:北京的天氣是……  // 不應該出現這種情況

最後,兩個應用模組共同的more語法要能被正確的模組處理:

Q:查天氣
A:你要查哪裡的天氣
Q:上海
A:上海的天氣是……
Q:導航
A:你要去哪裡
Q:上海
Q:到上海的路線是……

總結起來,我們的目標有:

  1. 模組的more可以繼承上文的部分語義組合成一個完整的語義;

  2. 一個模組的more不應該繼承另一個模組的上文;

  3. 相同的句式(例如整句是一個地點)能被正確的模組處理。

方案

計劃用兩個功能模組和一個公共模組來演示上下文的功能。兩個功能模組一個是天氣,功能是查指定城市的天氣;另一個是導航,考慮到簡化流程,只接收目的地的設定,出發地理解為當前地點。公共模組用來處理一些兩個模組都要支援的more語句,例如“北京”這種。

實現

語法平臺的操作

首先在平臺上建三個新的模組,分別是weather(天氣),navi(導航)和common(公共模組)。

然後,由於三個模組都需要一個地點的slot,所以每個模組都新增上:

增加location slot

高階設定中設定驗證類別,之後應用時系統會自動驗證抓到的內容是不是這個型別。同時引用型別設定為地點,以後可以用”那裡“來引用上文的地點:

設定slot型別

接下來依次新增需要的grammar,注意要覆蓋上文需要實現場景中的所有情況。普通的grammar和平常一樣寫,more的grammar將modifier設定為more:

weather

不帶location:[我要|幫我|我想]查[[一]下]天氣<{@=query}>
帶location的完整grammar:(<location>|(那裡|那兒)<{[email protected]=last}>)的天氣[怎[麼]樣]<{@=query}>
more grammar:([那]<location>的[呢|怎麼樣|如何]|查<location>[的]|<location>(呢|怎麼樣|如何))<{@=more}>

navi

(幫我|我要)導航<{@=navi}>
導航(去|到)(<location>|(那裡|那兒)<{[email protected]=last}>)<{@=navi}>
([那](到|去)<location>[的]呢|[我要|我想]去<location>)<{@=more}>

common

抓整句作為location slot:(<location>|(那兒|那裡)<{[email protected]=last}>)<{@=slot}>

客戶端的內容

本文用swt來實現客戶端。匯入olami java client sdk之後就能使用其提供的nli介面了。由於olami伺服器目前只能提供一句話的語義解析,而不能指定上下文,所以客戶端需要儲存一些狀態。這些狀態包括:

  1. 處理上一回合語義的模組。用於判斷本回合的more語義是否要進行處理。

  2. 上一回合模組是否處於等待slot輸入的狀態,以及需要的slot名稱。這樣同型別不同名稱的slot不會被混淆。

  3. 上一回合的語義。目的是和本回合的more或slot語義進行合併,得到完整的語義。

和nli介面的互動封裝在NliService類中。另外我們定義的每一個應用模組都有一個相應的app類處理其語義。這些類由NliService統一管理。NliService收到伺服器返回語義後,會做一些簡單的處理,如果是公共模組的語義就交給正確的上文app,其他的語義按照它的app名稱進行分配。下面是NliService的程式碼:

package moredemo;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import ai.olami.cloudService.APIConfiguration;
import ai.olami.cloudService.APIResponse;
import ai.olami.cloudService.APIResponseData;
import ai.olami.cloudService.TextRecognizer;
import ai.olami.nli.NLIResult;
import ai.olami.nli.Semantic;
import app.App;
import app.NaviApp;
import app.WeatherApp;
public class NliService {
private static final String appkey = "85d85d62d2b3450c97c2f547c7d8de48";
private static final String appSercert = "57467df93d4c4f65a176ab06cca660ee";
private HashMap<String, App> appService = new HashMap<>();
private static String lastapp = null;
private TextRecognizer recognizer = null;
public void init() {
APIConfiguration config = new APIConfiguration(appkey, appSercert, APIConfiguration.LOCALIZE_OPTION_SIMPLIFIED_CHINESE);
recognizer = new TextRecognizer(config);
// 初始化app服務,key與nli系統中的app名稱對應,方便使用。
appService.put("weather", new WeatherApp());
appService.put("navi", new NaviApp());
}
// 通過使用者輸入得到處理結果
public String process(String input) {
String result = null;
if (recognizer != null) {
try {
APIResponse response = recognizer.requestNLI(input);
result = handleResponse(response);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
private String handleResponse(APIResponse response) {
if (response.hasData()) {
APIResponseData data = response.getData();
if (data.hasNLIResults()) {
NLIResult nliResult = data.getNLIResults()[0];
String content = nliResult.getDescObject().getReplyAnswer();
if (nliResult.getDescObject().getStatus() == 0 && nliResult.hasSemantics()) {
Semantic sem = nliResult.getSemantics()[0];
String appname = sem.getAppModule();
if (appname.equals("common")) {
// 處理公共語義,交給lastapp處理
if (lastapp != null) {
App app = appService.get(lastapp);
if ("slot".equals(app.status())) {
return app.getResult(sem);
} else {
return "抱歉,你說的我還不懂";
}
}
} else {
App app = appService.get(appname);
if (app != null) {
String result = app.getResult(sem);
lastapp = appname;
return result;
} else {
return "錯誤:未知服務型別";
}
}
} else {
return content;
}
}
}
return "抱歉,你說的我還不懂";
}
public static String getLastApp () {
return lastapp;
}
}

模組app中儲存了上一回合的語義,以及一個回合結束之後app的狀態。通過語義中的特殊標記(“more”或”slot)和普通的modifier,與slot資訊一起得到結果。由於沒有資料來源,所以結果都只用一句話來代替。為了清晰的展示返回結果是由哪個app處理的,程式碼中在每個回答前加上了【app名稱】的字首。下面是weatherapp的程式碼:

package app;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ai.olami.nli.Semantic;
import ai.olami.nli.slot.Slot;
import moredemo.NliService;
public class WeatherApp implements App {
private List<String> modifiers = new ArrayList<>();
private List<String> lastmods = new ArrayList<>();
private Map<String, Slot> slots = new HashMap<>();
// 儲存當前的app狀態,目前有"slot"一種可能,表示拿到的語義缺少slot資訊,需要使用者繼續輸入。
// 此時slotgrammar才會生效
private String status = null;
private String op = null;
@Override
public String getResult(Semantic sem) {
// 儲存前一個回合的modifier,便於處理more和slot的情況
List<String> temp = lastmods;
lastmods = modifiers;
modifiers = temp;
modifiers.clear();
// 設定新的modifier
for (String mod : sem.getGlobalModifiers()) {
modifiers.add(mod);
}
// 目前的功能只需要,並且語法中的語句都只有一個modifier,擴充套件的話需另作處理
if (modifiers.size() == 1) {
String modifier = modifiers.get(0);
if (modifier.equals("more")) {
// 通過nliservice中的lastapp是否是這個app判斷是否要解析此more語義
if (!"weather".equals(NliService.getLastApp())) {
return "【weather】抱歉,我不明白你的意思";
} else {
setSlots(sem.getSlots());
return result();
}
} else if (modifier.equals("slot")) {
// lastapp是此app且此app處於“slot”狀態時才處理此slot語義
if (!"weather".equals(NliService.getLastApp())) {
return "【weather】你給我的資訊太少了";
} else if (!"slot".equals(status)) {
return "【weather】我不明白你跟我說的是什麼";
} else {
setSlots(sem.getSlots());
return result();
}
} else {
// app的一般流程,這裡表示modifier是“query”的情況
reset();
op = modifier;
setSlots(sem.getSlots());
return result();
}
} else {
return "【weather】我的神經發生了混亂";
}
}
// 通過儲存的op和slots的組合確定當前執行哪種操作,並設定對應的app狀態
private String result() {
if (op.equals("query")) {
if (slots.containsKey("location")) {
String location = slots.get("location").getValue();
status = null;
return "【顯示"   location   "的天氣】";
} else {
status = "slot";
return "【weather】你要查哪裡的天氣呢?";
}
} else {
status = null;
return "【weather】我好像出了點問題?";
}
}
// 把引數slots中的slot資訊儲存起來
private void setSlots(Slot[] slots) {
for (Slot slot : slots) {
this.slots.put(slot.getName(), slot);
}
}
private void reset() {
slots.clear();
op = null;
}
@Override
public String status() {
return status;
}
}

navi的程式碼和它大同小異。

執行結果

加上一個簡單的介面,就可以測試輸出結果。

  1. 可以進行追問:

天氣的追問

  1. 天氣的more grammar不會處理導航的上文:

天氣more與導航上文

  1. 相同的句式不會混淆:

相同句式

這樣前文期望的目標基本都實現了。

總結與展望

通過給語法做標記,再在客戶端做一些工作,我們可以實現一些基本的上下文處理。這已經可以滿足相當一部分應用對上下文理解的需求。當然由於不是在語法匹配上支援的上下文,這裡的辦法有一些侷限性,當應用數量和複雜度提高時很可能會出現問題。希望olami平臺能儘快在服務端增加上下文的功能,這樣使用起來就可以更加簡便,應用的處理邏輯也可以更加簡明和清晰。

原始碼地址:https://gitee.com/stdioh_cn/moredemo.git