本文档总结了在 Arch Linux 系统上,从零开始搭建环境并成功部署 vLLM 推理服务的全过程。

一、 环境准备 (Level 2 前置任务)

在正式开始项目前,我们在 Arch Linux 主机上完成了所有必要的环境配置。

1. NVIDIA 驱动安全安装

为了确保稳定性和性能,我们采用了一套稳妥的方案来安装 NVIDIA 驱动。

  • 安装方式: 使用 nvidia-dkms 包,确保驱动在内核更新后能自动重新编译。
  • 关键配置:
    • Early KMS: 修改 /etc/mkinitcpio.conf 文件,在 MODULES 数组中添加 nvidia nvidia_modeset nvidia_uvm nvidia_drm,以防止启动时黑屏。
    • 内核参数: 修改 /etc/default/grub,在 GRUB_CMDLINE_LINUX_DEFAULT 中添加 nvidia_drm.modeset=1,以优化兼容性。
  • 成果: 驱动安装成功,nvidia-smi 命令可正确显示 RTX 4060 显卡信息。

2. Docker 环境搭建

  • 安装 Docker: 使用 sudo pacman -Syu docker 安装 Docker 引擎。

  • 配置服务: 使用 systemctl start dockersystemctl enable docker 启动并设置开机自启。

  • 用户组配置: 通过 sudo usermod -aG docker $USER 将当前用户添加到 docker 组,实现了免 sudo 执行 docker 命令。

ps:docker会被墙,需要使用国内源,这里推荐一个github项目DockerHub,这里面提供了最新的可用的国内镜像源。

3. NVIDIA Container Toolkit 安装

  • 安装: 使用 sudo pacman -S nvidia-container-toolkit 在 Arch Linux 上安装。

  • 验证: 成功运行了测试容器,证明 Docker 可以正确调用 GPU。

    Bash

    docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi

    该命令在容器内部成功打印出了 nvidia-smi 的信息,确认了主机驱动、Docker 和 Toolkit 之间的桥梁已搭建成功。

1758640771-5f72c9db6f138cc33eb344ce877db07d

二、 Level 1: 运行第一个 Web 服务

此阶段的目标是掌握 Docker 的基本工作流程。

1. app.py (Python Flask 应用)

一个简单的 Python Flask 应用。

from flask import Flask, jsonify
import platform

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify({
        "message": "恭喜!你的第114514个Docker容器正在运行!",
        "platform": platform.platform(),
        "python_version": platform.python_version()
    })

@app.route('/health')
def health():
    return jsonify({"status": "healthy"})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

2. requirements.txt (Python 依赖文件)

包含 Flask 依赖。

Flask

3. Dockerfile (容器构建指令)

定义了如何构建 Python 应用镜像的指令集。

# 基础镜像 - 就像选择操作系统
FROM python:3.10-slim

# 设置工作目录 - 就像进入特定文件夹
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装依赖 - 就像安装软件
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY app.py .

# 暴露端口 - 告诉Docker哪个端口会被使用
EXPOSE 5000

# 启动命令 - 容器启动时执行
CMD ["python", "app.py"]
  • 核心命令:

    Bash

    # 1. 构建镜像 (我们将镜像命名为 first)
    docker build -t first .
    
    # 2. 运行容器 (将主机的5000端口映射到容器的5000端口)
    docker run --rm -p 5000:5000 first
  • 成果: 成功通过浏览器访问 http://localhost:5000,并看到了应用返回的 JSON 祝贺信息。

1758640786-41e3fbaacb7e41bcef5a156e403011f9

Level 2: 搭建 GPU 加速的 Docker 环境

目标

此阶段的核心目标是让 Docker 容器能够调用你主机(Arch Linux)的 NVIDIA GPU 强大算力。这是所有后续 AI 应用容器化部署的绝对基础。

可以把它想象成给你的 Docker“集装箱”安装一个特殊的接口,让它能够连接到电脑的“涡轮增压引擎”(GPU)。

第一步:安装核心组件 - NVIDIA Container Toolkit

要实现 Docker 和 GPU 之间的通信,我们需要 NVIDIA 官方提供的一个关键工具:NVIDIA Container Toolkit。

  1. 在 Arch Linux 上安装 Toolkit 这个工具包已经被收录在 Arch 的官方仓库中,安装非常简单。

    sudo pacman -S nvidia-container-toolkit
  2. 重启 Docker 服务 安装完成后,必须重启 Docker 服务,这样 Docker 才能识别到新安装的 Toolkit 并加载它的配置。

    sudo systemctl restart docker

