最近在做大模型应用落地时,我发现自己反复卡在同一个环节:DeepSeek API 的参数调试、Prompt 测试和结果对比分散在 Postman、临时脚本和网页对话框里,来回切换效率很低。于是花了一周时间,把整个联调流程整合成一个 DeepSeek Harness 可视化工作台,并把代码整理后开源。本文会完整拆解这个项目的设计思路、技术选型、核心代码、容器化部署以及开源发布时的注意事项,适合正在用 DeepSeek API 做应用开发、想提升 Prompt 调试效率的同学参考。
1. 背景与核心概念
1.1 什么是 DeepSeek Harness 可视化工作台
先来解释一下项目名称。Harness 在中文里常见的意思是“马具、缰绳”,但在 AI 工程领域,它通常指“测试驱动工具”或“接入编排框架”。你可以把 DeepSeek Harness 理解为一套围绕 DeepSeek 模型 API 的接入、测试和评估工作流,而可视化工作台则把这个工作流从命令行变成了 Web 界面。
在没有工作台之前,一次完整的模型联调通常是这样的:
- 打开 Postman,构造一份 JSON 请求体;
- 复制一大段 Prompt 进去;
- 修改 temperature、max_tokens 等参数;
- 发送请求,查看返回结果;
- 如果效果不好,再改参数重试。
这个过程本身不复杂,但一旦涉及多轮对话、多组参数对比、多个 Prompt 模板,就会变得非常混乱。可视化工作台解决的核心问题就是:把模型调用、参数调优、Prompt 模板管理、流式输出和历史记录统一到一个界面里,减少上下文切换成本。
1.2 为什么选择“爆改”而不是“从零开发”
这个项目的定位是“爆改一周”,而不是“从零造轮子”。原因很直接:大模型应用的可视化工具链已经有很多成熟组件,FastAPI 负责后端接口、Vue 3 + Element Plus 负责前端界面、OpenAI SDK 负责调用 DeepSeek 兼容 API,这些基础设施都不需要重新发明。
所谓“爆改”,指的是基于这些成熟脚手架,快速搭建一个贴合 DeepSeek 模型调试场景的工作台。重点工作量集中在三个部分:
- 后端 API 代理与流式转发;
- 前端参数面板与流式渲染;
- 工程化部署与开源发布准备。
如果你正好也在做类似的大模型工具,完全可以参考这种思路:先明确最小闭环,再逐步扩充功能,而不是一开始就追求大而全的架构。
1.3 工作台的能力范围
这个项目最终包含以下核心能力:
| 功能模块 | 能力说明 |
|---|---|
| 模型接入 | 通过 OpenAI 兼容 SDK 封装 DeepSeek API 调用 |
| 参数配置 | 支持模型选择、temperature、max_tokens 等参数实时调整 |
| Prompt 模板 | 预置多套常用 Prompt,可一键加载 |
| 流式对话 | 前端实时渲染模型生成内容,支持打字机效果 |
| 历史记录 | 将调试记录保存到 SQLite,支持回看和复用 |
| 容器化部署 | 前后端分别构建镜像,通过 docker-compose 一键启动 |
这里要强调一点:DeepSeek 的 API 兼容 OpenAI 消息格式,因此很多 OpenA 生态工具可以直接复用,这是项目能在一周内完成的关键原因之一。
2. 环境准备与版本说明
2.1 技术栈选型
这个项目在技术选型上遵循“够用就好”的原则,所有组件都是社区成熟方案,不存在冷门依赖。
| 层级 | 技术栈 | 用途 |
|---|---|---|
| 后端框架 | FastAPI | 提供 HTTP 接口与流式响应 |
| 模型 SDK | openai | 兼容 DeepSeek API 的官方 SDK |
| 数据库 | SQLite + SQLAlchemy | 保存历史调试记录 |
| 前端框架 | Vue 3 + Vite | 构建可视化页面 |
| UI 组件库 | Element Plus | 表单、面板、按钮等基础组件 |
| 容器化 | Docker + docker-compose | 一键部署前后端服务 |
这里的版本号需要根据你实际的环境调整。我在本地开发时使用的是 Python 3.10 和 Node.js 20,FastAPI 使用稳定版,Vue 使用 3.4 系列。不同小版本之间差异不大,但建议你保持关键依赖的版本一致性,尤其是 openai SDK,不同大版本之间接口可能有变化。
2.2 基础环境准备清单
在开始之前,你需要准备好以下环境:
- Python 3.10 或更高版本;
- Node.js 18 或更高版本;
- Docker 与 docker-compose 插件;
- DeepSeek 开放平台账号,并创建 API Key;
- 一个可用的终端工具和代码编辑器。
如果你是在本地开发,只需要 Python 和 Node 就能跑起来。如果你习惯容器化,需要注意本机 Docker 的运行状态。
2.3 项目目录结构
前后端分离的工程需要从一开始规划清楚目录,避免后面越来越乱。我使用的目录结构如下:
deepseek-harness-workbench/ ├── backend/ │ ├── app/ │ │ ├── __init__.py │ │ ├── main.py │ │ ├── config.py │ │ ├── models.py │ │ ├── schemas.py │ │ ├── routers/ │ │ │ ├── __init__.py │ │ │ ├── chat.py │ │ │ ├── config.py │ │ │ └── history.py │ │ └── services/ │ │ └── deepseek_client.py │ ├── requirements.txt │ └── .env.example ├── frontend/ │ ├── src/ │ │ ├── api/ │ │ │ └── index.js │ │ ├── views/ │ │ │ └── Workbench.vue │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vite.config.js ├── docker-compose.yml ├── README.md └── .gitignore这个结构不复杂,但已经把前后端、核心路由、服务层和部署文件都分开了,后续扩展功能时不需要大改目录。
3. 核心功能与数据模型设计
3.1 功能拆解
在写代码之前,我先把工作台需要做的事情拆成几个独立模块,每个模块只负责一件事:
- 配置读取:从环境变量读取 API Key、模型名称和允许的跨域来源;
- 模型调用:封装 DeepSeek Chat Completions 接口,支持流式返回;
- 参数管理:前端维护一组调试参数,发送请求时传递给后端;
- 消息渲染:前端按角色渲染用户消息和助手消息,流式追加内容;
- 历史记录:请求和响应保存到 SQLite,下次打开可以继续调试。
这个拆分方式避免了在单个文件里塞入所有逻辑。后端路由只负责接收请求和返回响应,真正的模型调用放在 service 层,前端也基本按照这个思路组织代码。
3.2 核心数据模型
历史记录功能需要一个最简单的数据表,我用了 SQLAlchemy 的 ORM 模型。字段设计遵循够用原则,不引入复杂的关系表。
# 文件路径:backend/app/models.py from datetime import datetime from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Float from sqlalchemy.orm import declarative_base, sessionmaker DATABASE_URL = "sqlite:///./deepseek_harness.db" engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() class ChatRecord(Base): __tablename__ = "chat_records" id = Column(Integer, primary_key=True, index=True) model = Column(String, default="deepseek-chat") temperature = Column(Float, default=0.7) max_tokens = Column(Integer, default=2048) prompt = Column(Text, default="") response = Column(Text, default="") created_at = Column(DateTime, default=datetime.now) Base.metadata.create_all(bind=engine)这里有一个细节:connect_args={"check_same_thread": False}是 SQLite 在多线程场景下的常用配置。FastAPI 的接口可能由多个线程处理请求,如果不加这个参数,可能出现跨线程使用数据库连接的报错。
3.3 为什么用 SSE 而不是 WebSocket
流式输出是大模型工作台的必要功能。在技术方案上,我选择了 SSE(Server-Sent Events)而不是 WebSocket,原因很简单:
- 大模型生成内容是单向的,服务器向客户端推送增量文本即可;
- SSE 基于 HTTP,协议更简单,FastAPI 的 StreamingResponse 天然支持;
- 前端可以用原生 fetch 配合 ReadableStream 读取,不需要引入额外依赖;
- SSE 有自动重连机制,调试阶段崩溃恢复更友好。
如果你需要做多轮对话的实时取消、服务端主动推送多路消息,再考虑 WebSocket 也不迟。现阶段 SSE 的性价比最高。
4. 后端核心实现
4.1 后端依赖与配置模板
后端依赖写在requirements.txt中,内容如下:
fastapi==0.111.0 uvicorn[standard]==0.30.1 openai==1.35.0 python-dotenv==1.0.1 sqlalchemy==2.0.30 pydantic==2.7.4版本号只是示例,你可以根据当前 PyPI 上的稳定版调整。配置模板放到.env.example中,真正使用时复制为.env并填入自己的密钥:
# 文件路径:backend/.env.example DEEPSEEK_API_KEY=sk-xxx DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_MODEL=deepseek-chat CORS_ORIGINS=http://localhost:5173,http://localhost:8080这里特别提醒:.env文件包含真实 API Key,必须加入.gitignore,不能提交到公开仓库。
4.2 配置读取模块
配置模块负责加载环境变量,并给服务层提供统一的访问入口。
# 文件路径:backend/app/config.py import os from dotenv import load_dotenv load_dotenv() class Settings: DEEPSEEK_API_KEY: str = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_BASE_URL: str = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com") DEEPSEEK_MODEL: str = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") CORS_ORIGINS: list = os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",") settings = Settings()这里做了两件事:第一,把环境变量集中到一个对象中,避免在多个文件里重复读取;第二,给CORS_ORIGINS提供了默认值,开发阶段前端可以直接访问http://localhost:5173。
4.3 DeepSeek 客户端封装
调用 DeepSeek API 的核心代码在 service 层。由于 DeepSeek 兼容 OpenAI 的消息格式,我们可以直接使用 openai 库,不必自己实现 HTTP 请求和鉴权逻辑。
# 文件路径:backend/app/services/deepseek_client.py from openai import OpenAI from app.config import settings class DeepSeekClient: def __init__(self): self.client = OpenAI( api_key=settings.DEEPSEEK_API_KEY, base_url=settings.DEEPSEEK_BASE_URL ) def chat_stream(self, messages, model=None, temperature=0.7, max_tokens=None): model = model or settings.DEEPSEEK_MODEL response = self.client.chat.completions.create( model=model, messages=messages, temperature=temperature, max_tokens=max_tokens, stream=True ) return response这里需要注意base_url的写法。DeepSeek 开放平台的接口地址是https://api.deepseek.com,SDK 会在这个地址下拼接/chat/completions路径。如果你的项目使用了其他兼容网关,替换base_url即可。
4.4 流式对话接口
有了客户端封装,接下来写路由。这里使用 StreamingResponse 将模型返回的增量内容直接转发给前端。
# 文件路径:backend/app/routers/chat.py from fastapi import APIRouter from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import List, Optional from app.services.deepseek_client import DeepSeekClient router = APIRouter(prefix="/api/chat", tags=["chat"]) class ChatMessage(BaseModel): role: str content: str class ChatRequest(BaseModel): messages: List[ChatMessage] model: Optional[str] = None temperature: float = 0.7 max_tokens: Optional[int] = None class ChatResponse(BaseModel): content: str @router.post("/stream", response_model=None) async def stream_chat(req: ChatRequest): client = DeepSeekClient() stream = client.chat_stream( messages=[m.model_dump() for m in req.messages], model=req.model, temperature=req.temperature, max_tokens=req.max_tokens ) def generate(): try: for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: yield chunk.choices[0].delta.content except Exception as exc: yield f"\n[ERROR] 调用失败: {exc}" return StreamingResponse(generate(), media_type="text/event-stream")在这个实现中,前端获取到的是纯文本增量,而不是 SSE 标准的事件格式。这样前端处理更简单,但在做断线重连时需要自己记录已接收的文本。实时性要求不高的调试场景,这种简化完全够用。
4.5 历史记录接口
历史记录模块提供保存和查询两个基础接口:
# 文件路径:backend/app/routers/history.py from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.models import SessionLocal, ChatRecord router = APIRouter(prefix="/api/history", tags=["history"]) def get_db(): db = SessionLocal() try: yield db finally: db.close() @router.get("") def list_history(db: Session = Depends(get_db)): records = db.query(ChatRecord).order_by(ChatRecord.created_at.desc()).limit(50).all() return [ { "id": r.id, "model": r.model, "prompt": r.prompt[:50], "created_at": r.created_at.strftime("%Y-%m-%d %H:%M:%S") } for r in records ] @router.post("") def save_history(data: dict, db: Session = Depends(get_db)): record = ChatRecord( model=data.get("model", "deepseek-chat"), temperature=data.get("temperature", 0.7), max_tokens=data.get("max_tokens", 2048), prompt=data.get("prompt", ""), response=data.get("response", "") ) db.add(record) db.commit() return {"status": "ok", "id": record.id}实际项目中你可能还会补充详情查询和删除接口,但最小闭环只需要这两个接口。历史记录的价值是让你能回看之前的调试参数和结果,而不是单纯积累数据。
4.6 后端入口
最后把路由注册到 FastAPI 实例中。
# 文件路径:backend/app/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.config import settings from app.routers import chat, history app = FastAPI(title="DeepSeek Harness 可视化工作台") app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(chat.router) app.include_router(history.router) @app.get("/") def root(): return {"message": "DeepSeek Harness backend is running."}这里的 CORS 配置需要和生产环境区分开。开发阶段为了方便可以放开*,但部署到公网后,建议改为前端实际的域名,避免被其他站点非法调用接口。
5. 前端可视化页面
5.1 前端工程初始化
前端部分我使用 Vite 快速创建 Vue 3 项目,并安装 Element Plus。
npm create vite@latest frontend -- --template vue cd frontend npm install element-plus npm install @element-plus/icons-vue如果你不想用 Vite 也可以,但 Vite 的开发服务器端口默认是 5173,这和我在后端配置的 CORS 默认值一致,减少了沟通成本。
5.2 前端 API 模块
前端通过 fetch 调用后端接口。流式读取的代码是重点,我专门封装了一个方法:
// 文件路径:frontend/src/api/index.js const BASE_URL = 'http://localhost:8000' export async function streamChat({ messages, model, temperature, maxTokens }, onDelta) { const resp = await fetch(`${BASE_URL}/api/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, model, temperature, max_tokens: maxTokens }) }) if (!resp.ok) { throw new Error(`HTTP ${resp.status}`) } const reader = resp.body.getReader() const decoder = new TextDecoder('utf-8') while (true) { const { value, done } = await reader.read() if (done) break onDelta(decoder.decode(value, { stream: true })) } }这里用到了ReadableStream,它是 fetch 在浏览器中的原生能力,不需要额外引入 axios 或 SSE 库。需要注意的是,decoder.decode的stream: true参数可以正确处理多字节字符的边界情况,避免中文出现乱码。
5.3 工作台主页面
页面布局分为三部分:左侧参数面板、中间对话区、底部输入区。
<!-- 文件路径:frontend/src/views/Workbench.vue --> <template> <div class="workbench"> <aside class="config-panel"> <h3>模型参数</h3> <el-form label-width="96px"> <el-form-item label="模型"> <el-select v-model="params.model"> <el-option label="DeepSeek Chat" value="deepseek-chat" /> <el-option label="DeepSeek Reasoner" value="deepseek-reasoner" /> </el-select> </el-form-item> <el-form-item label="Temperature"> <el-slider v-model="params.temperature" :min="0" :max="2" :step="0.1" show-input /> </el-form-item> <el-form-item label="Max Tokens"> <el-input-number v-model="params.maxTokens" :min="16" :max="8192" /> </el-form-item> <el-form-item label="Prompt 模板"> <el-select v-model="selectedTemplate" @change="applyTemplate"> <el-option label="通用对话" value="general" /> <el-option label="代码专家" value="coder" /> <el-option label="产品评审" value="product" /> </el-select> </el-form-item> </el-form> </aside> <main class="chat-panel"> <div class="messages"> <div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]"> <div class="role">{{ msg.role === 'user' ? '用户' : 'DeepSeek' }}</div> <pre>{{ msg.content }}</pre> </div> </div> <div class="input-area"> <el-input v-model="input" type="textarea" :rows="3" placeholder="输入你的问题,Ctrl+Enter 发送" @keydown.ctrl.enter.prevent="send" /> <el-button type="primary" :loading="loading" @click="send">发送</el-button> </div> </main> </div> </template>这个模板负责展示界面,下面还需要对应的脚本逻辑。
5.4 前端逻辑与流式渲染
接下来是 Vue 组件的 script 部分,这里实现了消息列表维护和流式渲染。
// 文件路径:frontend/src/views/Workbench.vue import { ref } from 'vue' import { streamChat } from '../api' const params = ref({ model: 'deepseek-chat', temperature: 0.7, maxTokens: 2048 }) const templates = { general: '你是一个乐于助人的 AI 助手。', coder: '你是一名资深软件工程师,请用中文回答问题,并尽可能给出可运行的代码示例。', product: '你是一名产品专家,请从用户价值、可实现性和风险三个角度分析问题。' } const messages = ref([]) const input = ref('') const loading = ref(false) const selectedTemplate = ref('general') function applyTemplate() { messages.value = [ { role: 'system', content: templates[selectedTemplate.value] } ] } async function send() { const text = input.value.trim() if (!text || loading.value) return const userMsg = { role: 'user', content: text } messages.value.push(userMsg) input.value = '' loading.value = true const assistantMsg = { role: 'assistant', content: '' } messages.value.push(assistantMsg) try { const sendMessages = messages.value.slice(0, -1).map(m => ({ role: m.role, content: m.content })) await streamChat( { messages: sendMessages, model: params.value.model, temperature: params.value.temperature, maxTokens: params.value.maxTokens }, (delta) => { assistantMsg.content += delta } ) } catch (err) { assistantMsg.content += `\n\n请求失败:${err.message}` } finally { loading.value = false } }这里有一个值得注意的细节:发送给后端时,把最后一条 assistantMsg 排除掉了,因为它还没有内容。如果把它一并发送,会让模型误以为它之前已经回复过一轮,导致对话逻辑混乱。
6. 完整联调与运行验证
6.1 启动后端服务
在backend目录下创建虚拟环境并安装依赖:
cd backend python -m venv venv source venv/bin/activate pip install -r requirements.txt cp .env.example .env # 编辑 .env,填入真实 DEEPSEEK_API_KEY uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload启动后可以在浏览器访问http://localhost:8000/docs,FastAPI 会自动生成 Swagger 接口文档,这是一个很实用的联调入口。
6.2 启动前端开发服务器
打开另一个终端:
cd frontend npm install npm run devVite 默认在http://localhost:5173启动开发服务器。打开页面后,你应该能看到左侧参数面板和中间空的对话区域。
6.3 功能验证清单
建议按下面的顺序验证核心功能:
| 验证项 | 预期结果 |
|---|---|
| 页面加载 | 左侧参数面板正常显示,中间为空白对话区 |
| 模型选择 | 下拉可选 deepseek-chat 和 deepseek-reasoner |
| 发送消息 | 消息区出现用户消息,随后出现流式生成的助手消息 |
| 调整参数 | 改变 temperature 后,下次请求按新参数执行 |
| Prompt 模板切换 | 切换模板后,对话列表重置,并注入 system 消息 |
| 历史记录接口 | GET /api/history能返回已保存的记录 |
如果以上都通过,说明最小闭环已经跑通。
7. 容器化部署
7.1 后端镜像
为了让项目可以一键启动,我为前后端分别编写了 Dockerfile。后端镜像基于 Python 3.11-slim:
# 文件路径:backend/Dockerfile FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]这里需要注意.dockerignore。构建镜像时不要复制本地的.env、venv、__pycache__等文件,否则可能泄露密钥或导致镜像体积过大。
7.2 前端镜像
前端使用多阶段构建,先编译静态资源,再放到 Nginx 中托管:
# 文件路径:frontend/Dockerfile FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80对应的 Nginx 配置需要把 API 请求反向代理到后端服务:
# 文件路径:frontend/nginx.conf server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段配置里的http://backend:8000是 docker-compose 服务名,在容器网络内部可以直接解析到后端容器。
7.3 使用 docker-compose 一键启动
根目录下的docker-compose.yml把两个服务串起来:
# 文件路径:docker-compose.yml services: backend: build: ./backend ports: - "8000:8000" env_file: - ./backend/.env restart: unless-stopped frontend: build: ./frontend ports: - "8080:80" depends_on: - backend restart: unless-stopped新版 Docker Compose 可以省略version字段,这里就没有写。启动命令:
docker compose up -d --build启动完成后访问http://localhost:8080,前端页面应该可以通过 Nginx 反向代理访问后端接口,同时后端的 8000 端口也可以直接用于调试。
8. 开源发布准备
8.1 为什么开源前要处理敏感信息
标题里说了项目已经开源,这就意味着仓库会被其他人看到。开源不是把代码丢到 GitHub 就结束了,第一件事是清理敏感信息。
我整理的.gitignore如下:
# 文件路径:.gitignore .env *.db __pycache__/ venv/ node_modules/ dist/ .DS_Store其中.env和*.db是最容易泄漏的。.env里是真实 API Key,*.db里是调试记录,可能包含业务 Prompt 和数据内容,都不能进仓库。
8.2 README 应该包含哪些内容
README 是开源项目的第一印象。我推荐包含以下结构:
- 项目名称和一句话简介;
- 功能截图或效果展示;
- 功能清单,方便读者快速判断项目是否适合自己;
- 技术栈说明;
- 快速开始,明确告知用户需要自己准备 DeepSeek API Key;
- 目录结构说明;
- 常见问题汇总;
- 贡献指南;
- 免责声明。
关于免责声明,建议写明“本项目的代码仅用于学习和技术交流,使用 DeepSeek API 产生的费用由使用者自行承担”。这能避免一些不必要的误解。
8.3 许可证选择
许可证我选的是 MIT,它在开源社区最常用,约束宽松,允许商用、修改和再分发,只需要保留版权声明。
如果你不确定怎么选,可以从 MIT、Apache-2.0 和 GPL-3.0 里选一个。MIT 最省心,GPL-3.0 要求衍生项目也必须开源,Apache-2.0 则多了一些专利条款。中小工具类项目选 MIT 基本不会错。
9. 常见问题与排查思路
9.1 高频问题清单
这类项目在开发和使用中遇到的报错主要集中在接口调用、跨域和流式读取上。我整理了下面的排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 401 | API Key 缺失、错误或已过期 | 检查 .env 中 DEEPSEEK_API_KEY |
| 返回 404 | base_url 配置错误 | 确认使用 https://api.deepseek.com |
| 前端流式内容乱码 | 编码解码不一致 | 后端返回 UTF-8,前端用 TextDecoder('utf-8') |
| 浏览器报 CORS 错误 | 后端未允许前端域名 | 检查 CORSMiddleware 的 allow_origins |
| 容器内请求失败 | localhost 指向容器自身 | 使用 docker-compose 服务名 backend |
| npm install 慢 | 网络原因 | 配置 npmmirror 国内镜像 |
| 端口占用 | 8000 或 5173 被占用 | 修改 uvicorn 或 Vite 端口 |
| 历史记录为空 | SQLite 文件未创建 | 检查 backend 目录写权限 |
9.2 一个典型排查案例
我在联调时遇到过一个比较隐蔽的问题:本地前端能正常收到流式内容,但打包部署到 Docker 后,发送请求一直不返回。
排查顺序是这样的:
- 看后端日志,发现没有请求进来;
- 检查 Nginx 的 proxy_pass,确认指向
http://backend:8000; - 检查容器网络,发现 frontend 容器和 backend 容器不在同一网络;
- 重新执行
docker compose up,两个服务被 Compose 默认加入同一网络后恢复正常。
这个案例说明,容器化部署后调试的复杂度比本地高,一定要先确认网络连通性,再怀疑业务代码。
9.3 排查通用流程
当你遇到问题时,我建议按下面的顺序排查:
- 先看后端终端日志,确认请求是否到达后端;
- 再看浏览器开发者工具 Network 面板,确认响应状态码;
- 如果是接口报错,把错误信息完整记录下来;
- 如果是前端渲染问题,检查 message 数据流;
- 最后才考虑是不是代码逻辑问题。
遵循这个流程,大多数问题都能快速定位。
10. 最佳实践与工程建议
10.1 API Key 的安全边界
这是大模型应用最重要的安全边界。API Key 一旦泄漏,别人可以使用你的账户调用模型,产生费用。
安全建议总结为三点:
- API Key 始终由后端读取,前端永远不接触真实密钥;
- 不要在 GitHub、博客、工单中贴出 .env 文件内容;
- 部署到服务器时,建议将密钥配置在 Docker 容器的环境变量或密钥管理服务中,而不是写入镜像。
如果你要把项目部署到公网,强烈建议再加一层访问鉴权。最简单的方案是在后端加一个 token 校验,前端登录后拿到 token 再调用接口,避免工作台直接暴露在公网。
10.2 配置管理与日志
调试大模型接口时,日志至关重要。至少要在两个位置打日志:
- 请求进来时,记录模型、参数、消息条数;
- 流式结束或异常时,记录最终错误信息。
但要注意,日志中不要完整打印 messages 和 response 的全文。Prompt 和模型输出可能包含敏感信息,建议只记录前几十个字符或统计长度。
10.3 调用失败时的兜底策略
大模型接口并不保证 100% 成功,网络超时、限流、内容安全过滤都可能发生。我在后端做了最简单的异常捕获,把错误信息以文本形式追加到流式响应中,这样前端能直接展示错误,而不是整个请求失败。
更成熟的方案可以做指数退避重试:
import time from openai import OpenAI def chat_with_retry(client, **kwargs, max_retries=3): for attempt in range(max_retries): try: return client.chat.completions.create(**kwargs) except Exception as exc: if attempt == max_retries - 1: raise exc time.sleep(2 ** attempt)但我建议只对网络类错误重试,业务类错误如 401、400 直接抛给前端处理即可,盲目重试反而会放大问题。
10.4 性能与成本优化
流式输出本身不做完整 buffering,所以响应速度很快。但如果你不开启流式,遇到长文本生成,用户等待时间会非常长,这是很多小白容易忽略的点。
成本优化的常见手段包括:
- 在 Prompt 模板中明确限制输出长度;
- 合理设置 max_tokens,不设得过大;
- 调试阶段使用 deepseek-chat 而非 deepseek-reasoner,推理模型成本通常更高;
- 历史记录定期清理,避免 SQLite 文件无限增长。
10.5 代码可维护性建议
一周内赶出来的项目,最怕后面想扩展时无从下手。我的经验是:即便时间紧,也要把路由、服务层、模型层分开,至少要做到“前端不直接拼 API URL”“后端不把核心逻辑写在入口文件”。这个习惯能省下未来大量的重构时间。
11. 总结与后续规划
这个项目最核心的收获有四个:FastAPI + StreamingResponse 实现流式接口、前端 fetch + ReadableStream 实现打字机效果、OpenAI SDK 封装 DeepSeek API、docker-compose 完成前后端容器化部署。一周时间完成最小闭环完全可行,关键是把精力集中在模型调用和流式渲染两件最核心的事情上,不要被周边工具分散注意力。
我计划给工作台继续叠加几个功能:支持多组参数自动对比、增加批量 Prompt 测试集导入、把调试记录导出为 Markdown 报告、接入更多兼容 OpenAI 协议的模型。如果你也在做类似工作台,建议先把文中的最小闭环跑通,再根据自己的实际场景逐步扩展。项目开源后,最需要注意的仍然是 API Key 安全和 .env 文件管理,这两个坑踩一次就能记住一辈子。