一、引言
随着大模型本地化部署的普及,基于 FastAPI 封装大模型接口并实现鉴权、可视化交互,成为实现落地大模型应用的核心场景。前一篇博文我们讲解了大模型本地化部署以及api鉴权调用的基础示例,今天我们在初级理论的基础上强化实际应用,以"本地大模型文本生成 API+Streamlit 可视化前端"为核心案例,从代码分解、执行流程、技术栈解析、价值细节、实际应用意义五个维度,由浅入深讲解完整的开发与扩展逻辑。
今天我们有针对性的实现的本地大模型轻量级应用解决方案,核心目标是将本地化部署的大模型,模型我们选择相对较小的Qwen1.5-1.8B-Chat模型,封装为带鉴权的 HTTP 接口,并配套可视化交互前端,实现"大模型能力→API 服务→可视化操作"的全链路落地,案例既保留基础的 API Key/JWT 鉴权核心,又通过扩展实现参数定制、限流、历史记录等结合实际应用的功能,适配我们在实际业务落地的需求场景。

二、实例应用价值
1. 基础介绍
简单来说,这个示例解决了两个核心问题:
-
- 技术层面:无需复杂的前后端分离开发、无需专业运维知识,快速实现大模型的 API 封装,带 API Key/JWT 双鉴权以及可视化交互界面;
-
- 业务层面:通过网页界面调用本地大模型生成文本,同时满足生产级的安全,鉴权、限流、灵活的参数定制、可追溯历史记录的需求。
2. 核心组成
**前端(app.py):**技术栈 Streamlit
核心功能:
-
- 可视化选择鉴权方式;
-
- 配置生成参数(提示词、随机性、生成长度等);
-
- 调用后端接口展示结果;4. 同步 / 查看历史记录
**后端(main.py):**技术栈:FastAPI + Transformers
核心功能:
-
- 加载本地大模型;
-
- 实现 API Key/JWT 双鉴权;
-
- 提供文本生成 / 令牌管理 / 历史记录接口;
-
- 限流防护
3. 应用功能
- 安全防护:API Key/JWT 双鉴权避免大模型被滥用,IP 限流防止高频调用导致服务崩溃;
- 灵活定制:支持调节生成参数(temperature 控制随机性、top_p 控制采样策略),适配不同文本生成场景(文案创作、技术解释、代码生成等);
- 可追溯性:本地 JSON 文件存储生成记录,支持可视化查询 / 同步,便于业务复盘。
- 硬件适配:CPU 部署,通过内存优化参数(low_cpu_mem_usage=True)降低硬件要求,普通硬件环境即可运行;
三、执行流程
1. 整体执行流程

流程说明:
-
- 用户准备阶段
- 选择API Key或JWT令牌的认证方式,配置生成参数:提示词、生成长度、温度、Top-p等
-
- 请求发送阶段
- 前端将参数封装为POST请求发送到后端API
-
- 后端校验阶段
- 鉴权校验:验证API Key或JWT令牌的有效性
- 限流校验:检查用户请求频率是否超出限制
-
- 模型推理阶段
- 调用大模型进行文本生成,根据参数控制生成质量和多样性
-
- 结果处理阶段
- 保存生成记录到数据库,将结果返回给前端
-
- 前端展示阶段
- 以可读格式展示生成文本,同步更新历史记录列表
2. JWT令牌全生命周期流程