第二步:基础验证 - 在容器内运行 nvidia-smi

这是最直接、最快速的验证方法,用来确认 Toolkit 是否安装成功。我们将启动一个包含 CUDA 环境的官方容器,并在容器内部尝试运行 nvidia-smi 命令。

  1. 运行测试容器

    docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

    命令解释:

    • --rm: 容器运行结束后自动删除,保持系统干净。
    • --gpus all: 这是最关键的参数,它告诉 Docker 把主机上所有可用的 NVIDIA GPU 都授权给这个容器使用。
  2. 预期成果 如果一切配置正确,你会在终端看到和你在自己电脑上直接运行 nvidia-smi一模一样的显卡信息表格。这证明 Docker 已经成功地“看”到了你的 GPU。

第三步:进阶验证 - 在容器内用 PyTorch 检测 GPU

nvidia-smi 只能证明底层驱动是通的。为了确保 AI 框架也能正常使用 GPU,我们可以创建一个包含 PyTorch 的环境来做一次最终测试。

  1. 创建 Python 测试脚本 (gpu_test.py) 在你的项目根目录 ~/hunyuan-project 下,创建一个名为 gpu_test.py 的文件。

    cd ~/hunyuan-project
    nano gpu_test.py

    将以下代码粘贴进去:

    import torch
    
    print("--- PyTorch CUDA 检测 ---")
    if torch.cuda.is_available():
        print("✅ 太棒了!PyTorch 成功检测到 CUDA!")
        print(f"CUDA 设备数量: {torch.cuda.device_count()}")
        gpu_index = 0
        print(f"当前 GPU 索引: {gpu_index}")
        print(f"当前 GPU 名称: {torch.cuda.get_device_name(gpu_index)}")
        total_mem = torch.cuda.get_device_properties(gpu_index).total_memory / 1e9
        print(f"显存总量: {total_mem:.2f} GB")
    else:
        print("❌ 糟糕... PyTorch 未能检测到 CUDA 设备。")
    print("-------------------------")
  2. 为 PyTorch 测试创建专用 Dockerfile 为了避免污染我们之前的 vllm-service 镜像,我们创建一个新的 Dockerfile,专门用于这个测试。我们把它命名为 Dockerfile.gpu-test

    nano Dockerfile.gpu-test

    将以下内容粘贴进去:

    # 直接使用 PyTorch 官方提供的、内置 CUDA 和 CUDNN 的镜像,这是最方便可靠的方式
    FROM pytorch/pytorch:2.3.1-cuda12.1-cudnn8-runtime
    
    # 设置工作目录
    WORKDIR /app
    
    # 将我们的测试脚本复制进去
    COPY gpu_test.py .
    
    # 设置容器启动时要执行的命令
    CMD ["python", "gpu_test.py"]
  3. 构建并运行 PyTorch 测试镜像

    # 使用 -f 参数指定我们刚刚创建的 Dockerfile 文件名来构建镜像
    docker build -t gpu-test-app -f Dockerfile.gpu-test .
    
    # 运行这个测试容器,同样需要 --gpus all 参数
    docker run --rm --gpus all gpu-test-app
  4. 预期成果 运行成功后,你会在终端看到类似下面的输出:

    --- PyTorch CUDA 检测 ---
    ✅ 太棒了!PyTorch 成功检测到 CUDA!
    CUDA 设备数量: 1
    当前 GPU 索引: 0
    当前 GPU 名称: NVIDIA GeForce RTX 4060 Laptop GPU
    显存总量: 7.99 GB
    -------------------------

三、 Level 3: 部署 vLLM 推理服务

此阶段是项目的核心,目标是构建并运行一个功能强大的 vLLM 服务容器。这个过程遇到了多次依赖和环境不兼容问题,最终通过一个稳健的 Dockerfile 方案得以解决。

Dockerfile

# 使用一个非常新的 CUDA 12.4.1 基础镜像,以获得最好的兼容性
FROM nvidia/cuda:12.4.1-base-ubuntu22.04

# 设置环境变量,避免 apt-get 在构建时弹出交互窗口
ENV DEBIAN_FRONTEND=noninteractive

