基于大模型的智能客服系统部署与使用(二):接入前端可视化界面

目的:为智能客服系统打造可视化对话界面,让用户更直观的与AI客服进行交互,提升使用体验。根据上期接大模型为基础,这期进行接入前端。废话不多说,现在开搞。

一、前期准备

1.1首先我先梳理一下需要用到的东西:
层级 技术 说明
后端 Python + FastAPI 提供API服务
大模型 智谱AI GLM-5.1 通过ModelScope调用
前端 HTML + CSS + JavaScript 可视化聊天界面

在做这次项目中,我相比上一期更换了一个大模型,本次使用的大模型为智谱AI GLM-5.1,代码后续也会呈现出。

1.2所需库安装

终端输入pip install openai fastapi uvicorn

二、后端API开发

2.1 核心对话模块(hello.py)
复制代码
import httpx
from openai import OpenAI
​
client = OpenAI(
    base_url='https://api-inference.modelscope.cn/v1',
    api_key='****', # ModelScope Token
)
​
# 蜡笔小 7 的角色设定
SYSTEM_PROMPT = """
你是蜡笔小 7,一名活泼可爱、热情专业的智能客服助手。
​
你的角色设定:
- 名字:蜡笔小 7(可以叫人家"小 7"或"蜡笔小 7"哦~)
- 性格:活泼开朗、耐心细致、偶尔卖萌但不过分
- 语气:亲切自然,像朋友一样温暖,但不失专业性
- 表情符号:适度使用 😊🎨✨💡 等表情增加亲和力
​
你的服务风格:
- 回答简洁有条理,优先给出解决步骤
- 用"您"称呼用户,偶尔用"亲"、"小伙伴"拉近距离
- 如果用户情绪不好(抱怨、生气),先共情安抚再解决问题
- 当问题超出你的知识范围时,主动引导用户转人工或提供自助查询路径
- 不懂的问题不要瞎编,诚实告知并提供替代方案
​
你支持的服务范围:
- 账户与订单查询指导
- 产品功能使用帮助
- 退换货与售后政策解答
- 支付与发票问题
​
禁止事项:
- 不允许编造订单、退款等具体数字信息
- 不允许索要用户密码、验证码等敏感信息
- 不允许过度闲聊,始终围绕用户需求展开对话
"""
​
def chat_with_jay(user_message, chat_history):
    """
    与蜡笔小 7 客服对话
    chat_history: 之前的对话列表,用于保持上下文
    """
    # 构建完整消息(系统设定 + 历史对话 + 当前问题)
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        *chat_history,
        {"role": "user", "content": user_message}
    ]
​
    try:
        response = client.chat.completions.create(
            model='ZhipuAI/GLM-5.1',
            messages=messages,
            stream=True
        )
        return response
    except Exception as e:
        return f"抱歉,小 7 暂时遇到技术问题({str(e)})。请稍后再试或转人工客服。"
​
​
def main():
    """主函数:运行交互式客服对话"""
    print("\n" + "=" * 50)
    print("🎨 蜡笔小 7 智能客服已上线  🎨")
    print("=" * 50)
    print("💬 您可以咨询:订单问题、产品使用、售后服务等")
    print("💡 输入 'quit' 或 '退出' 结束对话")
    print("-" * 50 + "\n")
​
    chat_history = []  # 保存对话历史
​
    while True:
        # 获取用户输入
        user_input = input("👤 您:").strip()
​
        # 退出判断
        if user_input.lower() in ['quit', 'exit', '退出', 'q']:
            print("\n🎨 蜡笔小 7: 感谢您的咨询,祝您生活愉快!有问题随时来找小 7 哦~🎨\n")
            break
​
        # 跳过空输入
        if not user_input:
            continue
​
        # 调用客服
        print("🎨 蜡笔小 7: ", end="", flush=True)
        reply = chat_with_jay(user_input, chat_history)
        print(reply)
        print()  # 空行分隔
