# 部署说明 · 服务器侧助手 更新: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/ 4 个测试,共 88 项断言 ├── 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 相反(越多越快)。 ## 启动 ```bash # 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 传输层服务端 —— 实验状态,帧路由未通 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),目前靠直接跑脚本。 ## 部署代码 ```bash 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 预算下回答只吐出两个字。 ### 语音层 5. **`max_num_sentences=1` 会静默截断多句输入**(sherpa-onnx VITS), 必须自己先分句。 6. **VITS 是非自回归的**,没有"边合成边播",首音 = 整句合成耗时。 提前出声的唯一手段是按语义短句拆分。 7. **Qwen3-TTS 短句 RTF 反而最差**(0.94 vs 长句 0.55):每次调用有约 1 秒 固定开销,与文本长度无关。所以"让模型先说个短句"并不划算。 ### 记忆层 8. **检索兜底要限量**:关键词全不命中时退回最近记忆,但只在记忆总数 ≤10 时 生效。不加限制就变成"每轮全量注入",不再是按需检索。 9. **"被引用的 user 必须存在"由存储层自己保证**(`_ensure_user_row`), 不能让每个调用点各自记得——漏一处就报 `FOREIGN KEY constraint failed`。 ### 依赖层 10. **qwen-tts 与 vllm-omni 依赖互斥**(transformers 4.57 vs 5.14、 hf_hub 0.36 vs 1.31)。装同一个 venv 会互相破坏,实测把 vllm-omni 搞崩过。 所以 TTS 跑独立进程 + HTTP。 11. **flash-attn 装不上**:预编译 wheel 最高只支持 torch 2.9,本机是 2.14 (vLLM 0.28 拉上来的);源码编译要从 GitHub 拉依赖,而这台机器下 GitHub release 会断连。 ## 当前卡点:Pipecat 帧路由未通 2026-09-15 的传输层实验:**传输层跑通(信令/ICE/data channel/双向媒体轨), 但帧送不进自定义处理器**——`process_frame` 调用次数为 0,引擎从未运行。 现象:连接全对、音轨协商成功、助手永远不说话,**日志里一条错误都没有**。 这直接影响 A/B 架构方案(A:Pipecat 只做传输;B:按框架 service 模型重构)。 根因未定位。完整记录见 Obsidian 笔记库里的 `P0-Pipecat传输层实验报告.md`。 **注意** `services/bot_server.py` 里目前还带着 `[诊断]` 前缀的日志, 是留给下一轮排查用的,定位后应清理。 ## 未做的事 - **没有常驻服务**:TTS 服务和助手都没有 systemd 开机自启(方案 §10 要求 P2 后做) - **没接 Pipecat 管线**:引擎与传输解耦,接 WebRTC 时写薄适配层即可 - **设备侧代码不存在**:N5095 的外设桥(USB、WebRTC 客户端、串口)尚未开始 - **STT 没换**:仍是 2023 年的 Zipformer int8,中文识别率从未测过 - **记忆库在服务器上**,而人脸特征按方案必须留在设备端——将来做 "删除我的数据"要跨两台机器协调,这个数据边界问题需要单独想 - **代码仍在 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 选型与部署