# 直接下载并从源码编译安装 Python 3.12,以绕过 PPA 网络问题
RUN apt-get update && \
    apt-get install -y wget build-essential libssl-dev zlib1g-dev libncurses5-dev \
    libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev libdb5.3-dev \
    libbz2-dev libexpat1-dev liblzma-dev tk-dev libffi-dev git && \
    wget https://www.python.org/ftp/python/3.12.4/Python-3.12.4.tgz && \
    tar -xf Python-3.12.4.tgz && \
    cd Python-3.12.4 && \
    ./configure --enable-optimizations && \
    make -j $(nproc) && \
    make altinstall && \
    cd .. && \
    rm -rf Python-3.12.4.tgz Python-3.12.4 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 为 python3 和 pip3 创建软链接,指向我们新安装的 3.12 版本
RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 && \
    ln -s /usr/local/bin/pip3.12 /usr/local/bin/pip3

# 安装一组已知兼容的最新核心库
RUN pip3 install \
    "torch==2.4.0" \
    "vllm==0.5.3.post1" \
    "transformers==4.42.4" \
    "outlines==0.0.34"

# 设置工作目录和模型缓存目录
WORKDIR /app
RUN mkdir -p /models
ENV TRANSFORMERS_CACHE=/models
EXPOSE 8000

# 启动 vLLM 服务,使用小的测试模型
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
     "--model", "facebook/opt-125m", \
     "--host", "0.0.0.0", \
     "--port", "8000"]

核心命令与成果

  • 构建与运行:

    Bash

    # 构建 vLLM 服务镜像
    docker build -t vllm-service .
    
    # 在后台以 GPU 模式启动容器,并挂载模型存储卷
    docker run --gpus all \
      -p 8000:8000 \
      -v $(pwd)/models:/models \
      --name vllm-server \
      -d \
      vllm-service
  • 验证:

    • 通过 docker logs vllm-server 确认服务成功启动。
    • 编写并运行 test_api.py 脚本,成功向 http://localhost:8000 发送请求并收到了由 facebook/opt-125m 模型生成的文本。

1758640793-1fb975c9fde535b179f977a4b913091c

项目实践记录:Level 4 - 大模型部署总结

1. 核心思路

由于本蒟蒻的 RTX 4060 显卡有 8GB 显存,所以选择了一个很小、适合个人电脑部署的 Qwen2-0.5B-Instruct 模型。

  • 模型选择Qwen2-0.5B-Instruct 是一个参数量仅为 5 亿的模型,它对硬件资源的需求极低,能够稳定、快速地在设备上运行。
  • 部署工具:继续使用 DockervLLM。通过精简的启动命令,我们避免了复杂的显存参数调试,让部署过程变得更加简单和可靠。

2. 部署详细步骤

这是一个从零开始部署这个新模型的完整指南。

第1步:下载 Qwen2 0.5B 模型

首先,停止你当前正在运行的 Docker 容器,然后下载新的模型。这个过程比之前快得多,也更稳定。

Bash

# 克隆 Qwen2 0.5B 模型
git clone https://huggingface.co/Qwen/Qwen2-0.5B-Instruct

第2步:启动 vLLM 服务

新模型的启动命令非常简洁。你不再需要 --quantization--max-model-len--gpu-memory-utilization 等复杂参数。

Bash

docker run --gpus all --rm \
    -p 8000:8000 \
    -v $(pwd)/Qwen2-0.5B-Instruct:/models \
    --name vllm-server \
    vllm/vllm-openai \
    --model /models \
    --served-model-name qwen2-0.5b-instruct

第3步:验证服务与模型

服务启动后,打开一个新的终端,使用 curl 命令验证 API 接口。

  • 验证服务就绪

    Bash

    curl http://localhost:8000/v1/models

    你应该会看到一个 JSON 响应,其中包含模型 ID qwen2-0.5b-instruct

  • 测试翻译功能:

    使用对话格式向模型发送一个翻译请求。

    Bash

    curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
      "model": "qwen2-0.5b-instruct",
      "messages": [
          {
              "role": "system",
              "content": "You are a professional translator. Translate all user text into English."
          },
          {
              "role": "user",
              "content": "你好,世界"
          }
      ],
      "max_tokens": 50
    }'

    如果一切正常,模型将返回正确的翻译结果,证明你已经成功部署了服务。


1758640792-1afa7e393fcb6cd4a5536ff51e6024fb

项目进阶教程 (Level 5-7)

