Files
ai-companion/部署说明.md
T

9.8 KiB
Raw Blame History

部署说明 · 服务器侧助手

更新:2026-09-15。状态:已部署并跑通,端到端测试 16/16 通过。

架构定位

cadadmin-server(192.168.0.20)          N5095                     ESP32-S3
助手本体                                 外设桥                     表情/动作
├─ 会话引擎                              ├─ USB 麦克风阵列          ├─ 屏幕
├─ LLM:DeepSeek 云 API                  ├─ USB 摄像头              ├─ 电机
├─ 记忆 SQLite                           ├─ DoA 读取(不过网)      └─ 按键
├─ 身份裁决                              ├─ 人脸检测
├─ STT:Zipformer(CPU)                 ├─ WebRTC 客户端
└─ TTS:Qwen3-TTS(3090)                └─ 串口 → ESP32
                    手机 / 浏览器:额外客户端

"服务器是助手,本地是客户端"(2026-09-14 确定)。 助手本体、记忆库、语音模型全部在服务器;N5095 只做 USB 外设接入、 DoA 读取和网络桥接,不跑任何推理。

当前技术栈

环节 方案 位置 实测
LLM DeepSeek deepseek-flash(关思考) 云 API 首 token 0.8–1.2s
TTS Qwen3-TTS 1.7B-CustomVoice 3090 RTF 0.55–0.94
STT Zipformer 中英双语 int8 CPU 4 线程 RTF 0.03
记忆 SQLite 服务器本地 —
身份 手动切换 + 访客兜底 服务器 —

首音延迟约 1.5 秒(文本输入)。方案 P1 目标是 P50≤2 秒, 加上 VAD 判定后处在 2.0–2.2 秒,刚好卡线,没有余量。

为什么 LLM 走云端而 TTS 在本地

两者都需要算力,但 3090 只有 24GB:

方案 显存 首音延迟
本地 Qwen3.8-27B + 本地 vits-zh-ll 27B 占 21.5GB 179ms(音色不满意)
DeepSeek + 本地 Qwen3-TTS TTS 占 8.9GB 1545ms

2026-09-15 选了后者:用约 600ms 的网络往返 + 约 900ms 的 TTS 固定开销, 换音质从"用户不满意"到"用户试听选定"。27B 的 vLLM 已停。

这是可逆的(改 AI_LLM_PRIMARY 即可切回),但切回去 GPU 就不够跑 Qwen3-TTS。

服务器上的布局

~/ai-companion/            助手主体
├── client/test_client.py  自写 aiortc 客户端(实验用)
├── services/bot_server.py Pipecat SmallWebRTC 服务端(实验,端口 7860)
├── app/                   24 个 py 文件
│   ├── config.py          配置,支持环境变量覆盖
│   ├── prompts.py         单条 system 每轮重建
│   ├── engine.py          会话引擎(身份→记忆→LLM→TTS)
│   ├── llm/               后端抽象 + vLLM + DeepSeek + 降级路由
│   ├── memory/            接口 + SQLite 实现
│   ├── identity/          接口 + 手动切换实现
│   ├── voice/             STT / TTS(本地 + 远端两种后端)
│   └── tests/             5 个测试脚本(含传输适配层回归)
├── models -> ~/ai_companion_bench/models    软链,未重复占用
└── .venv/                 608MB(Python 3.12)

~/tts-service/             Qwen3-TTS 模型(4.3GB)
~/tts-qwen/                Qwen3-TTS 服务
├── tts_server.py          FastAPI 服务,OpenAI 兼容 /v1/audio/speech
├── samples/               音色试听样本
└── .venv/                 独立 venv(与 vllm-omni 依赖互斥,必须分开)

~/ai_companion_bench/      P0 基准脚本与模型(STT/TTS/vits)
~/ai/qwen-serving/         原来的 27B vLLM(已停,保留可随时启回)

配置

~/.config/ai_assistant/env(权限 600,含密钥)

变量 服务器值 说明
AI_LLM_PRIMARY deepseek 主 LLM;改 local 可切回本机 vLLM
AI_TTS_BACKEND remote remote=Qwen3-TTS 服务;local=vits-zh-ll
TTS_SERVICE_URL http://127.0.0.1:18040 TTS 服务地址
AI_TTS_SPEAKER Serena 用户试听选定
AI_TTS_INSTRUCTIONS 用轻快活泼的语气说 风格指令
AI_STT_THREADS 4 N5095 上必须改成 2(见下)
AI_MODEL_DIR /home/yonglin/ai-companion/models 两边模型位置不同

两台机器的线程配置不能照抄:N5095(4核无AVX2)STT 超过 2 线程急剧退化 (4 线程慢 3.9 倍);服务器(i9-14900K 有AVX2)给 4 线程没问题。 TTS 的线程扩展方向又和 STT 相反(越多越快)。

启动

# 1. TTS 服务(模型常驻显存,约 4 秒加载完)
cd ~/tts-qwen && setsid nohup .venv/bin/python tts_server.py --port 18040 \
  > logs/tts_service.log 2>&1 < /dev/null &

# 2. Pipecat 实时服务端(含 /client/ 网页测试入口)
cd ~/ai-companion && setsid nohup .venv/bin/python services/bot_server.py \
  --host 0.0.0.0 --port 7860 > logs/bot_server.log 2>&1 < /dev/null &

