实战案例1:智能客服机器人

中级 ⏱️ 预计学习时间:8-10小时
RAG FastAPI Vue3 向量数据库 WebSocket

一、项目概述

1.1 项目背景和价值

随着企业业务规模扩大,传统人工客服面临响应慢、成本高、服务时间受限等挑战。本案例将构建一个基于大语言模型的智能客服系统,具备以下核心价值:

  • 7×24小时服务:AI自动回答常见问题,无需人工值守
  • 秒级响应:平均响应时间控制在2秒以内
  • 成本降低:减少80%的人工客服工作量
  • 服务质量一致:避免人工情绪波动影响服务体验
  • 无缝升级:复杂问题自动转人工,确保服务质量

1.2 功能特性列表

模块功能说明
对话系统多轮对话支持上下文理解,连续追问
知识库问答基于企业文档精准回答
意图识别自动判断用户意图
Markdown渲染支持富文本格式回复
人工接管智能转接关键词/置信度触发转人工
实时通信WebSocket双向通信
会话接管客服可接管AI对话
知识库文档上传支持PDF/Word/TXT
自动向量化文档自动切分索引
增量更新支持知识库热更新
管理后台对话历史查看所有会话记录
数据统计问答准确率统计

1.3 技术架构图

┌─────────────────────────────────────────────────────────────────┐
│                           客户端层                               │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
│  │ 用户聊天界面 │  │ 客服工作台  │  │      管理后台            │  │
│  │  (Vue3)     │  │   (Vue3)    │  │      (Vue3)              │  │
│  └──────┬──────┘  └──────┬──────┘  └───────────┬─────────────┘  │
└─────────┼────────────────┼────────────────────┼────────────────┘
          │                │                    │
          └────────────────┴────────────────────┘
                           │ HTTPS/WSS
┌──────────────────────────┼──────────────────────────────────────┐
│                          ▼                                      │
│                      Nginx 反向代理                              │
│                    (SSL终止/负载均衡)                             │
└──────────────────────────┬──────────────────────────────────────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐
│   前端静态资源   │ │  REST API   │ │  WebSocket服务  │
│   (Nginx托管)   │ │   FastAPI   │ │    FastAPI      │
└─────────────────┘ └──────┬──────┘ └─────────────────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐
│   RAG问答引擎    │ │  对话管理    │ │   人工接管服务   │
│  (LangChain)    │ │   Service   │ │    Service      │
└─────────────────┘ └──────┬──────┘ └─────────────────┘
                           │
                           ▼
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐
│   向量数据库     │ │   LLM API   │ │    关系数据库    │
│    (Chroma)     │ │ OpenAI/国产  │ │   (SQLite/PG)   │
└─────────────────┘ └─────────────┘ └─────────────────┘

1.4 最终效果预览

用户端界面:

🤖
您好!我是智能客服小助手,请问有什么可以帮助您?
我想退货怎么操作?
👤
🤖
您好!退货流程如下:
1. 登录账户 → 我的订单
2. 找到对应订单,点击"申请退货"
3. 填写退货原因,提交申请
4. 审核通过后,按地址寄回商品
5. 仓库签收后3个工作日内退款

如需人工协助,请回复"转人工"

二、技术选型

🎨 前端技术栈

  • Vue 3 - 渐进式JavaScript框架,组合式API
  • Element Plus - 企业级UI组件库
  • Pinia - 状态管理
  • Axios - HTTP客户端
  • Marked - Markdown解析
  • Vite - 构建工具

⚙️ 后端技术栈

  • Python 3.10+ - 编程语言
  • FastAPI - 高性能Web框架
  • LangChain - LLM应用开发框架
  • Uvicorn - ASGI服务器
  • WebSockets - 实时通信
  • Pydantic - 数据验证

🤖 AI/LLM 技术

  • OpenAI GPT-4 - 主力LLM(需翻墙)
  • 文心一言 - 百度国产替代
  • 通义千问 - 阿里国产替代
  • 智谱GLM - 清华系国产替代
  • Sentence-Transformers - Embedding模型

🗄️ 数据存储

  • Chroma - 向量数据库
  • FAISS - Facebook向量检索库
  • SQLite - 开发环境关系数据库
  • PostgreSQL - 生产环境关系数据库
  • Redis - 缓存/会话存储