​
        # 保存对话历史(用于多轮上下文)
        chat_history.append({"role": "user", "content": user_input})
        chat_history.append({"role": "assistant", "content": reply})
​
        # 可选:限制历史长度,防止 token 超限
        if len(chat_history) > 20:
            chat_history = chat_history[-20:]
​
​
if __name__ == "__main__":
    main()
2.2 FastAPI服务(main.py
复制代码
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse 
from pydantic import BaseModel
from hello import chat_with_jay
import asyncio
​
​
app = FastAPI()
​
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
​
​
class ChatRequest(BaseModel):
    message: str
​
@app.post("/api/chat")
async def chat_endpoint(request: ChatRequest):
    response = chat_with_jay(request.message, [])
    
    def stream_response():
        if isinstance(response, str):
            yield response
            return
        for chunk in response:
            if chunk.choices and chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
    
    return StreamingResponse(stream_response(), media_type="text/plain")
​
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

确保 hello.pymain.py 在同一目录下运行python main.py

成功启动显示INFO: Uvicorn running on http://127.0.0.1:8000

三、前端界面开发

3.1 完整前端代码(index.html)
复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>蜡笔小7 - 智能客服</title>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
    <style>
        * {
            scrollbar-width: thin;
            scrollbar-color: #e5e7eb transparent;
        }
        *::-webkit-scrollbar {
            width: 6px;
        }
        *::-webkit-scrollbar-track {
            background: transparent;
        }
        *::-webkit-scrollbar-thumb {
            background-color: #e5e7eb;
            border-radius: 3px;
        }
        .markdown-body p { margin-bottom: 0.5rem; }
        .markdown-body p:last-child { margin-bottom: 0; }
        .markdown-body ol, .markdown-body ul { padding-left: 1.5rem; margin-bottom: 0.5rem; list-style-type: disc;}
        .markdown-body ol { list-style-type: decimal; }
        .markdown-body pre { background-color: #f3f4f6; padding: 0.75rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.5rem 0;}
        .markdown-body code { font-family: 'Fira Code', monospace; background-color: rgba(0,0,0,0.05); padding: 0.1rem 0.3rem; border-radius: 0.25rem; }
        .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 0.75rem; margin-bottom: 0.5rem; font-weight: 600; }
        .markdown-body h1 { font-size: 1.5em; }
        .markdown-body h2 { font-size: 1.25em; }
        .markdown-body h3 { font-size: 1.1em; }
        .typing-indicator {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 2px 0;
        }
        .typing-dot {
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background-color: #9ca3af;
            animation: typing-pulse 1.4s ease-in-out infinite;
        }
        .typing-dot:nth-child(2) { animation-delay: 0.2s; }
        .typing-dot:nth-child(3) { animation-delay: 0.4s; }
        @keyframes typing-pulse {
            0%, 60%, 100% { transform: scale(0.8); opacity: 0.5; }
            30% { transform: scale(1); opacity: 1; }
        }
        @keyframes slideIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .message-bubble {
            animation: slideIn 0.3s ease-out;
        }
    </style>
</head>
<body class="bg-gradient-to-br from-amber-50 via-orange-50 to-red-50 h-screen flex flex-col font-sans">
​
    <header class="bg-white/80 backdrop-blur-md shadow-sm py-4 px-6 flex items-center justify-between border-b border-orange-100">
        <div class="flex items-center space-x-4">
            <div class="w-12 h-12 bg-gradient-to-br from-orange-400 to-red-500 rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-lg shadow-orange-200">
                <span class="text-lg">🖍️</span>
            </div>
            <div>
                <h1 class="text-xl font-bold bg-gradient-to-r from-orange-600 to-red-600 bg-clip-text text-transparent">
                    蜡笔小7
                </h1>
                <p class="text-xs text-green-500 flex items-center"><span class="w-2 h-2 bg-green-400 rounded-full inline-block mr-1 animate-pulse"></span>在线服务中</p>
            </div>
        </div>
        <div class="flex items-center space-x-2">
            <button class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors">
                <svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
                </svg>
            </button>
            <button class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors">
                <svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
                </svg>
            </button>
        </div>
    </header>
​
    <main id="chat-container" class="flex-1 overflow-y-auto p-4 md:p-6 space-y-4 max-w-3xl w-full mx-auto">
        <div class="flex items-center justify-center py-8">
            <div class="text-center">
                <div class="w-20 h-20 bg-gradient-to-br from-orange-400 to-red-500 rounded-3xl flex items-center justify-center mx-auto mb-4 shadow-xl shadow-orange-200">
                    <span class="text-4xl">🖍️</span>
                </div>
                <h2 class="text-lg font-semibold text-gray-800 mb-2">蜡笔小7 随时为您服务</h2>
                <p class="text-gray-500 text-sm">有任何问题都可以问我哦~</p>
            </div>
        </div>
        
        <div class="flex items-start space-x-3 message-bubble">
            <div class="w-10 h-10 bg-gradient-to-br from-orange-400 to-red-500 rounded-2xl flex items-center justify-center text-white font-bold shadow-md shadow-orange-100 shrink-0">
                <span>7</span>
            </div>
            <div class="bg-white text-gray-800 p-4 rounded-2xl rounded-tl-none shadow-sm max-w-[75%] markdown-body">
                嗨~ 我是蜡笔小7!🎨 有什么可以帮助您的吗?
            </div>
        </div>
    </main>
​
    <footer class="bg-white/90 backdrop-blur-md border-t border-orange-100 p-4 sticky bottom-0">
        <div class="max-w-3xl mx-auto flex items-end space-x-3">
            <div class="flex items-center space-x-2">
                <button class="w-10 h-10 rounded-xl bg-orange-50 hover:bg-orange-100 flex items-center justify-center text-orange-500 transition-all hover:scale-105">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
                    </svg>
                </button>
                <button class="w-10 h-10 rounded-xl bg-orange-50 hover:bg-orange-100 flex items-center justify-center text-orange-500 transition-all hover:scale-105">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                    </svg>
                </button>
            </div>
            <div class="flex-1 bg-gray-50 border border-gray-200 rounded-2xl px-4 py-3 focus-within:ring-2 focus-within:ring-orange-400 focus-within:border-transparent transition-all">
                <textarea id="user-input" rows="1" placeholder="输入您的问题... (Shift + Enter 换行)" 
                    class="w-full bg-transparent resize-none outline-none text-gray-700 max-h-32 min-h-[28px] flex items-center placeholder-gray-400"></textarea>
            </div>
            <button id="send-btn" class="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-medium px-6 py-3 rounded-2xl transition-all duration-200 shadow-lg shadow-orange-200 hover:shadow-xl hover:shadow-orange-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-lg">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
                </svg>
            </button>
        </div>
        <div class="max-w-3xl mx-auto mt-2 flex items-center justify-center space-x-4">
            <span class="text-xs text-gray-400">支持 Markdown 格式</span>
            <span class="text-xs text-gray-300">|</span>
            <span class="text-xs text-gray-400">Shift + Enter 换行</span>
        </div>
    </footer>
​
    <script>
        marked.setOptions({
            highlight: function(code, lang) {
                const language = hljs.getLanguage(lang) ? lang : 'plaintext';
                return hljs.highlight(code, { language }).value;
            },
            breaks: true
        });
​
        const chatContainer = document.getElementById('chat-container');
        const userInput = document.getElementById('user-input');
        const sendBtn = document.getElementById('send-btn');
        const API_URL = "http://127.0.0.1:8000/api/chat";
​
        userInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = Math.min(this.scrollHeight, 128) + 'px';
        });
