如何在本地搭建一个类似 ChatGPT 的聊天系统(完整实战)

如何在本地搭建一个类似 ChatGPT 的聊天系统(完整实战)

背景

去年底我给公司内部搭了个 AI 辅助写周报的工具,用的是 OpenAI API —— 结果某天下午突然全量超时,排查发现是代理链路被封,而审批采购新 API Key 又卡了 5 个工作日。那一刻我下定决心:必须把核心推理能力握在自己手里。

我的需求很具体:不是跑个 demo 看着酷,而是每天稳定服务 30+ 同事,支持 5 轮上下文、中文优先、首 token 延迟 ≤800ms,且不能动不动 OOM。试过 Ollama(太慢)、Text Generation WebUI(显存吃满后崩三次)、Llama.cpp(Qwen2 的 tokenizer 兼容性差)。最终在 vLLM 0.6.3 + Qwen2-7B-Instruct 组合上稳住了——这篇就是我把整个流程拧干水分、删掉所有‘理论上可行’步骤后的实战手记。

五步落地:从裸机到可对话的 Web 界面

我用的环境:Ubuntu 24.04 LTS、NVIDIA driver 535.183.01、CUDA 12.4、Python 3.10.12。服务器是单张 RTX 4090(24GB),不接 NVLink,纯 PCIe 4.0 x16。

第 1 步:装好基础依赖(别跳!vLLM 对 CUDA 版本极其敏感)

sudo apt update && sudo apt install -y build-essential python3-dev python3-venv git
wget https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda_12.4.1_535.104.05_linux.run
sudo sh cuda_12.4.1_535.104.05_linux.run --silent --override --toolkit --no-opengl-libs
export PATH=/usr/local/cuda-12.4/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH

第 2 步:创建隔离环境并安装 vLLM(关键:指定 CUDA 构建)

python3 -m venv vllm-env
source vllm-env/bin/activate
pip install --upgrade pip wheel setuptools
pip install vllm==0.6.3 --no-cache-dir

第 3 步:拉取模型并启动 vLLM API 服务(注意:Qwen2 需加 --enforce-eager)

huggingface-cli download --resume-download Qwen/Qwen2-7B-Instruct --local-dir ./qwen2-7b-instruct
vllm serve Qwen/Qwen2-7B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --tensor-parallel-size 1 \
  --gpu-memory-utilization 0.92 \
  --enforce-eager \
  --max-model-len 4096 \
  --trust-remote-code

⚠️ 不加 --enforce-eager 会报错:RuntimeError: 'qwen2' is not supported for flash attention —— 这是 vLLM 0.6.3 对 Qwen2 的已知限制,绕不开。

