3、对话状态追踪:意图识别、槽位填充、状态更新策略
好,咱们进入第三个核心模块。对话状态追踪,英文叫 Dialogue State Tracking,简称 DST。这玩意儿说白了,就是让系统记住「用户刚才说了什么,现在想要什么,还缺什么信息」。
我刚开始做多轮对话那会儿,觉得这不就是个记笔记嘛,有啥难的?结果第一个项目就被打脸了。用户说「帮我订个明天去北京的机票」,系统记住了。用户接着说「哦不对,是后天」,系统没反应过来,直接崩了。嗯,这就是状态没追踪好。
3.1 意图识别:先搞清楚用户想干嘛
意图识别,就是判断用户这句话属于哪个「类别」。比如是查询天气、订机票,还是骂客服?
我个人习惯把意图分为三类:
- 主意图:用户的核心诉求。比如「我要退票」
- 修正意图:用户想改之前说的内容。比如「不是明天,是后天」
- 辅助意图:确认、否定、询问细节。比如「有靠窗的座位吗?」
我在项目中遇到过最头疼的情况,是用户一句话里藏了多个意图。比如「帮我查一下明天北京的天气,顺便订个酒店」。这时候怎么办?
我的做法:采用分层意图识别。第一层判断是否包含多个意图,第二层再分别解析。如果系统能力有限,就优先处理第一个意图,然后主动询问是否需要继续处理第二个。
代码示例(简化版):
def detect_intent(user_input):
# 先判断是否多意图
if contains_multiple_intents(user_input):
intents = split_intents(user_input)
return intents[0], intents[1:] # 返回主意图和待处理列表
else:
return classify_single_intent(user_input), []
小技巧:意图识别的模型不要搞太复杂。我试过用 BERT 做全量分类,效果是好,但推理速度慢得离谱。后来改用 fastText + 规则兜底,又快又稳。
3.2 槽位填充:把信息填到框框里
意图定下来了,接下来就是提取关键信息。这些信息就是「槽位」,比如日期、地点、人数。
槽位填充,说白了就是做三件事:
- 识别槽位值:从文本里抠出「北京」「明天」「2个人」
- 标准化:把「明天」转成具体日期,把「2个人」转成数字 2
- 去重与覆盖:如果用户之前说了「北京」,现在又说「上海」,以哪个为准?
这里有个坑,我曾经踩过。用户说「帮我订两张票」,系统提取了「人数=2」。用户接着说「哦不对,是三张」。如果直接覆盖,没问题。但如果用户说「再加一张」,那就不是覆盖,是累加。
注意:槽位更新策略不能一刀切。要区分「替换型更新」和「增量型更新」。日期、地点通常是替换型,数量、名单通常是增量型。
我常用的槽位填充流程:
class SlotFiller:
def __init__(self):
self.slots = {}
self.update_policy = {
'date': 'replace',
'destination': 'replace',
'passenger_count': 'increment',
'passenger_names': 'append'
}
def update_slot(self, slot_name, value):
policy = self.update_policy.get(slot_name, 'replace')
if policy == 'replace':
self.slots[slot_name] = value
elif policy == 'increment':
self.slots[slot_name] = self.slots.get(slot_name, 0) + value
elif policy == 'append':
if slot_name not in self.slots:
self.slots[slot_name] = []
self.slots[slot_name].append(value)
3.3 状态更新策略:别让对话「失忆」
状态更新,是整个 DST 的核心。你想想看,用户说了五句话,系统得记住每一句里哪些信息是有效的,哪些已经被覆盖了。
我总结了三层更新策略:
| 策略层级 | 适用场景 | 我的经验 |
|---|---|---|
| 全局覆盖 | 用户明确说「改成...」 | 最简单,但容易丢失上下文 |
| 槽位级更新 | 用户只改某个字段 | 最常用,需要配合槽位策略 |
| 历史回退 | 用户说「还是刚才那个吧」 | 最难,需要维护状态栈 |
我曾经做过一个订餐机器人,用户说「帮我订明天晚上7点,2位,川菜馆」。系统记下了。用户又说「算了,改成粤菜」。系统把餐厅改了,但时间和人数没动。用户接着说「不对,还是川菜吧」。这时候如果系统没有历史状态,就彻底懵了。
我的解决方案:维护一个「状态栈」。每次更新前,先把当前状态压栈。用户反悔时,直接从栈里弹出。栈深度我一般设3层,太深了用户自己都记不住。
状态栈的代码思路:
class StateTracker:
def __init__(self, max_history=3):
self.current_state = {}
self.history_stack = []
self.max_history = max_history
def update(self, new_state):
# 保存当前状态到历史栈
self.history_stack.append(self.current_state.copy())
if len(self.history_stack) > self.max_history:
self.history_stack.pop(0)
# 应用新状态
self.current_state.update(new_state)
def rollback(self):
# 回退到上一个状态
if self.history_stack:
self.current_state = self.history_stack.pop()
return True
return False
避坑指南:状态更新时,一定要做「完整性校验」。比如订机票,用户说了目的地但没说出发地,状态就是不完整的。我习惯在每次更新后,检查必填槽位是否齐全,不齐就主动追问。
最后说一句,状态追踪不是越复杂越好。我见过有人把状态搞成图结构,节点之间还有依赖关系,结果维护成本高得吓人。对于大多数场景,一个字典 + 一个栈,够用了。真的。