File size: 7,220 Bytes
40e5de3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import streamlit as st
import requests
import json
import time
import os
from openai import OpenAI
import psutil
import platform

# 页面配置
st.set_page_config(
    page_title="Qwen3.5 27B Chat",
    page_icon="🤖",
    layout="wide",
    initial_sidebar_state="expanded"
)

# 自定义 CSS
st.markdown("""
<style>
    .stApp {
        background-color: #f5f5f5;
    }
    .main-header {
        color: #1f77b4;
        font-size: 2.5rem;
        font-weight: 600;
        margin-bottom: 1rem;
    }
    .info-box {
        background-color: #e8f4f8;
        padding: 1rem;
        border-radius: 0.5rem;
        margin-bottom: 1rem;
    }
</style>
""", unsafe_allow_html=True)

# 初始化 OpenAI 客户端
@st.cache_resource
def init_client():
    """初始化 vLLM 客户端"""
    # 等待 vLLM 服务就绪
    max_retries = 30
    for i in range(max_retries):
        try:
            response = requests.get("http://localhost:8000/health")
            if response.status_code == 200:
                st.success("✅ 已连接到推理服务器")
                break
        except:
            if i % 5 == 0:
                st.info(f"⏳ 正在连接推理服务器... ({i+1}/{max_retries})")
            time.sleep(2)
    
    return OpenAI(
        base_url="http://localhost:8000/v1",
        api_key="EMPTY",
    )

# 侧边栏
with st.sidebar:
    st.image("https://qianwen-res.oss-cn-beijing.aliyuncs.com/assets/blog/2025/qwen3.5-logo.png", 
             use_column_width=True)
    
    st.markdown("## ⚙️ 配置参数")
    
    # 模型状态
    with st.expander("🖥️ 系统状态", expanded=False):
        col1, col2 = st.columns(2)
        with col1:
            st.metric("CPU 使用率", f"{psutil.cpu_percent()}%")
        with col2:
            mem = psutil.virtual_memory()
            st.metric("内存使用", f"{mem.percent}%")
        
        # GPU 信息(如果可用)
        try:
            import torch
            if torch.cuda.is_available():
                gpu_memory = torch.cuda.memory_allocated() / 1024**3
                gpu_total = torch.cuda.get_device_properties(0).total_memory / 1024**3
                st.metric("GPU 显存", f"{gpu_memory:.1f}GB / {gpu_total:.1f}GB")
        except:
            pass
    
    # 采样参数
    st.markdown("### 🎛️ 采样参数")
    temperature = st.slider("Temperature", 0.0, 2.0, 0.7, 0.1,
                           help="控制输出的随机性,越高越随机")
    top_p = st.slider("Top P", 0.0, 1.0, 0.8, 0.05,
                     help="核采样,控制词汇选择的累积概率")
    max_tokens = st.number_input("Max Tokens", 512, 32768, 4096, 512,
                                help="最大生成token数")
    
    # 高级选项
    with st.expander("🔧 高级选项"):
        presence_penalty = st.slider("Presence Penalty", -2.0, 2.0, 0.0, 0.1,
                                    help="话题重复惩罚")
        frequency_penalty = st.slider("Frequency Penalty", -2.0, 2.0, 0.0, 0.1,
                                     help="词频惩罚")
        enable_thinking = st.checkbox("🧠 启用思考模式", True,
                                     help="启用模型的内部思考过程")
    
    st.divider()
    st.markdown("### 📝 预设配置")
    preset = st.selectbox("快速选择", ["通用对话", "创意写作", "代码生成", "精确回答"])
    if preset == "通用对话":
        temperature, top_p = 0.7, 0.8
    elif preset == "创意写作":
        temperature, top_p = 1.2, 0.95
    elif preset == "代码生成":
        temperature, top_p = 0.2, 0.1
    elif preset == "精确回答":
        temperature, top_p = 0.3, 0.5

# 主界面
st.markdown('<p class="main-header">🤖 Qwen3.5 27B Chat</p>', 
            unsafe_allow_html=True)

# 信息框
with st.container():
    st.markdown("""
    <div class="info-box">
    📌 基于 Qwen3.5-27B-GPTQ-Int4 模型,4-bit 量化,可在 T4 16GB 显存高效运行。
    支持多轮对话、代码生成、文档理解等功能。
    </div>
    """, unsafe_allow_html=True)

# 初始化客户端
client = init_client()

# 初始化聊天历史
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "You are Qwen3.5, a helpful assistant."}
    ]

# 显示聊天历史
for message in st.session_state.messages[1:]:  # 跳过系统消息
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# 聊天输入
if prompt := st.chat_input("输入你的问题..."):
    # 添加用户消息
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    
    # 生成回复
    with st.chat_message("assistant"):
        message_placeholder = st.empty()
        full_response = ""
        
        try:
            # 准备消息
            messages_for_api = st.session_state.messages.copy()
            
            # 调用 API
            stream = client.chat.completions.create(
                model="Qwen/Qwen3.5-27B-GPTQ-Int4",
                messages=messages_for_api,
                temperature=temperature,
                top_p=top_p,
                max_tokens=max_tokens,
                presence_penalty=presence_penalty,
                frequency_penalty=frequency_penalty,
                stream=True,
                extra_body={
                    "chat_template_kwargs": {
                        "enable_thinking": enable_thinking
                    }
                } if not enable_thinking else None
            )
            
            # 处理流式响应
            for chunk in stream:
                if chunk.choices[0].delta.content:
                    full_response += chunk.choices[0].delta.content
                    message_placeholder.markdown(full_response + "▌")
            
            message_placeholder.markdown(full_response)
            
            # 添加到历史
            st.session_state.messages.append({
                "role": "assistant",
                "content": full_response
            })
            
        except Exception as e:
            st.error(f"生成失败: {str(e)}")
            if "connection" in str(e).lower():
                st.info("正在重新连接推理服务器,请稍候...")
                st.cache_resource.clear()
                time.sleep(2)
                st.rerun()

# 底部工具栏
col1, col2, col3 = st.columns([1, 1, 4])
with col1:
    if st.button("🗑️ 清空对话"):
        st.session_state.messages = [st.session_state.messages[0]]
        st.rerun()
with col2:
    if st.button("📋 复制对话"):
        import pyperclip
        texts = [f"{m['role']}: {m['content']}" 
                for m in st.session_state.messages[1:]]
        pyperclip.copy("\n\n".join(texts))
        st.toast("已复制到剪贴板")

# 显示 token 使用统计(如果有)
if "last_response" in st.session_state:
    st.caption(f"📊 最后响应: ~{len(st.session_state.last_response)} tokens")

# 心跳机制(防止休眠)
st.markdown("---")
st.caption("💡 提示:此页面保持打开可防止Space自动休眠")