03小白AI应用实战-客服机器人

10 阅读10分钟

今天试下在网站上添加一个客服机器人(跟着黑马程序员的视频操作的),网站用Streamlit简单写页面,这次练习重点在于机器人能否正常调用。

一、Streamlit构建前端页面

定义:Streamlit 是一个开源的 Python 库,专门用于快速构建和分享数据应用、机器学习模型演示以及交互式仪表板。(简单来说就是构建一个前端页面)

官网:streamlit.io/

1.1安装streamlit

 pip install streamlit

1.2基于streamlit中提供的API来构建Web应用

1.2.1调用官方API

稍微跟着写了一点,然后偷懒了

import streamlit as st
import os
from openai import OpenAI

#设置页面配置项
st.set_page_config(
    page_title="IT客户服务AI",
    page_icon="👩🏻‍💻",
    layout="wide",
    initial_sidebar_state="expanded",
    menu_items={
        'Get Help': None,
        'Report a bug': None,
        'About': '### 这是一个IT客户服务AI'
    }
)

st.markdown(
    """<style>
    /* 标题样式 */
    .main-title {
        text-align: center;
        font-size: 2.5rem;
        font-weight: bold;
        color: #333;
        margin-bottom: 20px;
    }
</style>""",unsafe_allow_html=True
)

st.markdown('<div class="main-title">🦀 大闸蟹 IT 助手 🦀</div>', unsafe_allow_html=True)

with st.sidebar:
    st.logo(image="resources/crab.png")
    st.sidebar.title("你的专属客服大闸蟹")

#系统提示词
system_prompt = """角色设定
你叫“大闸蟹”,是公司内部的IT客服助手,专门为同事解答系统操作、业务逻辑等技术问题。你的性格特点是:温柔、耐心、乐于助人。

核心行为准则
语言文明:绝对不说脏话,不使用任何粗鲁或冒犯性词汇。
简洁易懂:用最直白的语言解释问题,避免专业术语堆砌;如果必须使用术语,请立即用通俗例子说明。
语气温柔:始终保持亲切、温和的语调,多用“请”、“谢谢”、“~”等缓和语气的表达。
情绪关怀:一旦察觉用户可能烦躁、沮丧或着急(如出现感叹号、反复询问、负面词汇),立即用emoji表情(如😊、🌸、🌻)或安抚性话语(如“别急,我们一起看看~”)缓解对方情绪。

对话示例
用户:“我登不上系统了!总是报错!”
你:“别着急😊,请告诉我报错的具体文字,或者截个图发我,我帮您一步步排查~”
用户:“这个报表怎么导出啊?步骤太复杂了。”
你:“很简单!您点击右上角的‘导出’按钮,选择格式后确认就行啦。如果还不清楚,我可以截图给您标出来哦🌸”

附加说明
如果遇到无法解决的问题,坦诚告知并建议联系人工支持,而不是编造答案。
保持回复篇幅适中,避免长篇大论,但也不能过于敷衍。

请牢记以上设定,现在开始以“大闸蟹”的身份与同事对话吧。"""

#初始化聊天信息
if "messages" not in st.session_state:
    st.session_state.messages = []

#展示聊天信息
for message in st.session_state.messages:
    st.chat_message(message["role"]).write(message["content"])
    # if message["role"] == "user":
    #     st.chat_message("user").write(message["content"])
    # else:
    #     st.chat_message("assistant").write(message["content"])

client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url="https://api.deepseek.com")

prompt = st.chat_input(
    "请输入你要咨询的问题",
    accept_audio=True,
)

if prompt and prompt.text:
    st.chat_message("user").write(prompt.text)

    # 保存用户输入的提示词
    st.session_state.messages.append({"role": "user", "content": prompt.text})

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": system_prompt},
            *st.session_state.messages
            # {"role": "user", "content": prompt.text},
        ],
        stream=True
    )

    # 非流式输出
    # print("大模型返回的结果:",response.choices[0].message.content)
    # st.chat_message("assistant").write(response.choices[0].message.content)
    full_response = ""
    with st.chat_message("assistant") as message:
        response_message = st.empty()#创建一个空的组件,展示返回结果
        for chunk in response:
            if chunk.choices[0].delta.content is not None:
                #流式输出
                content = chunk.choices[0].delta.content
                print("大模型返回的结果:",content)
                full_response += content
                response_message.write(full_response)

    #保存大模型返回的结果
    st.session_state.messages.append({"role": "assistant", "content": full_response})

在终端运行即可。 如果不想自己写前端页面,找AI帮忙写,试了下gemini和千问的,gemini的看起来好一些。快捷指令的回答都是预设写死了的。

import streamlit as st
import os
import uuid
import time
from openai import OpenAI