🚀 部署运维

  • Docker - 容器化
  • Docker Compose - 多容器编排
  • Nginx - 反向代理/静态服务器
  • Certbot - SSL证书
  • PM2 - Node进程管理

三、开发环境准备

3.1 Python 3.10+ 安装

Windows 安装
# 1. 访问 https://www.python.org/downloads/ 下载 Python 3.10+
# 2. 安装时勾选 "Add Python to PATH"
# 3. 验证安装
python --version  # 应显示 Python 3.10.x 或更高版本

# 安装 pip 包管理器(通常已包含)
python -m ensurepip --upgrade
macOS 安装
# 使用 Homebrew 安装
brew install python@3.10

# 验证安装
python3 --version

3.2 Node.js 18+ 安装

Windows/macOS 安装
# 访问 https://nodejs.org/ 下载 LTS 版本
# 或使用 nvm 管理多版本

# Windows 使用 nvm-windows
nvm install 18.17.0
nvm use 18.17.0

# macOS/Linux 使用 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18

# 验证安装
node --version  # v18.x.x
npm --version   # 9.x.x

3.3 开发工具推荐

工具推荐插件用途
VS Code Python Python语法高亮、调试、智能提示
PylancePython语言服务器,增强类型检查
VolarVue3官方插件
ESLint + Prettier代码格式化和规范检查
数据库工具 SQLite Viewer VS Code内查看SQLite数据库
DBeaver通用数据库管理工具
API测试 Thunder Client VS Code内REST API测试

3.4 API Key获取

OpenAI API Key(需要国际信用卡)

  1. 访问 https://platform.openai.com
  2. 注册/登录账号
  3. 进入 Settings → Billing,绑定信用卡
  4. 进入 API Keys → Create new secret key
  5. 复制生成的 key(以 sk- 开头),仅显示一次

国产替代方案(推荐)

平台注册地址特点
智谱AI (GLM)https://open.bigmodel.cn注册送额度,GLM-4效果接近GPT-4
百度文心https://console.bce.baidu.com国内稳定,文档丰富
阿里通义https://dashscope.console.aliyun.com免费额度充足
DeepSeekhttps://platform.deepseek.com性价比高

四、后端开发(详细步骤)

4.1 项目初始化

创建项目目录和虚拟环境
# 创建项目根目录
mkdir customer-service-bot
cd customer-service-bot

# 创建后端目录
mkdir backend
cd backend

# 创建Python虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate

# 升级pip
pip install --upgrade pip

# 安装依赖包
pip install fastapi uvicorn langchain langchain-openai chromadb pydantic python-multipart python-dotenv websockets sqlalchemy alembic

# 生成依赖清单
pip freeze > requirements.txt

4.2 项目结构

backend/
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI主入口
│   ├── config.py            # 配置文件
│   ├── models.py            # 数据模型
│   ├── database.py          # 数据库连接
│   ├── dependencies.py      # 依赖注入
│   ├── services/
│   │   ├── __init__.py
│   │   ├── knowledge_base.py    # 知识库服务
│   │   ├── chat_service.py      # 对话服务
│   │   ├── llm_service.py       # LLM服务
│   │   └── handover.py          # 人工接管
│   └── routers/
│       ├── __init__.py
│       ├── chat.py          # 聊天接口
│       ├── admin.py         # 管理接口
│       └── websocket.py     # WebSocket接口
├── data/
│   ├── knowledge/           # 知识库文档存储
│   └── chroma_db/           # Chroma向量数据库
├── logs/                    # 日志目录
├── tests/                   # 测试目录
├── .env                     # 环境变量
├── requirements.txt         # Python依赖
└── Dockerfile               # Docker镜像配置

4.3 完整源码

config.py - 系统配置

backend/app/config.py
"""
配置文件模块 - 管理所有系统配置参数
使用 pydantic_settings 实现配置验证和环境变量加载
"""

import os
from typing import Optional
from pydantic_settings import BaseSettings
from functools import lru_cache


