基于LUIS的多语言Bot开发问题:LuisDialog序列化后MakeRoot方法未触发
我完全懂你现在的困扰——Bot Framework为了维持会话状态,会自动序列化对话实例,这就导致你在Conversation.SendAsync里写的那个创建RootDialog的lambda,只有第一次请求(新会话)时会执行,后续请求都是直接反序列化之前的对话实例,根本不会再调用你的初始化逻辑,自然没法传递最新的languageData了。
下面给你几个可行的解决方案,按推荐程度排序:
1. 用依赖注入动态注入语言数据
这是最规范的做法,因为依赖注入能很好地处理请求级别的动态数据,避开序列化的坑:
首先定义一个接口来封装语言相关数据:
public interface ILanguageData { string LuisAppId { get; } string LuisApiKey { get; } string LuisDomain { get; } // 其他字符串集等属性 }
然后在你的Post方法里,检测语言后更新对话容器中的实例:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { ILanguageData languageData = DetectLanguage(activity); // 更新容器,让后续对话能拿到最新的语言数据 Conversation.UpdateContainer(builder => { builder.RegisterInstance(languageData).As<ILanguageData>(); }); // 这里RootDialog的构造函数不需要传参数,靠注入获取 await Conversation.SendAsync(activity, () => new Dialogs.RootDialog()); } else { HandleSystemMessage(activity); } return Request.CreateResponse(HttpStatusCode.OK); }
最后修改RootDialog,通过构造函数注入ILanguageData来初始化LUIS:
public class RootDialog : LuisDialog<object> { public RootDialog(ILanguageData languageData) : base(new LuisModelAttribute( languageData.LuisAppId, languageData.LuisApiKey, domain: languageData.LuisDomain)) { // 这里还可以初始化字符串集等 } // 你的LUIS意图处理方法... }
这样不管是新会话还是恢复会话,对话都能拿到当前请求对应的语言数据。
2. 在对话StartAsync方法中重新检测语言
如果不想用依赖注入,也可以利用对话的生命周期方法——StartAsync每次对话启动(包括恢复会话)都会被调用,所以在这里重新检测语言并更新LUIS识别器:
public class RootDialog : LuisDialog<object> { private ILanguageData _languageData; public override async Task StartAsync(IDialogContext context) { var messageActivity = context.Activity as Activity; if (messageActivity != null) { _languageData = DetectLanguage(messageActivity); // 重新初始化LUIS识别器,覆盖序列化过来的旧实例 this.Recognizer = new LuisRecognizer(new LuisModelAttribute( _languageData.LuisAppId, _languageData.LuisApiKey, domain: _languageData.LuisDomain)); } await base.StartAsync(context); } // 意图处理方法中可以直接用_languageData里的字符串集 }
这个方法更直接,不需要改依赖容器,适合快速调整。
3. 修复中间对话框的Forward问题
如果你之前尝试用中间对话框转发,问题大概率出在没有在对话生命周期里处理语言检测,而是靠构造函数传参。调整后的中间对话框应该这样写:
public class LanguageSelectorDialog : IDialog<object> { public async Task StartAsync(IDialogContext context) { var messageActivity = context.Activity as Activity; var languageData = DetectLanguage(messageActivity); // 创建RootDialog并转发请求 var rootDialog = new RootDialog(languageData); context.Forward(rootDialog, ResumeAfterRoot, context.Activity, CancellationToken.None); } private async Task ResumeAfterRoot(IDialogContext context, IAwaitable<object> result) { context.Done<object>(null); } }
然后在Post方法里启动这个中间对话框:
await Conversation.SendAsync(activity, () => new LanguageSelectorDialog());
不过要注意,这个方案还是会受序列化影响,如果会话恢复,LanguageSelectorDialog会被反序列化,所以最好还是把DetectLanguage放到StartAsync里,而不是构造函数。
核心总结一下:Bot Framework的对话实例会被序列化存储,绝对不要依赖构造函数传递请求级别的动态数据,要么用依赖注入动态更新,要么在对话的生命周期方法里重新获取数据。
内容的提问来源于stack exchange,提问作者Miskov

