本文目录导读:

多轮对话的实现,核心在于让程序记住上下文,单轮对话就像一个只有瞬时记忆的人,你问一句他答一句;多轮对话则像一个有长期记忆的人,能理解你基于前面聊天的后续问题。
实现方式主要分为以下几个层次,从简单到复杂:
核心思路:记忆上下文
多轮对话的本质是 “历史对话 + 当前问题 = 新回复”,我们需要把之前的对话内容以一种结构化的方式传递给模型或规则引擎。
基于规则的简单实现(适合冷启动、客服QA)
这种方法不依赖于大模型,适合业务逻辑固定、需要精确控制的场景。
实现原理:
- 状态管理:用一个变量(如
session_state)记录当前对话的阶段(asking_name->asking_age->done)。 - 槽位填充(Slot Filling):定义一个表单(Form),包含需要收集的信息(槽位),用户每回答一次,就填充一个槽位。
- 规则判断:根据当前状态和用户输入,触发不同的回复逻辑。
代码示例(伪代码):
# 用户会话状态管理
sessions = {}
def handle_chat(user_id: str, user_input: str) -> str:
# 1. 获取或初始化用户状态
if user_id not in sessions:
sessions[user_id] = {"state": "ask_name", "data": {}}
session = sessions[user_id]
state = session["state"]
data = session["data"]
# 2. 状态机逻辑
if state == "ask_name":
data["name"] = user_input
session["state"] = "ask_age"
return f"你好 {user_input}!请问你多大了?"
elif state == "ask_age":
# 可以加入简单的验证
if user_input.isdigit():
data["age"] = int(user_input)
session["state"] = "done"
return f"好的 {data['name']},你今年 {data['age']} 岁了,有什么可以帮你的?"
else:
return "请输入一个有效的年龄数字。"
elif state == "done":
# 在完成状态,可以回到初始状态或执行其他逻辑
# 这里可以调用一个意图识别,如果用户说“重新开始”,则重置
if "重新开始" in user_input:
sessions[user_id] = {"state": "ask_name", "data": {}}
return "好的,让我们重新开始,请问你叫什么名字?"
else:
# 执行其他业务,比如查询
return f"{data['name']},你说的是:{user_input},我会记录下来。"
else:
return "我不明白你的意思,请重新说一遍。"
# 测试
print(handle_chat("user_1", "小明")) # 你好 小明!请问你多大了?
print(handle_chat("user_1", "25")) # 好的 小明,你今年 25 岁了,有什么可以帮你的?
print(handle_chat("user_1", "帮我查一下天气")) # 小明,你说的是:帮我查一下天气,我会记录下来。
优点:逻辑清晰、可控、响应快、不依赖外部API。 缺点:对话流程固定,无法处理复杂的、开放式的闲聊。
基于大语言模型(LLM,如GPT-4、通义千问、文心一言)
这是目前最主流、体验最好的方式,关键点在于 向前端(用户)和模型隐藏内部实现,只传递一个不断增长的“对话历史”列表。
实现原理:
- 维护一个消息列表:
messages = [{"role": "system", "content": "你是...设定..."}] - 每次用户提问:
- 将用户输入作为
{"role": "user", "content": "用户问题"}追加到messages列表。 - 将整个
messages列表发送给LLM API。 - LLM返回回复,将其作为
{"role": "assistant", "content": "模型回复"}追加到messages列表。 - 将回复返回给用户。
- 将用户输入作为
- 上下文管理:这就是关键,列表的长度会无限增长,最终超出模型的最大上下文长度(如4K、8K、128K tokens)。
代码示例(使用类似OpenAI的API):
import openai # 假设使用OpenAI标准API
# 存储所有用户的对话历史 (实际项目会用Redis等)
user_sessions = {}
def chat_with_history(user_id: str, user_input: str, api_key: str, base_url: str) -> str:
# 1. 获取或初始化用户的对话历史
if user_id not in user_sessions:
user_sessions[user_id] = [
{"role": "system", "content": "你是一个乐于助人的AI助手,你的回答要简洁、准确。"}
]
messages = user_sessions[user_id]
# 2. 添加用户当前输入到历史
messages.append({"role": "user", "content": user_input})
# --- 上下文窗口管理 (关键!) ---
# 如果消息太长,需要裁剪,简单策略:只保留最近的N条消息 + system prompt
MAX_HISTORY = 10 # 最多保留最近10轮对话 (用户+助手各算1条)
if len(messages) > MAX_HISTORY + 1: # +1 是 system message
# 保留 system prompt,然后只取最近的 MAX_HISTORY 条
messages = [messages[0]] + messages[-(MAX_HISTORY):]
user_sessions[user_id] = messages
# 3. 调用LLM API (伪代码)
# client = openai.OpenAI(api_key=api_key, base_url=base_url)
# response = client.chat.completions.create(
# model="gpt-3.5-turbo", # 或其他模型
# messages=messages
# )
# assistant_reply = response.choices[0].message.content
# --- 模拟LLM回复 (实际请替换为上述API调用) ---
# 这里为了演示,假设LLM就是简单拼接一下
if "我的名字" in user_input:
assistant_reply = "你好!我不知道你的名字,请告诉我。"
elif "我叫" in user_input:
name = user_input.split("我叫")[1].strip()
assistant_reply = f"你好 {name}!很高兴认识你。"
elif "quot; in user_input:
assistant_reply = "好的,我已经记住了。"
else:
# 假设LLM能基于历史回复
# 这里我们简单模拟:如果历史中有名字,就引用
name = None
for msg in messages:
if msg["role"] == "user" and "我叫" in msg["content"]:
name = msg["content"].split("我叫")[1].strip()
if name:
assistant_reply = f"{name},你说的是:{user_input}"
else:
assistant_reply = f"你刚才说的是:{user_input}"
# --- 模拟结束 ---
# 4. 添加模型回复到历史
messages.append({"role": "assistant", "content": assistant_reply})
# 5. 更新session
user_sessions[user_id] = messages
return assistant_reply
# 测试
print(chat_with_history("user_1", "你好")) # 你刚才说的是:你好
print(chat_with_history("user_1", "我叫小明")) # 你好 小明!很高兴认识你。
print(chat_with_history("user_1", "你记得我吗?")) # 小明,你说的是:你记得我吗?
print(chat_with_history("user_1", "我的名字是什么?")) # 依赖于模型的推理能力,这里假设它能从历史中找到
优点:体验流畅、能理解复杂上下文、适合开放域对话。 缺点:成本高(API调用费)、需要上下文窗口管理、模型有时会“遗忘”或“幻觉”。
关键挑战与解决方案
无论用哪种方法,都会遇到以下问题:
-
上下文长度限制:LLM有token上限(如4K、128K)。
- 解决方案:
- 滑动窗口:只保留最近N轮对话。
- 摘要:当历史太长时,让LLM对前面的对话进行摘要,用摘要替换长历史。
- 向量数据库:只检索与当前问题最相关的历史片段(RAG技术)。
- 解决方案:
-
用户身份识别:如何区分用户A和用户B的对话?
- 解决方案:使用唯一标识符(用户ID、会话ID、设备ID等)作为Key,存储在数据库(Redis、Memory)中。
-
多轮状态机 + LLM混合:在需要固定流程(如订餐)的场景,结合规则(方法一)和LLM(方法二)。
- 实现:LLM负责提取槽位信息(名字、日期、菜品),规则引擎负责状态跳转(是否填满所有槽位、是否确认订单),例如Rasa框架。
-
成本控制:对于某些简单轮次,不调用LLM,直接用规则回复。
- 实现:先用意图识别(小模型或规则)判断用户意图,如果意图是“问候”,直接回复“你好!”,不用调用昂贵的GPT-4。
| 实现方式 | 核心原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 基于规则 | 状态机,槽位填充 | 客服QA、表单填写、简单问答 | 可控、快速、成本低 | 僵化、无法处理复杂/未预定义问题 |
| 基于大模型 | 维护消息历史列表 | 闲聊、知识问答、创意写作、复杂业务 | 体验好、灵活、理解上下文 | 成本高、管理上下文有挑战、有幻觉风险 |
| 混合架构 | 规则+大模型 | 大部分真实商业场景(如Rasa框架) | 取长补短,控制与灵活兼具 | 架构较复杂 |
最佳实践建议: 对于大多数需要“自然对话”场景(如个人助手、智能客服),推荐使用基于大模型的方法,但一定要做好上下文窗口管理和用户会话隔离,如果业务流程非常固定(如收集信息、简单FAQ),则可以优先考虑基于规则的方法,或者将规则与大模型结合。