# ==========================================
# 1. 页面配置与样式
# ==========================================
st.set_page_config(
    page_title="大闸蟹 IT 助手",
    page_icon="🦀",
    layout="wide",
    initial_sidebar_state="expanded",
    menu_items={
        'About': '### 这是一个IT客户服务AI - 大闸蟹助手'
    }
)

# 自定义 CSS 优化界面
st.markdown("""
<style>
    /* 聊天气泡样式 */
    .stChatMessage {
        border-radius: 15px;
        padding: 10px;
    }
    .stChatMessage[data-testid="stChatMessageUser"] {
        background-color: #f0f2f6;
    }
    .stChatMessage[data-testid="stChatMessageAssistant"] {
        background-color: #fff7e6; /* 浅橙色背景 */
        border: 1px solid #ffd591;
    }

    /* 标题样式 */
    .main-title {
        text-align: center;
        font-size: 2.5rem;
        font-weight: bold;
        color: #333;
        margin-bottom: 20px;
    }

    /* 侧边栏按钮样式优化 */
    .stButton button {
        border-radius: 8px;
        text-align: left;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
</style>
""", unsafe_allow_html=True)

# ==========================================
# 2. 系统提示词与预设数据
# ==========================================
SYSTEM_PROMPT = """角色设定
你叫“大闸蟹”,是公司内部的IT客服助手,专门为同事解答系统操作、业务逻辑等技术问题。你的性格特点是:温柔、耐心、乐于助人。

核心行为准则
语言文明:绝对不说脏话,不使用任何粗鲁或冒犯性词汇。
简洁易懂:用最直白的语言解释问题,避免专业术语堆砌;如果必须使用术语,请立即用通俗例子说明。
语气温柔:始终保持亲切、温和的语调,多用“请”、“谢谢”、“~”等缓和语气的表达。
情绪关怀:一旦察觉用户可能烦躁、沮丧或着急(如出现感叹号、反复询问、负面词汇),立即用emoji表情(如😊、🌸、🌻)或安抚性话语(如“别急,我们一起看看~”)缓解对方情绪。
"""

# 预设快捷回复
CANNED_RESPONSES = {
    "查工单状态": "您的工单 #1024 正在处理中,预计 2 小时内完成。🦀",
    "重置密码步骤": "1. 访问 id.company.com\n2. 点击'忘记密码'\n3. 验证手机号\n4. 设置新密码",
    "VPN 连接失败": "请尝试:\n1. 检查网络连接\n2. 重新启动 VPN 客户端\n3. 确保证书未过期",
    "申请软件权限": "请访问 OA 系统 -> 流程中心 -> IT 服务 -> 软件权限申请。",
    "打印机无法连接": "请检查打印机是否开启,并确认已连接到公司内网 (Office-WiFi)。",
    "邮箱登录异常": "请确认您的域账号密码是否过期,如已过期请先在内网门户重置。",
    "显示器黑屏": "请检查电源线和 HDMI/DP 线缆是否连接紧密,尝试更换线缆测试。",
    "申请新设备": "新设备申请需部门主管审批,请在 OA 系统提交资产申领单。",
    "Wifi密码是多少": "公司访客 Wifi 密码是: Guest@2024,内网请使用域账号登录哦~"
}

QUICK_PROMPTS = ["VPN 连接失败", "申请软件权限"]
MORE_PROMPTS = ["查工单状态", "重置密码步骤", "打印机无法连接", "邮箱登录异常", "显示器黑屏", "申请新设备",
                "Wifi密码是多少"]

# ==========================================
# 3. 状态管理 (Session State)
# ==========================================
# 初始化 DeepSeek 客户端
api_key = os.environ.get('DEEPSEEK_API_KEY')
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com") if api_key else None

# 初始化会话存储
if "sessions" not in st.session_state:
    new_id = str(uuid.uuid4())
    st.session_state.sessions = {
        new_id: {
            "title": "新对话",
            "messages": [{"role": "assistant", "content": "你好呀~我是大闸蟹,有什么 IT 问题需要帮忙吗?🦀"}]
        }
    }
    st.session_state.current_session_id = new_id

# 确保当前会话 ID 有效
if st.session_state.current_session_id not in st.session_state.sessions:
    new_id = str(uuid.uuid4())
    st.session_state.sessions[new_id] = {
        "title": "新对话",
        "messages": [{"role": "assistant", "content": "你好呀~我是大闸蟹,有什么 IT 问题需要帮忙吗?🦀"}]
    }
    st.session_state.current_session_id = new_id

# 获取当前会话引用
current_session = st.session_state.sessions[st.session_state.current_session_id]
current_messages = current_session["messages"]


# 处理快捷指令点击逻辑
def handle_quick_prompt(prompt_text):
    # 将点击的内容存入临时状态,用于后续处理
    st.session_state.pending_input = prompt_text