我们已经成功部署了 vLLM 后端服务。接下来的目标是将这个强大的 AI 能力,封装成一个用户可以直观交互的全功能 Web 应用。

准备工作:最终的项目结构

为了让项目清晰有序,我们先规划好最终的目录结构。请在你的 hunyuan-project 根目录下,组织或创建如下结构:

hunyuan-project/
├── backend/
│   ├── app.py         # 我们的 Flask 后端(API 网关 + 前端服务)
│   └── Dockerfile     # 用于构建 Flask 后端的 Dockerfile
├── frontend/
│   └── index.html     # 用户看到的网页前端
├── models/
│   └── ...            # 你下载的模型文件
└── docker-compose.yml # 编排所有服务的“总指挥”

(请注意,我们将把 Flask 后端的代码放在 backend 目录,前端页面放在 frontend 目录)


Level 5 & 6: 构建前后端应用

在这个阶段,我们将同时完成 Level 5 (创建后端) 和 Level 6 (创建前端),因为它们是紧密协作的。

第一部分:创建后端服务 (backend/app.py)

我们的后端是一个 Python Flask 应用。它有两个核心职责:

  1. 作为一个 API 网关,接收前端的翻译请求,转发给 vLLM 服务,并把结果流式返回。
  2. 作为一个 Web 服务器,向用户的浏览器提供前端的 index.html 页面。

1. 创建 backend/app.py 文件

Bash

# 确保在项目根目录
cd ~/hunyuan-project
# 创建 backend 目录
mkdir -p backend
# 创建并编辑 app.py
nano backend/app.py

2. 编写 app.py 完整代码

请将以下完整代码复制到 app.py 文件中。代码的详细解释在注释里。

Python

# 导入所有需要的库
from flask import Flask, request, jsonify, Response, send_from_directory
from flask_cors import CORS  # 用于处理跨域请求
import requests  # 用于向 vLLM 服务发送 HTTP 请求
import json
import os

# 初始化 Flask 应用
# static_folder='../frontend' 告诉 Flask 去上一级的 'frontend' 目录寻找静态文件 (index.html)
app = Flask(__name__, static_folder='../frontend')
CORS(app)  # 启用CORS,允许前端页面访问此后端

# --- vLLM 服务配置 ---
# vLLM 服务的地址。'vllm-backend' 是我们在 docker-compose.yml 中定义的服务名。
# 因为在同一个 Docker 网络中,服务之间可以通过名字直接通信。
VLLM_API_URL = "http://vllm-backend:8000/v1/chat/completions"
# 我们将要提供服务的模型名称
MODEL_NAME = "qwen2-0.5b-instruct"

# --- 路由定义 ---

# 根路径路由 ('/'):当用户访问网站根目录时,返回前端的 index.html 文件
@app.route('/')
def serve_frontend():
    return send_from_directory(app.static_folder, 'index.html')

# 翻译 API 路由 ('/translate'):处理前端发来的翻译请求
@app.route('/translate', methods=['POST'])
def translate():
    try:
        # 1. 从前端请求中获取 JSON 数据
        data = request.json
        user_text = data.get('text')
        source_lang = data.get('source_lang', 'Chinese')
        target_lang = data.get('target_lang', 'English')

        if not user_text:
            return jsonify({"detail": "No text provided"}), 400

        # 2. 构建发送给 vLLM 的请求体 (payload),遵循 OpenAI API 格式
        vllm_payload = {
            "model": MODEL_NAME,
            "messages": [
                {
                    "role": "system",
                    "content": "You are a helpful translation assistant."
                },
                {
                    "role": "user",
                    "content": f"Translate the following text from {source_lang} to {target_lang}. Provide only the translated text, without any additional explanations.\n\nText to translate: {user_text}"
                }
            ],
            "max_tokens": 150,
            "stream": True  # 开启流式传输,这是实现打字机效果的关键
        }

        # 3. 向 vLLM 服务发送请求,并设置为流式接收
        vllm_response = requests.post(VLLM_API_URL, json=vllm_payload, stream=True)
        vllm_response.raise_for_status()  # 如果 vLLM 返回错误,这里会抛出异常

        # 4. 定义一个生成器函数,逐块处理并返回流式数据
        def generate():
            for chunk in vllm_response.iter_lines(decode_unicode=True):
                if chunk:
                    # 按照 Server-Sent Events (SSE) 格式返回给前端
                    yield f"{chunk}\n\n"

        # 5. 将生成器作为流式响应返回给前端
        return Response(generate(), mimetype='text/event-stream')

    except requests.exceptions.RequestException as e:
        return jsonify({"detail": f"连接 vLLM 服务失败: {e}"}), 500
    except Exception as e:
        return jsonify({"detail": f"发生意外错误: {e}"}), 500

