跳转至

项目 4: 多轮对话系统

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

难度: ⭐⭐⭐⭐ 较难 时间: 15-20 小时 涉及知识: 对话管理、上下文理解、记忆机制、 LangChain


📖 项目概述

项目背景

单轮对话系统只能处理独立的问答,无法理解对话的上下文和历史信息。多轮对话系统能够维护对话历史,理解上下文,进行连贯的多轮交互。这在智能客服、虚拟助手、教育辅导等场景中非常重要。

项目目标

构建一个完整的多轮对话系统,能够: - 维护对话历史和上下文 - 理解用户意图和槽位 - 进行多轮对话管理 - 支持对话状态跟踪 - 提供个性化回复 - 支持多用户并发对话

技术栈

  • 对话框架: LangChain, Rasa
  • LLM: OpenAI GPT-4 / 通义千问
  • 向量数据库: ChromaDB
  • 后端框架: FastAPI
  • 前端框架: Streamlit
  • 状态管理: Redis

🏗️ 项目结构

Text Only
multi-turn-dialogue-system/
├── app/                      # 应用主目录
│   ├── __init__.py
│   ├── config.py            # 配置文件
│   ├── dialogue_manager.py   # 对话管理器
│   ├── context_manager.py   # 上下文管理器
│   ├── intent_classifier.py # 意图分类器
│   ├── slot_filler.py       # 槽位填充器
│   ├── response_generator.py # 回复生成器
│   ├── memory/              # 记忆模块
│   │   ├── __init__.py
│   │   ├── short_term.py    # 短期记忆
│   │   ├── long_term.py     # 长期记忆
│   │   └── vector_store.py  # 向量存储
│   └── api/                 # API接口
│       ├── __init__.py
│       ├── chat.py          # 聊天API
│       └── sessions.py      # 会话管理API
├── frontend/                 # 前端目录
│   ├── app.py              # Streamlit应用
│   └── components/         # UI组件
├── data/                    # 数据目录
│   ├── intents/            # 意图数据
│   ├── responses/          # 回复模板
│   └── knowledge_base/     # 知识库
├── models/                  # 模型目录
│   ├── intent_classifier/  # 意图分类模型
│   └── embeddings/         # 嵌入模型
├── tests/                   # 测试目录
│   ├── test_dialogue_manager.py
│   ├── test_context_manager.py
│   └── test_response_generator.py
├── utils/                   # 工具函数
│   ├── __init__.py
│   ├── text_utils.py       # 文本工具
│   └── time_utils.py       # 时间工具
├── requirements.txt         # Python依赖
└── README.md               # 项目说明

🎯 核心功能

1. 对话管理

  • 会话管理: 管理多个用户的会话
  • 对话历史: 维护完整的对话历史
  • 上下文理解: 理解当前对话的上下文
  • 状态跟踪: 跟踪对话状态

2. 意图识别

  • 意图分类: 识别用户意图
  • 槽位提取: 提取关键信息
  • 实体识别: 识别命名实体
  • 意图消歧: 处理模糊意图

3. 回复生成

  • 模板回复: 使用预定义模板
  • 生成式回复: 使用 LLM 生成回复
  • 个性化回复: 根据用户画像个性化
  • 多模态回复: 支持文本、图片等

4. 记忆机制

  • 短期记忆: 存储当前会话信息
  • 长期记忆: 存储用户历史信息
  • 向量检索: 基于向量检索历史对话
  • 记忆更新: 动态更新记忆

5. 对话策略

  • 主动提问: 主动询问缺失信息
  • 澄清确认: 澄清模糊信息
  • 话题转换: 处理话题转换
  • 对话结束: 识别对话结束时机

💻 代码实现

1. 配置文件 (app/config.py)

Python
"""
多轮对话系统配置文件
"""
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    """应用配置"""

    # API配置
    API_HOST: str = "0.0.0.0"
    API_PORT: int = 8000
    API_PREFIX: str = "/api/v1"

    # LLM配置
    OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
    OPENAI_MODEL: str = "gpt-4o"
    OPENAI_TEMPERATURE: float = 0.7
    OPENAI_MAX_TOKENS: int = 500

    # 记忆配置
    MAX_HISTORY_LENGTH: int = 10
    MAX_SHORT_TERM_MEMORY: int = 100
    MAX_LONG_TERM_MEMORY: int = 1000

    # 向量数据库配置
    CHROMA_PERSIST_DIR: str = "./data/vector_store"
    CHROMA_COLLECTION_NAME: str = "dialogue_history"

    # 对话配置
    MAX_TURNS: int = 20
    TIMEOUT_SECONDS: int = 300

    # Redis配置
    REDIS_HOST: str = "localhost"
    REDIS_PORT: int = 6379
    REDIS_DB: int = 0

    # 意图分类配置
    INTENT_MODEL_PATH: str = "./models/intent_classifier"
    CONFIDENCE_THRESHOLD: float = 0.7

    class Config:
        env_file = ".env"