第 4 步:写一个极简 FastAPI 接口(api.py

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx

app = FastAPI()
class ChatRequest(BaseModel):
    messages: list
    temperature: float = 0.7

@app.post("/chat")
async def chat(req: ChatRequest):
    async with httpx.AsyncClient() as client:
        try:
            r = await client.post(
                "http://localhost:8000/v1/chat/completions",
                json={
                    "model": "Qwen/Qwen2-7B-Instruct",
                    "messages": req.messages,
                    "temperature": req.temperature,
                    "max_tokens": 2048,
                    "stream": False
                },
                timeout=60.0
            )
            if r.status_code != 200:
                raise HTTPException(status_code=r.status_code, detail=r.text)
            return r.json()
        except Exception as e:
            raise HTTPException(status_code=500, detail=str(e))

启动它:uvicorn api:app --host 0.0.0.0 --port 8001 --reload

第 5 步:Gradio 前端(ui.py),重点解决 Qwen2 的 chat template 兼容

import gradio as gr
import httpx

def predict(message, history):
    # Qwen2 要求 messages 格式严格为 [{"role":"user","content":"xxx"}, ...]
    messages = []
    for h in history:
        messages.append({"role": "user", "content": h[0]})
        messages.append({"role": "assistant", "content": h[1]})
    messages.append({"role": "user", "content": message})

    r = httpx.post("http://localhost:8001/chat", json={"messages": messages})
    resp = r.json()
    return resp["choices"][0]["message"]["content"]

gr.ChatInterface(predict, title="本地 Qwen2 助手").launch(server_name="0.0.0.0", server_port=7860)

运行:python ui.py —— 访问 http://你的IP:7860 即可开聊。

实测记录

我用同一台机器、同一轮 prompt(“请用 30 字总结量子纠缠”)连续跑了 10 次,用 time curl -s -X POST http://localhost:8001/chat -H "Content-Type: application/json" -d '{"messages":[{"role":"user","content":"请用 30 字总结量子纠缠"}]}' | jq '.usage' 抓耗时:

  • 首 token 延迟(time_to_first_token):平均 682ms(范围 611–743ms)
  • 总响应时间(total_time):平均 1.42s(含网络+JSON 解析)
  • 显存占用(nvidia-smi):19.2GB / 24GB,稳定无抖动
  • 并发测试:用 ab -n 20 -c 5 http://localhost:7860/ 模拟轻度并发,全部成功,无超时

真实对话体验:输入“写一封辞职信,语气诚恳但坚定”,2 秒内返回结构完整、带分段和空行的文本;追问“第二段加一句感谢培养”,能准确续写上下文——证明 KV Cache 和 session 管理正常。不过遇到长代码块(如 Python 函数)时,输出偶尔截断,这是 --max-model-len 4096 的硬限制,调到 8192 会爆显存,我选择接受这个 trade-off。

踩坑备忘

  • 模型路径权限问题:vLLM 默认以当前用户身份读模型,如果 huggingface-cli download 是 root 执行的,普通用户启动会报 PermissionError: [Errno 13] Permission denied。解法:sudo chown -R $USER:$USER ./qwen2-7b-instruct
  • Gradio CORS 报错:浏览器控制台报 No 'Access-Control-Allow-Origin' header。不是前端问题,是 FastAPI 缺少中间件——在 api.py 开头加:from fastapi.middleware.cors import CORSMiddleware; app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
  • 中文乱码:vLLM 返回 JSON 中文是 Unicode 转义(如 \u4f60\u597d),Gradio 默认不解码。解法:在 ui.pyhttpx.post 后加 r.json().encode('utf-8').decode('unicode_escape')(但更推荐直接让 FastAPI 返回 utf-8 响应,见下条)
  • FastAPI 返回中文为乱码:默认 JSONResponse 不设 charset。修复:在 api.py 中,把 return r.json() 改成 from fastapi.responses import JSONResponse; return JSONResponse(content=r.json(), headers={"Content-Type": "application/json; charset=utf-8"})
  • RTX 4090 温度墙:连续请求 3 分钟后 GPU 温度冲到 83°C,风扇狂转。我在 /etc/modprobe.d/nvidia.conf 加了 options nvidia NVreg_InteractiveTimeout=0 并重启驱动,温度回落至 72°C 稳定。

我的结论

这套方案不是‘玩具’,而是我生产环境跑了 87 天的真实栈。它适合三类人:

  • 推荐给:有 NVIDIA GPU(≥24GB 显存)、需要离线可控、对中文理解要求高、且能接受 7B 级别推理能力的技术团队。比如内部知识库问答、客服话术生成、代码补全助手——这些场景里,Qwen2-7B 的中文语义准确率远超 Llama3-8B,且 vLLM 的吞吐比 Text Generation WebUI 高 3.2 倍(实测 12 vs 3.7 req/s)。
  • 谨慎尝试:只有 12GB 显存(如 3090)的朋友,建议换 Qwen2-1.5B-Instruct 或改用 llama.cpp + GGUF(但要忍受 token 生成慢 4 倍);或者加 --quantization awq(需重训量化权重,我试过 AWQ 后首 token 延迟升到 1.1s,不值)。
  • 不推荐:想直接对标 GPT-4 多模态或长文档分析的——7B 模型上限就摆在这,强行喂 100 页 PDF 会崩溃;也别指望它替代 Claude 3.5 Sonnet 做法律文书推理,那是算力和架构的代差。

最后说句实在话:本地 ChatGPT 不是技术炫技,而是把‘确定性’拿回来。当 API 服务商半夜升级接口、当合规审查要求数据不出域、当你需要调试模型输出的每一步 token 概率——这时候,你摸得到的 GPU 风扇声,就是最踏实的白噪音。