用户只回复一个“1”,系统应该查订单,还是选择房型?用户说“改成明天”,是在修改一个尚未提交的预订,还是修改已经存在的订单?
这些问题没有脱离上下文的固定答案。多轮意图识别需要结合当前任务理解输入,判断哪些信息继续保留、哪些需要修改或已经失效。
本文使用酒店助手,演示短回复、指代、槽位修改、任务切换和恢复。配套工程将“解释输入”和“更新状态”分开,便于复现错误。下载完整语料与 Python 工程,解压后使用 Python 3.10+ 即可运行离线示例。
一、区分意图与对话任务
用户说“想订北京的酒店”,建立了预订目标;接着说“2026-10-01 入住”,是在补参数;再说“不要大床房,换双床房”,是在修改参数。这三轮可以共用 book_room 意图,但对状态的影响不同。
反过来,同一个意图也可能对应新任务。用户说“另外订一间上海的房”,仍然属于 book_room,却不能把北京任务的城市覆盖成上海。
因此,仅保存 last_intent 不够。至少还要判断本轮是在新建、继续、切换、恢复还是放弃任务,并保留更新发生在哪个任务上的依据。
二、设计对话状态
附件使用以下教学结构:
{
"version": 0,
"active": {
"intent": "book_room",
"slots": {
"city": "北京"
}
},
"suspended": [],
"pending": {
"kind": "slot",
"slot": "check_in"
}
}
active 表示当前任务,slots 保存该任务已经确定的参数。suspended 保存临时挂起的任务。pending 记录系统当前在等待什么,version 则帮助发现基于旧状态计算出的结果。
这个结构保存对话任务和参数,执行结果需要单独记录。生产系统还应单独保存任务 ID、完成状态、参数来源、工具结果、确认记录和过期时间。附件是单进程内存教学实现,尚未包含这些持久化能力。
为什么要保存 pending?因为“嗯”通常是在回答某个问题。如果不知道上一问是什么,就无法确定它确认了哪个参数,甚至无法确定它是不是有效回答。
三、结合上下文理解短回复
数字选项
系统问“1 大床房,2 双床房”时,待选项可以记录为:
{
"kind": "choice",
"options": {
"1": {"room_type": "大床房"},
"2": {"room_type": "双床房"}
}
}
用户回复“1”,表示选择大床房。系统问“1 DEMO-A,2 DEMO-B”时,相同数字则选择订单。数字来自某一轮具体选项,不是全局命令。
还要检查选项是否仍有效。用户中途换了话题或页面刷新后,旧选项可能已经过期。实际服务可以使用选项消息 ID 与状态版本绑定,防止迟到回复作用于新问题。附件通过状态字段演示如何解释选项,尚未实现消息过期和选项生命周期。
确认回复与参数补充
如果系统问“入住日期是 2026-10-01,对吗”,用户回复“嗯”,可以确认该候选值。如果系统问“你想哪天入住”,同样的“嗯”没有提供日期,应继续追问。
这说明需要区分确认问题与开放式补槽问题。识别到肯定词后,仍需根据上一轮问题判断用户确认了什么。尤其是参数确认、价格确认和实际操作授权,各自需要明确的业务语境。
业务范围外的称呼偏好
“以后叫我大英雄”表达称呼偏好。如果系统具备用户偏好模块,可以路由过去;如果当前识别器只有酒店业务标签,应走范围外分支。这不应覆盖正在进行的酒店任务,也不应该因为找不到合适类别就随意选择一个标签。
四、槽位的修改、清除与保留