settings = Settings()

2. 对话管理器 (app/dialogue_manager.py)

Python
"""
对话管理器
"""
from datetime import datetime
import json

class DialogueManager:
    """对话管理器"""

    def __init__(self, max_history_length: int = 10):
        """
        初始化对话管理器

        Args:
            max_history_length: 最大历史长度
        """
        self.max_history_length = max_history_length
        self.dialogues: dict[str, list[dict]] = {}

    def create_session(self, session_id: str) -> bool:
        """
        创建新会话

        Args:
            session_id: 会话ID

        Returns:
            是否创建成功
        """
        if session_id in self.dialogues:
            return False

        self.dialogues[session_id] = {
            'history': [],
            'context': {},
            'state': 'active',
            'created_at': datetime.now().isoformat(),
            'last_activity': datetime.now().isoformat(),
        }
        return True

    def add_message(
        self,
        session_id: str,
        role: str,
        content: str,
        metadata: dict | None = None,
    ) -> bool:
        """
        添加消息到对话历史

        Args:
            session_id: 会话ID
            role: 角色 (user/assistant/system)
            content: 消息内容
            metadata: 元数据

        Returns:
            是否添加成功
        """
        if session_id not in self.dialogues:
            return False

        dialogue = self.dialogues[session_id]
        message = {
            'role': role,
            'content': content,
            'timestamp': datetime.now().isoformat(),
            'metadata': metadata or {},
        }

        dialogue['history'].append(message)

        # 限制历史长度
        if len(dialogue['history']) > self.max_history_length:
            dialogue['history'] = dialogue['history'][-self.max_history_length:]

        dialogue['last_activity'] = datetime.now().isoformat()
        return True

    def get_history(self, session_id: str) -> list[dict]:
        """
        获取对话历史

        Args:
            session_id: 会话ID

        Returns:
            对话历史
        """
        if session_id not in self.dialogues:
            return []
        return self.dialogues[session_id]['history']

    def update_context(
        self,
        session_id: str,
        context: dict,
    ) -> bool:
        """
        更新对话上下文

        Args:
            session_id: 会话ID
            context: 上下文信息

        Returns:
            是否更新成功
        """
        if session_id not in self.dialogues:
            return False

        self.dialogues[session_id]['context'].update(context)
        return True

    def get_context(self, session_id: str) -> dict:
        """
        获取对话上下文

        Args:
            session_id: 会话ID

        Returns:
            上下文信息
        """
        if session_id not in self.dialogues:
            return {}
        return self.dialogues[session_id]['context']

    def update_state(
        self,
        session_id: str,
        state: str,
    ) -> bool:
        """
        更新对话状态

        Args:
            session_id: 会话ID
            state: 对话状态

        Returns:
            是否更新成功
        """
        if session_id not in self.dialogues:
            return False

        self.dialogues[session_id]['state'] = state
        return True

    def get_state(self, session_id: str) -> str:
        """
        获取对话状态

        Args:
            session_id: 会话ID

        Returns:
            对话状态
        """
        if session_id not in self.dialogues:
            return 'inactive'
        return self.dialogues[session_id]['state']

    def delete_session(self, session_id: str) -> bool:
        """
        删除会话

        Args:
            session_id: 会话ID

        Returns:
            是否删除成功
        """
        if session_id not in self.dialogues:
            return False

        del self.dialogues[session_id]
        return True

    def get_session_info(self, session_id: str) -> dict | None:
        """
        获取会话信息

        Args:
            session_id: 会话ID

        Returns:
            会话信息
        """
        if session_id not in self.dialogues:
            return None

        dialogue = self.dialogues[session_id]
        return {
            'session_id': session_id,
            'state': dialogue['state'],
            'message_count': len(dialogue['history']),
            'context': dialogue['context'],
            'created_at': dialogue['created_at'],
            'last_activity': dialogue['last_activity'],
        }

3. 上下文管理器 (app/context_manager.py)

Python
"""
上下文管理器
"""
from datetime import datetime
import re