class Settings(BaseSettings):
    """系统配置类 - 所有配置项均可通过环境变量覆盖"""
    
    # ========== 应用基础配置 ==========
    APP_NAME: str = "智能客服机器人系统"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = False
    SECRET_KEY: str = "your-secret-key-change-in-production"
    
    # ========== 服务器配置 ==========
    HOST: str = "0.0.0.0"
    PORT: int = 8000
    WORKERS: int = 1
    
    # ========== LLM API 配置 ==========
    OPENAI_API_KEY: Optional[str] = None
    OPENAI_BASE_URL: str = "https://api.openai.com/v1"
    OPENAI_MODEL: str = "gpt-4o-mini"
    
    ZHIPU_API_KEY: Optional[str] = None
    ZHIPU_MODEL: str = "glm-4"
    
    LLM_PROVIDER: str = "zhipu"  # 可选: openai, zhipu
    
    # ========== 向量数据库配置 ==========
    CHROMA_PERSIST_DIRECTORY: str = "./data/chroma_db"
    CHROMA_COLLECTION_NAME: str = "knowledge_base"
    EMBEDDING_MODEL: str = "BAAI/bge-large-zh"
    TOP_K: int = 5
    SIMILARITY_THRESHOLD: float = 0.6
    
    # ========== 对话系统配置 ==========
    MAX_HISTORY: int = 10
    MAX_TOKENS: int = 2000
    TEMPERATURE: float = 0.7
    SESSION_TIMEOUT: int = 3600
    
    # ========== 人工接管配置 ==========
    HANDOVER_KEYWORDS: list = ["人工", "客服", "投诉", "找人工", "转人工"]
    HANDOVER_CONFIDENCE_THRESHOLD: float = 0.3
    
    # ========== 数据库配置 ==========
    DATABASE_URL: str = "sqlite:///./data/app.db"
    
    # ========== 日志配置 ==========
    LOG_LEVEL: str = "INFO"
    LOG_FILE: str = "./logs/app.log"
    
    # ========== CORS配置 ==========
    CORS_ORIGINS: list = ["http://localhost:5173", "http://localhost:3000"]
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"
        case_sensitive = True


@lru_cache()
def get_settings():
    return Settings()


settings = get_settings()

models.py - 数据模型

backend/app/models.py
"""数据模型定义 - Pydantic模型和SQLAlchemy ORM模型"""

from datetime import datetime
from typing import List, Optional, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field
from sqlalchemy import Column, String, DateTime, Text, Integer, Float, Boolean, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import uuid

# SQLAlchemy ORM 模型
Base = declarative_base()


class ConversationDB(Base):
    """对话记录数据库模型"""
    __tablename__ = "conversations"
    
    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    session_id = Column(String(36), index=True, nullable=False)
    user_id = Column(String(50), nullable=True)
    role = Column(String(20), nullable=False)  # user, assistant, human, system
    content = Column(Text, nullable=False)
    message_type = Column(String(20), default="text")
    created_at = Column(DateTime, default=datetime.utcnow)
    is_deleted = Column(Boolean, default=False)


class KnowledgeDocDB(Base):
    """知识库文档数据库模型"""
    __tablename__ = "knowledge_docs"
    
    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    filename = Column(String(255), nullable=False)
    title = Column(String(255), nullable=True)
    file_type = Column(String(20), nullable=False)
    file_path = Column(String(500), nullable=False)
    file_size = Column(Integer, default=0)
    status = Column(String(20), default="pending")
    chunk_count = Column(Integer, default=0)
    uploaded_at = Column(DateTime, default=datetime.utcnow)


# Pydantic 模型(API请求/响应)
class ChatMessage(BaseModel):
    """单条聊天消息模型"""
    role: str
    content: str = Field(..., min_length=1, max_length=10000)
    message_type: str = Field(default="text")
    timestamp: Optional[datetime] = None


class ChatRequest(BaseModel):
    """聊天请求模型"""
    session_id: Optional[str] = None
    message: str = Field(..., min_length=1, max_length=5000)
    user_id: Optional[str] = None


class ChatResponse(BaseModel):
    """聊天响应模型"""
    session_id: str
    message: ChatMessage
    sources: Optional[List[Dict[str, Any]]] = None
    confidence: float = 0.0
    handover_triggered: bool = False
    handover_reason: Optional[str] = None


def init_database(database_url: str):
    """初始化数据库连接"""
    engine = create_engine(
        database_url,
        connect_args={"check_same_thread": False} if "sqlite" in database_url else {},
        echo=False
    )
    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    Base.metadata.create_all(bind=engine)
    return engine, SessionLocal

knowledge_base.py - 知识库服务

backend/app/services/knowledge_base.py
"""知识库服务模块 - 负责文档处理、向量化和检索"""

