做一个能回答单轮问题的AI应用并不难,接一个大模型API就够了。但一旦进入真实业务场景,比如订机票、查询订单、售后咨询,用户不会规规矩矩地一句话说完所有需求,而是想到哪说到哪:先问机票,中途改问酒店,回头又继续订票。这时候系统必须能记住之前说了什么、判断用户当前到底想干嘛、还能在新意图和旧意图之间做好衔接。这三件事分别对应多轮对话管理、上下文记忆和意图切换,是一个生产级对话系统的核心骨架。

一、多轮对话管理的两种经典模型
多轮对话管理要解决的根本问题是:给定用户最新一句输入和之前所有对话历史,系统应该进入什么状态、执行什么动作。业界主要有两种建模思路,一种是基于填槽(Frame-Based)的框架式对话,另一种是基于状态机的流程式对话。
填槽模型把每个任务意图抽象成一组必填槽位。以订机票为例,槽位包括出发城市、到达城市、出发日期、舱位等级。用户的每一句话都会被解析成槽位值,系统检查哪些槽位还没填满,没填满就追问,填满了就执行任务。这种模型结构清晰、易于维护,非常适合任务型对话,缺点是当任务复杂、槽位之间有依赖关系时,追问策略容易写得很乱。
状态机模型则把对话流程画成一张图,每个节点代表一个对话阶段,边上的条件代表用户输入触发的跳转。它的好处是流程可视化、每一步的行为都可控可回溯,客服类、引导类场景用得非常多。但状态机的分支数量会随业务复杂度爆炸式增长,后期维护成本高。实际工程中,常见做法是把两者结合:用状态机管理大流程,用填槽处理每个节点内部的信息收集。
二、上下文记忆的实现方案:从滑动窗口到向量记忆
大模型本身是无状态的,所谓记忆,本质上是每次请求时把历史对话重新塞进上下文窗口。问题在于窗口有长度限制,而且历史越长,推理成本越高、响应越慢、关键信息越容易被淹没。工程上主要有三种记忆方案,可以按业务复杂度逐级选用。
第一种是滑动窗口记忆,只保留最近N轮对话。实现最简单,短期上下文连贯性也不错,适合闲聊和简单查询。第二种是摘要压缩记忆,用一个轻量模型对超出窗口的历史做摘要,把长对话压缩成几百字的背景描述再拼进上下文,兼顾了信息保留和成本控制。第三种是向量检索记忆,把每轮对话编码成向量存入向量库,新请求到来时先检索最相关的若干条历史,再做增强生成。这种方案适合跨会话的长期记忆,比如记住用户一个月前提过的偏好。
下面用一个可运行的Python示例演示滑动窗口加摘要的组合实现:
class DialogMemory:
def __init__(self, window_size=6):
self.window_size = window_size # 滑动窗口保留的轮数
self.history = [] # 最近N轮对话
self.summary = "" # 更早历史的摘要
def add(self, role, content):
self.history.append({"role": role, "content": content})
# 超出窗口时,把最旧的一轮合并进摘要
if len(self.history) > self.window_size:
dropped = self.history.pop(0)
self.summary += f"{dropped['role']}: {dropped['content']}\n"
def build_context(self):
parts = []
if self.summary:
# 摘要压缩有上限,实际项目中应调用模型做二次压缩
if len(self.summary) > 500:
self.summary = self.summary[-500:]
parts.append({"role": "system", "content": "历史摘要:" + self.summary})
parts.extend(self.history)
return parts
这个实现的关键点在于build_context方法:摘要被包装成system消息放在最前面,最近的对话保持原始形态放在后面,模型既能看到长期背景,又不会被截断关键信息。生产环境中还应该加上用户身份维度做记忆隔离,避免不同用户的上下文串味。
三、意图切换的判定与槽位继承策略
意图切换是多轮对话里最容易翻车的环节。用户在订机票到一半时突然问了一句明天天气怎么样,如果系统没有切换能力,就会把这句话硬塞进订票流程,解析出一堆错误槽位。反过来,如果切换判定太敏感,用户随口一句相关追问就会被误判成新意图,正在进行的任务被打断,体验同样糟糕。
处理意图切换的第一步是让意图分类器输出多标签加置信度,而不是单标签硬判定。当新输入的意图置信度明显高于当前活跃意图,且两者不一致时,才触发切换。同时要设计好栈式的意图管理:切换到新意图时,旧意图及其已填槽位压栈保存而不是直接丢弃,等新意图完成后弹栈恢复。这样用户问完天气回到订票时,之前填的出发城市、日期都还在,不需要重新说一遍,这就是槽位继承。
下面是一个简化版的意图栈管理器:
class IntentStack:
def __init__(self, switch_threshold=0.7):
self.stack = [] # 意图栈,栈顶是当前活跃意图
self.switch_threshold = switch_threshold
def handle(self, intent_name, confidence, slots):
if not self.stack:
self.stack.append({"intent": intent_name, "slots": slots})
return "new_task"
current = self.stack[-1]
if intent_name == current["intent"]:
# 同一意图,合并槽位
current["slots"].update(slots)
return "continue"
if confidence >= self.switch_threshold:
# 高置信度的新意图,压栈而不是丢弃旧任务
self.stack.append({"intent": intent_name, "slots": slots})
return "switch"
# 置信度不足,维持当前意图,当作补充信息处理
current["slots"].update(slots)
return "ambiguous"
def finish_current(self):
"""当前意图完成后弹栈,恢复上一个任务"""
if self.stack:
return self.stack.pop()
return None
这里的ambiguous分支值得注意:低置信度的新意图信号被降级为槽位合并,宁可慢一步确认,也不要贸然切换。在真实系统中,还可以让模型在歧义时主动反问一句,您是想先查询天气,还是继续订机票,把最终裁决权交还给用户,这比任何算法判定都可靠。
四、把三个模块串成完整链路
有了对话管理、记忆和意图栈,整条处理链路就清晰了:用户输入先经过意图识别和槽位抽取,结果送入意图栈判定是继续、切换还是歧义;根据栈顶意图确定下一步动作(追问缺失槽位或执行任务);同时每轮输入输出都写入记忆模块,供下一轮构建上下文。执行动作时再调用大模型做自然语言生成,把结构化的动作转化成人性化的回复。
需要提醒的是,意图识别本身也可以直接交给大模型完成,用一个精心设计的提示词让模型输出JSON格式的意图和槽位,往往比训练一个小分类器效果更好、迭代更快。但无论用哪种方案,意图库的边界定义、槽位继承规则、上下文长度控制这三件事都躲不掉,它们才是对话系统真正的护城河。建议从最小可用版本起步:先实现单一意图的填槽加追问,跑通后再加记忆模块,最后才引入意图栈处理多任务并行,循序渐进地堆砌复杂度,出问题时也容易定位。