目的:为智能客服系统打造可视化对话界面,让用户更直观的与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.py 和 main.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的接入前端可视化界面源码都在这里了,大家可以复制代码去尝试一下,此外下次更新一些学习中的一些过程,边学变更。大家敬请期待。