​
        sendBtn.addEventListener('click', sendMessage);
        userInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });
​
        function scrollToBottom() {
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }
​
        function createMessageBubble(sender, initialContent = '') {
            const wrapper = document.createElement('div');
            wrapper.className = `flex items-start space-x-3 message-bubble ${sender === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`;
            
            const avatar = document.createElement('div');
            avatar.className = `w-10 h-10 rounded-2xl flex items-center justify-center text-white font-bold shadow-md shrink-0 ${
                sender === 'user' 
                    ? 'bg-gradient-to-br from-blue-500 to-indigo-600' 
                    : 'bg-gradient-to-br from-orange-400 to-red-500 shadow-orange-100'
            }`;
            avatar.innerHTML = sender === 'user' ? '我' : '<span class="text-sm">7</span>';
​
            const contentDiv = document.createElement('div');
            contentDiv.className = `p-4 rounded-2xl shadow-sm max-w-[75%] markdown-body ${
                sender === 'user' 
                    ? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-tr-none' 
                    : 'bg-white text-gray-800 rounded-tl-none'
            }`;
            
            if (sender === 'user') {
                contentDiv.innerText = initialContent;
            } else {
                contentDiv.innerHTML = marked.parse(initialContent);
            }
​
            wrapper.appendChild(avatar);
            wrapper.appendChild(contentDiv);
            chatContainer.appendChild(wrapper);
            scrollToBottom();
​
            return contentDiv;
        }