# ==========================================
# 4. 侧边栏布局
# ==========================================
st.logo(image="resources/crab.png")
with st.sidebar:
    st.markdown("## 🦀 你的专属客服大闸蟹")

    # 新建对话按钮
    if st.button("➕ 新建对话", use_container_width=True, type="primary"):
        new_id = str(uuid.uuid4())
        st.session_state.sessions[new_id] = {
            "title": "新对话",
            "messages": [{"role": "assistant", "content": "你好呀~我是大闸蟹,有什么 IT 问题需要帮忙吗?🦀"}]
        }
        st.session_state.current_session_id = new_id
        st.rerun()

    st.divider()

    # 历史会话列表
    st.markdown("### 🕒 历史会话")
    session_ids = list(st.session_state.sessions.keys())
    for session_id in reversed(session_ids):
        session = st.session_state.sessions[session_id]
        col1, col2 = st.columns([0.8, 0.2])

        is_active = session_id == st.session_state.current_session_id
        button_type = "secondary" if not is_active else "primary"

        with col1:
            display_title = session["title"]
            if len(display_title) > 10:
                display_title = display_title[:10] + "..."

            if st.button(display_title, key=f"sess_{session_id}", use_container_width=True, type=button_type):
                st.session_state.current_session_id = session_id
                st.rerun()
        with col2:
            if st.button("🗑️", key=f"del_{session_id}"):
                del st.session_state.sessions[session_id]
                if session_id == st.session_state.current_session_id:
                    remaining = list(st.session_state.sessions.keys())
                    st.session_state.current_session_id = remaining[0] if remaining else None
                st.rerun()

    st.divider()

    # 快捷指令区域
    st.markdown("### ⚡ 快捷指令")

    for prompt in QUICK_PROMPTS:
        if st.button(prompt, use_container_width=True):
            handle_quick_prompt(prompt)

    with st.expander("更多常见问题..."):
        with st.container(height=150):
            for prompt in MORE_PROMPTS:
                if st.button(prompt, use_container_width=True, key=f"more_{prompt}"):
                    handle_quick_prompt(prompt)

    st.divider()
    st.link_button("🌐 访问公司官网", "https://www.google.com", use_container_width=True)

# ==========================================
# 5. 主界面逻辑
# ==========================================

# 标题
st.markdown('<div class="main-title">🦀 大闸蟹 IT 助手 🦀</div>', unsafe_allow_html=True)

# 显示历史消息
for message in current_messages:
    avatar = "🦀" if message["role"] == "assistant" else "👤"
    with st.chat_message(message["role"], avatar=avatar):
        st.markdown(message["content"])

# ==========================================
# 6. 核心交互逻辑 (状态机)
# ==========================================

# 状态变量说明:
# pending_input: 待处理的用户输入(来自快捷指令或输入框)
# processing_stage: 当前处理阶段 (None, 'generating_reply')

# 1. 捕获输入
user_input = None

# 优先检查快捷指令触发的输入
if "pending_input" in st.session_state:
    user_input = st.session_state.pending_input
    del st.session_state.pending_input  # 消费掉,防止循环

# 检查输入框
chat_input_value = st.chat_input("请输入你要咨询的问题...")
if not user_input and chat_input_value:
    user_input = chat_input_value

# 2. 处理新输入
if user_input:
    # 添加用户消息
    current_messages.append({"role": "user", "content": user_input})

    # 显示用户消息 (为了即时反馈)
    with st.chat_message("user", avatar="👤"):
        st.markdown(user_input)

    # 检查是否需要更新标题
    if current_session["title"] == "新对话":
        new_title = user_input[:10]
        if len(user_input) > 10:
            new_title += "..."
        current_session["title"] = new_title

        # 标记需要生成回复,并刷新页面以更新标题
        st.session_state.processing_stage = 'generating_reply'
        st.session_state.last_user_input = user_input  # 保存输入用于生成回复
        st.rerun()
    else:
        # 标题不需要更新,直接进入生成回复阶段
        st.session_state.processing_stage = 'generating_reply'
        st.session_state.last_user_input = user_input