import os
import logging
from typing import List, Dict, Any, Optional
from pathlib import Path

from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma

logger = logging.getLogger(__name__)


class KnowledgeBaseService:
    """知识库服务类 - 提供文档加载、切分、向量化和检索"""
    
    def __init__(self, persist_directory: str = "./data/chroma_db",
                 collection_name: str = "knowledge_base",
                 embedding_model: str = "BAAI/bge-large-zh"):
        self.persist_directory = persist_directory
        self.collection_name = collection_name
        self.embedding_model_name = embedding_model
        
        os.makedirs(persist_directory, exist_ok=True)
        
        self._embeddings = None
        self._vector_store = None
        
        # 文本分割器配置
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=50,
            length_function=len,
            separators=["\n\n", "\n", "。", ".", "!", "?", ";", " "],
        )
    
    @property
    def embeddings(self):
        """懒加载嵌入模型"""
        if self._embeddings is None:
            self._embeddings = HuggingFaceEmbeddings(
                model_name=self.embedding_model_name,
                model_kwargs={"device": "cpu"},
                encode_kwargs={"normalize_embeddings": True},
            )
        return self._embeddings
    
    @property
    def vector_store(self):
        """懒加载向量数据库"""
        if self._vector_store is None:
            self._vector_store = Chroma(
                collection_name=self.collection_name,
                embedding_function=self.embeddings,
                persist_directory=self.persist_directory,
            )
        return self._vector_store
    
    def load_document(self, file_path: str):
        """根据文件类型加载文档"""
        file_path = Path(file_path)
        suffix = file_path.suffix.lower()
        
        if suffix == ".pdf":
            loader = PyPDFLoader(str(file_path))
        elif suffix in [".txt", ".md"]:
            loader = TextLoader(str(file_path), encoding="utf-8")
        else:
            raise ValueError(f"不支持的文件类型: {suffix}")
        
        return loader.load()
    
    def split_documents(self, documents):
        """将文档切分为文本块"""
        for doc in documents:
            if not hasattr(doc, 'metadata'):
                doc.metadata = {}
        return self.text_splitter.split_documents(documents)
    
    def add_document(self, file_path: str, doc_id: Optional[str] = None, metadata: Optional[Dict] = None):
        """添加文档到知识库"""
        documents = self.load_document(file_path)
        chunks = self.split_documents(documents)
        
        for i, chunk in enumerate(chunks):
            chunk.metadata.update({
                "doc_id": doc_id or Path(file_path).stem,
                "chunk_index": i,
                "source_file": str(file_path),
                **(metadata or {})
            })
        
        chunk_ids = self.vector_store.add_documents(chunks)
        self.vector_store.persist()
        
        return len(chunks), chunk_ids
    
    def similarity_search(self, query: str, k: int = 5, threshold: float = 0.6):
        """相似度搜索 - RAG核心检索功能"""
        results = self.vector_store.similarity_search_with_score(query, k=k)
        
        filtered_results = []
        for doc, score in results:
            similarity = 1 - score  # 转换为相似度
            if similarity >= threshold:
                doc.metadata["similarity"] = similarity
                filtered_results.append((doc, similarity))
        
        return filtered_results


# 单例模式
_knowledge_base = None

def get_knowledge_base():
    global _knowledge_base
    if _knowledge_base is None:
        _knowledge_base = KnowledgeBaseService()
    return _knowledge_base

chat_service.py - 对话服务

backend/app/services/chat_service.py
"""对话服务模块 - RAG问答核心逻辑"""

import logging
from typing import List, Dict, Optional
from datetime import datetime
import uuid

from app.config import settings
from app.services.knowledge_base import get_knowledge_base
from app.services.llm_service import get_llm_service

logger = logging.getLogger(__name__)