# 3. 确认就绪
curl -s http://127.0.0.1:18040/health

# 3. 跑测试
cd ~/ai-companion && .venv/bin/python app/tests/test_engine_e2e.py

助手本体还没有常驻服务(没有 systemd),目前靠直接跑脚本。

部署代码

cd ~/workspace/ai-companion
rsync -az --delete --exclude '__pycache__' --exclude '*.pyc' \
  app/ yonglin@cadadmin-server:~/ai-companion/app/

已知的坑

都已写进代码注释,这里只列索引。

接口层

  1. 本机 vLLM 只接受一条 system 消息,多条 400。记忆不能作为第二条 system 注入,必须并进同一条每轮重建。
  2. 思考字段名各实现不同:vLLM 用 reasoning,DeepSeek 用 reasoning_content。 适配层两个都认,少认一个会静默丢弃思考内容。
  3. DeepSeek 默认开思考,此时要求把上一轮的 reasoning_content 原样回传, 否则多轮/工具回灌一律 400。主路径必须显式关闭。
  4. 开思考会吃掉 max_tokens 预算,实测 200 预算下回答只吐出两个字。

语音层

  1. max_num_sentences=1 会静默截断多句输入(sherpa-onnx VITS), 必须自己先分句。
  2. VITS 是非自回归的,没有"边合成边播",首音 = 整句合成耗时。 提前出声的唯一手段是按语义短句拆分。
  3. Qwen3-TTS 短句 RTF 反而最差(0.94 vs 长句 0.55):每次调用有约 1 秒 固定开销,与文本长度无关。所以"让模型先说个短句"并不划算。

记忆层

  1. 检索兜底要限量:关键词全不命中时退回最近记忆,但只在记忆总数 ≤10 时 生效。不加限制就变成"每轮全量注入",不再是按需检索。
  2. "被引用的 user 必须存在"由存储层自己保证(_ensure_user_row), 不能让每个调用点各自记得——漏一处就报 FOREIGN KEY constraint failed。

依赖层

  1. qwen-tts 与 vllm-omni 依赖互斥(transformers 4.57 vs 5.14、 hf_hub 0.36 vs 1.31)。装同一个 venv 会互相破坏,实测把 vllm-omni 搞崩过。 所以 TTS 跑独立进程 + HTTP。
  2. flash-attn 装不上:预编译 wheel 最高只支持 torch 2.9,本机是 2.14 (vLLM 0.28 拉上来的);源码编译要从 GitHub 拉依赖,而这台机器下 GitHub release 会断连。

Pipecat 实时链路状态

2026-09-15 已闭环:PipelineWorker 自动启动,音频帧进入自定义处理器,完整 STT → DeepSeek → Qwen3-TTS → WebRTC 返回非静音语音。保留 RTVI 与 /client/ 网页测试入口,并补齐 Playground 启动所需的 /start;架构确定采用 “Pipecat 管传输,自有引擎管语义”的 A 方案。

原帧路由问题的根因是 NLTK punkt_tab 首次运行下载拖住 Pipecat 预热,导致 20 秒 setup timeout;不是框架不支持自定义处理器。服务器已把数据预装到权限 700 的 ~/.local/share/ai-companion/nltk_data,服务启动时会 fail-fast 检查。

单次文件音频实测:源音频结束到客户端首个非静音约 5.8 秒;其中包含约 0.7 秒 VAD 等待,当前仍高于 P1 的 2 秒目标,需要后续单独优化。完整记录见 Obsidian 笔记库里的 P0-Pipecat传输层实验报告.md。

2026-09-16 通过 SSH 隧道打开 http://localhost:7860/client/ 真人实测成功;补齐 Playground 使用的 /start 后,浏览器麦克风、RTVI、语音回复均正常,单轮服务端 指标约 3.0 秒。当前是 P1 调试模式:连接后持续上传并响应每段有效语音,尚未实现 P2 的本地唤醒和 20 秒连续对话窗口。

未做的事

  • 没有常驻服务:TTS 服务和助手都没有 systemd 开机自启(方案 §10 要求 P2 后做)
  • 网页 HTTPS 尚未启用:/client/ 已挂载且 HTTP 返回正常,但浏览器远程取麦克风 需要 HTTPS;Tailscale Serve 仍需在 tailnet 管理页启用
  • 唤醒尚未实现:当前网页连接后会持续上传麦克风并响应每段有效语音;正式方案 是在 N5095 本地做关键词检测,待机时音频不出网
  • 设备侧代码不存在:N5095 的外设桥(USB、WebRTC 客户端、串口)尚未开始
  • STT 没换:仍是 2023 年的 Zipformer int8,中文识别率从未测过
  • 记忆库在服务器上,而人脸特征按方案必须留在设备端——将来做 "删除我的数据"要跨两台机器协调,这个数据边界问题需要单独想
  • 代码与设计文档分属两个仓库:实现位于 ai-companion,设计记录位于 note_sync,状态变更需同步更新两边

相关文档

  • AI桌面陪伴助手方案.md — 总体方案
  • P0-语音基准报告.md — STT/TTS 基准(vits 时代,含线程数结论)
  • P0-DeepSeek验证报告.md — DeepSeek API 四条路径
  • P0-本地LLM验证报告.md — 本机 Qwen3.8-27B(已停用)
  • P0-Qwen3TTS报告.md — Qwen3-TTS 选型与部署