日期:2026-07-20
目标:在学校 GPU 服务器上完成本地 ASR 与小参数 LLM 的部署验证,并让 Windows 业务后端通过可配置 URL 调用。后续到甲方服务器时,只替换模型服务地址、Token 和必要的模型别名,不再大改业务代码。
一、项目背景
现有系统是一套 AI 语音助手,主要流程如下:
浏览器录音
→ Windows FastAPI 后端
→ 语音识别 ASR
→ LLM 判断意图
→ 调用业务 API/工具
→ LLM 总结结果
→ 前端显示回答
原始实现中:
- 实时语音识别使用阿里云 DashScope Fun-ASR;
- 中间 LLM 使用云端 OpenAI 兼容 API;
- Windows FastAPI 后端核心接口为:
WebSocket /ws/asrPOST /api/chat/queryGET /health- 为了方便甲方私有化部署,希望改成:
- ASR 可替换;
- LLM 可替换;
- 业务后端只读取 URL、Token 和模型名;
- 学校测试环境与甲方生产环境使用同一套协议。
二、最终目标架构
本次确定的目标架构为:
浏览器
│
├─ WebSocket /ws/asr
│ ↓
│ Windows voice_api.py
│ ↓
│ ASR_REMOTE_WS_URL
│ ↓
│ Qwen3-ASR 服务
│
└─ POST /api/chat/query
↓
Windows voice_api.py
↓
LLM_BASE_URL
↓
OpenAI 兼容 LLM 服务
↓
工具调用 / API 查询
↓
总结回答
配置统一放在 .env:
ASR_REMOTE_WS_URL=ws://127.0.0.1:18095/v1/asr/stream
ASR_API_TOKEN=<ASR_TOKEN>
LLM_BASE_URL=http://127.0.0.1:18096/v1
LLM_API_KEY=<LLM_TOKEN>
LLM_MODEL=business-llm
LLM_TIMEOUT=60
以后甲方部署时,只需要改:
ASR_REMOTE_WS_URL=ws://<CUSTOMER_ASR_HOST>:10095/v1/asr/stream
LLM_BASE_URL=http://<CUSTOMER_LLM_HOST>:10096/v1
ASR_API_TOKEN=<CUSTOMER_ASR_TOKEN>
LLM_API_KEY=<CUSTOMER_LLM_TOKEN>
如果所有环境统一暴露模型名 business-llm,则 LLM_MODEL 也不需要修改。
三、服务器磁盘问题与目录规划
学校服务器根分区已经接近满载,数据盘 /app/hd/data01 仍有约 11 TB 空间。因此所有个人环境、模型、缓存和项目统一迁移到:
/app/hd/data01/username
目录规划:
/app/hd/data01/username/
├── conda_envs/
│ └── qwen3-asr/
├── conda_pkgs/
├── models/
│ ├── Qwen3-ASR-0.6B/
│ └── Qwen2.5-1.5B-Instruct-GPTQ-Int4/
├── projects/
│ ├── qwen-asr-service/
│ └── qwen-llm-service/
├── huggingface/
├── modelscope/
├── pip_cache/
├── tmp/
└── torch/
写入 ~/.bashrc:
export AI_DATA_ROOT="/app/hd/data01/username"
export TMPDIR="$AI_DATA_ROOT/tmp"
export PIP_CACHE_DIR="$AI_DATA_ROOT/pip_cache"
export HF_HOME="$AI_DATA_ROOT/huggingface"
export HUGGINGFACE_HUB_CACHE="$AI_DATA_ROOT/huggingface/hub"
export TRANSFORMERS_CACHE="$AI_DATA_ROOT/huggingface/transformers"
export MODELSCOPE_CACHE="$AI_DATA_ROOT/modelscope"
export TORCH_HOME="$AI_DATA_ROOT/torch"
执行:
source ~/.bashrc
注意事项
- 不要在根分区继续安装大型模型或 Python 环境;
- 不要执行
sudo pip install; - 不要对
/app/hd/data01整盘递归chown; - 只处理个人目录:
sudo chown -R username:username /app/hd/data01/username
四、VS Code Remote-SSH 项目目录切换
项目目录创建在数据盘:
mkdir -p /app/hd/data01/username/projects/qwen-asr-service
VS Code 中执行:
File → Open Folder...
选择:
/app/hd/data01/username/projects/qwen-asr-service
或者在远程终端执行:
code -r /app/hd/data01/username/projects/qwen-asr-service
建议结构:
qwen-asr-service/
├── asr_service.py
├── requirements.txt
├── .env
├── .env.example
├── .gitignore
├── logs/
└── scripts/
├── start_asr.sh
└── test_ws.py
五、Qwen3-ASR 部署
1. 环境与模型目录
Conda 环境:
/app/hd/data01/username/conda_envs/qwen3-asr
模型目录:
/app/hd/data01/username/models/Qwen3-ASR-0.6B
激活环境:
source ~/miniconda3/etc/profile.d/conda.sh
conda activate /app/hd/data01/username/conda_envs/qwen3-asr
2. GPU 选择
使用前检查:
nvidia-smi
nvidia-smi pmon -i <GPU_ID> -c 1
确认进程所属用户:
ps -o user,pid,etime,cmd -p <PID>
不要结束其他用户的任务。本次 ASR 最终使用物理 GPU 2。
注意:
CUDA_VISIBLE_DEVICES=2
后,程序日志里显示 cuda:0 是正常的:
物理 GPU 2 → 进程内逻辑 cuda:0
3. ASR 服务配置
.env 示例:
ASR_MODEL_PATH=/app/hd/data01/username/models/Qwen3-ASR-0.6B
ASR_GPU_MEMORY_UTILIZATION=0.60
ASR_CHUNK_SIZE_SEC=1.0
ASR_HOST=127.0.0.1
ASR_PORT=10095
CUDA_VISIBLE_DEVICES=2
ASR_API_TOKEN=<RANDOM_TOKEN>
PYTHONUNBUFFERED=1
Token 生成:
openssl rand -hex 32
设置权限:
chmod 600 .env
4. 启动脚本
scripts/start_asr.sh:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="/app/hd/data01/username/projects/qwen-asr-service"
CONDA_ENV="/app/hd/data01/username/conda_envs/qwen3-asr"
cd "$PROJECT_ROOT"
source "/home/username/miniconda3/etc/profile.d/conda.sh"
conda activate "$CONDA_ENV"
if [[ ! -f "$PROJECT_ROOT/.env" ]]; then
echo "错误:没有找到 $PROJECT_ROOT/.env"
exit 1
fi
set -a
source "$PROJECT_ROOT/.env"
set +a
mkdir -p "$PROJECT_ROOT/logs"
exec env \
CUDA_VISIBLE_DEVICES="$CUDA_VISIBLE_DEVICES" \
PYTHONUNBUFFERED="${PYTHONUNBUFFERED:-1}" \
python -m uvicorn asr_service:app \
--host "$ASR_HOST" \
--port "$ASR_PORT" \
--workers 1
赋权并启动:
chmod +x scripts/start_asr.sh
./scripts/start_asr.sh
5. 健康检查
curl -sS http://127.0.0.1:10095/health | python -m json.tool
成功结果:
{
"success": true,
"service": "qwen3-asr",
"version": "1.0.0",
"model_loaded": true,
"stream_endpoint": "/v1/asr/stream"
}
六、Windows 通过 SSH 隧道访问 ASR
远程服务监听:
127.0.0.1:10095
Windows 本地映射到:
127.0.0.1:18095
命令:
ssh -N `
-o ExitOnForwardFailure=yes `
-o ServerAliveInterval=30 `
-o ServerAliveCountMax=3 `
-L 127.0.0.1:18095:127.0.0.1:10095 `
username@<GPU_SERVER_IP>
测试:
Test-NetConnection 127.0.0.1 -Port 18095
curl.exe http://127.0.0.1:18095/health
遇到的问题:Could not resolve hostname app01
原因:
app01是服务器内部主机名;- Windows DNS 无法解析;
- 与项目目录无关。
解决:
- 使用真实 IP;
- 或使用 Windows SSH 配置中的真实
Host别名; - 不要把示例名
school-gpu当成真实地址。
七、Windows ASR 测试
环境变量:
$env:TEST_ASR_URL = "ws://127.0.0.1:18095/v1/asr/stream"
$env:ASR_API_TOKEN = "<ASR_TOKEN>"
测试 WAV:
python .\test_remote_asr.py .\audio\1.wav
成功输出:
READY: {"type":"session.ready", ...}
transcript.partial : ...
transcript.completed : ...
完整链路:
Windows WAV
→ SSH 隧道
→ Qwen3-ASR
→ partial
→ completed
遇到的问题:输出繁体字
示例:
查詢二零二六年七月十六日的生化進水氨氮
处理思路:
- 提示词要求输出简体中文;
- 服务端使用 OpenCC 进行繁转简;
- 术语识别错误与繁简问题分开处理。
例如:
淨水 → 净水
属于繁简转换;
净水 → 进水
属于 ASR 识别错误,需要上下文词表或更好的模型处理。
八、Windows voice_api.py 改造成远程 ASR 代理
原始 /ws/asr:
浏览器 PCM
→ DashScope Recognition
→ 阿里云 Fun-ASR
修改后:
浏览器 PCM
→ Windows /ws/asr
→ ASR_REMOTE_WS_URL
→ 远程 Qwen3-ASR
配置:
ASR_REMOTE_WS_URL=ws://127.0.0.1:18095/v1/asr/stream
ASR_API_TOKEN=<ASR_TOKEN>
WebSocket 参数:
ASR_REMOTE_OPEN_TIMEOUT = 10
ASR_REMOTE_PING_INTERVAL = 20
ASR_REMOTE_PING_TIMEOUT = 20
ASR_REMOTE_CLOSE_TIMEOUT = 5
单位均为秒:
- 连接建立超时 10 秒;
- 每 20 秒发送心跳;
- 心跳响应等待 20 秒;
- 关闭握手等待 5 秒。
这些不是一次语音识别的总时长限制。
九、业务核心接口
真正需要保留:
GET /health
WebSocket /ws/asr
POST /api/chat/query
/ws/asr
接收:
session.start
PCM16 二进制音频
session.stop
返回:
session.ready
transcript.partial
transcript.completed
error
/api/chat/query
负责:
识别文字
+ 页面上下文
+ 对话历史
→ Agent
→ 工具调用
→ 总结结果
→ answer_text
旧接口可在完整测试稳定后逐步删除:
/api/asr/transcribe
/api/tts/synthesize
/api/voice/query
/audio
十、小参数 LLM 选型与部署
由于学校服务器下载需要校园网流量,最终选择:
Qwen/Qwen2.5-1.5B-Instruct-GPTQ-Int4
模型目录约 1.1G,权重约 1.07 GiB:
/app/hd/data01/username/models/Qwen2.5-1.5B-Instruct-GPTQ-Int4
选型定位
- 下载量低;
- 可复用现有 vLLM 环境;
- 适合作为链路测试模型;
- 不适合作为最终生产模型。
不需要重新安装十几 GB 框架
直接复用:
/app/hd/data01/username/conda_envs/qwen3-asr
该环境已有:
PyTorch
CUDA 依赖
vLLM
Transformers
因此只新增模型权重,不再重复下载大型框架。
十一、模型下载问题
1. hf --dry-run 不支持
错误:
hf: error: unrecognized arguments: --dry-run
原因:服务器上的 Hugging Face CLI 版本较旧。
解决:去掉 --dry-run,不为了该参数升级整个环境,避免破坏已运行的 ASR。
2. Hugging Face 无法访问
错误:
OSError: [Errno 101] Network is unreachable
原因:学校服务器无法访问 huggingface.co:443。
解决路径:
优先 ModelScope
→ 若仍不可用
→ Windows 下载
→ SCP 上传
3. 缺少 quantize_config.json
检查脚本误判模型不完整。实际上 GPTQ 配置位于:
config.json → quantization_config
因此不需要单独的 quantize_config.json。
十二、vLLM LLM 服务部署
LLM 服务监听:
127.0.0.1:10096
Token 保存:
mkdir -p /app/hd/data01/username/projects/qwen-llm-service
openssl rand -hex 32 \
> /app/hd/data01/username/projects/qwen-llm-service/.llm_token
chmod 600 \
/app/hd/data01/username/projects/qwen-llm-service/.llm_token
载入 Token:
export LLM_API_TOKEN="$(
cat /app/hd/data01/username/projects/qwen-llm-service/.llm_token
)"
启动:
MODEL_DIR="/app/hd/data01/username/models/Qwen2.5-1.5B-Instruct-GPTQ-Int4"
CUDA_VISIBLE_DEVICES=<LLM_GPU_ID> \
vllm serve "$MODEL_DIR" \
--served-model-name business-llm \
--host 127.0.0.1 \
--port 10096 \
--max-model-len 4096 \
--gpu-memory-utilization 0.25 \
--api-key "$LLM_API_TOKEN" \
--enable-auto-tool-choice \
--tool-call-parser hermes
关键参数:
--served-model-name business-llm
无论实际部署什么模型,对外统一叫 business-llm。
问题:自动工具调用未启用
错误:
"auto" tool choice requires
--enable-auto-tool-choice
and
--tool-call-parser
解决:
--enable-auto-tool-choice
--tool-call-parser hermes
问题:模型别名不存在
错误:
The model `business-llm` does not exist.
原因:Windows 请求 business-llm,但 vLLM 暴露的是原始模型名。
推荐解决:重新启动 vLLM,并加入:
--served-model-name business-llm
十三、Windows 同时建立 ASR 与 LLM 隧道
服务器端口:
ASR:10095
LLM:10096
Windows 本地端口:
ASR:18095
LLM:18096
统一 SSH 命令:
ssh -N `
-o ExitOnForwardFailure=yes `
-o ServerAliveInterval=30 `
-o ServerAliveCountMax=3 `
-L 127.0.0.1:18095:127.0.0.1:10095 `
-L 127.0.0.1:18096:127.0.0.1:10096 `
username@<GPU_SERVER_IP>
映射关系:
Windows 18095 → 服务器 10095 → ASR
Windows 18096 → 服务器 10096 → LLM
测试:
Test-NetConnection 127.0.0.1 -Port 18095
Test-NetConnection 127.0.0.1 -Port 18096
十四、Windows LLM 配置
Windows 项目 .env:
LLM_BASE_URL=http://127.0.0.1:18096/v1
LLM_API_KEY=<LLM_TOKEN>
LLM_MODEL=business-llm
LLM_TIMEOUT=60
通用客户端:
def get_chat_llm_base_url() -> str:
base_url = (
os.environ.get("LLM_BASE_URL")
or ""
).strip()
if not base_url:
raise RuntimeError(
"LLM_BASE_URL is not configured."
)
return base_url.rstrip("/")
def get_chat_llm_api_key() -> str:
return (
os.environ.get("LLM_API_KEY")
or "local-llm"
).strip()
def get_chat_llm_model() -> str:
model = (
os.environ.get("LLM_MODEL")
or ""
).strip()
if not model:
raise RuntimeError(
"LLM_MODEL is not configured."
)
return model
def create_chat_llm_client() -> OpenAI:
return OpenAI(
api_key=get_chat_llm_api_key(),
base_url=get_chat_llm_base_url(),
timeout=float(
os.environ.get(
"LLM_TIMEOUT",
"60",
)
),
)
/api/chat/query 中使用:
model_client = create_chat_llm_client()
answer_text = await run_in_threadpool(
run_agent,
model_client,
user_text,
page_context,
get_chat_llm_model(),
)
十五、.env 没有读取的问题
错误:
RuntimeError: LLM_BASE_URL is not configured.
原因:
- Uvicorn 进程启动前没有设置环境变量;
.env没有主动加载;- PowerShell 中设置变量后没有重启 Uvicorn。
正确做法:
from pathlib import Path
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent
load_dotenv(
PROJECT_ROOT / ".env"
)
注意 import 必须在调用之前。
粘贴代码导致的错误
错误:
FiePROJECT_ROOT = Path(__file__).resolve().parentld,
正确:
from pydantic import BaseModel, Field
PROJECT_ROOT = Path(__file__).resolve().parent
语法检查:
python -m py_compile .\voice_api.py
配置检查:
python -c "
from voice_api import get_chat_llm_base_url, get_chat_llm_model
print(get_chat_llm_base_url())
print(get_chat_llm_model())
"
十六、Windows 中文乱码
现象:
answer_text = æ¨ç...
说明请求已经成功,只是 Windows PowerShell 5.1 解码 UTF-8 时出现乱码。
终端设置:
chcp 65001 > $null
$env:PYTHONUTF8 = "1"
$env:PYTHONIOENCODING = "utf-8"
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
$OutputEncoding = [System.Text.UTF8Encoding]::new()
发送 UTF-8 请求体:
$json = $payload | ConvertTo-Json -Depth 10
$utf8Body = [System.Text.Encoding]::UTF8.GetBytes(
$json
)
Invoke-RestMethod `
-Uri "http://127.0.0.1:8001/api/chat/query" `
-Method Post `
-ContentType "application/json; charset=utf-8" `
-Body $utf8Body
更稳定的方式是使用 Python 请求并显式 decode("utf-8")。
十七、小模型效果差的原因
实际前端测试中,模型出现:
- 用户已经提供完整查询条件,模型仍反复追问;
- 只会复述需求;
- 多轮理解较差;
- 工具参数容易丢失;
- “生化进水氨氮”缩成“氨氮”;
- 单日查询未补齐
end_date。
原因主要有两类。
1. 模型太小
当前模型:
Qwen2.5-1.5B-Instruct-GPTQ-Int4
定位应是:
链路测试模型
而不是最终生产模型。
2. 对话上下文可能未持续传回
后端的 append_conversation(...) 只更新本次响应中的 context,不会自动永久保存。
前端必须保存后端返回的 result.context,并在下一轮继续发送。不能每次都重新发送:
{
"conversation": []
}
十八、切回云端 LLM API
当前架构已经是 OpenAI 兼容客户端,因此从本地 vLLM 切回云端 API,只改 .env。
本地:
LLM_BASE_URL=http://127.0.0.1:18096/v1
LLM_API_KEY=<LOCAL_TOKEN>
LLM_MODEL=business-llm
云端示例:
LLM_BASE_URL=https://<WORKSPACE>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
LLM_API_KEY=<CLOUD_API_KEY>
LLM_MODEL=qwen-plus
LLM_TIMEOUT=60
切回云端后:
LLM SSH 隧道不再需要
ASR SSH 隧道继续保留
十九、甲方生产环境建议
学校测试模型只验证:
URL 可替换
OpenAI 协议可替换
SSH/内网链路可替换
模型别名可统一
甲方正式部署建议:
- 至少 4B~8B 级别指令模型;
- 优先选择工具调用稳定的模型;
- 生产环境统一
served-model-name=business-llm; - 业务后端只读取 URL、Token 和模型名;
- 生产模型不要直接裸露公网。
示例:
CUDA_VISIBLE_DEVICES=0 \
vllm serve /models/<BETTER_MODEL> \
--served-model-name business-llm \
--host 0.0.0.0 \
--port 10096 \
--max-model-len 8192 \
--gpu-memory-utilization 0.60 \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--api-key "$LLM_API_TOKEN"
网络建议:
业务后端与模型同机:127.0.0.1
业务后端与模型分机:内网 IP + 防火墙白名单
跨网络:Nginx / 网关 + TLS + WSS/HTTPS
二十、甲方部署检查清单
A. 硬件检查
nvidia-smi
df -h
free -h
确认:
- GPU 型号与显存;
- CUDA 驱动可用;
- 模型盘空间充足;
- 根分区不会被模型写满。
B. 目录规划
/data/ai/
├── conda_envs/
├── models/
├── projects/
├── logs/
└── cache/
C. ASR 服务
端口:10095
协议:WebSocket
路径:/v1/asr/stream
健康检查:/health
验证:
curl http://127.0.0.1:10095/health
D. LLM 服务
端口:10096
协议:OpenAI compatible
模型名:business-llm
验证:
curl \
http://127.0.0.1:10096/v1/models \
-H "Authorization: Bearer $LLM_API_TOKEN"
E. 业务后端 .env
ASR_REMOTE_WS_URL=ws://<ASR_HOST>:10095/v1/asr/stream
ASR_API_TOKEN=<ASR_TOKEN>
LLM_BASE_URL=http://<LLM_HOST>:10096/v1
LLM_API_KEY=<LLM_TOKEN>
LLM_MODEL=business-llm
LLM_TIMEOUT=60
F. 端到端测试顺序
1. ASR /health
2. LLM /v1/models
3. LLM /v1/chat/completions
4. 业务后端 /health
5. /api/chat/query
6. WebSocket /ws/asr
7. 浏览器麦克风
8. 工具调用
9. 最终回答
二十一、建议补充的 LLM 启动脚本
建议创建:
/app/hd/data01/username/projects/qwen-llm-service/start_llm.sh
示例:
#!/usr/bin/env bash
set -euo pipefail
CONDA_ENV="/app/hd/data01/username/conda_envs/qwen3-asr"
MODEL_DIR="/app/hd/data01/username/models/Qwen2.5-1.5B-Instruct-GPTQ-Int4"
TOKEN_FILE="/app/hd/data01/username/projects/qwen-llm-service/.llm_token"
source "/home/username/miniconda3/etc/profile.d/conda.sh"
conda activate "$CONDA_ENV"
export LLM_API_TOKEN="$(cat "$TOKEN_FILE")"
exec env CUDA_VISIBLE_DEVICES="<LLM_GPU_ID>" \
vllm serve "$MODEL_DIR" \
--served-model-name business-llm \
--host 127.0.0.1 \
--port 10096 \
--max-model-len 4096 \
--gpu-memory-utilization 0.25 \
--api-key "$LLM_API_TOKEN" \
--enable-auto-tool-choice \
--tool-call-parser hermes
赋权:
chmod +x start_llm.sh
二十二、今日最终成果
今天已经完成:
- 数据盘目录规划;
- Conda 环境、模型和缓存迁移到大容量数据盘;
- Qwen3-ASR 本地服务部署;
- 自定义 ASR WebSocket 协议;
- Windows SSH 隧道连接;
- Windows WAV 远程识别测试;
voice_api.py从 DashScope 实时 ASR 改为远程 ASR 代理;- Qwen2.5 1.5B GPTQ Int4 模型下载;
- vLLM OpenAI 兼容 LLM 服务部署;
- 自动工具调用启用;
business-llm统一模型别名;- Windows LLM 隧道;
/api/chat/query完整链路验证;- 浏览器端 ASR → LLM → Agent → 回答链路验证;
- 云端 API 与本地 LLM 的可切换配置。
最终验证的核心架构:
浏览器
→ Windows FastAPI
→ 远程 ASR URL
→ 远程 LLM URL
→ 工具调用
→ 前端回答
后续到甲方服务器时,不需要重新设计业务系统,只需要:
部署更好的 ASR/LLM 模型
→ 保持相同协议
→ 修改 .env
→ 重启服务
→ 执行验收测试
二十三、下一次部署时最关键的五条经验
- 模型、环境、缓存全部放数据盘,不要放根分区。
- ASR 与 LLM 对外协议固定,业务代码只读取 URL。
- vLLM 模型统一暴露为
business-llm。 - 小模型只做链路测试,甲方验收必须换更好的模型。
- 先逐层测试,再测浏览器完整链路。
逐层顺序:
端口
→ 健康检查
→ 模型接口
→ 后端接口
→ WebSocket
→ 浏览器
→ 工具调用
→ 最终回答