class ChatService:
    """对话服务类 - 处理用户输入到AI回复的完整流程"""
    
    def __init__(self):
        self.kb = get_knowledge_base()
        self.llm = get_llm_service()
        self.sessions: Dict[str, List[Dict]] = {}
        
        self.system_prompt = """你是智能客服助手,基于以下知识回答用户问题。

【知识库内容】
{context}

【对话历史】
{history}

请基于以上信息回答,如果不确定请诚实告知。"""
    
    def _get_session_history(self, session_id: str):
        return self.sessions.get(session_id, [])
    
    def _add_to_history(self, session_id: str, role: str, content: str):
        if session_id not in self.sessions:
            self.sessions[session_id] = []
        self.sessions[session_id].append({
            "role": role, "content": content,
            "timestamp": datetime.utcnow().isoformat()
        })
        # 保留最近N轮
        max_history = settings.MAX_HISTORY * 2
        if len(self.sessions[session_id]) > max_history:
            self.sessions[session_id] = self.sessions[session_id][-max_history:]
    
    def _format_context(self, search_results):
        if not search_results:
            return "(知识库中暂无相关信息)"
        parts = []
        for i, (doc, score) in enumerate(search_results, 1):
            parts.append(f"[{i}] {doc.page_content}")
        return "\n".join(parts)
    
    def _check_handover_keywords(self, message: str):
        message_lower = message.lower()
        for keyword in settings.HANDOVER_KEYWORDS:
            if keyword in message_lower:
                return True, f"触发关键词: {keyword}"
        return False, None
    
    async def chat(self, message: str, session_id: Optional[str] = None, user_id: Optional[str] = None):
        """处理用户对话请求"""
        session_id = session_id or str(uuid.uuid4())
        
        # 检查转人工关键词
        handover_triggered, handover_reason = self._check_handover_keywords(message)
        if handover_triggered:
            return {
                "session_id": session_id,
                "message": {"role": "assistant", "content": "正在为您转接人工客服..."},
                "handover_triggered": True,
                "handover_reason": handover_reason,
                "confidence": 0.0
            }
        
        # 检索相关知识
        search_results = self.kb.similarity_search(
            query=message, k=settings.TOP_K, threshold=settings.SIMILARITY_THRESHOLD
        )
        
        confidence = sum(s for _, s in search_results) / len(search_results) if search_results else 0.0
        
        # 组装提示词
        system_msg = self.system_prompt.format(
            context=self._format_context(search_results),
            history=str(self._get_session_history(session_id))
        )
        
        messages = [
            {"role": "system", "content": system_msg},
            {"role": "user", "content": message}
        ]
        
        # 调用LLM
        try:
            response_text = await self.llm.chat(messages)
        except Exception as e:
            response_text = "抱歉,服务暂时不可用。"
            confidence = 0.0
        
        # 保存历史
        self._add_to_history(session_id, "user", message)
        self._add_to_history(session_id, "assistant", response_text)
        
        return {
            "session_id": session_id,
            "message": {"role": "assistant", "content": response_text},
            "confidence": round(confidence, 3),
            "handover_triggered": handover_triggered,
            "sources": [{"content": d.page_content[:100], "score": s} for d, s in search_results]
        }


# 单例
_chat_service = None

def get_chat_service():
    global _chat_service
    if _chat_service is None:
        _chat_service = ChatService()
    return _chat_service

main.py - FastAPI主入口

backend/app/main.py
"""FastAPI应用主入口"""

import os
import sys
import logging
from contextlib import asynccontextmanager

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from app.config import settings
from app.models import init_database
from app.routers import chat, admin, websocket

# 配置日志
os.makedirs("./logs", exist_ok=True)
logging.basicConfig(
    level=getattr(logging, settings.LOG_LEVEL),
    format=settings.LOG_FORMAT,
    handlers=[
        logging.FileHandler(settings.LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """应用生命周期管理"""
    logger.info(f"启动 {settings.APP_NAME} v{settings.APP_VERSION}")
    engine, SessionLocal = init_database(settings.DATABASE_URL)
    app.state.engine = engine
    app.state.SessionLocal = SessionLocal
    os.makedirs("./data/knowledge", exist_ok=True)
    os.makedirs("./data/chroma_db", exist_ok=True)
    yield
    logger.info("应用关闭")


app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    description="智能客服机器人系统API",
    lifespan=lifespan
)

# CORS中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册路由
app.include_router(chat.router, prefix="/api/chat", tags=["对话"])
app.include_router(admin.router, prefix="/api/admin", tags=["管理"])
app.include_router(websocket.router, prefix="/ws", tags=["WebSocket"])


@app.get("/health")
async def health_check():
    return {"status": "healthy", "app": settings.APP_NAME, "version": settings.APP_VERSION}


@app.get("/")
async def root():
    return {"message": f"欢迎使用 {settings.APP_NAME}", "docs": "/docs"}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)

requirements.txt

backend/requirements.txt
# Web框架
fastapi==0.109.0
uvicorn[standard]==0.27.0
websockets==12.0
python-multipart==0.0.6
python-dotenv==1.0.0

# 数据库
sqlalchemy==2.0.25

# LangChain和LLM
langchain==0.1.4
langchain-community==0.0.16
langchain-openai==0.0.5

# 向量数据库
chromadb==0.4.22

# 文档处理
pypdf==4.0.0

# Embedding模型
sentence-transformers==2.2.2

# 工具库
numpy==1.26.3

五、前端开发(详细步骤)

5.1 项目初始化

创建Vue3项目
# 创建项目
cd customer-service-bot
npm create vue@latest frontend
# 选择 Vue Router 和 Pinia

cd frontend
npm install
npm install element-plus axios marked @element-plus/icons-vue
npm run dev

5.2 完整源码

api/index.js

frontend/src/api/index.js
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000',
  timeout: 30000,
  headers: { 'Content-Type': 'application/json' }
})