# 应用启动入口
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

第二部分:创建前端界面 (frontend/index.html)

这个文件是用户在浏览器中直接看到的界面。它包含了页面的结构 (HTML)、样式 (CSS) 和交互逻辑 (JavaScript)。

1. 创建 frontend/index.html 文件

Bash

# 确保在项目根目录
cd ~/hunyuan-project
# 创建 frontend 目录
mkdir -p frontend
# 创建并编辑 index.html
nano frontend/index.html

2. 编写 index.html 完整代码

请将以下完整代码复制到 index.html 文件中。

HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>AI 翻译应用</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">

<style>
        /* CSS 样式部分:负责页面的美化 */
        :root {
            --primary-color: #007bff;
            --primary-hover-color: #0056b3;
            --background-start: #e0f7fa;
            --background-end: #b2ebf2;
            --card-background: rgba(255, 255, 255, 0.7);
            --text-color: #34495e;
            --border-color: rgba(0, 0, 0, 0.1);
            --shadow-color: rgba(0, 0, 0, 0.1);
        }
        body {
            font-family: 'Noto Sans SC', sans-serif;
            margin: 0;
            padding: 40px 20px;
            background: linear-gradient(135deg, var(--background-start), var(--background-end));
            color: var(--text-color);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            box-sizing: border-box;
        }
        .container {
            width: 100%;
            max-width: 700px;
            padding: 30px;
            background: var(--card-background);
            backdrop-filter: blur(10px);
            box-shadow: 0 8px 32px 0 var(--shadow-color);
            border-radius: 15px;
            border: 1px solid rgba(255, 255, 255, 0.18);
        }
        h1 { text-align: center; font-weight: 700; margin-bottom: 30px; }
        .language-selector { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 10px; }
        .language-selector select { flex-grow: 1; padding: 12px; border-radius: 8px; border: 1px solid var(--border-color); font-size: 16px; }
        #swapButton { padding: 10px; border: 1px solid var(--border-color); border-radius: 50%; cursor: pointer; width: 44px; height: 44px; display: flex; justify-content: center; align-items: center; transition: transform 0.3s ease; }
        #swapButton:hover { transform: rotate(180deg); }
        textarea { width: 100%; box-sizing: border-box; padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); font-size: 16px; min-height: 150px; resize: vertical; margin-bottom: 20px; }
        textarea:focus { outline: none; border-color: var(--primary-color); }
        button { width: 100%; padding: 15px; font-size: 18px; font-weight: 700; color: white; background: var(--primary-color); border: none; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; }
        button:hover { background: var(--primary-hover-color); }
        button:disabled { background-color: #95a5a6; cursor: not-allowed; }
        .result-box { min-height: 150px; padding: 15px; background-color: rgba(255, 255, 255, 0.5); border-radius: 8px; border: 1px solid var(--border-color); margin-top: 20px; white-space: pre-wrap; word-wrap: break-word; font-size: 16px; }
    </style>
</head>
<body>

<div class="container">

<h1>AI 翻译应用</h1>
    <div class="language-selector">
        <select id="sourceLang">
            <option value="Chinese">中文 (Chinese)</option>
            <option value="English">英文 (English)</option>
            <option value="German">德语 (German)</option>
            <option value="Japanese">日语 (Japanese)</option>
            <option value="French">法语 (French)</option>
        </select>
        <button id="swapButton" title="交换语言">
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline></svg>
        </button>
        <select id="targetLang">
            <option value="English">英文 (English)</option>
            <option value="Chinese">中文 (Chinese)</option>
            <option value="German">德语 (German)</option>
            <option value="Japanese">日语 (Japanese)</option>
            <option value="French">法语 (French)</option>
        </select>
    </div>
    <textarea id="inputText" rows="6" placeholder="在此输入要翻译的文本..."></textarea>
    <button id="translateButton">翻译</button>
    <div class="result-box" id="resultText">翻译结果将显示在这里。</div>
</div>

<script>
    // 1. 获取所有需要操作的 HTML 元素
    const sourceLangSelect = document.getElementById('sourceLang');
    const targetLangSelect = document.getElementById('targetLang');
    const swapButton = document.getElementById('swapButton');
    const translateButton = document.getElementById('translateButton');
    const inputTextElement = document.getElementById('inputText');
    const resultTextElement = document.getElementById('resultText');

    // 2. 实现“交换语言”功能
    swapButton.addEventListener('click', () => {
        const temp = sourceLangSelect.value;
        sourceLangSelect.value = targetLangSelect.value;
        targetLangSelect.value = temp;
    });

    // 3. 实现核心的“翻译”功能
    translateButton.addEventListener('click', async () => {
        const inputText = inputTextElement.value;
        if (!inputText.trim()) {
            resultTextElement.innerText = "请输入要翻译的文本。";
            return;
        }

        // 提供即时反馈:禁用按钮并更新文本
        resultTextElement.innerText = "";
        translateButton.disabled = true;
        translateButton.innerText = "翻译中...";

        try {
            // 使用 fetch API 向我们的 Flask 后端发送请求
            const response = await fetch('/translate', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    text: inputText,
                    source_lang: sourceLangSelect.value,
                    target_lang: targetLangSelect.value
                })
            });

            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.detail || '未知服务器错误');
            }

            // 处理流式响应
            const reader = response.body.getReader();
            const decoder = new TextDecoder('utf-8');

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                const chunk = decoder.decode(value, { stream: true });
                const lines = chunk.split('\n');

                // 解析 SSE 数据块
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const jsonData = line.substring(6);
                        if (jsonData === '[DONE]') break;
                        try {
                            const parsed = JSON.parse(jsonData);
                            const content = parsed.choices[0].delta.content;
                            if (content) {
                                // 将内容实时追加到结果区,实现打字机效果
                                resultTextElement.innerText += content;
                            }
                        } catch (e) {
                            // 忽略偶尔的解析错误
                        }
                    }
                }
            }
        } catch (error) {
            resultTextElement.innerText = `请求失败: ${error}`;
        } finally {
            // 无论成功或失败,最后都恢复按钮状态
            translateButton.disabled = false;
            translateButton.innerText = "翻译";
        }
    });