流程说明:
-
- 初始令牌获取
- 用户首次获取令牌,系统同时颁发访问令牌和刷新令牌,前端安全存储两个令牌
-
- 正常使用流程
- 用户调用API时携带访问令牌;令牌有效时,系统正常响应请求
-
- 令牌过期处理
- 访问令牌过期后,前端提示用户刷新;使用刷新令牌请求新的访问令牌;无需用户重新登录,体验更流畅
-
- 异常处理
- 令牌无效时,提示用户重新获取,确保安全性和数据保护
四、示例完整解析
1. 完善依赖配置
新建requirements.txt,补充可视化所需依赖:
原有依赖
fastapi>=0.104.1
uvicorn>=0.24.0
pydantic>=2.4.2
transformers>=4.35.2
torch>=2.1.0
python-dotenv>=1.0.0
python-jose>=3.3.0
cryptography>=41.0.7
新增可视化依赖
streamlit>=1.28.2
requests>=2.31.0
安装依赖:pip install -r requirements.txt
核心库简介:
- **FastAPI:**Web 框架,用于后端 API 开发,高性能异步 Web 框架,自带参数校验、接口文档,适合快速封装大模型接口,容易上手
- **Streamlit:**可视化框架,用于前端交互开发,无需HTML/CSS/JS相关前端知识,纯 Python 实现可视化界面,快速搭建大模型交互平台
- **Transformers:**模型调用,用于大模型加载与推理,HuggingFace开源库,支持加载 Qwen、GPT2 等主流大模型,提供统一的 generate 推理接口
- **PyTorch:**深度学习框架,用于模型运行依赖,大模型推理的底层框架,提供张量计算、显存管理等核心能力
- **python-jose:**鉴权工具,用于JWT令牌生成和验证,轻量级库,支持 JWT 的加密、解密、过期校验,实现无状态鉴权
- **python-dotenv:**配置管理,用于环境变量加载,从.env 文件读取配置(如 API Key、JWT 密钥),避免硬编码敏感信息
- **Uvicorn:**服务器,用于FastAPI 运行依赖,ASGI 服务器,用于启动 FastAPI 应用,支持热重载、日志配置
- **Requests:**网络请求,用于前后端交互,前端调用后端 API 的核心库,简化 HTTP 请求的发送与响应解析
2. 后端代码(main.py)分解
2.1 基础架构层
python
# 配置加载模块
load_dotenv() # 加载环境变量,解耦敏感配置
VALID_API_KEY = os.getenv("VALID_API_KEY", "default_key_123") # 缺省值保证容错
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "my_jwt_secret_123")
# 单例模型加载类(LocalLLM)
class LocalLLM:
_instance = None # 单例模式:避免重复加载模型占用内存
_initialized = False
def __new__(cls): # 控制实例唯一
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not LocalLLM._initialized:
# 模型加载核心逻辑:路径校验、内存优化、异常捕获
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float32,
device_map="cpu", # 适配无GPU环境
low_cpu_mem_usage=True # 减少内存占用
)
self.model.eval() # 推理模式,禁用梯度计算
LocalLLM._initialized = True
**核心价值:**单例模式避免多次加载模型导致的内存溢出,环境变量解耦敏感配置,异常捕获提升程序鲁棒性。
2.2 鉴权核心层(API Key/JWT 双鉴权)
python
# API Key鉴权:请求头校验
def generate_by_apikey(request: LLMRequest, x_api_key: str = Header(None)):
if not x_api_key or x_api_key != VALID_API_KEY:
raise HTTPException(status_code=401, detail="API Key错误")
# JWT鉴权:令牌生成与验证
def create_jwt_token():
token_data = {"sub": "local_llm_user", "exp": datetime.utcnow() + timedelta(minutes=30)}
return jwt.encode(token_data, JWT_SECRET_KEY, algorithm="HS256")
def verify_jwt_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
try:
payload = jwt.decode(credentials.credentials, JWT_SECRET_KEY, algorithms=["HS256"])
if payload["exp"] < datetime.utcnow().timestamp():
raise HTTPException(status_code=401, detail="令牌过期")
return payload["sub"]
except JWTError:
raise HTTPException(status_code=401, detail="令牌无效")
**核心价值:**双鉴权模式适配不同场景(API Key 适合服务器间调用,JWT 适合多端用户调用),过期校验避免令牌滥用。
2.3 业务扩展层
python
# 1. 生成参数扩展:支持temperature/top_p/返回格式
def generate_text(self, prompt, max_length, temperature=0.7, top_p=0.9, return_format="text"):
outputs = self.model.generate(
**inputs,
max_length=max_length,
temperature=temperature, # 控制随机性
top_p=top_p, # 控制采样范围
pad_token_id=self.tokenizer.eos_token_id
)
if return_format == "json":
return {"prompt": prompt, "result": result} # 多格式返回
# 2. 限流功能:内存级IP限流
rate_limit_store = {} # 无需数据库,内存存储限流记录
def check_rate_limit(client_ip: str):
now = time.time()
if client_ip not in rate_limit_store:
rate_limit_store[client_ip] = {"count": 0, "start_time": now}
if now - rate_limit_store[client_ip]["start_time"] > 60: # 1分钟窗口
rate_limit_store[client_ip]["count"] = 0
if rate_limit_store[client_ip]["count"] >= 10: # 每分钟10次
raise HTTPException(status_code=429, detail="调用频率超限")
rate_limit_store[client_ip]["count"] += 1
# 3. 历史记录:本地JSON文件存储
def save_generate_history(prompt: str, result: str, auth_type: str, client_ip: str):
history = []
if os.path.exists("generate_history.json"):
with open("generate_history.json", "r", encoding="utf-8") as f:
history = json.load(f)
history.append({
"id": len(history)+1,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"prompt": prompt,
"result": result,
"auth_type": auth_type,
"client_ip": client_ip
})
with open("generate_history.json", "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
**核心价值:**无插件扩展实现生产级功能,内存限流/文件存储降低部署成本,参数扩展提升接口灵活性。
2.4 接口层(核心 API 定义)
python
# 基础接口:获取JWT令牌
@app.post("/get-token")
def get_jwt_token(): ...
# 扩展接口:刷新JWT令牌
@app.post("/refresh-token")
def refresh_jwt_token(refresh_token: str = Header(None)): ...
# 核心接口:文本生成(API Key/JWT双版本)
@app.post("/generate-text-apikey")
def generate_by_apikey(...): ...
@app.post("/generate-text-jwt")
def generate_by_jwt(...): ...
# 扩展接口:查询历史记录
@app.get("/get-history")
def get_history(limit: int = Query(10, ge=1, le=100)): ...
**核心价值:**接口职责单一,扩展接口兼容原有逻辑,Query 参数校验避免非法查询。
3. 前端代码(app.py)分解
3.1 基础配置层(页面与状态管理)
python
# 页面配置:适配大屏/小屏
st.set_page_config(
page_title="本地大模型可视化平台",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded"
)
# 会话状态:保存鉴权信息/历史记录(跨刷新不丢失)
if "api_key" not in st.session_state:
st.session_state.api_key = "default_key_123"
if "jwt_token" not in st.session_state:
st.session_state.jwt_token = ""
if "history" not in st.session_state:
st.session_state.history = []
**核心价值:**Streamlit 会话状态替代前端存储,无需 Cookie/本地存储,简化开发。
3.2 鉴权交互层
python
# 侧边栏鉴权选择:API Key/JWT切换
auth_type = st.sidebar.radio("选择鉴权方式", ("API Key鉴权", "JWT令牌鉴权"))
# JWT令牌获取/刷新
def get_jwt_token_full():
response = requests.post(f"{BASE_URL}/get-token")
if response.status_code == 200:
st.session_state.jwt_token = response.json()["access_token"]
st.session_state.refresh_token = response.json()["refresh_token"]
def refresh_jwt_token():
headers = {"Refresh-Token": st.session_state.refresh_token}
response = requests.post(f"{BASE_URL}/refresh-token", headers=headers)
st.session_state.jwt_token = response.json()["access_token"]
**核心价值:**可视化操作替代手动调用接口,令牌刷新自动化,降低用户操作成本。
3.3 业务交互层
python
# 生成参数可视化:滑块+单选框
max_length = st.slider("生成长度", 10, 1000, 150, 10)
temperature = st.slider("随机性(0.1-1.0)", 0.1, 1.0, 0.7, 0.1)
top_p = st.slider("采样策略(0.1-1.0)", 0.1, 1.0, 0.9, 0.1)
return_format = st.radio("返回格式", ("text", "json"), horizontal=True)
# 生成请求:适配后端参数
def generate_text(prompt, max_length, temperature, top_p, return_format):
headers = {"Content-Type": "application/json"}
if auth_type == "API Key鉴权":
headers["X-API-Key"] = st.session_state.api_key
url = f"{BASE_URL}/generate-text-apikey"
else:
headers["Authorization"] = f"Bearer {st.session_state.jwt_token}"
url = f"{BASE_URL}/generate-text-jwt"
data = {
"prompt": prompt,
"max_length": max_length,
"temperature": temperature,
"top_p": top_p,
"return_format": return_format
}
response = requests.post(url, headers=headers, json=data)
# 异常适配:限流/令牌过期
if response.status_code == 429:
return False, "调用频率超限,请稍后重试"
if response.status_code == 401 and "过期" in response.json()["detail"]:
return False, "令牌过期,请点击刷新令牌重试"
**核心价值:**可视化参数适配后端扩展,异常提示本地化,提升用户体验。
3.4 结果展示层
python
# 结果展示:文本/JSON切换
if success:
st.subheader("✅ 生成结果")
if return_format == "json":
st.json(json.loads(result)) # JSON格式化展示
else:
st.markdown(f"> {result}") # 文本友好展示
# 历史记录:本地+后端同步
if st.sidebar.button("🔄 同步后端历史"):
response = requests.get(f"{BASE_URL}/get-history", params={"limit": history_limit})
st.session_state.history = response.json()["data"]
# 历史记录展示:倒序+展开式
for idx, item in enumerate(reversed(st.session_state.history)):
with st.expander(f"📝 {item['timestamp']} | 鉴权:{item['auth_type']}"):
st.markdown(f"**提示词**:{item['prompt']}")
st.markdown(f"**生成结果**:{item['result']}")
**核心价值:**多格式展示适配不同需求,历史记录同步实现多端数据统一。
五、示例运行
1. 基础使用步骤
步骤 1:环境准备
- 安装依赖:pip install fastapi uvicorn streamlit transformers torch python-jose python-dotenv requests;
- 准备本地模型:将 Qwen1.5-1.8B-Chat 模型下载到指定路径(示例中为D:\\modelscope\\hub\\qwen\\Qwen1___5-1___8B-Chat),或替换为其他兼容 Transformers 的大模型(如 DistilGPT2、Baichuan-7B-Chat)。
步骤 2:启动服务
- 启动后端:运行python main.py,确认日志显示 "模型加载完成""Uvicorn running on http://0.0.0.0:8080";

- 启动前端:新开终端运行streamlit run app.py,自动打开浏览器页面(默认地址:http://localhost:8501)。

步骤 3:核心操作
- 选择鉴权方式:侧边栏选 "API Key 鉴权"(默认 Key:default_key_123)或 "JWT 令牌鉴权"(点击 "获取令牌" 自动生成);

- 配置生成参数:输入提示词(如 "写一段秋天的文案"),调节生成长度、随机性等参数;
- 生成文本:点击 "开始生成",查看结果(支持文本 / JSON 格式);

- 查看历史:同步后端历史记录,回顾过往生成内容。

2. Postman调试API Key鉴权的接口
步骤 1:新建请求
-
- 打开 Postman,点击左侧【+】号 → 选择【HTTP Request】;
-
- 请求方法:下拉框选择【POST】;
步骤 2:添加 API Key 请求头
-
- 切换到【Headers】标签页;
-
- 点击【Add Header】,新增一行:
- Key:X-API-Key(严格匹配,大小写都要对);
- Value:default_key_123(代码里的默认值,或.env 里配置的 VALID_API_KEY)。
步骤 3:填写请求体(提示词 + 生成长度)
- 切换到【Body】标签页;
- 选择【raw】→ 右侧下拉框选【JSON】;
- 输入 JSON 格式的请求内容(示例):
python
{
"prompt": "写一段秋天的文案,温馨风格",
"max_length": 150
}
步骤 4:发送请求并查看结果
- 点击右上角【Send】按钮;
- 正常情况,下方【Response】区域会返回:
python
{
"code": 200,
"message": "生成成功",
"data": {
"prompt": "写一段秋天的文案,温馨风格",
"result": "在这个金黄的季节里,我们迎来了秋风的旋律,带来了丰收的喜悦和宁静的夜晚。秋天,是大自然赋予大地最深沉的色彩,是万物生长的季节,更是情感交融、心灵洗礼的时刻。\n\n天空中的云彩被染成了淡淡的橙色,阳光透过稀疏的树叶,洒在大地上,像是一幅精致的画卷。田野里的稻谷金黄一片,像是海洋中金色的波浪,随风摇曳,散发着诱人的香气。果园里的苹果熟透了,红彤彤的果实挂满了枝头,仿佛是一个个羞涩的小姑娘,在向人们展示着它们的甜蜜和美丽。"
}
}
接口调用成功的结果展示:

key值错误时的提升:

验证Value错误时的提示:

3. 核心功能使用场景说明
- **生成多样化文案:**调高 temperature(如 0.9),多次生成同一提示词,获取不同风格的文案;
- **生成精准技术内容:**调低 temperature(如 0.2),提升输出内容的准确性和一致性;
- **团队内部安全使用:**更换 API Key 为自定义值(修改.env 文件),仅告知团队成员,避免外部调用;
- **避免服务过载:**依赖内置限流(单 IP 每分钟 10 次),或调整 RATE_LIMIT 参数适配团队规模;
- **对接其他程序:**调用后端 API(如 POST /generate-text-apikey),获取 JSON 格式结果,集成到自有系统;
六、示例扩展
1 模型层面扩展
- 支持多模型切换:修改LocalLLM类,新增模型路径配置(如支持 Qwen、Baichuan、Llama2),前端新增模型选择下拉框;
python
# 后端扩展示例:多模型加载
class LocalLLM:
def __init__(self):
self.models = {
"qwen": AutoModelForCausalLM.from_pretrained("path/to/qwen"),
"baichuan": AutoModelForCausalLM.from_pretrained("path/to/baichuan")
}
self.tokenizers = {
"qwen": AutoTokenizer.from_pretrained("path/to/qwen"),
"baichuan": AutoTokenizer.from_pretrained("path/to/baichuan")
}
- 支持更多生成参数:新增top_k(采样数)、repetition_penalty(重复惩罚)等参数,前端同步添加滑块 / 输入框。
2. 安全层面扩展
- API Key 分级授权:新增 Key 权限配置(如"只读 Key"仅能查询历史,"生成 Key"可调用生成接口),后端新增权限校验逻辑;
- IP 白名单:在限流函数中新增 IP 白名单,允许指定 IP 跳过限流,适配核心用户 / 服务器调用;
python
# 后端扩展示例:IP白名单
WHITE_LIST_IPS = ["192.168.1.100", "127.0.0.1"]
def check_rate_limit(client_ip: str):
if client_ip in WHITE_LIST_IPS:
return # 白名单IP不限流
# 原有限流逻辑...
- HTTPS 加密传输:为 Uvicorn 配置 SSL 证书,前端调用改为 HTTPS,防止鉴权凭证被抓包窃取。
3. 数据层面扩展
- 替换为数据库存储:将历史记录从 JSON 文件替换为 SQLite/MySQL,适配大量历史数据存储;
- 结果导出:前端新增 "导出历史记录" 按钮,支持导出为 Excel/Markdown,适配业务报表需求;
python
# 前端扩展示例:导出历史记录
import pandas as pd
if st.button("导出历史记录为Excel"):
df = pd.DataFrame(st.session_state.history)
df.to_excel("generate_history.xlsx", index=False)
st.success("导出成功!")
4. 交互层面扩展
- 批量生成:前端支持上传 CSV 文件(含多组提示词),批量调用后端接口生成结果;
- 上下文对话:新增会话管理,支持多轮对话(将上一轮生成结果作为上下文传入 prompt);
- 自定义主题:修改 Streamlit 页面样式(背景、配色),适配企业品牌风格。
七、总结
今天我们以"本地大模型 API 封装 + 可视化交互" 为核心,从基础的鉴权接口实现,到业务扩展,完整覆盖了大模型本地化应用的核心环节。我们可通过基础模块理解核心逻辑,实际应用可基于扩展模块实现业务定制;先跑通再折腾!第一步先按说明装依赖、启动前后端,用默认配置生成一段文本,先感受全流程。
初次运行一般会遇到一些问题,比如模型加载失败就检查路径,端口被占就改 8080 为 8090等,按照提示来修正即可。初期不用纠结复杂功能,先把基础的鉴权和生成用熟,比如试试两种鉴权方式的区别。想扩展就从简单的来,比如加个导出历史记录的按钮,或者调调 temperature 参数看效果,最后记住,这个示例是脚手架,不用死磕原有代码,按自己的需求小步改,跑通一个小功能就很收获!
附录一:完整的前端示例代码
python
# 260104-Streamlit_jwt_前端2
import streamlit as st
import requests
import json
from datetime import datetime
# ====================== 基础配置(无核心变化) ======================
st.set_page_config(
page_title="本地大模型可视化平台",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded"
)
BASE_URL = "http://127.0.0.1:8080"
# 扩展:初始化更多会话状态
if "api_key" not in st.session_state:
st.session_state.api_key = "default_key_123"
if "jwt_token" not in st.session_state:
st.session_state.jwt_token = ""
if "refresh_token" not in st.session_state: # 新增:刷新令牌
st.session_state.refresh_token = ""
if "history" not in st.session_state:
st.session_state.history = []
if "return_format" not in st.session_state: # 新增:返回格式
st.session_state.return_format = "text"
# ====================== 侧边栏:扩展鉴权配置 ======================
st.sidebar.title("🔐 鉴权配置")
auth_type = st.sidebar.radio(
"选择鉴权方式",
("API Key鉴权", "JWT令牌鉴权"),
index=0
)
# API Key鉴权配置(无变化)
if auth_type == "API Key鉴权":
api_key = st.sidebar.text_input(
"输入API Key",
value=st.session_state.api_key,
type="password"
)
st.session_state.api_key = api_key
# JWT令牌鉴权配置(扩展:新增刷新令牌)
else:
# 扩展:获取令牌(返回刷新令牌)
def get_jwt_token_full():
try:
response = requests.post(f"{BASE_URL}/get-token")
if response.status_code == 200:
data = response.json()
st.session_state.jwt_token = data["access_token"]
st.session_state.refresh_token = data["refresh_token"] # 保存刷新令牌
st.sidebar.success(f"✅ 令牌获取成功\n访问令牌30分钟\n刷新令牌7天")
else:
st.sidebar.error(f"❌ 获取失败:{response.json()['detail']}")
except requests.exceptions.ConnectionError:
st.sidebar.error("❌ 连接失败:请先启动FastAPI后端服务")
except Exception as e:
st.sidebar.error(f"❌ 未知错误:{str(e)}")
# 扩展:刷新令牌函数
def refresh_jwt_token():
if not st.session_state.refresh_token:
st.sidebar.error("❌ 无刷新令牌,请先获取访问令牌")
return
try:
headers = {"Refresh-Token": st.session_state.refresh_token}
response = requests.post(f"{BASE_URL}/refresh-token", headers=headers)
if response.status_code == 200:
data = response.json()
st.session_state.jwt_token = data["access_token"]
st.sidebar.success(f"✅ 令牌刷新成功\n新令牌有效期30分钟")
else:
st.sidebar.error(f"❌ 刷新失败:{response.json()['detail']}")
except requests.exceptions.ConnectionError:
st.sidebar.error("❌ 连接失败:请先启动FastAPI后端服务")
except Exception as e:
st.sidebar.error(f"❌ 刷新错误:{str(e)}")
# 扩展:新增按钮布局
col_btn1, col_btn2 = st.sidebar.columns(2)
with col_btn1:
st.button(
"获取令牌",
on_click=get_jwt_token_full,
type="primary",
use_container_width=True
)
with col_btn2:
st.button(
"刷新令牌", # 新增:刷新令牌按钮
on_click=refresh_jwt_token,
use_container_width=True
)
# 显示令牌
jwt_token = st.sidebar.text_input(
"当前访问令牌",
value=st.session_state.jwt_token,
type="password"
)
st.session_state.jwt_token = jwt_token
# 清空历史记录(无变化)
if st.sidebar.button("🗑️ 清空本地历史", use_container_width=True):
st.session_state.history = []
st.sidebar.success("本地历史记录已清空!")
# ====================== 核心函数(扩展) ======================
# 扩展:同步后端历史记录
def sync_history(limit: int = 10):
try:
response = requests.get(f"{BASE_URL}/get-history", params={"limit": limit})
if response.status_code == 200:
data = response.json()["data"]
st.session_state.history = data
st.sidebar.success(f"✅ 同步成功,获取{len(data)}条记录")
else:
st.sidebar.error(f"❌ 同步失败:{response.json()['detail']}")
except requests.exceptions.ConnectionError:
st.sidebar.error("❌ 连接失败:请先启动FastAPI后端服务")
except Exception as e:
st.sidebar.error(f"❌ 同步错误:{str(e)}")
# 扩展:历史记录配置(移到函数定义之后)
st.sidebar.divider()
st.sidebar.subheader("📜 历史记录配置")
history_limit = st.sidebar.number_input(
"查询历史记录条数",
min_value=1,
max_value=100,
value=10
)
if st.sidebar.button("🔄 同步后端历史", use_container_width=True):
sync_history(history_limit)
# 扩展:生成文本(适配新增参数)
def generate_text(prompt, max_length, temperature, top_p, return_format):
headers = {"Content-Type": "application/json"}
# 选择接口和鉴权信息(无核心变化)
if auth_type == "API Key鉴权":
url = f"{BASE_URL}/generate-text-apikey"
headers["X-API-Key"] = st.session_state.api_key
else:
url = f"{BASE_URL}/generate-text-jwt"
headers["Authorization"] = f"Bearer {st.session_state.jwt_token}"
# 扩展:请求体新增参数
data = {
"prompt": prompt,
"max_length": max_length,
"temperature": temperature,
"top_p": top_p,
"return_format": return_format
}
try:
with st.spinner("🤖 模型正在生成内容..."):
response = requests.post(url, headers=headers, json=data)
# 扩展:适配限流错误(429)
if response.status_code == 200:
result = response.json()["data"]["result"]
# 保存到本地历史(兼容后端格式)
st.session_state.history.append({
"id": len(st.session_state.history) + 1,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"prompt": prompt,
"result": result,
"auth_type": auth_type,
"client_ip": "127.0.0.1"
})
return True, result
elif response.status_code == 429:
return False, f"❌ {response.json()['detail']}"
elif response.status_code == 401 and "过期" in response.json()["detail"] and auth_type == "JWT令牌鉴权":
# 自动提示刷新令牌
return False, f"❌ {response.json()['detail']}\n👉 可点击侧边栏「刷新令牌」按钮重试"
else:
return False, f"❌ 生成失败:{response.json()['detail']}"
except requests.exceptions.ConnectionError:
return False, "❌ 连接失败:请先启动FastAPI后端服务"
except Exception as e:
return False, f"❌ 未知错误:{str(e)}"
# ====================== 主页面:扩展交互区域 ======================
st.title("🤖 本地大模型文本生成平台")
st.divider()
# 扩展:生成参数配置(新增temperature、top_p、return_format)
col1, col2 = st.columns([6, 4])
with col1:
prompt = st.text_area(
"输入提示词",
placeholder="例如:写一段秋天的温馨文案、解释JWT鉴权的核心原理...",
height=100,
max_chars=500
)
with col2:
st.subheader("⚙️ 生成参数")
max_length = st.slider("生成长度", 10, 1000, 150, 10)
temperature = st.slider("随机性(0.1-1.0)", 0.1, 1.0, 0.7, 0.1)
top_p = st.slider("采样策略(0.1-1.0)", 0.1, 1.0, 0.9, 0.1)
# 扩展:返回格式选择
return_format = st.radio(
"返回格式",
("text", "json"),
index=0,
horizontal=True
)
st.session_state.return_format = return_format
# 生成按钮(无变化)
if st.button("🚀 开始生成", type="primary", use_container_width=True):
if not prompt.strip():
st.error("提示词不能为空!")
else:
success, result = generate_text(
prompt,
max_length,
temperature,
top_p,
return_format
)
if success:
st.subheader("✅ 生成结果")
# 扩展:按格式展示结果
if return_format == "json":
st.json(json.loads(result))
else:
st.markdown(f"> {result}")
else:
st.error(result)
# ====================== 历史记录区域(适配后端格式) ======================
st.divider()
st.subheader("📜 生成历史")
if st.session_state.history:
for idx, item in enumerate(reversed(st.session_state.history)):
# 适配后端返回的字段(id/timestamp/auth_type等)
time_str = item.get("timestamp", "未知时间")
auth_str = item.get("auth_type", "未知")
with st.expander(f"📝 {time_str} | 鉴权:{auth_str} | ID:{item.get('id', idx+1)}"):
st.markdown(f"**提示词**:{item['prompt']}")
st.markdown(f"**生成结果**:{item['result']}")
else:
st.info("暂无生成记录,开始你的第一次创作吧!\n👉 可点击侧边栏「同步后端历史」获取服务器记录")
附录二:完整的后端示例代码
python
# model_path = "D:\\modelscope\\hub\\qwen\\Qwen1___5-1___8B-Chat"
# 260103-调用JWT鉴权的接口
# 扩展版:新增生成参数、历史记录、令牌刷新、限流、格式控制
# 导入需要的库
from fastapi import FastAPI, Header, HTTPException, Depends, Query
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
from dotenv import load_dotenv
from jose import jwt, JWTError
from datetime import datetime, timedelta
import os
import uvicorn
import json
import time
from typing import Optional, Literal
# ====================== 第一步:加载配置(新增限流配置) ======================
try:
load_dotenv()
# API Key配置
VALID_API_KEY = os.getenv("VALID_API_KEY", "default_key_123")
# JWT配置
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "my_jwt_secret_123")
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_MINUTES = 30
JWT_REFRESH_EXPIRE_DAYS = 7 # 刷新令牌有效期7天
# 新增:限流配置
RATE_LIMIT = 10 # 单IP每分钟最大调用次数
RATE_LIMIT_WINDOW = 60 # 限流窗口(秒)
# 新增:历史记录存储路径
HISTORY_FILE = "generate_history.json"
print("✅ 配置加载成功")
except Exception as e:
print(f"❌ 配置加载失败:{str(e)}")
exit(1)
# ====================== 工具函数(新增:限流+历史记录) ======================
# 限流存储(内存级,重启后清空)
rate_limit_store = {}
def check_rate_limit(client_ip: str):
"""检查IP限流"""
now = time.time()
# 初始化IP记录
if client_ip not in rate_limit_store:
rate_limit_store[client_ip] = {"count": 0, "start_time": now}
ip_record = rate_limit_store[client_ip]
# 重置超时窗口
if now - ip_record["start_time"] > RATE_LIMIT_WINDOW:
ip_record["count"] = 0
ip_record["start_time"] = now
# 检查限流
if ip_record["count"] >= RATE_LIMIT:
raise HTTPException(
status_code=429,
detail=f"❌ 调用频率超限(单IP每分钟最多{RATE_LIMIT}次),请稍后重试"
)
# 计数+1
ip_record["count"] += 1
def save_generate_history(prompt: str, result: str, auth_type: str, client_ip: str):
"""保存生成记录到本地JSON文件"""
history = []
# 读取现有记录
if os.path.exists(HISTORY_FILE):
try:
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
history = json.load(f)
except:
history = []
# 新增记录
history.append({
"id": len(history) + 1,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"prompt": prompt,
"result": result,
"auth_type": auth_type,
"client_ip": client_ip
})
# 写入文件
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
def get_generate_history(limit: int = 10):
"""获取生成历史记录"""
if not os.path.exists(HISTORY_FILE):
return []
try:
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
history = json.load(f)
# 按ID倒序,取最新的limit条
return sorted(history, key=lambda x: x["id"], reverse=True)[:limit]
except:
return []
# ====================== 第二步:单例加载模型(无核心变化) ======================
class LocalLLM:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not LocalLLM._initialized:
model_path = "D:\\modelscope\\hub\\qwen\\Qwen1___5-1___8B-Chat"
try:
print("正在加载本地大模型...")
if not os.path.exists(model_path):
raise FileNotFoundError(
f"模型文件夹不存在:{model_path}\n解决:1. 运行download_small_model.py下载小模型;2. 确认路径正确")
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float32,
device_map="cpu",
trust_remote_code=True,
low_cpu_mem_usage=True
)
self.model.eval()
LocalLLM._initialized = True
print("✅ 模型加载完成!")
except FileNotFoundError as e:
print(f"❌ 模型加载失败:{str(e)}")
exit(1)
except RuntimeError as e:
if "out of memory" in str(e).lower():
print(f"❌ 内存不足,请关闭其他程序(如浏览器/微信),或换更小的模型")
else:
print(f"❌ 模型运行错误:{str(e)}")
exit(1)
except Exception as e:
print(f"❌ 模型加载未知错误:{str(e)}")
exit(1)
# 扩展:新增temperature、top_p参数,支持格式返回
def generate_text(self, prompt, max_length, temperature=0.7, top_p=0.9, return_format="text"):
try:
if not prompt.strip():
raise ValueError("提示词不能为空")
inputs = self.tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
if inputs.input_ids.shape[1] > 512:
raise ValueError(f"提示词过长({inputs.input_ids.shape[1]}token),最大支持512token")
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_length=max_length,
temperature=temperature, # 新增参数
top_p=top_p, # 新增参数
pad_token_id=self.tokenizer.eos_token_id
)
result = self.tokenizer.decode(outputs[0], skip_special_tokens=True).replace(prompt, "").strip()
if not result:
raise ValueError("模型未生成内容,请换提示词")
# 扩展:支持不同返回格式
if return_format == "json":
return {"prompt": prompt, "result": result}
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=f"参数错误:{str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"推理错误:{str(e)}")
# 创建模型实例
try:
llm = LocalLLM()
except Exception as e:
print(f"❌ 模型初始化失败:{str(e)}")
exit(1)
# ====================== 第三步:JWT鉴权扩展(新增刷新令牌) ======================
bearer_scheme = HTTPBearer()
# 扩展:生成刷新令牌
def create_refresh_token():
"""生成刷新令牌(有效期7天)"""
try:
token_data = {
"sub": "local_llm_refresh",
"exp": datetime.utcnow() + timedelta(days=JWT_REFRESH_EXPIRE_DAYS)
}
refresh_token = jwt.encode(token_data, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
return refresh_token
except Exception as e:
raise HTTPException(status_code=500, detail=f"生成刷新令牌失败:{str(e)}")
# 原有:生成访问令牌
def create_jwt_token():
token_data = {
"sub": "local_llm_user",
"exp": datetime.utcnow() + timedelta(minutes=JWT_EXPIRE_MINUTES)
}
token = jwt.encode(token_data, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
return token
# 原有:验证访问令牌
def verify_jwt_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
try:
token = credentials.credentials
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
expire = payload.get("exp")
if expire and datetime.utcfromtimestamp(expire) < datetime.utcnow():
raise HTTPException(status_code=401, detail="❌ JWT令牌已过期\n解决:调用/refresh-token刷新令牌")
return payload.get("sub")
except JWTError as e:
raise HTTPException(status_code=401, detail=f"❌ JWT令牌无效/签名错误\n解决:检查令牌是否正确,或重新获取")
except Exception as e:
raise HTTPException(status_code=401, detail=f"❌ 令牌验证失败:{str(e)}")
# 扩展:验证刷新令牌并生成新的访问令牌
def verify_refresh_token(refresh_token: str):
try:
payload = jwt.decode(refresh_token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
expire = payload.get("exp")
if expire and datetime.utcfromtimestamp(expire) < datetime.utcnow():
raise HTTPException(status_code=401, detail="❌ 刷新令牌已过期,请重新获取访问令牌")
# 生成新的访问令牌
new_access_token = create_jwt_token()
return new_access_token
except JWTError as e:
raise HTTPException(status_code=401, detail=f"❌ 刷新令牌无效/签名错误")
except Exception as e:
raise HTTPException(status_code=401, detail=f"❌ 刷新令牌验证失败:{str(e)}")
# ====================== 第四步:FastAPI接口扩展 ======================
app = FastAPI(title="本地大模型API(扩展版)", description="新增参数、历史、限流、刷新令牌")
# 扩展:请求体模型(新增temperature、top_p、return_format)
class LLMRequest(BaseModel):
prompt: str = Field(..., description="提示词", max_length=500)
max_length: int = Field(100, description="生成长度", ge=10, le=1000)
temperature: float = Field(0.7, description="随机性(0-1,值越高越随机)", ge=0.1, le=1.0)
top_p: float = Field(0.9, description="采样策略(0.1-1.0)", ge=0.1, le=1.0)
return_format: Literal["text", "json"] = Field("text", description="返回格式")
# 1. 扩展:获取JWT令牌(新增刷新令牌返回)
@app.post("/get-token", summary="获取JWT令牌(含刷新令牌)")
def get_jwt_token():
access_token = create_jwt_token()
refresh_token = create_refresh_token() # 新增返回刷新令牌
return {
"code": 200,
"message": "令牌生成成功(访问令牌30分钟,刷新令牌7天)",
"access_token": access_token,
"refresh_token": refresh_token, # 新增字段
"token_type": "bearer"
}
# 2. 新增:刷新JWT令牌接口
@app.post("/refresh-token", summary="刷新JWT访问令牌")
def refresh_jwt_token(refresh_token: str = Header(None)):
if not refresh_token:
raise HTTPException(status_code=400, detail="❌ 请传入Refresh-Token请求头")
new_access_token = verify_refresh_token(refresh_token)
return {
"code": 200,
"message": "令牌刷新成功(新令牌有效期30分钟)",
"access_token": new_access_token,
"token_type": "bearer"
}
# 3. 扩展:API Key鉴权生成接口(新增参数、限流、历史记录)
@app.post("/generate-text-apikey", summary="API Key鉴权-文本生成(扩展版)")
def generate_by_apikey(
request: LLMRequest,
x_api_key: str = Header(None),
client_ip: str = Header(None, alias="X-Real-IP") # 客户端IP(反向代理场景)
):
# 1. 鉴权校验
if not x_api_key:
raise HTTPException(status_code=401, detail="❌ 请传入X-API-Key")
if x_api_key.strip() != VALID_API_KEY:
raise HTTPException(status_code=401, detail="❌ API Key错误")
# 2. 限流校验(扩展)
real_ip = client_ip or "127.0.0.1"
check_rate_limit(real_ip)
# 3. 生成文本(扩展参数)
result = llm.generate_text(
request.prompt,
request.max_length,
request.temperature,
request.top_p,
request.return_format
)
# 4. 保存历史记录(扩展)
save_generate_history(request.prompt, result, "api_key", real_ip)
return {"code": 200, "message": "生成成功", "data": {"prompt": request.prompt, "result": result}}
# 4. 扩展:JWT鉴权生成接口(新增参数、限流、历史记录)
@app.post("/generate-text-jwt", summary="JWT鉴权-文本生成(扩展版)")
def generate_by_jwt(
request: LLMRequest,
username: str = Depends(verify_jwt_token),
client_ip: str = Header(None, alias="X-Real-IP")
):
# 1. 限流校验(扩展)
real_ip = client_ip or "127.0.0.1"
check_rate_limit(real_ip)
# 2. 生成文本(扩展参数)
result = llm.generate_text(
request.prompt,
request.max_length,
request.temperature,
request.top_p,
request.return_format
)
# 3. 保存历史记录(扩展)
save_generate_history(request.prompt, result, "jwt", real_ip)
return {
"code": 200,
"message": f"用户{username}生成成功",
"data": {"prompt": request.prompt, "result": result}
}
# 5. 新增:查询生成历史接口
@app.get("/get-history", summary="查询生成历史记录")
def get_history(limit: int = Query(10, ge=1, le=100)):
history = get_generate_history(limit)
return {
"code": 200,
"message": f"获取最近{limit}条记录成功",
"data": history
}
# ====================== 第五步:启动服务(无核心变化) ======================
if __name__ == "__main__":
try:
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8080,
reload=False,
log_level="debug",
access_log=True
)
except OSError as e:
if "address already in use" in str(e).lower():
print(f"❌ 服务启动失败:端口8080被占用\n解决:1. 改端口为8090;2. 结束占用8080的程序")
else:
print(f"❌ 服务启动失败:{str(e)}")
exit(1)
except Exception as e:
print(f"❌ 服务启动失败:{str(e)}\n解决:检查Python依赖是否安装完整")
exit(1)