api.interceptors.response.use(
  (response) => response.data,
  (error) => Promise.reject(new Error(error.response?.data?.detail || error.message))
)

export const chatApi = {
  sendMessage: (data) => api.post('/api/chat/send', data),
  clearSession: (id) => api.post(`/api/chat/session/${id}/clear`)
}

export default api

stores/chat.js

frontend/src/stores/chat.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { chatApi } from '@/api'

export const useChatStore = defineStore('chat', () => {
  const sessionId = ref(null)
  const messages = ref([])
  const isLoading = ref(false)

  const hasActiveSession = computed(() => !!sessionId.value && messages.value.length > 0)

  function initSession() {
    sessionId.value = 'sess_' + Math.random().toString(36).substr(2, 9)
    messages.value = []
  }

  function addMessage(msg) {
    messages.value.push({ id: Date.now(), timestamp: new Date().toISOString(), ...msg })
  }

  async function sendMessage(content) {
    if (!sessionId.value) initSession()
    addMessage({ role: 'user', content })
    isLoading.value = true
    
    try {
      const res = await chatApi.sendMessage({ session_id: sessionId.value, message: content })
      addMessage({ role: 'assistant', content: res.message.content, sources: res.sources })
      return res
    } finally {
      isLoading.value = false
    }
  }

  return { sessionId, messages, isLoading, hasActiveSession, sendMessage }
})

ChatWindow.vue

frontend/src/components/ChatWindow.vue
<template>
  <div class="chat-window">
    <div class="chat-header">
      <h3>智能客服助手</h3>
      <el-button size="small" @click="clear">清空</el-button>
    </div>
    
    <div class="messages">
      <div v-for="msg in messages" :key="msg.id" :class="msg.role">
        {{ msg.content }}
      </div>
    </div>
    
    <div class="input-area">
      <el-input v-model="input" @keyup.enter="send" />
      <el-button type="primary" @click="send" :loading="isLoading">发送</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useChatStore } from '@/stores/chat'

const store = useChatStore()
const input = ref('')
const { messages, isLoading, sendMessage, clearSession } = store

async function send() {
  if (!input.value.trim()) return
  await sendMessage(input.value)
  input.value = ''
}

function clear() {
  clearSession()
}
</script>

六、部署上线

6.1 Docker配置

docker-compose.yml
version: '3.8'
services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - ZHIPU_API_KEY=${ZHIPU_API_KEY}
    volumes:
      - ./data:/app/data
  
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - backend

6.2 Nginx配置

nginx.conf
server {
    listen 80;
    
    location / {
        root /var/www/frontend/dist;
        try_files $uri $uri/ /index.html;
    }
    
    location /api/ {
        proxy_pass http://backend:8000/;
    }
    
    location /ws/ {
        proxy_pass http://backend:8000/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

七、总结与扩展

📚 学习要点总结

  • RAG架构:理解检索增强生成的核心原理
  • 向量数据库:掌握文档向量化和相似度检索
  • 多轮对话:实现上下文感知的对话管理
  • 人工接管:设计无缝的人机协作流程
  • 部署运维:Docker化部署和Nginx配置

🎯 扩展练习

  1. 添加用户认证系统(JWT)
  2. 实现对话历史持久化到数据库
  3. 添加语音输入/输出功能
  4. 实现多语言支持
  5. 接入更多LLM提供商