class ContextManager:
    """上下文管理器"""

    def __init__(self):
        """初始化上下文管理器"""
        self.contexts: dict[str, dict] = {}

    def create_context(self, session_id: str) -> dict:
        """
        创建新上下文

        Args:
            session_id: 会话ID

        Returns:
            上下文字典
        """
        context = {
            'session_id': session_id,
            'user_profile': {},
            'current_topic': None,
            'mentioned_entities': [],
            'previous_intents': [],
            'slot_values': {},
            'created_at': datetime.now().isoformat(),
        }
        self.contexts[session_id] = context
        return context

    def update_user_profile(
        self,
        session_id: str,
        profile_data: dict,
    ) -> bool:
        """
        更新用户画像

        Args:
            session_id: 会话ID
            profile_data: 用户画像数据

        Returns:
            是否更新成功
        """
        if session_id not in self.contexts:
            return False

        self.contexts[session_id]['user_profile'].update(profile_data)
        return True

    def set_current_topic(
        self,
        session_id: str,
        topic: str,
    ) -> bool:
        """
        设置当前话题

        Args:
            session_id: 会话ID
            topic: 话题

        Returns:
            是否设置成功
        """
        if session_id not in self.contexts:
            return False

        self.contexts[session_id]['current_topic'] = topic
        return True

    def add_mentioned_entity(
        self,
        session_id: str,
        entity: str,
        entity_type: str,
    ) -> bool:
        """
        添加提及的实体

        Args:
            session_id: 会话ID
            entity: 实体
            entity_type: 实体类型

        Returns:
            是否添加成功
        """
        if session_id not in self.contexts:
            return False

        entity_info = {
            'entity': entity,
            'type': entity_type,
            'timestamp': datetime.now().isoformat(),
        }
        self.contexts[session_id]['mentioned_entities'].append(entity_info)
        return True

    def update_slot_value(
        self,
        session_id: str,
        slot_name: str,
        slot_value: str,
    ) -> bool:
        """
        更新槽位值

        Args:
            session_id: 会话ID
            slot_name: 槽位名称
            slot_value: 槽位值

        Returns:
            是否更新成功
        """
        if session_id not in self.contexts:
            return False

        self.contexts[session_id]['slot_values'][slot_name] = slot_value
        return True

    def get_slot_value(
        self,
        session_id: str,
        slot_name: str,
    ) -> str | None:
        """
        获取槽位值

        Args:
            session_id: 会话ID
            slot_name: 槽位名称

        Returns:
            槽位值
        """
        if session_id not in self.contexts:
            return None

        return self.contexts[session_id]['slot_values'].get(slot_name)

    def get_context_summary(
        self,
        session_id: str,
    ) -> dict | None:
        """
        获取上下文摘要

        Args:
            session_id: 会话ID

        Returns:
            上下文摘要
        """
        if session_id not in self.contexts:
            return None

        context = self.contexts[session_id]
        return {
            'session_id': session_id,
            'current_topic': context['current_topic'],
            'recent_entities': context['mentioned_entities'][-5:],
            'filled_slots': context['slot_values'],
            'user_profile': context['user_profile'],
        }

    def resolve_reference(
        self,
        session_id: str,
        reference: str,
    ) -> str | None:
        """
        解析指代

        Args:
            session_id: 会话ID
            reference: 指代词

        Returns:
            解析后的实体
        """
        if session_id not in self.contexts:
            return None

        context = self.contexts[session_id]

        # 解析代词
        if reference in ['他', '她', '它']:
            # 返回最近提及的人名/物名
            for entity_info in reversed(context['mentioned_entities']):
                if entity_info['type'] in ['PERSON', 'OBJECT']:
                    return entity_info['entity']

        elif reference in ['这个', '那个']:
            # 返回最近提及的实体
            if context['mentioned_entities']:
                return context['mentioned_entities'][-1]['entity']  # [-1]负索引取最后一个元素

        return None

4. 回复生成器 (app/response_generator.py)

Python
"""
回复生成器
"""
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

