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

231 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署说明 · 服务器侧助手
更新:2026-09-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` | 两边模型位置不同 |
| `SEARCH_ENABLED` | `true` | 向 LLM 注册只读 `web_search` 工具 |
| `SEARXNG_BASE_URL` | `http://127.0.0.1:18080` | 私有 SearXNG,仅服务器回环可达 |
| `SEARCH_TIMEOUT_S` | `6`(默认) | 搜索总超时,失败后让模型明确降级 |
| `SEARCH_MAX_RESULTS` | `5`(默认) | 单次最多回灌的搜索结果 |
**两台机器的线程配置不能照抄**: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. 私有 SearXNG(首次部署先从 example 生成 settings.yml 并替换随机密钥)
cd ~/ai-companion/deploy/searxng && docker compose up -d
# 3. 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 &
# 4. 确认就绪
curl -s http://127.0.0.1:18040/health
curl -GsS http://127.0.0.1:18080/search \
--data-urlencode 'q=DeepSeek' --data-urlencode 'format=json'
# 5. 跑测试
cd ~/ai-companion && .venv/bin/python app/tests/test_engine_e2e.py
.venv/bin/python app/tests/test_search.py
.venv/bin/python app/tests/test_search_live.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 已闭环:`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 秒连续对话窗口。
网页文字输入由 RTVI `send-text` 转为 `LLMMessagesAppendFrame`,适配层会调用
`ConversationEngine.respond_to_text()`;回复用标准 LLM 帧返回,因此 Conversation
区域可显示用户文字和助手回复,默认也会合成语音。当前 Playground 1.7 还需要
完整的 `bot-transcription` / `bot-output` 事件,不能只发送 token 级
`bot-llm-text`。
**联网搜索已实现并部署。** DeepSeek 通过 Function Calling 请求 `web_search`,引擎
聚合完整的流式工具参数,调用回环地址上的 SearXNG,并把标题、摘要、URL 作为
`tool` 消息回灌后生成最终回答。最多连续搜索两轮,搜索失败不会拖垮对话;网页保留
一到三个来源 URL,TTS 从“来源:”开始停止朗读。2026-09-16 真实验收中,模型主动
搜索、取得 5 条结果并生成带两个来源的回答。
服务器到 DuckDuckGo、Brave、Wikipedia 会超时,因此 SearXNG 当前只保留并显式
启用 Bing、360 搜索、搜狗;其中 360 与搜狗实测有结果,Bing 当前返回空结果。
容器只监听 `127.0.0.1:18080`,不向局域网或公网暴露。
## 未做的事
- **没有常驻服务**: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 选型与部署