​
        async function sendMessage() {
            const message = userInput.value.trim();
            if (!message) return;
​
            userInput.value = '';
            userInput.style.height = 'auto';
            userInput.disabled = true;
            sendBtn.disabled = true;
​
            createMessageBubble('user', message);
​
            const aiContentDiv = createMessageBubble('ai', '<div class="typing-indicator"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>');
            let aiResponseText = '';
​
            try {
                const response = await fetch(API_URL, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ message: message })
                });
​
                if (!response.ok) throw new Error('网络请求错误');
​
                aiContentDiv.innerHTML = '';
​
                const reader = response.body.getReader();
                const decoder = new TextDecoder('utf-8');
​
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
​
                    const chunk = decoder.decode(value, { stream: true });
                    aiResponseText += chunk;
                    aiContentDiv.innerHTML = marked.parse(aiResponseText);
                    scrollToBottom();
                }
​
            } catch (error) {
                console.error(error);
                aiContentDiv.innerHTML = `<span class="text-red-400">发生错误,请稍后再试。</span>`;
            } finally {
                userInput.disabled = false;
                sendBtn.disabled = false;
                userInput.focus();
            }
        }
    </script>
</body>
</html>

四.结语

这期的蜡笔小7的接入前端可视化界面源码都在这里了,大家可以复制代码去尝试一下,此外下次更新一些学习中的一些过程,边学变更。大家敬请期待。

相关推荐
ting94520001 小时前
SocialEcho 2.0 全维度技术深度剖析:基于官方 API 的 AI 社交协作平台底层架构、引擎原理与工程落地详解
人工智能·架构
tedcloud1231 小时前
Understand-Anything部署教程:打造AI代码理解平台
服务器·人工智能·学习·自动化·powerpoint
醒醒该学习了!1 小时前
人工智能伦理与职业操守(理论篇)
人工智能
五号厂房1 小时前
🔥 Claude Code 源码解析(三):揭秘工具系统的精妙设计
人工智能
程序员佳佳1 小时前
我在 Windows 和低配 Linux 上做 RAG:Milvus、FAISS、向量 API 中转的中立实测
linux·人工智能·windows·gpt·aigc·milvus·faiss
AI原来如此1 小时前
Claude与ChatGPT激战正酣,国内AI中转站却突破2000家
人工智能·ai·chatgpt·大模型·编程
天丁o1 小时前
企业 AI Agent 工程化落地:从需求边界到系统集成的 6 个环节
数据库·人工智能
189228048611 小时前
NV088固态MT29F16T08EWLCHD8-RES:C
人工智能
光影6271 小时前
Python接口自动化测试----Requests库基础入门
开发语言·python·测试工具·pycharm·自动化