</script>

</body>
</html>

Level 7: 使用 Docker Compose 编排

现在我们有了两个服务(vLLM 和我们的前后端应用),我们需要一个工具来同时管理它们。这就是 docker-compose 的作用。

  1. 创建 docker-compose.yml 文件

在你项目的根目录 ~/hunyuan-project 下创建这个文件。

2. 编写 docker-compose.yml 完整代码

请将以下完整代码复制到 docker-compose.yml 文件中。

YAML

services:
  # 服务一:vLLM 模型后端
  vllm-backend:
    container_name: vllm-server
    # 使用 vLLM 官方提供的、内置 OpenAI 兼容接口的镜像
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    volumes:
      # 将我们本地的 Qwen2 模型文件夹,挂载到容器内部的 /models 目录
      - ./models/Qwen2-0.5B-Instruct:/models
    # 覆盖镜像的默认启动命令,让它加载我们指定的模型
    command:
      - "--model"
      - "/models"
      - "--served-model-name"
      - "qwen2-0.5b-instruct"
      - "--host"
      - "0.0.0.0"
    # 这是在 Docker Compose v2 中为容器分配 GPU 资源的推荐方式
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - custom-network

  # 服务二:我们的 Flask 前后端应用
  frontend:
    container_name: web-frontend
    # build 指令告诉 Docker Compose 如何构建这个服务的镜像
    build:
      context: ./backend  # 使用 'backend' 目录作为构建上下文
      dockerfile: Dockerfile # 指定该目录下的 Dockerfile
    ports:
      - "5000:5000"
    # depends_on 确保 vllm-backend 服务先启动,再启动本服务
    depends_on:
      - vllm-backend
    networks:
      - custom-network

# 定义一个自定义桥接网络,让两个服务可以互相通信
networks:
  custom-network:
    driver: bridge

3. 一键启动整个应用

现在,在 hunyuan-project 根目录下,运行最终的命令:

Bash

docker-compose up --build

等待所有服务启动成功后,打开浏览器,访问 http://localhost:5000,你就能看到并使用你亲手打造的全栈 AI 翻译应用了!

1758640795-6a1e9fba48337ef15e2ba74150eb71f7
1758640800-d710292ff7e1c9ad60037a15e42888c1

恭喜🎉你完成了全部内容🎉