admin管理员组文章数量:1578030
背景与目的
Rasa状态日志以sender_id为key存储
利用Rasa构建对话机器人时,若使用TrackerStore记录日志,则默认以sender_id(sender,conversation_id,都一样)作为key进行存储,意味着对于用户sender_id=user1,无论何时与机器人对话,其对话记录永远会更新到以user1为key的字典中。
同一用户的状态日志越积越多
表面看起来没有问题,但存在隐含风险,因为Rasa Core的机制是,每次接收到用户message,会根据sender_id召回历史日志,解析历史状态,进而预测下一步该执行的Action。Rasa对历史日志默认使用InMemoryTrackerStore,保存在内存中,所以不太影响速度,但对于实际业务场景,Rasa的历史状态日志会记录到Redis、Mongo、ES等其他存储空间中,用户的历史状态日志会越来越大,召回日志会变得越来越困难,影响系统性能。
期望能按session切分日志
所以我们期望对日志进行分隔,很直观的思路就是按照用户回复是否超时进行会话session切分,每次只召回当前session的状态日志,进入到新session时丢弃上一session的所有状态。例如用户超过60分钟未回复,则下次再回复时,分配新的session_id,并以新session_id为key记录状态日志。
要点
- 默认情况下,conversation_id就是sender_id。
- 重启session不重启conversation。
思路与方案
方案一二三都是失败方案,想解决问题可以直接跳到方案四五。
方案一:session_expiration_time
失败原因:重启session不打断对话
在domain.yml配置session_config,例如:
session_config:
carry_over_slots_to_new_session: false
session_expiration_time: 0.2
表示用户回复超时0.2分钟后重启session,且丢弃所有槽值。
示例:第一次“你好”后等待至少0.2分钟,再次输入“你好”。日志如下,可以看到,虽然第二次“你好”触发了session重启,但并不是整个对话重启。
注意看上图最下面一行的“name”,这就是对话的sender_id和conversation_id,不管怎样重启session,这个值是不变的,也没有生成什么新的session_id,始终就是这一条大日志,只是多记录了“action_session_start”Action和"session_started"Event,这样无法对日志切分保存。
参考资料:
Session configuration
Conversation and Sender ID
方案二:Tracker中增加session_id
失败原因:牵扯太多,异常复杂,改动巨大
既然默认的conversation_id不受session重启影响,那可不可以我们自己在Tracker里新增加一个session_id,当重启session时更新session_id并以session_id做key存储新session的日志呢?这就涉及到修改源码了。
从上面的日志看到,重启时启动了一个名为“action_session_start”的Action,首先想在执行这一步时重置session_id,就从这里出发,查找源码,基本路线如下:
- action_session_start
- 类:rasa.core.actions.action.ActionSessionStart
函数:run()
代码:_events = [SessionStarted()] - 类:rasa.core.events.SessionStarted
函数:apply_to()
代码:tracker._reset() - 类:rasa.core.trackers.DialogueStateTracker
函数:_reset()
当你终于找到重启session实际操作Tracker的代码,并增加上session_id的更新程序
def _reset(self, reset_session_id=False) -> None:
"""Reset tracker to initial state - doesn't delete events though!."""
self._reset_slots()
self._paused = False
self.latest_action_name = None
self.latest_message = UserUttered.empty()
self.latest_bot_utterance = BotUttered.empty()
self.followup_action = ACTION_LISTEN_NAME
self.active_form = {}
if reset_session_id:
self._reset_session_id()
def _reset_session_id(self) -> None:
"""Set session id to sender_id + timestamp"""
self.session_id = str(self.sender_id) + '_' + datetime.now().strftime("%Y%m%d%H%M%S")
print("self.session_id: ", self.session_id)
然后你在DialogueStateTracker.init()里增加session_id的时候发现,这货每次初始化都会调用一次_reset(),即,每次Tracker更新,session_id也会跟着自动更新…结果就是session_id没办法在某段时间内固定住。
除了上述问题,你还会发现,rasa底层涉及到conversation的基本上都是以sender_id作为基准,要想记日志以session_id做key,那每次解析日志获取历史状态时也要以session_id为准,包括状态复制、状态更新等等,导致rasa.core.processor甚至rasa_sdk全都要跟着改,异常难搞。
参考资料:
Customizing the session start action
Start a new conversation session
Resetting a session (slots, active forms etc) if last user event is older than N seconds
方案三:Tracker.events_after_latest_restart()
失败原因:没啥卵用
你以为这个函数会获取每次session重启到现在的events,然后在session启动时间上来做文章。但实际上他是获取conversation重启到现在的events,而session又不打断conversation,所以没用。
方案四:自定义Channel
成功原因:在sender进入对话前就进行处理
看来从Tracker内部进行改造是比较困难的,只能保持Rasa现在以sender_id记录对话的逻辑,通过传不同的sender_id来切分不同session(方案五),或者在Rasa接收到sender_id时先进行一步超时的判断处理,然后再传进对话内部。
具体做法是,自定义个性化Channel,在自己的webhook里对sender进行超时判断。接收到sender后,获取缓存的该用户上次交互时间,判断当前时间是否超时或是否是新用户,若超时或是新用户则更新sender_id为sender_timestamp,并记录缓存。
-
继承rasa/core/channels/channel.py中的InputChannel类,创建个性化Channel,如:ChatBotChannel,重写sender_id获取部分等。
-
在credentials.yml中进行配置,如:ChatBotChannel:
-
启动rasa后,向自定义的Webhook发送信息,如:POST http://localhost:5005/webhooks/chatbot/webhook
{
“sender”: “user1”,
“message”: “你好”
}
这样,即使后台调用时传过来的是同一用户,也可以在Webhook接收到sender且尚未传给conversation时,将sender_id变成sender_id+’_’+timestamp,并进行超时判断与更新。
可以看到超时后统一用户记录的日志里“name”和"_id"都更新了。
参考资料:
Rasa user session management
Custom Connectors
方案五:java后台处理
成功原因:同方案四
也可以在java后台侧做超时判断,把更新的session_id传给Rasa,但这样会导致java侧也引入对话相关的逻辑,不够解耦,所以没有采用。
版权声明:本文标题:Rasa根据会话session超时情况切分日志 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dianzi/1725457642a1024393.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论