class ResponseGenerator:
    """回复生成器"""

    def __init__(
        self,
        model_name: str = "gpt-4o",
        temperature: float = 0.7,
        max_tokens: int = 500,
    ):
        """
        初始化回复生成器

        Args:
            model_name: 模型名称
            temperature: 温度参数
            max_tokens: 最大token数
        """
        self.llm = ChatOpenAI(
            model_name=model_name,
            temperature=temperature,
            max_tokens=max_tokens,
        )

        # 使用LCEL方式构建对话链
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", "你是一个有帮助的AI助手,请根据上下文和对话历史进行自然的多轮对话。"),
            MessagesPlaceholder(variable_name="history"),
            ("human", "{input}"),
        ])
        self.chain = self.prompt | self.llm
        self.history: list = []  # 对话历史(Message对象列表)

    def generate_response(
        self,
        user_input: str,
        context: dict | None = None,
        history: list[dict] | None = None,
    ) -> str:
        """
        生成回复

        Args:
            user_input: 用户输入
            context: 对话上下文
            history: 对话历史

        Returns:
            生成的回复
        """
        # 构建提示
        prompt = self._build_prompt(user_input, context, history)

        # 生成回复(LCEL invoke方式)
        response = self.chain.invoke({
            "history": self.history,
            "input": prompt,
        })

        # 更新对话历史
        self.history.append(HumanMessage(content=prompt))
        self.history.append(AIMessage(content=response.content))

        return response.content

    def _build_prompt(
        self,
        user_input: str,
        context: dict | None = None,
        history: list[dict] | None = None,
    ) -> str:
        """
        构建提示

        Args:
            user_input: 用户输入
            context: 对话上下文
            history: 对话历史

        Returns:
            提示字符串
        """
        prompt_parts = []

        # 添加上下文信息
        if context:
            if context.get('current_topic'):
                prompt_parts.append(f"当前话题: {context['current_topic']}")

            if context.get('filled_slots'):
                slots_info = ", ".join(
                    f"{k}={v}"
                    for k, v in context['filled_slots'].items()
                )
                prompt_parts.append(f"已知信息: {slots_info}")

        # 添加对话历史
        if history:
            prompt_parts.append("\n对话历史:")
            for msg in history[-3:]:  # 只使用最近3轮
                role = "用户" if msg['role'] == 'user' else "助手"
                prompt_parts.append(f"{role}: {msg['content']}")

        # 添加当前输入
        prompt_parts.append(f"\n用户: {user_input}")
        prompt_parts.append("助手:")

        return "\n".join(prompt_parts)

    def generate_template_response(
        self,
        template: str,
        variables: dict[str, str],
    ) -> str:
        """
        生成模板回复

        Args:
            template: 回复模板
            variables: 变量字典

        Returns:
            生成的回复
        """
        return template.format(**variables)

    def generate_clarification(
        self,
        missing_slots: list[str],
    ) -> str:
        """
        生成澄清问题

        Args:
            missing_slots: 缺失的槽位列表

        Returns:
            澄清问题
        """
        if len(missing_slots) == 1:
            return f"请问{missing_slots[0]}是什么?"
        else:
            slots_str = "、".join(missing_slots)
            return f"请问{slots_str}分别是什么?"

5. Streamlit 前端 (frontend/app.py)

Python
"""
多轮对话系统Web界面
"""
import streamlit as st
from app.dialogue_manager import DialogueManager
from app.context_manager import ContextManager
from app.response_generator import ResponseGenerator
from app.config import settings
import uuid

# 页面配置
st.set_page_config(
    page_title="多轮对话系统",
    page_icon="💬",
    layout="wide"
)

# 初始化会话状态
if 'session_id' not in st.session_state:
    st.session_state.session_id = str(uuid.uuid4())

if 'dialogue_manager' not in st.session_state:
    st.session_state.dialogue_manager = DialogueManager(
        max_history_length=settings.MAX_HISTORY_LENGTH
    )
    st.session_state.dialogue_manager.create_session(st.session_state.session_id)

if 'context_manager' not in st.session_state:
    st.session_state.context_manager = ContextManager()
    st.session_state.context_manager.create_context(st.session_state.session_id)

if 'response_generator' not in st.session_state:
    st.session_state.response_generator = ResponseGenerator(
        model_name=settings.OPENAI_MODEL,
        temperature=settings.OPENAI_TEMPERATURE,
        max_tokens=settings.OPENAI_MAX_TOKENS,
    )

# 标题
st.title("💬 多轮对话系统")
st.markdown("---")

# 获取管理器
dialogue_manager = st.session_state.dialogue_manager
context_manager = st.session_state.context_manager
response_generator = st.session_state.response_generator
session_id = st.session_state.session_id

# 侧边栏
st.sidebar.header("对话信息")

# 显示会话ID
st.sidebar.text(f"会话ID: {session_id}")

# 显示对话状态
state = dialogue_manager.get_state(session_id)
st.sidebar.metric("对话状态", state)

# 显示消息数量
history = dialogue_manager.get_history(session_id)
st.sidebar.metric("消息数量", len(history))

# 显示上下文信息
context = context_manager.get_context_summary(session_id)
if context:
    st.sidebar.subheader("上下文信息")
    if context.get('current_topic'):
        st.sidebar.text(f"话题: {context['current_topic']}")

    if context.get('filled_slots'):
        st.sidebar.text("已知信息:")
        for slot, value in context['filled_slots'].items():
            st.sidebar.text(f"  {slot}: {value}")