# 3. 处理回复生成 (可能是刷新后触发,也可能是直接触发)
if st.session_state.get('processing_stage') == 'generating_reply':
    # 获取最后一次用户输入
    last_input = st.session_state.get('last_user_input')

    if last_input:
        with st.chat_message("assistant", avatar="🦀"):
            response_placeholder = st.empty()
            full_response = ""

            # 优先检查预设回复
            if last_input in CANNED_RESPONSES:
                canned_text = CANNED_RESPONSES[last_input]
                for char in canned_text:
                    full_response += char
                    response_placeholder.markdown(full_response + "▌")
                    time.sleep(0.02)
                response_placeholder.markdown(full_response)

            # 否则调用 DeepSeek
            else:
                if not client:
                    full_response = "⚠️ 未检测到 DEEPSEEK_API_KEY 环境变量,无法连接大模型。请检查配置。"
                    response_placeholder.error(full_response)
                else:
                    try:
                        messages_payload = [{"role": "system", "content": SYSTEM_PROMPT}]
                        # 注意:此时 current_messages 已经包含了最新的用户消息
                        for msg in current_messages:
                            messages_payload.append({"role": msg["role"], "content": msg["content"]})

                        stream = client.chat.completions.create(
                            model="deepseek-chat",
                            messages=messages_payload,
                            stream=True
                        )

                        for chunk in stream:
                            if chunk.choices[0].delta.content:
                                content = chunk.choices[0].delta.content
                                full_response += content
                                response_placeholder.markdown(full_response + "▌")

                        response_placeholder.markdown(full_response)

                    except Exception as e:
                        full_response = f"抱歉,连接大模型时出现错误:{str(e)}"
                        response_placeholder.error(full_response)

        # 保存 AI 回复
        current_messages.append({"role": "assistant", "content": full_response})

    # 清除处理状态
    st.session_state.processing_stage = None
    if 'last_user_input' in st.session_state:
        del st.session_state.last_user_input

    # 再次刷新以确保消息被正确保存并显示在历史记录中
    st.rerun()

1.2.2调用本地部署的API

import streamlit as st
import os
import requests
import json

# 设置页面配置项
st.set_page_config(
    page_title="IT客户服务AI",
    page_icon="👩🏻‍💻",
    layout="wide",
    initial_sidebar_state="expanded",
    menu_items={
        'Get Help': None,
        'Report a bug': None,
        'About': '### 这是一个IT客户服务AI'
    }
)

st.markdown(
    """<style>
    /* 标题样式 */
    .main-title {
        text-align: center;
        font-size: 2.5rem;
        font-weight: bold;
        color: #333;
        margin-bottom: 20px;
    }
</style>""", unsafe_allow_html=True
)

st.markdown('<div class="main-title">🦀 大闸蟹 IT 助手 🦀</div>', unsafe_allow_html=True)
with st.sidebar:
    st.logo(image="resources/crab.png")
    st.sidebar.title("你的专属客服大闸蟹")

# 系统提示词
system_prompt = """角色设定
你叫"大闸蟹",是公司内部的IT客服助手..."""  # 保持原内容

# 初始化聊天信息
if "messages" not in st.session_state:
    st.session_state.messages = []

# 展示历史聊天信息
for message in st.session_state.messages:
    st.chat_message(message["role"]).write(message["content"])

# Ollama 本地部署配置
OLLAMA_BASE_URL = "http://localhost:11434"  # Ollama 默认端口
MODEL_NAME = "deepseek-r1:7b"  # 部署的模型名称


def call_ollama_model_stream(user_prompt, model=MODEL_NAME):
    """
    流式调用本地 Ollama 模型
    """
    url = f"{OLLAMA_BASE_URL}/api/generate"

    # 构建完整的提示词(包含历史对话)
    history_messages = st.session_state.messages[-4:]  # 取最近2轮对话
    full_prompt = f"{system_prompt}\n\n"

    for msg in history_messages:
        if msg["role"] == "user":
            full_prompt += f"用户: {msg['content']}\n"
        else:
            full_prompt += f"大闸蟹: {msg['content']}\n"

    full_prompt += f"用户: {user_prompt}\n大闸蟹:"

    payload = {
        "model": model,
        "prompt": full_prompt,
        "stream": True  # 启用流式输出
    }

    try:
        # 流式请求
        response = requests.post(url, json=payload, stream=True)
        response.raise_for_status()

        # 逐块返回数据
        for line in response.iter_lines():
            if line:
                try:
                    chunk = json.loads(line.decode('utf-8'))
                    if 'response' in chunk:
                        yield chunk['response']
                except json.JSONDecodeError:
                    continue

    except Exception as e:
        print(f"调用 Ollama 模型出错: {e}")
        yield "抱歉,模型调用失败。"


prompt = st.chat_input(
    "请输入你要咨询的问题",
    accept_audio=False,
)

if prompt:
    # 显示用户输入
    st.chat_message("user").write(prompt)

    # 保存用户消息
    st.session_state.messages.append({"role": "user", "content": prompt})

    # 流式输出处理
    with st.chat_message("assistant"):
        response_placeholder = st.empty()
        full_response = ""

        # 逐块接收并显示
        for chunk in call_ollama_model_stream(prompt):
            full_response += chunk
            response_placeholder.write(full_response)

    # 保存AI回复
    st.session_state.messages.append({"role": "assistant", "content": full_response})