房型改变时保留城市与入住日期,只更新对应槽位
假设当前状态是:
{"city": "北京", "check_in": "2026-10-01", "room_type": "大床房"}
三个用户输入分别带来不同更新:
| 输入 | 操作 | 其他字段 |
|---|---|---|
| 入住改成 2026-10-02 | set check_in | 保持不变 |
| 日期先不定,删掉刚才的日期 | clear check_in | 保持不变 |
| 换成双床房 | set room_type | 日期与城市保持不变 |
本版用明确的补丁表达:
{"task_index": 0, "key": "check_in", "op": "clear", "value": null}
如果直接让模型每次输出完整槽位字典,再用新字典覆盖旧字典,就容易丢失当前轮没提到的信息。反过来,只做 dict.update 又无法区分真正删除与未知值。
补丁同样要校验。同一个槽位在一次输出中既 set 又 clear,应拒绝冲突结果。修改入住日期后,还要重新检查退房日期是否晚于入住;通过了意图与槽位校验,不等于通过日期业务校验。
五、任务切换与恢复
考虑这段对话:
| 轮次 | 用户输入 | 当前任务 | 挂起任务 |
|---|---|---|---|
| 1 | 想订北京的酒店 | 预订:北京 | 无 |
| 2 | 先说说退房时间 | 政策:退房时间 | 预订:北京 |
| 3 | 继续刚才的预订 | 预订:北京 | 政策:退房时间 |
| 4 | 另外订一间上海的房 | 新预订:上海 | 预订:北京,以及之前政策任务 |
附件保留挂起任务,便于展示隔离与恢复。真正服务需要把已经回答完的政策任务标记完成并清理,不能让任务栈无限增长。
恢复也不能只取“最近一个标签”。如果挂起列表里有北京和上海两个预订,“继续刚才那个”未必有唯一解释。应该结合可用上下文确认任务,无法唯一确定时追问。实际系统应使用稳定的任务 ID 区分不同任务,不能只用意图标签作为标识。
六、实现状态更新
完整识别契约在大模型意图识别实战中说明。下面直接构造解释结果,验证状态机的行为。这些结果是教学标注,不是模型预测。
把代码保存到解压后的工程根目录运行,或在该目录的 Python 交互环境中执行:
from core import empty_state, reduce_state
def event(intent, action, changes):
return {
"status": "known",
"intents": [intent],
"slot_updates": [
{"task_index": 0, "key": key, "op": "set", "value": value}
for key, value in changes.items()
],
"conditions": [],
"relation": "none",
"dialogue_action": action,
"clarification": None,
"evidence": [],
}
state = empty_state()
steps = [
event("book_room", "new", {"city": "北京"}),
event("book_room", "continue", {"check_in": "2026-10-01"}),
event("policy_query", "switch", {"topic": "退房时间"}),
event("book_room", "resume", {}),
]
for step in steps:
state, decision = reduce_state(state, step, state["version"])
print(state["version"], state["active"], decision)
执行后,状态版本从 1 增长到 4。最后恢复的任务仍包含:
{"intent": "book_room", "slots": {"city": "北京", "check_in": "2026-10-01"}}
由于还缺退房日期,下一步仍是 ask_slot。政策查询的 topic 没有混进预订参数,切换话题时,城市与入住日期也得到了保留。
附件的 reducer 大致按以下顺序工作:先校验输出和状态版本;不确定输入保持任务状态;多意图或条件请求转交规划器;单任务再根据动作新建、继续或恢复;最后应用槽位补丁并计算缺失参数。
它返回 ready_for_business_validation 而不是“执行成功”。必填槽位是否存在只是一个初步判断;类型、日期范围、库存、身份和授权仍需要业务层检查。
七、用版本检查处理状态冲突
假设两个请求都读取了版本 10。用户先补入住日期,随后修改城市。如果较慢的旧识别结果最后写回,直接覆盖可能把新城市或新状态丢掉。
附件要求调用者传入 expected_version,不匹配就抛出错误。这演示了乐观并发检查,但内存中的检查不是数据库级并发控制。
实际持久化时,需要在同一次原子写入中比较版本,例如只有 version=10 时才能更新为 11;若影响行数为零,则重新读取状态并决定如何处理。还需要按消息或事件 ID 去重,以免重试造成重复状态变化。识别结果可以重新计算,已经发生的写操作则必须遵守业务幂等规则。
对历史和当前意图进行分数加权,无法处理消息先后顺序,也无法防止旧结果覆盖新状态。状态冲突仍需版本检查和原子更新。
八、历史消息、状态与摘要的分工
| 信息形式 | 适合保留的内容 | 风险 |
|---|---|---|
| 最近消息 | 刚提出的选项、指代与纠正措辞 | 截断后丢失远处参数 |
| 结构化状态 | 当前任务、槽位、待回答问题 | 错误更新会持续传播 |
| 历史摘要 | 较长对话的背景与曾讨论的话题 | 摘要可能遗漏否定或改口 |
| 工具事实 | 订单状态、执行结果、价格有效期 | 需要来源、时间和权限检查 |
它们的作用互补。已经保存在状态中的城市可以直接读取,无需每轮重新从历史消息中提取;刚显示的选项则不能只保留“助手提供了几个房型”这种摘要。
上下文压缩之后,应专门测试否定、条件和已取消任务有没有被保留。可以将相同案例分别送入完整历史与压缩历史链路,观察状态更新是否一致,再定位摘要引入的错误。
多轮状态是成熟的研究任务。MultiWOZ 和中文 RiSAWOZ 可以帮助理解多领域对话及状态标注;本文使用简化的教学结构,与这些数据集的原始标注格式不同。MultiWOZ、RiSAWOZ
九、设计回归测试
测试不应只验证正常补槽。下面这些案例直接影响任务是否走错:
- 同样的“1”在两个选项上下文中更新不同槽位。
- “嗯”确认候选日期,但不能凭空回答开放式日期问题。
- “日期先不定”应删除旧日期。
- 切到订单查询后不携带预订任务的城市和房型。
- 恢复任务时保留原槽位,多个候选时先澄清。
- 新建同类任务时不覆盖旧任务。
- 放弃未提交预订不触发取消真实订单。
- 带条件的取消请求不被单任务更新器自动执行。
- 旧版本结果不能修改新版本状态。
配套工程的 28 个离线测试覆盖其中的状态更新与契约检查。短回复的语义判断则通过语料提供期望结果,尚未用真实模型进行识别效果测试。状态更新测试与模型识别评测需要分别记录。
十、运行示例与扩展功能
python3 cli.py replay --id hotel-031
python3 cli.py replay --id hotel-035
python3 cli.py replay --id hotel-038
python3 cli.py replay --id hotel-041
python3 -m unittest discover -s tests -v
它们依次展示选房型、开放式问题下的“嗯”、恢复预订,以及恢复目标不明确时保持状态。回放使用标注,默认不产生网络请求或费用。
第一版实现了输出校验、单任务状态更新、挂起恢复和版本检查的教学逻辑。选择题生成、任务完成清理、真实存储、日期归一化、权限以及工具执行尚未实现;多意图与条件任务保留结构后交给规划器。路由与完整助手文章已继续实现本地持久化、选项衔接、确认和模拟工具,可沿文末链接运行。
多轮意图识别需要把每轮输入对应到正确的任务和参数。系统应保存必要上下文,按明确规则更新状态;无法确定用户含义时,先追问,再继续处理。
相关内容:意图识别总览、48 条复杂意图语料、大模型结构化识别。
延伸阅读:标签、方法与决策
- 意图标签怎么设计:分类粒度、槽位边界与多级意图体系。
- 意图识别模型怎么选:规则、TF-IDF、向量检索与小模型基线。
- 一句话多个意图怎么办:多标签识别、条件与任务依赖。
- 意图不明确时怎么办:未知意图识别、拒识与澄清追问。