# 主界面
col1, col2 = st.columns([3, 1])

with col1:
    st.subheader("对话历史")

    # 显示对话历史
    chat_container = st.container()

    for msg in history:
        if msg['role'] == 'user':
            with chat_container.chat_message("user"):
                st.write(msg['content'])
        else:
            with chat_container.chat_message("assistant"):
                st.write(msg['content'])

    # 用户输入
    user_input = st.chat_input("请输入您的消息...")

    if user_input:
        # 添加用户消息
        dialogue_manager.add_message(
            session_id,
            'user',
            user_input,
        )

        # 获取上下文和历史
        context = context_manager.get_context_summary(session_id)
        history = dialogue_manager.get_history(session_id)

        # 生成回复
        with st.spinner("正在思考..."):
            response = response_generator.generate_response(
                user_input,
                context,
                history,
            )

        # 添加助手消息
        dialogue_manager.add_message(
            session_id,
            'assistant',
            response,
        )

        # 显示回复
        with chat_container.chat_message("assistant"):
            st.write(response)

        # 重新运行页面以更新显示
        st.rerun()

with col2:
    st.subheader("操作")

    # 清空对话
    if st.button("清空对话"):
        dialogue_manager.delete_session(session_id)
        dialogue_manager.create_session(session_id)
        context_manager.create_context(session_id)
        st.rerun()

    # 显示会话详情
    if st.button("查看会话详情"):
        session_info = dialogue_manager.get_session_info(session_id)
        st.json(session_info)

🧪 测试方法

1. 单元测试

Python
"""
单元测试示例
"""
import pytest
from app.dialogue_manager import DialogueManager

def test_dialogue_manager():
    """测试对话管理器"""
    manager = DialogueManager()
    session_id = "test_session"

    # 创建会话
    assert manager.create_session(session_id) == True  # assert断言:条件False时抛出AssertionError
    assert manager.create_session(session_id) == False

    # 添加消息
    assert manager.add_message(session_id, 'user', 'Hello') == True

    # 获取历史
    history = manager.get_history(session_id)
    assert len(history) == 1
    assert history[0]['content'] == 'Hello'

    # 删除会话
    assert manager.delete_session(session_id) == True

2. 集成测试

Python
"""
集成测试示例
"""
def test_multi_turn_dialogue():
    """测试多轮对话"""
    manager = DialogueManager()
    context_manager = ContextManager()
    response_generator = ResponseGenerator()

    session_id = "test_session"
    manager.create_session(session_id)
    context_manager.create_context(session_id)

    # 第一轮对话
    manager.add_message(session_id, 'user', '我叫小明')
    context_manager.update_user_profile(session_id, {'name': '小明'})

    history = manager.get_history(session_id)
    context = context_manager.get_context_summary(session_id)
    response = response_generator.generate_response('我叫小明', context, history)

    manager.add_message(session_id, 'assistant', response)

    # 第二轮对话
    manager.add_message(session_id, 'user', '我今年20岁')
    context_manager.update_user_profile(session_id, {'age': '20'})

    history = manager.get_history(session_id)
    context = context_manager.get_context_summary(session_id)
    response = response_generator.generate_response('我今年20岁', context, history)

    # 验证回复包含用户信息
    assert '小明' in response or '20' in response

📊 扩展建议

1. 功能扩展

  • 多模态支持: 支持图片、语音输入
  • 情感分析: 识别用户情感并调整回复
  • 知识图谱: 集成知识图谱增强对话
  • 个性化推荐: 基于用户画像推荐内容
  • 多语言支持: 支持多语言对话

2. 性能优化

  • 缓存机制: 缓存常见回复
  • 异步处理: 使用异步处理提升性能
  • 模型压缩: 使用小模型提升响应速度
  • 分布式部署: 支持分布式部署

3. 智能增强

  • 主动学习: 从对话中学习
  • 对话策略优化: 优化对话策略
  • 意图迁移: 处理意图迁移
  • 话题管理: 智能管理话题转换

📚 学习收获

完成本项目后,你将掌握:

  • ✅ 多轮对话系统架构设计
  • ✅ 对话状态跟踪技术
  • ✅ 上下文理解和管理
  • ✅ LangChain 框架应用
  • ✅ 记忆机制设计
  • ✅ Streamlit Web 应用开发
  • ✅ 完整的对话系统开发流程

🔗 参考资源


项目完成时间: 15-20 小时 难度等级: ⭐⭐⭐⭐ 较难 推荐指数: ⭐⭐⭐⭐⭐