这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。
目的:为智能客服系统打造可视化对话界面,让用户更直观的与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)
蜡笔小7 - 智能客服
* {
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;
}
🖍️
# 蜡笔小7
在线服务中
🖍️
## 蜡笔小7 随时为您服务
有任何问题都可以问我哦~
7
嗨~ 我是蜡笔小7!🎨 有什么可以帮助您的吗?
支持 Markdown 格式
|
Shift + Enter 换行
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' ? '我' : '7';
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', '
');
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 = `发生错误,请稍后再试。`;
} finally {
userInput.disabled = false;
sendBtn.disabled = false;
userInput.focus();
}
}
四.结语
这期的蜡笔小7的接入前端可视化界面源码都在这里了,大家能够复制代码去尝试一下,此外下次更新一些学习中的一些过程,边学变更。大家敬请期待。
本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。
评论 (0)
暂无评论