Compare commits
11
Commits
14090b625a
...
8a12b2de6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a12b2de6a | ||
|
|
f0dce05ac1 | ||
|
|
05933d168f | ||
|
|
26182e971e | ||
|
|
2ef6fe215c | ||
|
|
bb4fa3028e | ||
|
|
2b6093d392 | ||
|
|
bdd6bfb63d | ||
|
|
152b919b25 | ||
|
|
525e9f20d9 | ||
|
|
62d5acaecd |
+151
@@ -0,0 +1,151 @@
|
||||
# Ubuntu 中文输入法(Fcitx5 + Rime)安装与配置
|
||||
|
||||
> 环境:Ubuntu 22.04,GNOME + Wayland
|
||||
> 方案:Fcitx5 + Rime(朙月拼音),默认输出简体中文
|
||||
|
||||
## 一、卸载 IBus(避免冲突)
|
||||
|
||||
Ubuntu 默认使用 IBus,建议彻底移除以防止干扰:
|
||||
|
||||
```bash
|
||||
sudo apt remove ibus
|
||||
killall ibus-daemon # 确保进程终止
|
||||
```
|
||||
|
||||
## 二、安装 Fcitx5 及 Rime 组件
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install fcitx5 fcitx5-chinese-addons fcitx5-rime fcitx5-module-cloudpinyin fcitx5-config-qt
|
||||
```
|
||||
|
||||
各组件说明:
|
||||
|
||||
- `fcitx5-rime`:Rime 引擎支持
|
||||
- `fcitx5-chinese-addons`:中文相关扩展
|
||||
- `fcitx5-module-cloudpinyin`:可选,启用云拼音(新词联想)
|
||||
- `fcitx5-config-qt`:图形化配置工具(即使在 GNOME 也能用)
|
||||
|
||||
## 三、配置环境变量
|
||||
|
||||
为了让所有应用(GTK/Qt)识别 Fcitx5,需配置环境变量:
|
||||
|
||||
```text
|
||||
vim ~/.pam_environment
|
||||
```
|
||||
|
||||
添加以下内容:
|
||||
|
||||
```bash
|
||||
GTK_IM_MODULE=fcitx
|
||||
QT_IM_MODULE=fcitx
|
||||
XMODIFIERS=@im=fcitx
|
||||
```
|
||||
|
||||
> 💡 注意:Ubuntu 22.04 使用 GNOME + Wayland,**必须使用 `.pam_environment`**,`.profile` 或 `.bashrc` 在某些场景下会失效。
|
||||
|
||||
## 四、重启或重新登录
|
||||
|
||||
注销当前用户并重新登录,使环境变量生效。
|
||||
|
||||
## 五、配置输出简体中文
|
||||
|
||||
刚安装完 Rime,默认可能输出**繁体字**(如「妳好」)。我们需要配置默认使用简体方案。
|
||||
|
||||
Rime 的配置文件位于:
|
||||
|
||||
```bash
|
||||
~/.local/share/fcitx5/rime/
|
||||
```
|
||||
|
||||
创建或编辑:
|
||||
|
||||
```bash
|
||||
vim ~/.local/share/fcitx5/rime/default.custom.yaml
|
||||
```
|
||||
|
||||
填入以下内容(关键!):
|
||||
|
||||
```yaml
|
||||
patch:
|
||||
# 把简体拼音放在第一个,默认就用它
|
||||
schema_list:
|
||||
- schema: luna_pinyin_simp # 朙月拼音·简化字(简体)
|
||||
- schema: luna_pinyin # 朙月拼音(繁体,保留备用)
|
||||
# 你其他的方案,比如五笔、双拼等
|
||||
```
|
||||
|
||||
> ⚠️ 重要提醒:
|
||||
>
|
||||
> - 通过 `schema_list` 设置输入方案列表,**放在第一个的方案即为默认方案**。
|
||||
> - `luna_pinyin_simp` 是朙月拼音简化字版本,直接输出简体中文。
|
||||
|
||||
修改配置后,**必须执行 Deploy 才会生效**:
|
||||
|
||||
右键点击系统托盘中的 **Fcitx5 图标**(键盘或"中"字),选择 **"Deploy"**(部署),等待弹出"成功"提示。
|
||||
|
||||
> ✅ 部署成功后,输入 `nihao` 应显示 **「你好」**(简体)。
|
||||
|
||||
## 六、美化面板主题
|
||||
|
||||
默认面板较为朴素,可通过主题美化。
|
||||
|
||||
### 1. 安装 Material 主题
|
||||
|
||||
```bash
|
||||
sudo apt install fcitx5-material-color
|
||||
```
|
||||
|
||||
### 2. 配置主题与字体
|
||||
|
||||
打开 Fcitx5 配置工具:
|
||||
|
||||
```bash
|
||||
fcitx5-config-qt
|
||||
```
|
||||
|
||||
在图形界面中:
|
||||
|
||||
1. 点击左侧的 **"附加组件"**(Addons)
|
||||
2. 找到 **"经典用户界面"**(Classic User Interface)
|
||||
3. 点击右侧的 **"配置"** 按钮
|
||||
4. 在弹出的窗口中设置:
|
||||
|
||||
- **主题**:选择 `Material-Blue`(或其他喜欢的主题)
|
||||
- **字体**:设置为 `Noto Sans CJK SC 12`
|
||||
- **垂直候选列表**:勾选(可选)
|
||||
|
||||
点击 **"应用"** 或 **"确定"** 保存设置。
|
||||
|
||||
> 其他可选主题:`Material-Red`、`Material-Green`、`Dark` 等。
|
||||
|
||||
重启 Fcitx5 或重新登录即可看到效果:圆角、阴影、彩色高亮,现代感十足。
|
||||
|
||||
### 3. 使用 GitHub 主题包(更丰富)
|
||||
|
||||
如果想要更接近 macOS 风格的主题,可以使用开源主题包:
|
||||
|
||||
- 仓库地址:<https://github.com/thep0y/fcitx5-themes-candlelight>
|
||||
|
||||
下载并安装:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/thep0y/fcitx5-themes-candlelight.git
|
||||
cd fcitx5-themes-candlelight
|
||||
./install.sh # 按照下面提示输入然后回车就能自动安装好
|
||||
```
|
||||
|
||||
> 安装完成后,主题已经就绪,此时只需要执行一次 Deploy:
|
||||
>
|
||||
> - 右键点击系统托盘中的 **Fcitx5 图标**(键盘或"中"字)
|
||||
> - 选择 **"Deploy"**(部署)
|
||||
> - 等待弹出"成功"提示
|
||||
|
||||
> 💡 主题效果预览图来自知乎图床,未随笔记保留;实际效果以本地安装后为准。
|
||||
|
||||
## 七、常用技巧
|
||||
|
||||
- **切换中英文**:`Ctrl + Space`(可自定义)
|
||||
- **切换简繁**:`F4`
|
||||
- **翻页候选词**:`PageDown` / `+` 键
|
||||
- **部署配置快捷方式**:右键 Fcitx5 图标 → Deploy
|
||||
@@ -0,0 +1,499 @@
|
||||
# AI 桌面陪伴助手 · 实施规划 v2.4
|
||||
|
||||
> 📁 **代码已与本文档分离**(2026-09-15)。本目录(Obsidian 笔记库)只保留
|
||||
> 方案、P0 报告和硬件记录;所有代码在 **`~/workspace/ai-companion/`**
|
||||
> (独立 git 仓库),包括:助手本体、TTS 服务、P0 基准脚本、ESP32 固件、
|
||||
> `README.md`(代码说明)、`部署说明.md`(运维)——这两份也在代码仓库。
|
||||
>
|
||||
> 更新:2026-09-15。状态:**助手本体已在服务器上实现并跑通**(端到端测试 16/16);
|
||||
> **Pipecat 实时语音链路已闭环**(见 §10 与
|
||||
> `P0-Pipecat传输层实验报告.md`)——A 方案已确定:Pipecat 管传输/帧/RTVI,
|
||||
> 自有引擎管会话语义;当前风险转为真实首音延迟与真人中文识别率;
|
||||
> 设备侧(N5095 外设桥、ESP32)尚未开始。
|
||||
> 已确认:Ubuntu 24.04;N5095 / 16G 作为**客户端**;助手本体部署在 cadadmin-server(i9-14900K / 3090);麦克风、音箱、摄像头均未购;屏幕已于 09-14 到货并点亮(2.8 寸 ILI9341 触摸屏,非原定的 1.28 寸圆屏)。
|
||||
> 核心目标:听得清、回应及时、可打断、表情明确、转头自然。
|
||||
> 最新外形要求:屏幕不用太大;摄像头水平可用旋转总行程至少180°,按正前方左右各90°理解,不是镜头视场角180°。
|
||||
> 原方案备份:AI桌面陪伴助手方案.v1备份.md。本版指标为拟定目标,不是已测性能。
|
||||
>
|
||||
> **本版重大变更:架构从"单机"改为"服务器即助手、本地为客户端"。**
|
||||
>
|
||||
> 起因要说得准确:**N5095 并非跑不动本地语音**。实测证明它能跑
|
||||
> Zipformer STT(RTF 0.22)和 Piper TTS(RTF 0.36),是一条可用的低配本地链路。
|
||||
> 真正的问题是——**它达不到当前的目标音质与延迟**:音质令人满意的
|
||||
> Qwen3-TTS 在 N5095 上 RTF 高达 1.86(慢于实时),而服务器上只有 0.09。
|
||||
> 所以是"本地语音可用但不够好",不是"本地语音不可行"。
|
||||
> 详见 `P0-语音基准报告.md`。
|
||||
>
|
||||
> 变更后 N5095 只做 USB 外设接入与网络桥接,**不运行 STT/TTS/LLM 等重型推理**
|
||||
> (轻量推理仍在本地,见 §1)。
|
||||
> 详细部署现状见代码仓库里的 `部署说明.md`,实测数据见本目录的五份 P0 报告。
|
||||
|
||||
## 1. 范围与待讨论事项
|
||||
|
||||
第一版做好中文语音、屏幕表情、按用户隔离的记忆;同一轮开发加入多人脸跟踪、轮流交互和显式身份切换。录入式人脸识别作为可关闭的试验功能,实测达标后用于身份建议;重叠说话分离、可靠声纹认人不作为首版要求。视觉问答和米家后续加入。
|
||||
|
||||
| 项目 | 状态/建议 | 影响 |
|
||||
|---|---|---|
|
||||
| **主机分工** | **服务器(cadadmin-server)跑助手本体;N5095 降为客户端/外设桥** | **v2.4 重大变更,见 §2** |
|
||||
| 服务器 | i9-14900K / 32线程 / 188G / RTX 3090 24G,局域网延迟 1–2ms | 与 N5095 同局域网,1ms 级 |
|
||||
| N5095 角色 | USB 麦克风阵列、USB 摄像头、DoA 读取、WebRTC 客户端、串口给 ESP32 | **不运行 STT/TTS/LLM 等重型推理**;本地仍跑 VAD、唤醒词、人脸检测等轻量推理(这些延迟敏感或依赖本地硬件,放服务器反而更慢) |
|
||||
| 使用人数 | 本人使用为主,同时考虑多人扩展 | 首版基础多人,身份与会话从一开始分离 |
|
||||
| 新增预算 | 服务器为复用已有机器,不算追加采购 | 音频优先,其次屏幕,再机械 |
|
||||
| 操作系统、现有外设 | Ubuntu 24.04 已确认;麦克风、音箱、摄像头都没有;屏幕已到货 | 需采购音频与摄像头 |
|
||||
| 使用距离 | 待确认;暂以桌面约0.5~2米作为测试设计假设 | 不能当作已确认需求或性能保证 |
|
||||
| 屏幕 | **已到货:2.8 寸 ILI9341 触摸屏**(与原定 1.28 寸圆屏不符,待确认最终形态) | 见 `ESP32-S3与2.8寸SPI触摸屏接线记录.md` |
|
||||
| 摄像头旋转 | 水平可用总行程至少180°,正前方左右各90° | 优先屏幕与摄像头共用一轴头部,麦克风固定底座 |
|
||||
| **LLM** | **DeepSeek `deepseek-flash`(云端,关思考)**;本机 Qwen3.8-27B 已停用 | 首 token 0.8–1.2s;月费来自 LLM 与按需视觉 |
|
||||
| **TTS** | **Qwen3-TTS 1.7B(服务器 3090)**,Serena 音色 + 轻快活泼指令 | RTF 0.55–0.94;首音延迟的主要来源 |
|
||||
| **STT** | Zipformer 中英双语 int8(CPU)——**仍是 2023 年模型,中文识别率未测** | v2.4 未替换,遗留项 |
|
||||
| 米家设备 | 到扩展阶段收集型号、地区、网关/HA情况 | 集成与采购选择 |
|
||||
| 结构 | 屏幕/摄像头到货后称重,确认加工条件 | 电机、支架、电源 |
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
```text
|
||||
┌─ cadadmin-server(172.16.1.101)—— 助手本体 ────────────────┐
|
||||
│ 会话引擎(身份 → 记忆 → LLM → TTS) │
|
||||
│ ├─ LLM:DeepSeek deepseek-flash(云端,关思考) │
|
||||
│ ├─ TTS:Qwen3-TTS 1.7B(RTX 3090,独立 HTTP 服务 :18040) │
|
||||
│ ├─ 传输:Pipecat SmallWebRTC :7860(实时链路已闭环) │
|
||||
│ ├─ STT:Zipformer 中英宽带 int8(CPU 4 线程) │
|
||||
│ ├─ 记忆:SQLite(按 owner_user_id 强制隔离) │
|
||||
│ └─ 身份:手动切换 + 访客兜底 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
▲ WebRTC(实验中) ▲ 手机/浏览器客户端
|
||||
│ 局域网 1–2ms │
|
||||
┌─ N5095 / 16G —— 客户端 / 外设桥 ───────────────────────────┐
|
||||
│ USB 麦克风阵列(固定底座)──处理后音频、DoA──┐ │
|
||||
│ USB 摄像头(随头部转动)──本地图像───────────┤ │
|
||||
│ ├─ DoA 读取(本地,**不过网**——转头回路延迟敏感) │
|
||||
│ ├─ 本地人脸检测 / 目标锁定 → 角度控制(不依赖LLM响应) │
|
||||
│ ├─ 插话检测、音频编解码、WebRTC 客户端 │
|
||||
│ └─ USB 串口 115200 → ESP32 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
ESP32-S3:本地表情、运动轨迹、限位、心跳超时处理
|
||||
├─ SPI → 2.8寸 ILI9341 触摸屏(已到货)
|
||||
└─ 控制信号 → 一轴执行器(独立合适供电支路)
|
||||
```
|
||||
|
||||
- **分工原则:延迟敏感且硬件绑定的留在本地**(音频进出、唤醒、VAD、
|
||||
插话检测、DoA、表情/动作),**算力密集的放服务器**(STT、TTS、LLM 调用、记忆)。
|
||||
- Pipecat作为唯一会话管线框架。服务器侧用 WebRTC 传输(SmallWebRTC),
|
||||
它**自带** AEC、抖动缓冲、丢包恢复的实现,省去了自行编写。
|
||||
[传输选型](https://docs.pipecat.ai/client/concepts/choosing-a-transport)
|
||||
- ⚠️ **但"WebRTC 自带 AEC"不等于 AEC 问题已解决。** 框架有实现,不代表
|
||||
链路配置对了。至少要满足:
|
||||
1. N5095 的 WebRTC 客户端把**麦克风采集**和**扬声器播放参考信号**都正确
|
||||
送入音频处理模块——播放参考缺失或时间戳不对齐,AEC 就不会工作;
|
||||
2. XVF3800 自身的 AEC 路由要实测(它自己也会做回声消除,两处 AEC
|
||||
串联可能互相干扰,需要确认该用哪一个、另一个是否要关);
|
||||
3. 打断检测依赖 AEC 的输出质量,AEC 不对,打断就会误触发或漏触发。
|
||||
**P2 的 AEC 项保持"待验证",在真实麦克风+音箱接上并实测前不算通过。**
|
||||
- 不叠加Vision Agents:它是Stream的独立框架,不是Pipecat官方扩展。人脸检测另选轻量本地模型。[项目官方仓库](https://github.com/GetStream/vision-agents)
|
||||
- STT/TTS采用可替换适配器(本地/远端两种后端已实现)。LLM 为 DeepSeek
|
||||
`deepseek-flash`;这是可更新别名,实施时记录实际版本并做流式/工具调用验证。
|
||||
**实测必须显式关闭思考模式**,否则多轮与工具回灌一律 400。
|
||||
[官方公告](https://deepseek.com/news/deepseek-v4-1-flash/) · 详见 `P0-DeepSeek验证报告.md`
|
||||
- **视觉跟随在客户端做**(N5095 本地),先试640×480、5~10fps,
|
||||
按CPU、温度和延迟调整,未承诺该帧率已可达到。视觉问答按请求抓拍和缩放上传,
|
||||
不持续发送视频。
|
||||
- CPU标称TDP不能当整机常开功耗;实际插座功率、风扇噪声、温度在原型阶段测量。
|
||||
- **降级策略**:服务器或网络失效时,本地按键、静音和屏幕继续工作;
|
||||
建议保留一个极小的本地 TTS 兜底(piper-x_low 仅 20MB、RTF 0.17),
|
||||
用于播报"网络断了"——这是设备失联时唯一能出声的东西。
|
||||
|
||||
### 视频采集与框架边界
|
||||
|
||||
- Ubuntu由单一CameraService通过OpenCV/V4L2打开摄像头,维护带单调时间戳的最新帧;检测、身份候选和视觉问答共享采集源,不分别抢占设备。
|
||||
- 本地检测以低分辨率/限帧消费最新画面,积压时丢旧帧;按需问答读取足够清晰的新帧并缩放编码。必要时从较高分辨率采集再降采样,实际USB带宽和解码负载需测试。
|
||||
- Pipecat通过自定义适配器接入图像帧/取图工具。模型支持图像不等于当前Pipecat适配器已支持该模型的图像格式;P0补测纯文本流式、图片输入、工具调用、取消/超时四条路径。图像不通过时先修适配,不因此更换整套框架。[Pipecat DeepSeek服务](https://docs.pipecat.ai/api-reference/server/services/llm/deepseek)
|
||||
- 图像请求带request_id、拍摄时间和会话轮次,过期帧拒绝使用,图片不无限留在对话历史中。摄像头拔出后禁用视觉,语音保持可用。
|
||||
- 音频回调只做轻量读写;识别、合成和视觉推理在受控工作线程/进程执行,不能阻塞会话事件循环。各队列有限长,TTS限制预生成量。
|
||||
|
||||
## 3. 实施阶段与验收
|
||||
|
||||
阶段通过后再扩大范围,不预先给出未经验证的固定工期。
|
||||
|
||||
**进度列说明**:✅ 完成 · 🔶 部分完成 · ⬜ 未开始
|
||||
|
||||
| 阶段 | 实施内容 | 拟定完成标准 | 进度 |
|
||||
|---|---|---|---|
|
||||
| P0 环境与服务 | 盘点Ubuntu版本/USB;固定依赖;本地STT/TTS基准及DeepSeek请求 | 样例音频/文本先测试;设备到货后10次录放测试;记录RTF、首音延迟、CPU和内存 | 🔶 **环境盘点、STT/TTS/LLM 基准、DeepSeek 四条路径均已实测**;缺 10 次录放(无音频硬件)。**STT 中文识别率仍是未闭合项** |
|
||||
| P1 基础语音 | 按键启动;流式回复;停止按钮;超时提示 | 20轮固定中文用例至少18轮完成;短问答结束到首段有声回复,目标P50≤2秒、P95≤4秒 | 🔶 **链路已跑通**(文本/音频 → STT → 记忆 → LLM → TTS → 音频,16/16 通过);缺实时音频进出、按键、20 轮用例。首音实测约 1.5s,加 VAD 后 **2.0–2.2s,卡在目标线** |
|
||||
| P2 外放与表情 | AEC、打断、唤醒;听/想/说/离线/静音表情 | 指定距离音量下20次打断至少18次成功;检测到有效用户语音至停播目标≤300ms;30分钟外放无自我触发;运行2小时稳定 | ⬜ 未开始。**AEC 待验证**——WebRTC 有现成实现,但需要麦克风采集与播放参考正确送入、且 XVF3800 自身的 AEC 路由要实测(见 §2) |
|
||||
| P3 多用户记忆 | 用户档案、显式切换、偏好/摘要/修改/遗忘 | 两个用户同一问题得到各自偏好;未知用户无私人记忆;重启保留、更正/删除有效 | 🔶 数据层已实现并测试(身份隔离、越权删除、级联清理、候选不参与检索共 28 项);缺"修改/遗忘"的用户入口 |
|
||||
| P4 一轴转头 | 静态DoA/人脸验证,再接电机;锁定和限位 | 左/中/右各10次唤醒至少27次转向正确区域;轻微移动不频繁摆动;断联/重启不撞限位;运动时重复P2测试 | ⬜ 未开始,待摄像头与电机 |
|
||||
| P4a 基础多人/身份试验 | 2~3张脸跟踪;轮流发言;可选录入式人脸识别 | 20次轮流唤醒至少18次选中正确目标;同方向/交叉/遮挡时允许拒判;未知人、身份切换不得串记忆 | 🔶 `IdentityProvider` 接口与手动切换已实现(人脸候选**不自动切换**已测试);缺人脸检测 |
|
||||
| P5 扩展 | 按需视觉问答;HA接一盏灯 | 检查10次视觉请求;开关灯、离线、超时均反馈真实结果 | 🔶 工具调用链路已验证(DeepSeek 与本机 vLLM 均通过);缺米家与视觉 |
|
||||
|
||||
验收记录距离、背景声音、播放音量、网络、模型版本、样本数。目标用于定位瓶颈,不保证所有环境都达到。耳麦通过不能代替远场外放验收。
|
||||
|
||||
**v2.4 新增的验收项**:
|
||||
|
||||
- **首音延迟必须按部署机(服务器)测**,不能沿用开发机的数字。
|
||||
同一份代码在 N5095 上首音 1980ms,在服务器上 179ms(vits 时代)。
|
||||
- **TTS 与 STT 的线程配置分别测**。实测两者线程扩展方向相反:
|
||||
STT 超过 2 线程急剧退化(N5095 无 AVX2,4 线程慢 3.9 倍),
|
||||
TTS 是线程越多越快。不能沿用一套配置。
|
||||
- **换语音模型时必须重测首音延迟**。Qwen3-TTS 比 vits-zh-ll 音质好一个档次,
|
||||
但首音从 179ms 涨到 1545ms(8.6 倍)。音质与延迟在这个硬件上是此消彼长的。
|
||||
|
||||
P2的30分钟/2小时测试仅是初步门槛,不能证明长期稳定。阶段通过后做一次正常音量下至少8小时运行:记录误唤醒次数、内存增长、音频欠载、重连及温度。P4a把“转对人”和“认对身份”分开统计,额外报告误认、拒判、切换次数,不用总成功率掩盖串身份。测试人数不足时,多用户记忆可用模拟身份测试,人脸/发言者匹配则明确标记未实测。
|
||||
|
||||
## 4. 分批采购清单
|
||||
|
||||
> **v2.4 更新(2026-09-15)**:ESP32-S3 开发板与屏幕**已于 09-14 到货并点亮**,
|
||||
> 但屏幕是 **2.8 寸 ILI9341 触摸屏(240×320)**,与原定的 1.28 寸 GC9A01 圆屏不符。
|
||||
> 接线与测试程序见 `ESP32-S3与2.8寸SPI触摸屏接线记录.md`。
|
||||
> **最终屏幕形态待确认**——这会影响外壳与转台结构设计,越早定越省返工。
|
||||
> 下列清单已按现状调整。
|
||||
|
||||
本表未报价:用户预算尚未确定,具体SKU和商家现价需核价。**当前实际还缺的只有音频(麦克风阵列+音箱)和摄像头。**
|
||||
|
||||
### 第一批:已基本到位
|
||||
|
||||
| 物品 | 数量 | 状态 |
|
||||
|---|---:|---|
|
||||
| ESP32-S3开发板 | 1 | ✅ 已有 N16R8(带 PSRAM) |
|
||||
| 屏幕 | 1 | ✅ 已有 2.8 寸 ILI9341 触摸屏(**非原定 1.28 寸圆屏**) |
|
||||
| USB数据线 | 按缺口 | 按缺口补 |
|
||||
| 杜邦线/面包板/绝缘固定件 | 1套 | 按缺口补;只用于逻辑低电流,不走电机大电流 |
|
||||
| 瞬时按键 | 2 | 待购;会话停止、软件静音,不宣称物理断麦 |
|
||||
|
||||
若最终决定用 1.28 寸圆屏形态,仍需单独采购 GC9A01 模块;若接受 2.8 寸方屏,则本批已完成。
|
||||
两种形态的表情素材、外壳和结构设计都不同,**先定形态再动结构**。
|
||||
|
||||
### 同期优先核价:音频套件
|
||||
|
||||
| 物品 | 数量 | 优先候选/要求 | 下单条件 |
|
||||
|---|---:|---|---|
|
||||
| USB麦克风阵列 | 1 | Seeed reSpeaker XVF3800 USB 4-Mic Array成品板 | 核对具体SKU、USB固件、目标系统、DoA例程、录放音通道、输出与附件;报价合适再买 |
|
||||
| 有源小音箱 | 1套 | 需新购;3.5mm模拟输入,体积小,合适独立供电 | 与阵列输出匹配;初期不选只能走USB音频/蓝牙的音箱 |
|
||||
| 音频线 | 按缺口 | 匹配两端的3.5mm音频线 | 查套装是否自带 |
|
||||
|
||||
阵列官方资料列出USB音频、DoA主机控制、可接耳机/有源音箱的AUX输出。采购成品板,不能仅凭XMOS芯片名替代。[Seeed官方文档](https://wiki.seeedstudio.com/respeaker_xvf3800_introduction/)
|
||||
|
||||
拟定路径(v2.4 修正:TTS 已不在 N5095):
|
||||
|
||||
```
|
||||
服务器 Qwen3-TTS → WebRTC → N5095 → 阵列 USB 播放端点 → AUX → 有源音箱
|
||||
```
|
||||
|
||||
AEC 需要实际播放参考;同一板卡也要验证固件/通道配置,不能假定接上即成功。[XMOS官方指南](https://www.xmos.com/documentation/XM-014888-PC/pdf/xvf3800_user_guide_v3.2.1.pdf)
|
||||
|
||||
**注意**:播放链路跨了服务器和客户端两侧,AEC 所需的"播放参考信号"必须在
|
||||
N5095 本地可得(即从阵列 USB 播放端点或本地播放缓冲取),**不能依赖服务器侧的回放时钟**——
|
||||
两侧时钟不同步,参考信号就对不齐。[详见 §2 的 AEC 说明]
|
||||
|
||||
不为了阵列额外购买XIAO ESP32S3,主机直接接USB即可。最终内置喇叭/功放留到结构阶段,按接口、电压、阻抗和功率匹配。
|
||||
|
||||
### 第二批:摄像头
|
||||
|
||||
- 需新购普通USB UVC单目摄像头1个,支持1080p及低分辨率模式。为早测多人/身份,可与第一批一起买;无需增加第二台摄像头。
|
||||
- 优先小巧轻便、可固定、柔软线缆、有视场角资料的型号;近距离物品问答需核对最近对焦距离。
|
||||
- 摄像头随头部水平旋转,可用总行程至少180°(左右各90°)。这是机械旋转范围,不要求购买180°鱼眼镜头。单摄像头只覆盖当前朝向的视野,转动可依次查看不同方向,不能同时看全180°范围。
|
||||
- 首版多人按当前视野内、轮流发言验证。镜头选型兼顾多人覆盖与脸部像素,过广视角会让远处脸变小。
|
||||
- 不需要双目/深度相机和ESP32-CAM。摄像头不影响P0~P3先行。
|
||||
|
||||
### 第三批:机械结构确定后购买
|
||||
|
||||
| 物品 | 为什么暂缓 | 解锁条件 |
|
||||
|---|---|---|
|
||||
| 舵机/步进 | 已要求可用行程≥180°,重量/重心/噪声未知,暂不锁MG996R/DS3218 | 称重后选型;额定/机械行程要有软限位余量,实测可用行程≥180° |
|
||||
| 电机电源/降压 | 电压及启动/堵转电流依赖电机 | 数据表确认,峰值留余量 |
|
||||
| 转台/轴承/支架/外壳 | 安装孔、重心、线缆弯曲需配合 | 原型尺寸确定 |
|
||||
| 步进驱动、回零开关、编码器 | 取决于执行器及是否需要实测位置 | 电机方案确定 |
|
||||
| USB Hub | 主机可能已有足够接口 | P0盘点确认不足 |
|
||||
| 米家网关/新设备 | 兼容性依型号地区而异 | 现有设备验证后再定 |
|
||||
|
||||
当前不采购:新主机、GPU、双目相机、HC-05蓝牙、第二块视觉MCU、电池系统。
|
||||
|
||||
## 5. 音频与陪伴行为
|
||||
|
||||
### 语音方案(v2.4 已实测定稿)
|
||||
|
||||
STT是语音转文字,TTS是文字转语音。**v2.4 起重型语音推理(STT/TTS/LLM)全在服务器**,
|
||||
N5095 只做音频进出与本地轻量推理。
|
||||
|
||||
准确的表述是:**N5095 能完成低配本地语音,但达不到当前的目标音质与延迟**。
|
||||
实测它能跑 Zipformer STT(RTF 0.22)和 Piper TTS(RTF 0.36);而音质达标的
|
||||
Qwen3-TTS 在它上面 RTF 1.86(慢于实时),在服务器上只有 0.09。见 `P0-语音基准报告.md`。
|
||||
|
||||
| 模块 | 采用方案 | 实测 | 备选与限制 |
|
||||
|---|---|---|---|
|
||||
| VAD | 本地轻量VAD | — | 单独处理唤醒和说完判断 |
|
||||
| 唤醒 | sherpa-onnx中文关键词检测作为候选 | — | 单独模型/词表,用户确定称呼后调阈值;普通STT不会自动等于唤醒引擎 |
|
||||
| STT | **Zipformer 中英双语 int8**(服务器 CPU 4线程) | RTF 0.03 | ⚠️ **最大功能风险**:仍是 2023 年模型,**中文真人识别率至今无可信数据**。音频硬件到手后优先录制本人语音算 CER,并与 SenseVoice / faster-whisper large-v3 对比(已下载待测) |
|
||||
| TTS | **Qwen3-TTS 1.7B-CustomVoice**(服务器 3090,独立 HTTP 服务) | RTF 0.55–0.94 | 音色 Serena + 指令"用轻快活泼的语气说"(用户试听选定)。备选:vits-zh-ll(RTF 0.09 但音色不满意)、CosyVoice 2/3(未测)。**首音延迟需按真实链路重测**,见 §10 |
|
||||
| LLM | **DeepSeek `deepseek-flash`**(云端,关思考) | 首 token 0.8–1.2s | 本机 Qwen3.8-27B 的 vLLM 已停用——GPU 让给 TTS。可改 `AI_LLM_PRIMARY=local` 切回,但切回后 GPU 不够跑 Qwen3-TTS |
|
||||
|
||||
[sherpa-onnx官方功能](https://k2-fsa.github.io/sherpa/intro.html)、[SenseVoice模型说明](https://k2-fsa.github.io/sherpa/onnx/sense-voice/pretrained.html)、[中文VITS模型](https://k2-fsa.github.io/sherpa/onnx/tts/pretrained_models/vits.html)。以上说明模型/运行时支持,不构成性能保证。
|
||||
|
||||
**音质与延迟的取舍(v2.4 的核心权衡)**:同一台服务器上
|
||||
|
||||
| 配置 | 首音延迟 | 音质 |
|
||||
|---|---|---|
|
||||
| 本地 Qwen3.8-27B + vits-zh-ll | **179 ms** | 用户不满意 |
|
||||
| DeepSeek + Qwen3-TTS | **1545 ms** | 用户试听选定 |
|
||||
|
||||
用约 1.4 秒的首音延迟换音质。这个取舍是可逆的(改配置即可),
|
||||
但**在 24GB 显存下两者不可兼得**。后续若装成 flash-attn 或实现真流式,
|
||||
延迟可望收回一部分——详见 `P0-Qwen3TTS报告.md`。
|
||||
|
||||
P0测试10~20条不同长度中文,覆盖数字、日期、中英混读。记录STT识别错误、语音结束后等待时间、TTS首段耗时和RTF(生成/处理耗时除以音频时长,低于1表示平均快于实时)。音频与视觉并发时重测,限制推理线程和视觉帧率,避免CPU争抢。
|
||||
**v2.4 补充:STT 与 TTS 的线程配置必须分别调**——实测两者线程扩展方向相反。
|
||||
|
||||
|
||||
TTS按语义短句合成并播放,不把整段一次合成称为原生流式;分句仍要测首音等待、接句停顿及取消响应。本地效果不达标时先调整模型/线程/分句,再讨论是否开启云端后备,不自动发送音频到第三方。
|
||||
|
||||
- 本地唤醒引擎保持工作;闲置时暂停完整识别、降低视觉频率。保留短时内存音频预卷(初始0.5~1秒)避免唤醒后吞掉指令开头,静音时清除缓冲。[关键词检测官方文档](https://k2-fsa.github.io/sherpa/onnx/kws/index.html)
|
||||
- 活跃会话内允许免唤醒接续,初始闲置超时20秒后回到待机,参数可调。多人时其他人的普通谈话不应自动抢占;以唤醒/显式切换为主要入口。
|
||||
- 记录阵列实际采样率、通道和采样格式,STT/TTS边界显式重采样;避免把原始麦克风/回放参考通道当成处理后语音。Ubuntu固定设备标识和录放音路由,不依赖重启后会变化的默认声卡序号。
|
||||
- DeepSeek输出区分最终回答、思考内容、工具调用与工具结果,只将面向用户的回答交给TTS;数字/日期/单位和Markdown做轻量朗读归一化。
|
||||
|
||||
- P1按键启动,P2再加唤醒。语音活动检测、说完判断、唤醒是不同环节。
|
||||
- 打断时取消旧回复生成、TTS和待播缓冲,丢弃旧轮次迟到结果,不能暂停后继续放旧回复。
|
||||
- 分别测安静、设备外放、背景音乐、风扇噪声;P4加入运动噪声,装壳后重测。
|
||||
- 默认简短自然回复;等待时及时更新表情,长回复按语义分段播放。
|
||||
- 明确待机、在听、思考、说话、静音、离线状态。专注/夜间默认不主动开口,主动问候后续再加。
|
||||
- 网络失败给简短提示,本地按键、静音和屏幕继续工作,不反复播旧回复。
|
||||
- 麦克风、音箱、电机尽量机械隔离;软件静音和真实物理断麦需明确区分。
|
||||
|
||||
### 客户端/服务器的音频边界(v2.4 定稿)
|
||||
|
||||
**决定日期:2026-09-15。这条边界决定 WebRTC 客户端的接口形态、
|
||||
首音延迟预算和隐私边界,先定下来再写代码。**
|
||||
|
||||
| 问题 | 决定 | 理由 |
|
||||
|---|---|---|
|
||||
| **传什么** | **闲置关闭,会话中持续上传**(含 0.5–1s 预卷) | 闲置时服务器**完全听不到**房间声音(隐私);会话中上传完整音频流,服务端才能做可靠的"说完判定"(方案 §5 要求)。折中方案见下 |
|
||||
| **唤醒** | **本地**(N5095) | 延迟低、断网也能响应;唤醒词模型轻量,N5095 跑得动。且它是"**注意力**的门",不是"音频传输"的门 |
|
||||
| **打断判定** | **客户端** | 三条理由:① WebRTC 的 AEC 本来就在客户端跑(需要播放参考),客户端的麦克风信号已是去过回声的;② 客户端**知道自己正在播什么**——"用户在助手说话时开口"本地就能判,不需要额外协议;③ 比服务端判定少一个网络来回,更容易满足 ≤300ms |
|
||||
| **断线/抖动** | WebRTC 内建 ICE restart 重连;抖动缓冲初始 **100–200ms**,实测调整 | 缓冲换稳定性,但**直接加首音延迟**,不能凭感觉设,必须实测 |
|
||||
|
||||
#### 状态机
|
||||
|
||||
```
|
||||
待机(不上传任何音频)
|
||||
│
|
||||
│ 本地唤醒词命中
|
||||
▼
|
||||
会话中(持续上传音频 + 控制信号)
|
||||
│
|
||||
│ 闲置超时 20s(可调)
|
||||
▼
|
||||
待机(停止上传)
|
||||
```
|
||||
|
||||
**关键点:唤醒控制的是"是否上传",不是"是否处理"。**
|
||||
闲置时麦克风数据只进本地唤醒引擎,不出网。
|
||||
|
||||
#### 接口约定
|
||||
|
||||
**N5095 → 服务器**
|
||||
- 音频流:16kHz 单声道(WebRTC 内部用 Opus 编码)
|
||||
- `wake` 信号:本地唤醒词命中,会话开始
|
||||
- `barge_in` 信号:用户在助手播放期间开口,服务端据此取消当前生成
|
||||
- `identity_hint`:本地人脸候选(**仅建议,不自动切身份**,见 §6)
|
||||
|
||||
**服务器 → N5095**
|
||||
- 音频流:TTS 合成结果(Qwen3-TTS 输出 24kHz,需重采样到播放端)
|
||||
- 状态事件:`listening` / `thinking` / `speaking` / `idle` —— 客户端据此渲染表情并判断"是否正在说话"
|
||||
|
||||
#### 三条必须遵守的约束
|
||||
|
||||
1. **播放参考信号必须在 N5095 本地可得**。AEC 需要它,而服务器与客户端的
|
||||
回放时钟不同步,不能依赖服务器侧的时间戳。从阵列 USB 播放端点或
|
||||
本地播放缓冲取。
|
||||
2. **预卷缓冲 0.5–1s**。唤醒/打断判定完成到开始上传之间有延迟,
|
||||
不留预卷会吞掉用户指令的开头。静音时清除缓冲。
|
||||
3. **会话中持续上传 ≠ 持续存储。** 服务端处理完即丢弃,
|
||||
默认不落盘(方案 §8:"默认不存原始音视频")。
|
||||
|
||||
#### 这条边界的已知代价
|
||||
|
||||
- 会话中上传的是**连续音频**,包括用户的停顿、思考声和周围环境音。
|
||||
服务器处理这些数据,但不存储。
|
||||
- 客户端 VAD/唤醒的**误判会切掉话头**——这是"闲置不上传"的固有代价,
|
||||
靠预卷缓冲缓解,不能消除。
|
||||
- 抖动缓冲直接计入首音延迟。**做 WebRTC 客户端时必须一并测**,
|
||||
否则首音延迟数字不完整(见 §10)。
|
||||
|
||||
## 6. 朝向与机械
|
||||
|
||||
DoA是声源来向,不代表身份或距离。阵列固定底座,摄像头随头部转动。
|
||||
|
||||
状态:待机 → 有效唤醒 → 可靠DoA粗转 → 匹配人脸 → 锁定 → 丢失短暂保持 → 超时待机。多人先按轮流发言处理;同方向多人或重叠说话时保持目标/请求轮流,不凭DoA猜身份。
|
||||
|
||||
### 多人能力与避免返工
|
||||
|
||||
- 基础多人:同画面维护多个临时track_id,记录位置和最后出现时间;用带时间戳的DoA与人脸位置关联当前发言目标,设置切换滞后。走位/遮挡后track_id可能变化,不能充当永久身份。
|
||||
- 读取DoA时核对固件、波束选择和单位。XVF3800可返回多个波束角度,它们不是“几个人的身份/独立语音”;不要直接把第一个角度当发言者。优先用与当前处理后音频对应的选中波束,查不到可靠对应时允许拒判。[官方主机控制代码](https://github.com/respeaker/reSpeaker_XVF3800_USB_4MIC_ARRAY/blob/master/python_control/xvf_host.py)
|
||||
- 首版可靠身份入口:手动/对话确认切换到已建档的user_id;无法确认时使用unknown/guest。看到熟人不等于确认他正在说话。
|
||||
- 显式报名字用于个性化档案选择,不是身份认证,不能据此解锁敏感信息或权限。信任等级与user_id分开,未来增加认证时不改普通偏好存储。
|
||||
- 可选人脸试验:OpenCV YuNet检测+SFace特征作为候选,需用户主动录入、相似度阈值和未知人拒判。低频识别,平时跟踪;不用于门锁/支付或敏感身份认证。[OpenCV官方教程](https://docs.opencv.org/5.0/tutorials/dnn/dnn_face/dnn_face.html)
|
||||
- 注册特征本地保存,可查看和删除;识别不确定时不读取/写入私人记忆。群体会话默认仅共享明确公开的上下文,不主动播报私密记录。
|
||||
- 预留IdentityProvider接口:输入时间戳、track_id、候选身份、置信度与来源;输出确认的user_id或unknown。后续声纹模块可接入此接口,不改记忆表结构。
|
||||
- 声纹认人、声音与人脸可靠绑定、重叠语音分离属于更高难度阶段,不承诺通过增加一块阵列即可解决。第一版不做全场多人同时自由插话。
|
||||
|
||||
- 调试候选:3~5°死区、丢失保持1~2秒;速度/加速度实机调节。
|
||||
- 坐标约定:从设备视角向右为正。标定阵列零点、摄像头中心、电机零点;统一DoA单位和方向。
|
||||
- 人脸像素位置经相机内参/标定换算成相对角度,再加头部角度估计,不能把像素直接当绝对角度。
|
||||
- 普通位置舵机先做平滑目标角度,不默认再加内环PID;无反馈时只有估计位置。
|
||||
- 摄像头与麦克风有位置偏移,近距离DoA和画面方位不完全重合;先粗转,再以视觉纠偏。运动过程中按时间戳使用对应角度估计,避免拿旧图配新角度。无反馈舵机的首版身份匹配优先在稳定停转后进行。
|
||||
- 步进需独立驱动器、明确STEP/DIR等接口,补回零/失步/软限位。
|
||||
- 只做左右一轴,用户要求摄像头可用总行程至少180°;初始工作范围设为[-90°, +90°],屏幕与摄像头共用转动头部,麦克风固定底座。不要求连续360°旋转。
|
||||
- 执行器实际机械行程须大于工作范围,以留软/硬限位余量;采购候选可考察标称270°的位置舵机,但型号、真实行程及噪声仍待核对。不能把普通标称180°舵机两端硬顶当作已满足要求,也不选无角度定位反馈的连续旋转舵机来直接代替位置舵机。
|
||||
- 转轴/轴承承担结构负载;USB摄像头线、屏幕/MCU线缆在左右端点均要有弯曲余量,避免拉扯插头。有限180°往返先用柔软线缆和固定应力释放,不把滑环列为必需采购件。
|
||||
- P4增加行程验收:实测摄像头左右端点至少相差180°,端点不碰撞、不拉线;低速往返100次检查线缆、USB掉线、抖动和供电。上电及重连仍须受限速和限位约束。
|
||||
- MCU独立限位和断联停止;失联先停止更新轨迹,保持/卸力按结构实测,避免盲目断电造成坠落。
|
||||
- 电机独立合适供电支路并正确共地,选合适线径/端子/去耦,不由ESP32板载电源或面包板供大电流。
|
||||
|
||||
## 7. 串口协议与恢复
|
||||
|
||||
先用UTF-8单行JSON、换行分帧,字符串换行由JSON转义。115200起步。Linux可能ttyUSB/ttyACM,Windows为COM,使用可配置稳定设备标识。
|
||||
|
||||
```json
|
||||
{"v":1,"seq":101,"type":"expression","state":"listening"}
|
||||
{"v":1,"seq":102,"type":"motion","yaw_deg":20,"max_speed_deg_s":25}
|
||||
{"v":1,"seq":102,"type":"ack","status":"accepted"}
|
||||
```
|
||||
|
||||
- 表情和运动分开;转向目标最新值优先,不排队执行过时动作。
|
||||
- 定义版本、字段范围、最大帧长(初始1KB)、坏帧丢弃和超时恢复。
|
||||
- seq关联请求与确认;重试不能重复动作。accepted仅表示收到,无传感器不能报告实测已到位。
|
||||
- 心跳500ms、失联2秒触发本地停止作为初始候选参数,实测后定稿。
|
||||
- MCU重启发hello/能力信息,重连恢复当前状态,不回放历史运动。
|
||||
- 字体和表情素材本地存储,串口不传全屏视频;WiFi/MQTT旁路暂不做。
|
||||
|
||||
## 8. 记忆、费用与数据
|
||||
|
||||
P3用SQLite,Mem0和向量库不是前置条件。从建表起就包含users、sessions、memories;记忆带owner_user_id、scope(private/shared)、来源会话/轮次、时间和确认状态。查询和写入均强制身份过滤,未知身份只用访客临时上下文。视觉track_id、会话session_id、永久user_id分开。
|
||||
|
||||
| 类型 | 保存策略 |
|
||||
|---|---|
|
||||
| 明确偏好 | 结构化键值,来源、确认状态、更新时间,新值替换旧值 |
|
||||
| 临时事项 | 带时间与过期规则;记住事项不等于实现提醒调度 |
|
||||
| 对话摘要 | 限长、按需检索、可清除,不默认永久保存逐字稿 |
|
||||
|
||||
- 支持记住/修改/忘掉/查看;模型推测不当作确认事实。
|
||||
- 默认显式切换身份;人脸只提供候选,经当前发言者关联及确认后才启用对应记忆。切换身份清理上一人的私有检索上下文;异步任务绑定创建时的user_id/session_id,避免迟到结果写入新用户。
|
||||
- 删除同时覆盖会话上下文、摘要和未来索引,防止删库后仍引用。
|
||||
- 回复后异步写记忆,检索有超时;记忆失败不阻塞对话。
|
||||
- 真实使用中检索不足再评估Mem0 + Chroma,支持不代表无需配置测试。[Mem0官方文档](https://docs.mem0.ai/components/vectordbs/dbs/chroma)
|
||||
- 本地STT/TTS记录耗时与资源,不产生语音API调用费;统计LLM/按需视觉及未来云端后备费用,按实际日用量估月费并讨论上限。
|
||||
- 默认不存原始音视频;视觉按需上传。密钥保存在本机配置,不写日志;提供静音和相机关闭入口。
|
||||
|
||||
## 9. 米家接入
|
||||
|
||||
P5通过Home Assistant统一调用。按实际设备比较官方Xiaomi Home和Xiaomi Miot Auto,核实地区、网关、本地/云端路径,不提前采购网关。[小米官方集成](https://github.com/XiaoMi/ha_xiaomi_home)
|
||||
|
||||
先接一盏现有灯:意图 → 工具/实体白名单 → 参数校验 → HA执行 → 状态确认 → 回复。请求受理和设备已改变分开描述,超时明确未确认。高风险设备不默认加入。
|
||||
|
||||
工具执行与语音回复生命周期分开:用户打断播报不意味着已发出的设备操作被撤销。每次动作保留执行ID和状态;超时先查状态,不盲目重试;优先使用“设为开/关”而非“切换”以降低重复执行风险。摄像头中的文字和检索记忆作为数据,不能自行扩大工具权限。
|
||||
|
||||
## 10. 运行维护与当前下一步
|
||||
|
||||
### 已完成(截至 2026-09-16)
|
||||
|
||||
- Ubuntu 24.04 已确认,USB 接口已盘点;依赖版本固定在代码仓库的 `requirements.txt`
|
||||
- 助手本体在服务器上实现并跑通,端到端测试 16/16(**用文件音频,非实时链路**)
|
||||
- STT/TTS/LLM 三方基准实测;DeepSeek 四条路径验证
|
||||
- 记忆与身份的数据层实现并测试(88 项断言)
|
||||
- 代码剥离为独立仓库 `~/workspace/ai-companion/`
|
||||
- 网页文字与语音对话、助手文字显示、DeepSeek Function Calling + 私有 SearXNG 已部署
|
||||
- 姓名档案跨连接恢复已修复;内部 user_id 与用户称呼隔离
|
||||
|
||||
### 下一步顺序(按风险从高到低)
|
||||
|
||||
评审建议的思路——**先把风险最大的实时语音链路闭环**——认可并采纳。
|
||||
服务端 WebRTC 入口和客户端骨架现已完成。2026-09-16 通过 SSH 隧道打开
|
||||
Pipecat Playground 做了真人浏览器对话,麦克风 → STT → LLM → TTS → 浏览器播放
|
||||
正常。当前仍是 P1 调试形态:连接后持续上传并响应有效语音;正式的本地唤醒、
|
||||
待机不上传和 20 秒连续对话窗口留在 P2,本轮按决定暂不实现。
|
||||
|
||||
| # | 步骤 | 状态 | 能验证什么 / 不能验证什么 |
|
||||
|---|---|---|---|
|
||||
| 0 | **音频边界定稿** | ✅ 2026-09-15 完成(见 §5) | 决定了客户端接口形态 |
|
||||
| 1 | **服务端 WebRTC 入口 + 客户端骨架** | ✅ **实时链路已闭环**(见 `P0-Pipecat传输层实验报告.md`) | 已验证:信令、ICE、data channel、帧路由、STT→LLM→TTS、文件与浏览器真人非静音回复 |
|
||||
| 1b | **查清帧路由根因 → 定 A/B 方案** | ✅ **根因已定位,采用 A 方案** | NLTK 运行时下载导致 setup timeout;不是模型不兼容。网页 RTVI 保留 |
|
||||
| 1c | **Agent 与通用记忆工具化** | 🟡 **已选 Pipecat Flows,待兼容性 spike** | 保留现有实时链路与 MemoryStore;先文字、后语音,legacy 可回滚。见 `P1-Pipecat Flows Agent化与记忆改造交接.md` |
|
||||
| 2 | **N5095 客户端骨架** | 🔶 雏形已有(`client/test_client.py`) | 非 trickle ICE、data channel 心跳、文件音频实时发送、回复录音都已实现。**不做 AEC/播客参考/打断**——那些要真实声卡 |
|
||||
| 3 | **接入真实麦克风与音箱** | 阻塞:硬件未购;网页仅验证普通麦克风对话 | 唯一能完整验证阵列 AEC、打断、本地唤醒的一步,也是 P2 的全部内容 |
|
||||
| 4 | **STT 中文识别正确率** | 阻塞:需录音 | 见下"最大功能风险" |
|
||||
| 5 | ESP32 表情串口 | 硬件已到货,可独立推进 | 串口协议、表情渲染 |
|
||||
| 6 | 摄像头、多人跟踪、电机 | 阻塞:硬件未购 | — |
|
||||
|
||||
### 第 1 步的实验结论(2026-09-15)
|
||||
|
||||
**实时链路已闭环,A 方案成立。** 原 `process_frame` 为 0 的根因是 Pipecat
|
||||
setup 阶段预热 NLTK `punkt_tab`,首次下载阻塞后触发 20 秒 timeout;worker 尚未
|
||||
发送自动 StartFrame,并非 Pipeline 接线或自定义处理器模型不兼容。
|
||||
|
||||
修复后自动 StartFrame、250+ 个输入音频帧均正常经过 `EngineProcessor`,完整
|
||||
文件音频 → STT → DeepSeek → Qwen3-TTS → WebRTC 返回非静音语音。保留 RTVI
|
||||
和 `/client/` 网页入口,同时继续支持自写 aiortc/N5095 客户端。
|
||||
|
||||
实验累计挖出 **13 个集成坑**(客户端需发 ping、offer 无媒体则不收集
|
||||
ICE 候选、Playground 需要 `/start`、`run()` 要 WorkerParams、后台异常被静默吞掉等),
|
||||
每个都表现为"什么都没发生",详见实验报告。
|
||||
|
||||
**第 2 步的边界要说清楚**:用合成音频能验证传输层,**验不了 AEC 和远场识别**——
|
||||
那些必须等真实麦克风和音箱。但传输层能提前跑通,硬件一到就能直接接,
|
||||
不用同时调两件事。
|
||||
|
||||
**理由**:语音链路是全局瓶颈。它若走不通,后面的表情、转头做得再好也没用;
|
||||
而它若走通,其余部分都是可分解的独立工作。
|
||||
|
||||
### Agent、记忆与打断决策(2026-09-16)
|
||||
|
||||
当前系统是固定实时对话流水线加一个最多两轮的 `web_search` 工具循环,不是通用
|
||||
Agent。框架对比后决定渐进引入 **Pipecat Flows**,不整体迁移 OpenClaw、Agno、
|
||||
LangGraph、LiveKit Agents 或 smolagents。原因是现有 Pipecat/WebRTC 链路已经闭环,
|
||||
Flows 能提供状态、函数注册和对话节点,而身份、隐私、记忆与低延迟策略仍保留在
|
||||
项目内。
|
||||
|
||||
必须先验证接口差异:Flows 期待 Pipecat `LLMService + context aggregator`,当前
|
||||
DeepSeek fallback、历史、STT/TTS 和工具循环都在自有 `ConversationEngine` 中。
|
||||
因此先做独立 spike,再以 `legacy|flows` feature flag 渐进迁移,禁止直接替换线上
|
||||
路径。完整实施范围、测试和回滚要求见 [[P1-Pipecat Flows Agent化与记忆改造交接]]。
|
||||
|
||||
当前通用长期记忆尚未闭环:姓名是同步 confirmed 档案;“记住/我喜欢/我习惯”等
|
||||
规则仍统一写 candidate,默认检索排除,也没有确认晋升入口。当前打断也只是能量 VAD
|
||||
切分话段加任务取消能力;助手说话时检测到用户声音只记日志,尚未同时取消生成并清空
|
||||
客户端播放缓冲。这两项是 Flows 改造后的核心验收,不得写成“已完成”。
|
||||
|
||||
### 最大功能风险:STT 中文识别率
|
||||
|
||||
Zipformer 的速度没问题(服务器 RTF 0.03),但**中文真人识别率至今没有可信数据**——
|
||||
自带的 4 段测试音频没有参考文本,而 TTS→STT 往返法在这个模型上不成立
|
||||
(合成音频会触发大量重复字,真人录音却正常)。
|
||||
|
||||
音频硬件到手后应优先做:**录一组本人语音**,覆盖普通话、数字、日期、中英混读、
|
||||
以及真实桌面距离(0.5–2m)与背景噪声,计算 CER,并**同时与 SenseVoice、
|
||||
faster-whisper large-v3 对比**(两者已下载待测)。这是 P0 唯一未闭合的功能项。
|
||||
|
||||
### 首音延迟必须按真实链路重测
|
||||
|
||||
当前 1545ms 是**非流式、无 VAD、无网络传输、无播放缓冲**的链路数据。
|
||||
真实对话还要加上:分句等待、WebRTC 传输抖动、N5095 侧播放缓冲。
|
||||
**必须测 P50/P95,不能只看 RTF。** 加 VAD 后已处在 2 秒目标边缘,
|
||||
这些额外开销很可能把它推过线。
|
||||
|
||||
### 运维待办
|
||||
|
||||
- **没有常驻服务**:TTS 服务和助手本体都没有 systemd 开机自启
|
||||
(方案要求 P2 后配置)。服务器重启后需手动拉起
|
||||
- 日志保存耗时、错误、费用、连接事件并轮转,不默认保存完整私人对话
|
||||
- P2 后配置异常重启、音频/串口重连,测试拔插、断网、主机重启恢复
|
||||
- 记录板卡 SKU、固件、通道、引脚及接线图(ESP32 部分已有记录)
|
||||
- 模型文件授权与运行时授权分别记录(Qwen3-TTS 为 Apache-2.0;
|
||||
Matcha 用的 Baker 数据集**标注仅限非商用**,若将来产品化需换音色)
|
||||
|
||||
### 仍未确认
|
||||
|
||||
- **屏幕最终形态**:2.8 寸方屏 vs 原定 1.28 寸圆屏,影响外壳与转台设计
|
||||
- **使用距离**:暂按 0.5–2m 设计,未实测确认
|
||||
- 音频套件与摄像头的具体 SKU 与核价
|
||||
@@ -0,0 +1,126 @@
|
||||
# ESP32-S3 与 2.8 英寸 SPI 触摸屏接线记录
|
||||
|
||||
更新:2026-09-14。
|
||||
|
||||
## 设备与当前状态
|
||||
|
||||
- 主控:ESP32-S3 N16R8 开发板,已焊排针。
|
||||
- 屏幕:2.8英寸、240×320、SPI TFT,带触摸和SD卡座。
|
||||
- 显示驱动:当前程序按ILI9341配置,用户提供的型号尚未通过芯片读取确认。
|
||||
- 触摸驱动:当前按XPT2046兼容控制器配置;具体芯片型号尚未独立确认。
|
||||
- 用户已反馈屏幕接好并点亮、触摸线已接好;动态眼睛程序将X/Y同时翻转后,用户确认点击与视线方向正确。
|
||||
- SD卡功能暂不连接、不初始化。
|
||||
|
||||
## 接线表
|
||||
|
||||
GPIO编号指开发板丝印数字,不是排针从一端数起的序号。插拔杜邦线前先断开USB供电。
|
||||
|
||||
### 显示部分
|
||||
|
||||
| 屏幕标注 | ESP32-S3 | 说明 |
|
||||
|---|---|---|
|
||||
| VCC | 3V3(此前试接方案) | 用户反馈已亮,但未单独确认最终电源接法;按实物补核,不据此改接5V |
|
||||
| GND | GND | 公共地 |
|
||||
| CS | GPIO10 | 显示片选,低有效 |
|
||||
| RESET | GPIO8 | 显示复位,由程序控制 |
|
||||
| DC | GPIO9 | 显示数据/命令选择 |
|
||||
| SDI〈MOSI〉 | GPIO11 | 主控向屏幕发送数据 |
|
||||
| SCK | GPIO12 | SPI时钟 |
|
||||
| LED | ESP32-S3 的 3V3(用户已确认) | 直接接3.3V,背光常亮;不接GPIO,当前程序不控制背光亮度或开关 |
|
||||
| SDO〈MISO〉 | 不接 | 暂不读取显示芯片寄存器或像素,不影响写入图像 |
|
||||
|
||||
### 触摸部分
|
||||
|
||||
| 屏幕标注 | ESP32-S3 | 说明 |
|
||||
|---|---|---|
|
||||
| T_CLK | GPIO12 | 与显示SCK共用 |
|
||||
| T_DIN | GPIO11 | 与显示SDI/MOSI共用 |
|
||||
| T_DO | GPIO13 | 触摸数据返回,即MISO |
|
||||
| T_CS | GPIO7 | 触摸独立片选,不能与显示CS合并 |
|
||||
| T_IRQ | GPIO6 | 触摸通知;现有程序以轮询读取,暂不依赖中断 |
|
||||
|
||||
### 共用连线示意
|
||||
|
||||
```text
|
||||
ESP32-S3 GPIO12 ─┬─ 屏幕 SCK
|
||||
└─ 触摸 T_CLK
|
||||
|
||||
ESP32-S3 GPIO11 ─┬─ 屏幕 SDI / MOSI
|
||||
└─ 触摸 T_DIN
|
||||
|
||||
ESP32-S3 GPIO13 ─── 触摸 T_DO
|
||||
ESP32-S3 GPIO10 ─── 屏幕 CS
|
||||
ESP32-S3 GPIO7 ─── 触摸 T_CS
|
||||
ESP32-S3 GPIO9 ─── 屏幕 DC
|
||||
ESP32-S3 GPIO8 ─── 屏幕 RESET
|
||||
ESP32-S3 GPIO6 ─── 触摸 T_IRQ
|
||||
ESP32-S3 GND ─── 屏幕 GND
|
||||
```
|
||||
|
||||
GPIO11、GPIO12各需要分接两根线,可通过面包板或分线连接。触摸T_DO与显示SDO是不同引脚,目前只连接T_DO。SD卡接口所有信号保持不接,测试时不插SD卡。
|
||||
|
||||
## 软件配置
|
||||
|
||||
Arduino IDE开发板:`ESP32S3 Dev Module`(安装Espressif Systems提供的esp32开发板包)。
|
||||
|
||||
库管理器安装:
|
||||
|
||||
- Adafruit ILI9341
|
||||
- Adafruit GFX Library,以及安装提示中的依赖
|
||||
- XPT2046_Touchscreen,作者Paul Stoffregen
|
||||
|
||||
与接线对应的关键配置:
|
||||
|
||||
```cpp
|
||||
Adafruit_ILI9341 tft(&SPI, 9, 10, 8); // DC, CS, RESET
|
||||
XPT2046_Touchscreen touch(7); // T_CS;采用轮询
|
||||
|
||||
// 初始化总线前,两个片选均设为输出高电平。
|
||||
pinMode(10, OUTPUT);
|
||||
digitalWrite(10, HIGH);
|
||||
pinMode(7, OUTPUT);
|
||||
digitalWrite(7, HIGH);
|
||||
pinMode(6, INPUT_PULLUP);
|
||||
|
||||
SPI.begin(12, 13, 11, 10); // SCK, MISO, MOSI, SS
|
||||
tft.begin(10000000); // 显示SPI初始10MHz
|
||||
tft.setRotation(1); // 横屏320×240
|
||||
touch.begin(SPI);
|
||||
touch.setRotation(1);
|
||||
```
|
||||
|
||||
## 测试程序
|
||||
|
||||
### 笑脸与触摸基础测试
|
||||
|
||||
文件:`~/workspace/ai-companion/esp32_smile_test/esp32_smile_test.ino`(在代码仓库,不在本笔记库)。
|
||||
|
||||
- 启动切换红、绿、蓝,再显示笑脸。
|
||||
- 按下屏幕时笑脸在黄色/青色之间切换,显示触摸次数。
|
||||
- 串口115200输出原始坐标、压力及IRQ电平;原始坐标不是屏幕像素坐标。
|
||||
|
||||
### 动态眼睛测试
|
||||
|
||||
文件:`~/workspace/ai-companion/esp32_touch_eyes/esp32_touch_eyes.ino`(在代码仓库)。
|
||||
|
||||
- 已取消开机三点校准,直接显示眼睛,使用代码顶部的默认触摸范围。
|
||||
- 点击时眨眼,视线朝向触摸位置;按住移动可跟随。
|
||||
- 最后一次有效触摸后3秒,视线平滑回到正前方。
|
||||
- 连续15秒没有触摸后,随机眨眼、向周围看,并间歇回正;触摸立即接管。当前“无人互动”仅按触摸计时,不代表摄像头检测到无人。
|
||||
- 根据用户实测上下左右均反向,已设TOUCH_FLIP_X=true、TOUCH_FLIP_Y=true,TOUCH_SWAP_XY仍为false;重新上传后用户确认方向正确。边缘误差调整TOUCH_MIN/MAX_X/Y。IDLE_AFTER_MS控制闲置等待时间。
|
||||
|
||||
两个程序分别打开、分别上传,不将两个ino复制到同一草图目录。动态眼睛程序已获用户实机反馈:触摸与视线方向正确;长期运行及完整功能验收尚未记录。
|
||||
|
||||
## 排查提示
|
||||
|
||||
- 背光亮不代表显示初始化成功;白屏时检查ILI9341型号、CS/DC/RESET和SPI连线。
|
||||
- `XPT2046_Touchscreen.h: No such file or directory`表示缺库,先安装触摸库。
|
||||
- 显示正常但触摸无响应:检查T_DO→GPIO13、T_CS→GPIO7,以及共用的T_CLK/T_DIN。
|
||||
- 触摸方向不对:调整动态眼睛程序顶部的轴交换/翻转和范围参数,更改屏幕旋转方向后需重新检查映射。
|
||||
- 当前不要将触摸信号接5V,也不要用显示GPIO给电机供电。
|
||||
|
||||
## 参考与适用边界
|
||||
|
||||
同类[MSP2807模块接口说明](https://www.lcdwiki.com/2.8inch_SPI_Module_ILI9341_SKU:MSP2807)与实物照片的引脚名称相符,但不能由外观证明具体板卡版本完全一致。用户已确认LED直接接ESP32-S3的3V3,背光常亮;VCC的最终接法和触摸芯片型号仍需单独核对,不能由LED接法推定。
|
||||
|
||||
本记录仅适用于当前2.8英寸模块,不能直接套用到此前的GC9A01小圆屏。
|
||||
@@ -0,0 +1,113 @@
|
||||
# P0 验证报告 · DeepSeek API
|
||||
|
||||
日期:2026-09-14。机器:N5095 / Ubuntu 24.04.3 / Python 3.13.5。
|
||||
脚本:`~/workspace/ai-companion/p0/verify_deepseek.py`。结果:**11/11 通过**。
|
||||
|
||||
复现:
|
||||
|
||||
```bash
|
||||
set -a; . ~/.config/ai_assistant/env; set +a
|
||||
cd ~/workspace/ai-companion && python3 p0/verify_deepseek.py
|
||||
```
|
||||
|
||||
密钥存放:`~/.config/ai_assistant/env`(权限 600,不在项目目录内,不进版本管理)。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
| # | 项目 | 结果 |
|
||||
|---|---|---|
|
||||
| 0 | 模型清单 | 仅 `deepseek-flash`、`deepseek-v4-pro` |
|
||||
| 1a | 纯文本流式 | 可用 |
|
||||
| 1b | 思考内容分离 | on 模式有 `reasoning_content`;off 模式无 |
|
||||
| 1c | 首 token 延迟 | **P50 = 786ms,P95 = 1100ms**(N=8) |
|
||||
| 2a | 流式工具调用 | 可用,参数 JSON 正确 |
|
||||
| 2b | 工具结果回灌 | 可用 |
|
||||
| 2c | 思考模式约束 | 开启时**必须回传 `reasoning_content`** |
|
||||
| 3a | **图片输入** | **可用**,正确读出图中数字 |
|
||||
| 4a | 中途断开(打断) | 无异常 |
|
||||
| 4b | 超时 | 正确抛出 `TimeoutError` |
|
||||
| 4c | 错误模型名 | HTTP 400,可作启动自检 |
|
||||
|
||||
## 逐条说明
|
||||
|
||||
### 1. 模型名:方案文档正确,Pipecat 默认值错误
|
||||
|
||||
`GET /v1/models` 实际返回:
|
||||
|
||||
```json
|
||||
[{"id": "deepseek-flash"}, {"id": "deepseek-v4-pro"}]
|
||||
```
|
||||
|
||||
- 方案文档 §2 记的 `deepseek-flash` **正确**。
|
||||
- **Pipecat 的 `DeepSeekLLMService` 默认模型是 `deepseek-v4-flash`,该模型不存在**,直接用会 400。
|
||||
实例化时必须显式覆盖为 `deepseek-flash`。
|
||||
|
||||
### 2. 思考模式必须显式关闭(重要)
|
||||
|
||||
`deepseek-flash` 默认开启思考模式。此时 API 要求**把上一轮 assistant 消息里的
|
||||
`reasoning_content` 原样回传**,否则任何多轮对话与工具回灌都返回:
|
||||
|
||||
```
|
||||
400 invalid_request_error: The `reasoning_content` in the thinking mode must be passed back to the API.
|
||||
```
|
||||
|
||||
实测三种手工构造 assistant 消息的写法(`content=None` / `content=""` / 不带 tools)**全部 400**;
|
||||
只有回传服务端返回的原始 assistant 消息才通过。
|
||||
|
||||
**影响**:多数框架的上下文层按 OpenAI 格式存储消息,不保留 `reasoning_content`——
|
||||
一旦开启思考模式,Pipecat 的上下文管理与工具循环会持续踩这个坑。
|
||||
|
||||
**决定**:会话主路径传 `"thinking": {"type": "disabled"}`。两条理由:
|
||||
|
||||
1. 彻底消除 `reasoning_content` 回传要求,上下文层可安全裁剪消息;
|
||||
2. 开启时**第一个流式 chunk 是 `reasoning_content` 而非 `content`**,若直接送 TTS 会念出思考过程。
|
||||
关闭后两者合一,无需分流。
|
||||
|
||||
Pipecat 该服务的默认值恰好是 `DeepSeekThinkingConfig(type="disabled")`,与本结论一致。
|
||||
若将来要开思考以提升回答质量,必须同时改造上下文层保证 `reasoning_content` 不丢。
|
||||
|
||||
### 3. 首 token 延迟
|
||||
|
||||
N=8,短问答,`thinking=disabled`:**P50 = 786ms,P95 = 1100ms**(min 515 / max 1100)。
|
||||
|
||||
对照方案 P1 验收目标"短问答结束到首段有声回复 P50≤2 秒、P95≤4 秒",
|
||||
LLM 占约 0.8 秒,**余下约 1.2 秒预算**留给 STT 收尾判定 + TTS 首段合成。这个余量是否够,
|
||||
取决于本地 TTS 的 RTF(待测,见下)。
|
||||
|
||||
注:thinking 开关对首包时间影响不大(约 700ms vs 718ms),但关闭后总耗时更短,因为不再生成思考 token。
|
||||
|
||||
### 4. 图片输入可用(P5 的意外好消息)
|
||||
|
||||
`deepseek-flash` 接受 `image_url` 的 base64 data URI,正确读出图中文字("7391")。
|
||||
|
||||
方案 §P5 的视觉问答不需要另找模型,按需抓拍 → 缩放 → base64 上传即可。
|
||||
**未测**:视觉请求的 TTFT(预估显著长于纯文本)、大图 token 消耗、并发下的表现。
|
||||
按需视觉是按次计费项,接 P5 时需单独测成本。
|
||||
|
||||
### 5. 工具调用链路完整
|
||||
|
||||
流式工具调用可用,`finish_reason="tool_calls"`,参数 JSON 正确。
|
||||
回灌工具结果后模型能生成自然语言确认("客厅的灯已经打开了")。
|
||||
这是 P5 米家控制的基础链路,已验证通畅。
|
||||
|
||||
**注意**:回灌时 `tool_call_id` 必须用服务端返回的真实值,assistant 消息必须用服务端返回的原始对象,不能手工构造。
|
||||
|
||||
### 6. 打断与超时
|
||||
|
||||
- **打断**(收到若干 chunk 后主动 close):无异常抛出,连接正常释放。对应方案"打断时取消旧回复生成"的客户端能力成立。
|
||||
- **超时**:非流式长生成 + 2 秒预算,正确抛出 `TimeoutError`。
|
||||
- 流式下 socket 超时不会因首包快而触发——**"打断"和"超时"必须分开实现检测**,不能只做一种。
|
||||
|
||||
### 7. 计费与缓存
|
||||
|
||||
`usage` 返回 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`,实测命中 256/398。
|
||||
系统提示词固定可吃到缓存,对月费有利。方案 §8 要求的费用统计应记录这两个字段。
|
||||
|
||||
## 遗留与未验证
|
||||
|
||||
- 视觉请求的延迟与 token 成本未测。
|
||||
- 长上下文(多轮 + 记忆注入)下的延迟退化未测。
|
||||
- 并发请求未测(多用户场景下的表现)。
|
||||
- 本次仅验证 API 链路,**未接入 Pipecat、未写任何管线代码**。
|
||||
- 本机 `github.com` / `huggingface.co` 直连不通,需走本地 mihomo 代理 `127.0.0.1:7890`
|
||||
或国内镜像。DeepSeek API 直连正常,不需要代理。
|
||||
@@ -0,0 +1,166 @@
|
||||
# P0 实验报告 · Pipecat 传输层
|
||||
|
||||
日期:2026-09-15。状态:**实时语音链路已闭环;A 方案验证通过。**
|
||||
|
||||
代码:`~/workspace/ai-companion/services/bot_server.py`(服务端)、
|
||||
`client/test_client.py`(自写 aiortc 客户端)
|
||||
|
||||
## 一、为什么做这个实验
|
||||
|
||||
方案文档写"Pipecat 作为唯一会话管线框架",但核实后发现**它一行都没被用上**
|
||||
(只在 `requirements.txt` 里声明,代码里零引用)。助手引擎是独立 asyncio,
|
||||
由测试脚本用文件音频驱动。
|
||||
|
||||
于是有个待定的架构问题:**Pipecat 该用多深?**
|
||||
|
||||
- **A 方案**:Pipecat 只做传输与帧模型,会话语义归自有引擎(一个 FrameProcessor 吞掉整条管线)
|
||||
- **B 方案**:按 Pipecat 的 service 模型重构,STT/LLM/TTS/记忆都做成框架原生 service
|
||||
|
||||
实验目的:验证 A 方案的核心假设能否成立,并拿到真实传输下的延迟数字。
|
||||
|
||||
## 二、结论摘要
|
||||
|
||||
| 部分 | 结果 |
|
||||
|---|---|
|
||||
| WebRTC 信令(offer/answer) | ✅ 往返 47–280ms |
|
||||
| ICE 连通 | ✅ 走 172.16.1.x 直连 |
|
||||
| data channel + 心跳 | ✅ |
|
||||
| 双向媒体轨 | ✅ 客户端音轨送达服务端,服务端音轨客户端收到 |
|
||||
| 管线启动、处理器 setup() | ✅,只产生一个自动 StartFrame |
|
||||
| **帧路由到自定义处理器** | ✅ StartFrame 与 250+ 个音频帧实测到达 |
|
||||
| 端到端出声 | ✅ 文件音频 → STT → DeepSeek → Qwen3-TTS → 非静音回复 |
|
||||
| 浏览器真人对话 | ✅ SSH 隧道打开 Playground,麦克风输入与语音回复正常 |
|
||||
| 单次真实链路延迟 | 源音频结束→客户端首个非静音约 5.8s;仍需优化 |
|
||||
|
||||
**即:Pipecat 负责传输/帧/RTVI、自有引擎负责会话语义的 A 方案成立。**
|
||||
|
||||
## 三、原问题根因与修复
|
||||
|
||||
原先看到 `EngineProcessor.setup()` 被调用、`process_frame` 却为 0,于是误判为
|
||||
Pipeline 接线或模型不兼容。服务器完整日志揭示了真实顺序:Pipecat 在 setup 阶段
|
||||
预热 NLTK 分句器,首次运行尝试下载 `punkt_tab`,20 秒后 setup timeout。worker
|
||||
尚未走到自动发送 `StartFrame` 的阶段,因此所有业务帧都还在等待。
|
||||
|
||||
修复后把 `punkt_tab` 预装到权限 700 的
|
||||
`~/.local/share/ai-companion/nltk_data`,启动时 fail-fast 检查;同时改用
|
||||
`PipelineWorker(params=PIPELINE_PARAMS)`,删除客户端连接事件中手工发送的第二个
|
||||
`StartFrame`。复测显示自动 StartFrame 正常抵达管线末端,音频持续进入处理器。
|
||||
|
||||
之后仍不出声是第二个独立问题:文件客户端播放完 WAV 后没有继续推进 PTS,服务端
|
||||
收不到 VAD 所需的 700ms 尾静音。客户端现在固定发送 1.2s 尾静音且持续推进时钟。
|
||||
接收端也改为只有检测到非静音音频才算成功,不再把 WebRTC 空闲静音当作回复。
|
||||
|
||||
## 四、挖出的 13 个集成坑
|
||||
|
||||
每个都会表现为"什么都没发生",极难排查。无论最终选 A 还是 B 都会踩。
|
||||
|
||||
| # | 坑 | 现象 | 怎么办 |
|
||||
|---|---|---|---|
|
||||
| 1 | Pipecat 客户端 SDK 不覆盖 N5095 形态 | 浏览器/移动端/C++/ESP32 都有,没有"Linux x86 盒子接 USB 阵列" | 自己写 aiortc 客户端(已有雏形) |
|
||||
| 2 | 客户端必须建 data channel 并定期发 `"ping"` | 不建则服务端**一帧音频都不推**,日志只有一句 "Data channel not established within 10s" | 建 channel + 1s 心跳 |
|
||||
| 3 | offer 里没有媒体时 aiortc **完全不收集 ICE 候选** | 信令成功、SDP 交换正常,但 ICE 永远停在 `new` | 即使只测连通也要加一条(静音)音轨 |
|
||||
| 4 | ICE 收集完成前就发 offer | SDP 里 0 个候选地址 | 等 `iceGatheringState == "complete"` |
|
||||
| 5 | `PipelineWorker.run()` 要 `WorkerParams(task_manager=...)`,**不是** `PipelineParams` | 不传报 "missing 1 required positional argument";传错报 "no attribute 'task_manager'" | 两个参数分别给构造函数和 run() |
|
||||
| 6 | `TaskManager()` 内部调 `asyncio.get_running_loop()` | 模块级创建报 "no running event loop" | 必须在异步上下文里建 |
|
||||
| 7 | **后台任务里的异常被静默吞掉** | 日志里查不到任何线索,表现为"连接正常但没反应" | 自己 try/except + log.exception |
|
||||
| 8 | 信令回调里 `await task.run()` | 阻塞整个会话生命周期,管线初始化时序走歪 | `asyncio.create_task(task.run(...))` |
|
||||
| 9 | NLTK `punkt_tab` 在首个 worker 内临时下载 | setup 20 秒超时,看起来像帧路由坏了 | 部署时预装到私有目录,启动前检查 |
|
||||
| 10 | `PipelineWorker` 本身会自动发送 StartFrame | 手工再发会看到两个启动帧 | 删除连接回调里的手工 StartFrame |
|
||||
| 11 | 文件结束后静音帧 PTS 不增长 | VAD 永远等不到说完 | 源位置与媒体时钟分离,额外发送 1.2s 静音 |
|
||||
| 12 | WebRTC 输出轨空闲时也有静音 | “收到 34 秒音频/首音 206ms”的假成功 | 按非静音 RMS 判断业务回复 |
|
||||
| 13 | prebuilt Playground 连接前先请求 `/start` | 页面正常但 Connect 在 authenticating 阶段报 Not Found | 增加 `/start`,返回 `/api/offer` 的 `webrtcRequestParams` |
|
||||
|
||||
第 5、6、7 条各花了好几轮才发现——因为异常在后台任务里,日志什么都不显示。
|
||||
|
||||
## 五、对 A/B 决策的影响
|
||||
|
||||
实验前的判断是"Pipecat 的独特价值收敛到传输层"(因为方案 §5 的音频边界决定
|
||||
把 AEC 和播放放在客户端,绕开了 Pipecat 的打断机制)。
|
||||
|
||||
根因已经证明既不是 Pipeline 接线,也不是自定义处理器模型不兼容。因此确定采用
|
||||
**A 方案**:保留已验证的 `ConversationEngine`,由薄 `EngineProcessor` 适配
|
||||
Pipecat 的输入输出帧。网页客户端需要的 RTVI 继续开启;N5095 精简客户端可以忽略
|
||||
暂不认识的 RTVI data-channel 消息。
|
||||
|
||||
不做 B 方案的全量重构。后续只在确有收益时渐进引入 Pipecat 的 VAD、turn processor、
|
||||
metrics 与 interruption,不重写已有的记忆、身份、LLM/TTS 适配。
|
||||
|
||||
## 六、下一步
|
||||
|
||||
1. 优化真实首音延迟:分别记录 VAD、STT、LLM 首 token、TTS 首段和客户端播放时间
|
||||
2. 继续用 prebuilt 网页客户端做真人语音、浏览器 AEC 与主观试听(基础对话已通过)
|
||||
3. Tailscale tailnet 开启 Serve 后,用 HTTPS 暴露 `/client/`
|
||||
4. 音频硬件到货后实现 N5095 声卡、AEC、播放参考与打断
|
||||
5. 录本人普通话语料,对 Zipformer / SenseVoice / faster-whisper 做 CER 对比
|
||||
|
||||
## 七、当前代码状态
|
||||
|
||||
**服务端** `services/bot_server.py`:
|
||||
|
||||
- 已实现:SmallWebRTC 信令端点 `/api/offer`、prebuilt 网页客户端挂载 `/client/`、
|
||||
Playground 启动端点 `/start`、`EngineProcessor`(自写能量 VAD + 引擎调用)
|
||||
- 已改用 `PipelineWorker`,保留 RTVI;启动/超时/错误有显式日志
|
||||
- 已适配 RTVI `send-text` 产生的 `LLMMessagesAppendFrame`,网页文字输入与
|
||||
Conversation 区回复走自有引擎,不依赖框架原生 LLM service;Playground 1.7
|
||||
需补发完整 `bot-transcription` / `bot-output`,token 级事件不足以稳定渲染
|
||||
- 音频缓冲加入 1 秒预卷、60 秒上限和短噪声丢弃,连接结束释放会话资源
|
||||
- VAD 是**极简能量判定**,不是最终方案(方案 §5 的说完判定还没选型)
|
||||
- 当前没有唤醒门控:网页连接后每段有效语音都会触发回复。正式唤醒仍在 P2
|
||||
的 N5095 本地实现,待机时不上传音频;本次按用户决定暂不处理
|
||||
- 已接入联网搜索:DeepSeek 通过 Function Calling 请求 `web_search`,执行层调用
|
||||
仅绑定 `127.0.0.1:18080` 的私有 SearXNG,结果作为 `tool` 消息回灌后继续生成;
|
||||
网页保留来源 URL,TTS 不朗读来源段
|
||||
- 2026-09-16 真实闭环验收通过:模型主动搜索,SearXNG 返回 5 条结果,最终回答带
|
||||
两个来源;服务器网络下 360 搜索、搜狗可用,DuckDuckGo/Brave/Wikipedia 超时
|
||||
- SearXNG 的完整选型、部署、API、运维和排错记录见
|
||||
[[SearXNG私有搜索服务部署与使用]]
|
||||
|
||||
**客户端** `client/test_client.py`:
|
||||
|
||||
- 已实现:非 trickle ICE、data channel + 心跳、文件音频实时发送、1.2s 尾静音、回复录音
|
||||
- 只有检测到非静音回复才成功;按 WebRTC 实际 48kHz 保存 WAV
|
||||
- **不做 AEC、不做播放参考、不做打断**——那些要真实声卡和硬件
|
||||
- 它是 N5095 客户端的雏形,不是产品代码
|
||||
|
||||
## 八点五、2026-09-16 后续架构补充
|
||||
|
||||
媒体传输闭环不等于 Agent、长期记忆和实时打断已经闭环。真人网页测试后确认:
|
||||
|
||||
- 姓名档案已经能跨连接恢复,但通用“记住/偏好”仍写 candidate,默认不会召回;
|
||||
- 当前 `SimpleVAD` 能判断 700ms 尾静音,助手说话时检测到用户声音却只记日志;
|
||||
- `InterruptionFrame` 和异步任务取消能力存在,但没有由 user-started-speaking
|
||||
可靠触发,也没有保证清空浏览器已经缓冲的音频;
|
||||
- 当前只有 `web_search` 的手写工具循环。
|
||||
|
||||
后续 Agent 层选择 **Pipecat Flows 渐进集成**,继续坚持 A 方案,不重写已经验证的
|
||||
传输层。因为 Flows 使用 Pipecat `LLMService` 与 context aggregator,而项目当前
|
||||
使用自有 `ConversationEngine`,必须先做独立兼容性 spike,并保留 legacy feature
|
||||
flag。详细交接见 [[P1-Pipecat Flows Agent化与记忆改造交接]]。
|
||||
|
||||
## 八、复现
|
||||
|
||||
```bash
|
||||
# 服务端(服务器上)
|
||||
cd ~/ai-companion
|
||||
.venv/bin/python services/bot_server.py --host 0.0.0.0 --port 7860
|
||||
|
||||
# 客户端(N5095 上)
|
||||
cd ~/workspace/ai-companion
|
||||
python client/test_client.py \
|
||||
--server http://cadadmin-server.taild0f91d.ts.net:7860 \
|
||||
--wav p0/stt_tts_bench/wavs/stt_1.wav --out /tmp/reply.wav
|
||||
```
|
||||
|
||||
当前实测:连接成功,收到非静音语音回复;源音频结束到首个非静音约 5.8 秒。
|
||||
浏览器通过 SSH 隧道访问 `http://localhost:7860/client/` 也已完成真人对话;
|
||||
2026-09-16 一轮服务端指标约 3.0 秒。
|
||||
|
||||
## 九、顺带发现的环境问题
|
||||
|
||||
**局域网被重新编址了**:两台机器从 `192.168.0.x` 变成 `172.16.1.x`
|
||||
(N5095 现为 `172.16.1.99`,服务器 `172.16.1.101`)。
|
||||
|
||||
已处理:客户端默认地址改用 tailscale 域名
|
||||
(`cadadmin-server.taild0f91d.ts.net`),抗重新编址。
|
||||
|
||||
**未处理**:方案文档与 N5095 配置里仍有 `192.168.0.20` 的残留引用。
|
||||
@@ -0,0 +1,128 @@
|
||||
# P0 报告 · Qwen3-TTS 选型与部署
|
||||
|
||||
日期:2026-09-15。起因:用户试听后反馈 vits-zh-ll 音色不满意,
|
||||
要求换"体量更大效果更好的模型",并提出 LLM 可以用线上 API、
|
||||
把 GPU 让给语音。
|
||||
|
||||
服务:`~/tts-qwen/`(服务器)· 模型:`~/tts-service/models/Qwen3-TTS-1.7B-CustomVoice`
|
||||
|
||||
## 一、为什么选 Qwen3-TTS
|
||||
|
||||
查证后的结论(来源见文末):
|
||||
|
||||
- **实时对话场景的首选是 Qwen3-TTS 或 CosyVoice 2/3**;
|
||||
IndexTTS2 和 F5-TTS 虽然中文指标也强,但流式不是它们的目标,不做实时助手首选
|
||||
- Qwen3-TTS 官方标称**流式首包 97ms**,RTX 3090 上实测约 130ms
|
||||
- 官方推荐硬件就是 **RTX 3090/4090 24GB**,与现有硬件吻合
|
||||
- Apache-2.0,10 种语言 + 中文方言
|
||||
- 9 个预置音色 + **自然语言风格指令**(这是 vits 那一代没有的能力)
|
||||
|
||||
## 二、部署路上撞到的两个障碍
|
||||
|
||||
### 障碍 1:vLLM-Omni 装不起来能用的服务
|
||||
|
||||
原本的打算是走 vLLM-Omni(它提供 OpenAI 兼容的流式 `/v1/audio/speech`
|
||||
分块接口,是延迟最优的路径)。两个问题叠加:
|
||||
|
||||
1. **`--task-type` 参数没被注册**。vllm-omni 把 CLI 扩展注册在
|
||||
`vllm.general_plugins`,但 vllm 0.28 的 serve 解析器没加载它,
|
||||
`vllm-omni serve --help` 里根本看不到这个参数。
|
||||
2. **绕过后仍崩溃**。模型架构能正确识别
|
||||
(`Resolved architecture: Qwen3TTSForConditionalGeneration`),
|
||||
但 vLLM 因为模型 config 里的 mrope 参数
|
||||
(transformers 5.14 报 `Unrecognized keys in rope_parameters:
|
||||
{'interleaved', 'mrope_section'}`)把它当多模态模型走 mm 路径,
|
||||
崩在 `assert isinstance(model, SupportsMRoPE)`。
|
||||
|
||||
根因是 **transformers / vllm / vllm-omni 三者的版本组合问题**,
|
||||
不是配置错误。没有继续深挖。
|
||||
|
||||
### 障碍 2:qwen-tts 与 vllm-omni 依赖互斥
|
||||
|
||||
改用厂商自己的 `qwen-tts` 包后,发现它会把 transformers 从
|
||||
**5.14.1 降到 4.57.3**、huggingface_hub 从 **1.31.0 降到 0.36.2**,
|
||||
直接破坏 vllm-omni(报 `StrictDataclassFieldValidationError`)。
|
||||
|
||||
**结论:两者不能共存于一个 venv。** 最终分两个 venv,
|
||||
TTS 跑独立进程,助手通过 HTTP 调用。
|
||||
|
||||
## 三、实测数据
|
||||
|
||||
硬件:RTX 3090 24GB,1.7B 模型,非流式,`Serena` 音色。
|
||||
|
||||
| 文本 | 音频时长 | 合成耗时 | RTF |
|
||||
|---|---|---|---|
|
||||
| 好的,我记住了。(8字) | 1.52s | 1429ms | **0.94** |
|
||||
| 今天北京晴转多云…(19字) | 3.36s | 1881ms | 0.56 |
|
||||
| 我查了一下,明天上午…(26字) | 4.64s | 2527ms | 0.55 |
|
||||
|
||||
**关键发现:短句 RTF 反而最差。**
|
||||
每次调用有约 1 秒的固定开销,与文本长度无关
|
||||
(talker 模型自回归解码的启动成本 + 声码器解码)。
|
||||
|
||||
这条推翻了"让模型先说个短句来降低首音延迟"的直觉——
|
||||
短句单位时间成本更高,反而不划算。
|
||||
|
||||
输出:**24kHz 单声道**(vits-zh-ll 是 16kHz,接播放链路时注意重采样)。
|
||||
|
||||
## 四、用户选定的配置
|
||||
|
||||
试听样本见 `out/tts_qwen/`。用户选定:
|
||||
|
||||
```
|
||||
音色:Serena —— 温暖柔和的年轻女声
|
||||
指令:用轻快活泼的语气说
|
||||
```
|
||||
|
||||
(`instruct_7849.wav`,1.44s。通过时长比对确认对应关系:
|
||||
默认 2.56s、轻快活泼 1.44s、温柔关切 1.72s。)
|
||||
|
||||
9 个预置音色中 5 个是中文:
|
||||
Vivian(明亮微冷)、**Serena(温暖柔和)**、Uncle_Fu(低沉男声)、
|
||||
Dylan(京腔男声)、Eric(成都话男声)。
|
||||
|
||||
## 五、代价:首音延迟涨了 8.6 倍
|
||||
|
||||
| 配置 | 首音延迟 |
|
||||
|---|---|
|
||||
| 本地 Qwen3.8-27B + vits-zh-ll | **179 ms** |
|
||||
| DeepSeek + Qwen3-TTS | **1545 ms** |
|
||||
|
||||
拆解这 1545ms:DeepSeek 网络往返约 600ms + Qwen3-TTS 固定开销约 900ms。
|
||||
|
||||
加上 VAD 判定(200–500ms),总首音落在 **2.0–2.2 秒**,
|
||||
方案 P1 目标是 P50≤2 秒——**刚好卡线,没有余量**。
|
||||
|
||||
换来的是音质从"用户不满意"到"用户试听选定"。
|
||||
|
||||
## 六、速度还能救,但都受阻
|
||||
|
||||
| 手段 | 预估收益 | 障碍 |
|
||||
|---|---|---|
|
||||
| flash-attn | 1.5–2× | 预编译 wheel 最高支持 torch 2.9,本机 2.14;源码编译要从 GitHub 拉依赖而该机下 GitHub release 断连 |
|
||||
| 真流式(接管 codes→音频 解码循环) | 可绕开约 900ms 固定开销 | qwen-tts 封装把解码放在全部 codes 生成完之后,拿不到增量;要自己实现 |
|
||||
| 降 torch 版本装 flash-attn | 1.5–2× | 会牵动 vLLM 0.28 的依赖 |
|
||||
|
||||
**真流式的收益最大**——那 900ms 固定开销正是流式能覆盖掉的部分。
|
||||
但工作量也最大。
|
||||
|
||||
## 七、备份路线(未采用)
|
||||
|
||||
若 Qwen3-TTS 的速度最终不可接受,可考虑的替代:
|
||||
|
||||
- **CosyVoice 2/3**:同为实时对话第一梯队,Apache-2.0,工程成熟度最高,
|
||||
18+ 中文方言。但搜索结果里首包延迟数据有冲突(150ms vs 1.5s),需实测。
|
||||
- **云端 TTS API**:最快出效果,但助手要说的话会出网,
|
||||
与方案"不自动发送音频到第三方"的口径有冲突(发的是文本不是音频,
|
||||
但内容一样出网),且按量计费。
|
||||
|
||||
未测:CosyVoice 的本地部署与本机延迟。
|
||||
|
||||
## 八、来源
|
||||
|
||||
- [2026 开源 TTS 模型横向对比](https://juejin.cn/post/7649764223339495475)
|
||||
- [Best Open-Source TTS Models for Production in 2026](https://instavar.com/research/tts/open-source-tts-models)
|
||||
- [Qwen3-TTS 与 vLLM 集成部署](https://blog.csdn.net/weixin_30633869/article/details/157918741)
|
||||
- [Speech Generation on vLLM-Omni:性能优化](https://docs.vllm.ai/projects/vllm-omni/en/stable/design/qwen3_omni_tts_performance_optimization/)
|
||||
- [vllm-omni PR #3932:Code2Wav CUDA Graph 调优](https://github.com/vllm-project/vllm-omni/pull/3932)
|
||||
- 模型 README:`~/tts-service/models/Qwen3-TTS-1.7B-CustomVoice/README.md`
|
||||
@@ -0,0 +1,169 @@
|
||||
# P0 验证报告 · 本地 Qwen3.8-27B
|
||||
|
||||
> ⚠️ **状态:该服务已于 2026-09-15 停用。**
|
||||
> 原因:用户要求换更强的语音模型(vits-zh-ll 音色不满意),
|
||||
> 而 3090 只有 24GB——27B 的 vLLM 占 21.5GB,装不下 Qwen3-TTS。
|
||||
> 决定让 LLM 走云端 DeepSeek,GPU 让给语音。见 `P0-Qwen3TTS报告.md`。
|
||||
>
|
||||
> **本报告的结论仍然有效**(用于将来重启该服务,或评估其它本地 LLM):
|
||||
> 关思考可让首 token 从 1390ms 降到 367ms;思考 token 会吃掉 max_tokens 预算;
|
||||
> 只接受一条 system 消息;思考字段名与 DeepSeek 不同。
|
||||
|
||||
日期:2026-09-14。端点:`http://192.168.0.20:18020/v1`(vLLM,局域网 1.4ms)。
|
||||
脚本:`~/workspace/ai-companion/p0/verify_local_llm.py`。结果:**5/6 通过**(未通过项为视觉,见 §5)。
|
||||
|
||||
复现:
|
||||
|
||||
```bash
|
||||
set -a; . ~/.config/ai_assistant/env; set +a
|
||||
cd ~/workspace/ai-companion && python3 p0/verify_local_llm.py
|
||||
```
|
||||
|
||||
## 一、服务实况
|
||||
|
||||
方案文档假设 LLM 用 DeepSeek 云 API。实测发现局域网内已有可用服务:
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 服务 | **vLLM**(不是 llama.cpp,服务器上两套都有) |
|
||||
| 模型 | `Qwen3.8-27B-W4A16-AutoRound-fast`(4bit 权重,16bit 激活) |
|
||||
| `max_model_len` | 65536 |
|
||||
| 思考解析 | `--reasoning-parser qwen3` |
|
||||
| 到 N5095 延迟 | **1.4ms** |
|
||||
| 视觉 | **不支持**(见 §5) |
|
||||
|
||||
## 二、思考模式:可关,且必须关
|
||||
|
||||
关闭方式(**已实测有效**):
|
||||
|
||||
```json
|
||||
"chat_template_kwargs": {"enable_thinking": false}
|
||||
```
|
||||
|
||||
实测三种模式(同一个数学题,`max_tokens=512`):
|
||||
|
||||
| 模式 | 思考字数 | 完成 tokens | 首 token(回答) |
|
||||
|---|---|---|---|
|
||||
| 默认(不传) | 168 | 214 | 527ms |
|
||||
| **enable_thinking=false** | **0** | 512 | **81ms** |
|
||||
| enable_thinking=true | 289 | 239 | 620ms |
|
||||
|
||||
**开思考的代价不只是慢**。看上面第二组数字:开思考时完成 tokens 只有 239,
|
||||
而关思考是 512——因为思考 token 占用 `max_tokens` 预算。
|
||||
|
||||
在真实语音 prompt 上这个问题更严重(`max_tokens=200`):
|
||||
|
||||
| 模式 | 首 token(回答)P50 | 完成 tokens 中位 | 实际回答 |
|
||||
|---|---|---|---|
|
||||
| **关思考** | **367ms** | 10 | 「物流显示昨天已签收。」 |
|
||||
| 开思考 | **1390ms** | 187 | 「我不」 ← **被截断** |
|
||||
|
||||
开思考时首 token 慢 3.8 倍,而且 200 的预算被思考吃光,回答只吐出两个字。
|
||||
|
||||
## 三、对比 DeepSeek 云 API
|
||||
|
||||
| 指标 | 本机 Qwen3.8-27B | DeepSeek `deepseek-flash` |
|
||||
|---|---|---|
|
||||
| 首 token(关思考) | **367ms** | 786ms |
|
||||
| 网络延迟 | 1.4ms | 含在 786ms 内 |
|
||||
| 思考模式 | 可关(`chat_template_kwargs`) | 可关(`thinking`),但字段名不同 |
|
||||
| 视觉 | ❌ 本实例不支持 | ✅ 可用 |
|
||||
| 费用 | 0(本地) | 按 token 计费 |
|
||||
| 数据 | 不出局域网 | 上传第三方 |
|
||||
|
||||
**本机首 token 比云端快一倍多**,因为省掉了网络往返与排队。
|
||||
|
||||
## 四、两个会改设计的集成约束
|
||||
|
||||
### 1. 思考内容的字段名不一致
|
||||
|
||||
| 后端 | 字段名 |
|
||||
|---|---|
|
||||
| 本机 vLLM | `reasoning` |
|
||||
| DeepSeek | `reasoning_content` |
|
||||
|
||||
适配层**必须两个都认**。只按 DeepSeek 的字段名写,会静默丢弃思考内容——
|
||||
不报错,但"把思考念出来"或"思考占用显示"这类问题会莫名其妙出现。
|
||||
|
||||
### 2. 只允许一条 system 消息
|
||||
|
||||
发送两条 system 消息直接 400:
|
||||
|
||||
```
|
||||
System message must be at the beginning.
|
||||
```
|
||||
|
||||
**这推翻了原记忆设计**。原方案打算"检索到的记忆作为一条独立 system message 注入",
|
||||
在本机 vLLM 上会直接失败。
|
||||
|
||||
修正后的做法:**保持唯一一条 system 消息,每轮重建**:
|
||||
|
||||
```
|
||||
system = 人设 + 规则 + 本轮检索到的记忆
|
||||
```
|
||||
|
||||
这其实更干净,也和"注入的记忆每轮替换而非追加"的要求一致。
|
||||
|
||||
## 五、未通过项:视觉
|
||||
|
||||
本实例返回:
|
||||
|
||||
```
|
||||
At most 0 image(s) may be provided in one prompt.
|
||||
```
|
||||
|
||||
模型目录 `Qwen3.8-27B-W4A16-AutoRound-fast/` 只有 safetensors 与 config,
|
||||
**没有视觉塔**,是纯文本部署。
|
||||
|
||||
服务器上另有视觉能力:`~/models/mmproj-F16.gguf`(928MB)配合
|
||||
llama.cpp 的 `128k_vision.sh`,以及 llama.cpp 的 `--mmproj` 参数。
|
||||
但那是另一套服务,需要单独起。
|
||||
|
||||
**影响**:方案 §P5 的视觉问答在本端点不可用,需要么另起一个带 mmproj 的
|
||||
llama.cpp 实例,要么走 DeepSeek。留到 P5 再定。
|
||||
|
||||
## 六、其余验证项
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 工具调用(关思考) | ✅ `set_light({"room":"客厅","on":true})` |
|
||||
| 纯文本流式 | ✅ |
|
||||
| 端点与模型清单 | ✅ |
|
||||
|
||||
**未测**:取消/超时、长上下文退化、多客户端并发。
|
||||
|
||||
## 七、对 P1 延迟预算的影响
|
||||
|
||||
更新后的预算(用本机 LLM):
|
||||
|
||||
| 环节 | 耗时 | 来源 |
|
||||
|---|---|---|
|
||||
| VAD 判定说完 | 200–500ms | 估算,待实测 |
|
||||
| STT 冲刷 | ≈0ms | 实测 |
|
||||
| **LLM 首 token** | **367ms** | 实测 |
|
||||
| TTS 首音(vits-zh-ll @服务器) | 57–152ms | 实测 |
|
||||
| 网络 | 1.4–2ms | 实测 |
|
||||
| **合计** | **约 0.6–1.0 秒** | |
|
||||
|
||||
方案 P1 目标 P50≤2 秒。**余量超过一倍。**
|
||||
|
||||
## 八、上下文建议
|
||||
|
||||
实测语音 prompt 约 406 tokens(人设 + 记忆 + 3 轮对话)。
|
||||
建议按 4k tokens 设上限,超出时截断最旧的轮次:
|
||||
|
||||
- system(人设 + 本轮记忆):400–800 tokens
|
||||
- 最近对话轮次:500–1000 tokens
|
||||
- 当前输入:~50 tokens
|
||||
|
||||
vLLM 服务端的 `max_model_len=65536` 是全局配置,**不需要为助手改动**
|
||||
(那是共享服务)——短上下文靠客户端控制实际 prompt 长度即可。
|
||||
短上下文不只省资源,还直接降低首 token 延迟,这对"回应及时"是关键。
|
||||
|
||||
## 九、工程陷阱记录
|
||||
|
||||
1. **本机 vLLM 思考字段名是 `reasoning`**,不是 `reasoning_content`(DeepSeek 的写法)。
|
||||
2. **只接受一条 system 消息**,多条直接 400。
|
||||
3. **开思考会吃掉 `max_tokens` 预算**,导致回答被截断。
|
||||
语音场景下 `max_tokens` 本来就要设小,这个问题会被放大。
|
||||
4. N5095 上系统 `python3` 无 PIL,测试脚本要用 venv 里的解释器。
|
||||
@@ -0,0 +1,228 @@
|
||||
# P0 基准报告 · 本地 STT / TTS
|
||||
|
||||
> ⚠️ **TTS 选型已于 2026-09-15 变更。**
|
||||
> 本报告选出的 vits-zh-ll 虽然速度达标(服务器 RTF 0.09),
|
||||
> 但用户试听反馈**音色不满意**,最终换成 Qwen3-TTS 1.7B(RTF 0.55–0.94)。
|
||||
> 见 `P0-Qwen3TTS报告.md`。
|
||||
>
|
||||
> **本报告的核心结论仍然有效且在用**:
|
||||
> - STT 线程数不能按"核多给多"猜(N5095 上 4 线程比 2 线程慢 3.9 倍)
|
||||
> - 冲刷耗时 ≈ 0,流式解码已持续输出
|
||||
> - VITS 非自回归、`max_num_sentences=1` 会静默截断——这两条对 Qwen3-TTS 同样适用
|
||||
> - STT 中文识别率未测(无参考文本),此结论仍然成立
|
||||
>
|
||||
> 另:本报告测出的"服务器(i9-14900K)上 STT RTF 0.03"仍是当前生产配置。
|
||||
|
||||
日期:2026-09-14。机器:Celeron N5095(4 核,**无 AVX2**)/ Ubuntu 24.04.3 / 16G。
|
||||
脚本目录:`p0/stt_tts_bench/`。全部为实测数据,非估算。
|
||||
|
||||
复现:
|
||||
|
||||
```bash
|
||||
cd ~/workspace/ai-companion/p0/stt_tts_bench
|
||||
./download_models.sh # 走 hf-mirror
|
||||
python3 bench_stt.py # STT
|
||||
python3 bench_tts_compare.py # TTS 横向对比
|
||||
python3 bench_concurrent.py # 并发
|
||||
python3 bench_roundtrip.py # 往返
|
||||
python3 bench_threads.py # 线程扫描
|
||||
```
|
||||
|
||||
## 一、结论
|
||||
|
||||
**本地语音方案成立,但必须换 TTS 模型。**
|
||||
|
||||
内核:**STT 没问题,vits-zh-ll 不行,Piper 可以。**
|
||||
|
||||
| 组件 | 选型 | RTF | 判定 |
|
||||
|---|---|---|---|
|
||||
| STT | 流式 Zipformer 中英双语 int8 | 0.22(单跑)/ 0.50(并发) | ✅ 可用 |
|
||||
| TTS | vits-zh-ll(115MB) | **1.86** | ❌ 慢于实时,不可用 |
|
||||
| TTS | **piper-huayan-medium(61MB)** | **0.36** | ✅ 推荐 |
|
||||
| TTS | piper-huayan-x_low(20MB) | 0.17 | ⚠ 快但音质存疑,待试听 |
|
||||
|
||||
## 二、STT
|
||||
|
||||
### 速度
|
||||
|
||||
| 线程数 | RTF 中位 | 单块解码 p95 | 判定 |
|
||||
|---|---|---|---|
|
||||
| 1 | 0.37 | 1.19 | 平均够,但存在突发积压 |
|
||||
| **2** | **0.22** | **0.79** | ✅ 最佳 |
|
||||
| 3 | 0.63 | — | 反而变慢 |
|
||||
| 4 | 0.86 | 3.78 | 严重变慢 |
|
||||
|
||||
**关键发现:线程数超过 2 会急剧退化**,2→4 线程 RTF 从 0.22 涨到 0.86(慢 3.9 倍)。
|
||||
重复 5 轮稳定复现,不是噪声。
|
||||
|
||||
原因判断:N5095(Jasper Lake)是**单通道内存**,且 L2 仅 1.5MB、L3 仅 4MB。
|
||||
并且 onnxruntime 空闲工作线程默认自旋占核;4 个 ORT 线程 + Python 主线程 = 5 线程抢 4 个核,
|
||||
主线程(负责喂音频)被饿死。
|
||||
|
||||
→ **实现时 STT 固定 `num_threads=2`。** 不要"核多就多给线程"。
|
||||
|
||||
### 其他指标
|
||||
|
||||
- **首字延迟**:喂进 0.5~1.4 秒音频后出现第一个中间结果。这是模型的算法前瞻量,与算力无关。
|
||||
- **冲刷耗时 = 0ms**:`input_finished()` 后立即拿到最终结果。流式解码过程中已经持续输出,
|
||||
收尾不额外花钱。**"说完到出结果"的开销主要在 VAD 判定说完的等待,不在识别器。**
|
||||
- 注:并非每个 100ms 块都触发解码(约每 3 块解一次,即模型 chunk 约 300ms),
|
||||
所以"单块 p95 > 1"不等于积压,应看 RTF。
|
||||
|
||||
### 识别正确率:未测
|
||||
|
||||
4 段测试音频是 sherpa-onnx 自带的中英双语真人录音,**没有参考文本**,无法算字错率。
|
||||
|
||||
尝试用"TTS 合成已知文本 → 喂给 STT"来算 CER,但**这个方法在这台机器上不成立**:
|
||||
合成音频会让 STT 产生大量重复("晴晴转转"、"月月月"),而同样的解码配置下真人录音完全正常。
|
||||
换 greedy / beam_search、换块大小、开关端点检测,四种配置结果完全一致 —— 说明问题出在合成音频
|
||||
本身,不是解码配置。
|
||||
|
||||
**结论:STT 在中文真人语音上的正确率目前没有可信数据。** 需要一份带标注的中文真人测试集,
|
||||
或用麦克风录一批带文本的语音。这是 P0 尚未闭合的一项,不能靠现有数据下结论。
|
||||
|
||||
## 三、TTS
|
||||
|
||||
### 速度
|
||||
|
||||
| 模型 | 大小 | RTF(2线程) | 19字句首音延迟 | 判定 |
|
||||
|---|---|---|---|---|
|
||||
| vits-zh-ll | 115MB | 1.86 | 2748ms | ❌ |
|
||||
| piper-x_low | 20MB | 0.17 | 585ms | ✅ 快 |
|
||||
| piper-huayan-medium | 61MB+espeak | 0.36 | ~650ms | ✅ 推荐 |
|
||||
|
||||
**VITS 非自回归**:验收时确认回调只在合成完成后触发一次,
|
||||
所以**首音延迟 = 总合成耗时**,没有"边合成边播"的可能。
|
||||
分句播放是唯一的提前出声手段(`max_num_sentences=1` 逐句合成)。
|
||||
|
||||
### 连续播放模拟(5 句)
|
||||
|
||||
| 模型 | 各句空档 | 最大空档 |
|
||||
|---|---|---|
|
||||
| vits-zh-ll | 2.0 / 1.6 / 0.5 / 2.9 / 3.8 秒 | **3.80s** |
|
||||
| piper-x_low | 0.5 / 0 / 0 / 0 / 0 秒 | 0.47s(仅首句) |
|
||||
|
||||
vits-zh-ll 每句之间都会出现 0.5~3.8 秒的静默空档,说话断断续续。
|
||||
Piper 除首句外无空档。
|
||||
|
||||
### 音质:必须人工试听
|
||||
|
||||
往返测试的 CER 不可信(见上),但可以给出**相对**信号:
|
||||
|
||||
| 模型 | 往返 CER | 输出峰值 | 输出 RMS |
|
||||
|---|---|---|---|
|
||||
| vits-zh-ll | 0.25 | **0.15** | 0.045 |
|
||||
| piper-x_low | 0.72 | 0.49 | 0.092 |
|
||||
| piper-huayan-medium | 0.32 | 0.59 | 0.105 |
|
||||
|
||||
两点值得注意:
|
||||
|
||||
1. **vits-zh-ll 输出音量极低**(峰值 0.15,只有 Piper 的 1/4)。接音箱时必须补增益,
|
||||
否则要么听不见,要么底噪被一起放大。
|
||||
2. **piper-x_low 音质存疑**:合成时有 `Unknown token: ̪` 警告,且往返 CER 明显偏差。
|
||||
速度虽好(RTF 0.17),但**不建议直接用**。
|
||||
|
||||
已生成三个模型同一句话的试听文件,请人工判断:
|
||||
|
||||
```
|
||||
~/workspace/ai-companion/p0/stt_tts_bench/listening_test/vits-zh-ll.wav
|
||||
p0/stt_tts_bench/listening_test/piper-x_low.wav
|
||||
p0/stt_tts_bench/listening_test/piper-medium.wav
|
||||
```
|
||||
|
||||
**音质只能靠耳朵定,仪器测不出来。这一项在试听前不能算通过。**
|
||||
|
||||
## 四、并发(这是最接近真实负载的测试)
|
||||
|
||||
助手说话时麦克风不关(要检测打断),所以 STT 必须与 TTS 同时跑。
|
||||
|
||||
| TTS 模型 | STT 单跑 | TTS 单跑 | STT 并发 | TTS 并发 | 退化 | 判定 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| piper-x_low | 0.26 | 0.19 | **0.50** | **0.33** | 1.9× / 1.7× | ✅ 余量 2.0× / 3.1× |
|
||||
| vits-zh-ll | 0.26 | 1.97 | 0.48 | **2.39** | 1.9× / 1.2× | ❌ 跟不上 |
|
||||
|
||||
并发下两者都退化约 1.8 倍(4 核争抢所致,符合预期)。
|
||||
用 Piper 时 STT 0.50 + TTS 0.33,都远低于 1,**架构成立**。
|
||||
|
||||
但这只是 STT + TTS 两项。方案还要同时跑人脸检测/跟踪。视觉再加进来是否还够,
|
||||
需要单独测(未测)。
|
||||
|
||||
## 五、对方案 P1 指标的影响
|
||||
|
||||
方案 P1 目标:"短问答结束到首段有声回复,P50≤2 秒、P95≤4 秒"。
|
||||
|
||||
用实测数据推算(`thinking` 关闭):
|
||||
|
||||
| 环节 | 耗时 | 来源 |
|
||||
|---|---|---|
|
||||
| VAD 判定说完 | 200~500ms(估) | 待实测 |
|
||||
| STT 冲刷 | ≈0ms | 实测 |
|
||||
| LLM 首 token | 786ms(P50) | P0-DeepSeek 报告 |
|
||||
| TTS 首音(Piper medium,短句) | ~650ms | 实测 |
|
||||
| **合计** | **约 1.6~2.0 秒** | |
|
||||
|
||||
**用 Piper 时 P50 目标勉强可达**,但没有富余;P95 需要看网络抖动。
|
||||
用 vits-zh-ll 则完全不可能(TTS 单项就 2.7 秒)。
|
||||
|
||||
## 六、未测事项(不要当成已通过)
|
||||
|
||||
- **STT 中文真人识别率**:无参考文本,未测。
|
||||
- **TTS 音质**:待人工试听 `listening_test/`。
|
||||
- **视觉并发**:人脸检测加入后的 CPU 争抢未测。
|
||||
- **真实音频链路**:麦克风阵列、远场、AEC、回声参考全部未测,无硬件。
|
||||
- **int8 量化损失**:只测了 int8 encoder,未与 fp32 对照(fp32 315MB,本机大概率跑不动)。
|
||||
- **长时稳定性**:未做 2 小时/8 小时测试。
|
||||
|
||||
## 六之二、补充:更多 TTS 候选(同日追加)
|
||||
|
||||
用户试听后反馈 **vits-zh-ll 听感最好**,但它是 RTF 1.86 的不可用项。
|
||||
据此追加测试了 5 个候选,全部实测:
|
||||
|
||||
| 模型 | 大小 | 单独 RTF(最佳线程) | 并发 RTF | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| piper-x_low | 20MB | 0.17 @2 | 0.33 | 有 Unknown token 警告,音质存疑 |
|
||||
| **piper-medium** | 61MB | **0.32 @2** | **0.51** | 用户反馈不如 vits |
|
||||
| **matcha-icefall-zh-baker** | 88MB + 3.6MB 声码器 | **0.38 @2** | **0.94** | 新候选,流匹配 + HiFi-GAN |
|
||||
| vits-zh-ll | 115MB | 1.14 @4 / 1.87 @2 | 1.48–2.39 | 听感最好,但不可用 |
|
||||
| kokoro fp32 | 383MB | 1.96 @4 | — | 排除 |
|
||||
| kokoro int8 | 181MB | **3.54 @4** | — | 排除 |
|
||||
|
||||
### 两条新增的机理结论
|
||||
|
||||
1. **TTS 的线程扩展性与 STT 相反**。STT 超过 2 线程急剧退化(4线程慢 3.9 倍),
|
||||
而 vits-zh-ll 是线程越多越快(1线程 3.59 → 4线程 1.14)。
|
||||
原因:STT 是大量小算子(每算子一次线程同步,线程越多同步开销越大),
|
||||
TTS 是少数大算子(CPU 密集,可有效并行)。
|
||||
**两类服务的线程配置必须分开调,不能统一。**
|
||||
|
||||
2. **int8 量化在这台机器上反而更慢**。Kokoro int8(3.54)比 fp32(1.96)慢近一倍。
|
||||
原因:Tremont 没有 VNNI 指令,int8 的快速点积路径用不上,
|
||||
权重需要现场反量化,额外开销盖过了带宽收益。
|
||||
→ **本机不要走 int8 量化路线做加速。**(STT 的 int8 encoder 是发布方直接训练的量化模型,
|
||||
不是运行时量化,不受此限。)
|
||||
|
||||
### 当前选型状态
|
||||
|
||||
- **速度与余量最优**:piper-medium(并发 0.51,余量约 2 倍)
|
||||
- **架构档次更高、速度尚可**:matcha-baker(并发 0.94,**几乎没有余量**)
|
||||
- matcha 的并发退化(0.38→0.94,2.5 倍)明显大于 piper-medium(0.32→0.51,1.6 倍),
|
||||
判断与 HiFi-GAN 声码器吃内存带宽有关(本机单通道内存)。
|
||||
**若选 matcha,人脸检测等其它负载再加进来很可能压垮实时性,需要实测。**
|
||||
|
||||
已生成响度归一化的 A/B 试听(统一到 -20 dBFS RMS,
|
||||
避免"响度差被误当成音质差"——vits-zh-ll 原始峰值仅 0.17,比 Piper 低 3.5 倍):
|
||||
|
||||
```
|
||||
~/workspace/ai-companion/p0/stt_tts_bench/listening_ab/{vits-zh-ll,matcha-baker,piper-medium}_{0,1}.wav
|
||||
```
|
||||
|
||||
## 七、工程陷阱记录
|
||||
|
||||
1. **`max_num_sentences=1` 会截断多句输入**为第一句,且不报错。
|
||||
传含多个句号的文本会静默丢失后半段。必须自己先分句逐句送。
|
||||
2. **hf-mirror 对 urllib 默认 UA 返回 403**,需伪装 User-Agent。
|
||||
3. **espeak-ng-data 里有带空格的文件名**(`voices/!v/Mr serious`),URL 必须转义。
|
||||
4. **代码曾位于 note_sync(Obsidian 笔记仓库)内,是经软链接访问的**,
|
||||
模型一度差点被推进笔记仓库。**2026-09-15 已把代码剥离为独立仓库**
|
||||
(`~/workspace/ai-companion/`),笔记库只剩文档,这个问题不复存在。
|
||||
教训:代码和笔记共用版本控制,迟早要在 ignore 规则上反复花时间。
|
||||
@@ -0,0 +1,256 @@
|
||||
# P1 · Pipecat Flows 兼容性 Spike 结论
|
||||
|
||||
> 日期:2026-09-16
|
||||
> 状态:**spike 完成,7 项验证全部通过**
|
||||
> 代码:`p1/flows_spike.py`(探索脚本)、`app/tests/test_flows_spike.py`(断言版)
|
||||
> 前置:[P1-Pipecat Flows Agent化与记忆改造交接](P1-Pipecat%20Flows%20Agent化与记忆改造交接.md)
|
||||
|
||||
交接文档规定:**未经 spike 结论,不进入 Phase 3**。本文即该结论。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论摘要
|
||||
|
||||
| 问题 | 结论 |
|
||||
|---|---|
|
||||
| 兼容的 flows 版本 | **不装独立包**。Pipecat 1.10.0 **内置** Flows(`pipecat.flows`) |
|
||||
| LLMService 选择 | 复用 Pipecat 的 `DeepSeekLLMService`;**代价是失去本地 vLLM 降级** |
|
||||
| token 级文本 | ✅ 实测 14–22 块/回复,满足网页显示与分句 TTS |
|
||||
| 工具调用 | ✅ 参数聚合完整,无重复拼接;工具结果回灌后能出最终回答 |
|
||||
| 取消 | ✅ `worker.cancel()` 能立即终止 LLM 流 |
|
||||
| context | ⚠️ **会产生两条 system 消息**(见 §五) |
|
||||
| 是否破坏现有功能 | 见 §六——**有两处需要处理** |
|
||||
|
||||
---
|
||||
|
||||
## 二、版本:不需要独立包,而且装了有害
|
||||
|
||||
**这是最重要的一条发现。**
|
||||
|
||||
`pipecat-ai 1.10.0` **已经内置了 Pipecat Flows**,模块路径 `pipecat.flows`。
|
||||
装独立的 `pipecat-ai-flows` 包时,Pipecat 会直接报错提示:
|
||||
|
||||
```
|
||||
ERROR | pipecat:_warn_if_standalone_flows_installed:45 -
|
||||
The separate `pipecat-ai-flows` package is installed alongside a version of
|
||||
Pipecat that already includes Pipecat Flows as `pipecat.flows`.
|
||||
You do not need both — uninstall `pipecat-ai-flows` and import Flows from `pipecat.flows`.
|
||||
```
|
||||
|
||||
而且独立包的版本区间有陷阱:
|
||||
|
||||
| 独立包版本 | 要求的 pipecat-ai | 后果 |
|
||||
|---|---|---|
|
||||
| 1.4.0 | `>=1.4.0,<1.5.0` | **会把 pipecat 从 1.10.0 降到 1.4.0** |
|
||||
| 1.3.0 | `>=1.4.0,<2` | 可安装,pytest dry-run 只新增自己 |
|
||||
|
||||
(1.4.0 是"更新"的版本,却钉死在更老的 pipecat 区间——如果按"装最新版"的直觉操作,
|
||||
会静默把整个框架降级。)
|
||||
|
||||
**决定**:
|
||||
|
||||
- `requirements.txt` **不添加** `pipecat-ai-flows`
|
||||
- 代码 `from pipecat.flows import FlowManager, NodeConfig, FlowsFunctionSchema`
|
||||
- 内置版与独立包 **API 一致**(FlowManager / NodeConfig / FlowsFunctionSchema 签名相同),
|
||||
所以这个切换不影响任何写法
|
||||
|
||||
内置版的导出比独立包更多(多出 `Flow`、`FlowConfig`、`FlowProblem`、`FlowReferenceError`),
|
||||
像是更演进的 API,但 spike 只用到了两者共有的部分。
|
||||
|
||||
---
|
||||
|
||||
## 三、LLMService 选择
|
||||
|
||||
**结论:复用 Pipecat 的 `DeepSeekLLMService`,不写 `FallbackProvider → LLMService` 适配器。**
|
||||
|
||||
理由:
|
||||
|
||||
1. **它自带 `DeepSeekLLMAdapter`**,源码注释明写 "Supplies the `reasoning_content`
|
||||
DeepSeek requires on assistant messages in thinking mode"——**这正是我们在 P0
|
||||
阶段踩过的坑**(见 `P0-DeepSeek验证报告.md`:开思考时多轮/工具回灌一律 400)。
|
||||
框架已经处理了。
|
||||
2. `thinking` 默认就是 `disabled`,与 P0 的结论一致。
|
||||
3. 支持自定义 `base_url`,将来要接本地 vLLM 也能用。
|
||||
4. **无法适配 `FallbackProvider`**:Flows 要求的是 `LLMService` 实例
|
||||
(`LLMService | LLMSwitcher`),而 `pipecat_flows` 里那个 `LLMAdapter`
|
||||
**只管摘要生成**,不是 provider 适配器。要保留 fallback 必须自己写
|
||||
`LLMService` 子类,成本中等。
|
||||
|
||||
**必须覆盖模型名**:默认是 `deepseek-v4-flash`,**该模型不存在**,用默认值会 400。
|
||||
必须显式设 `DeepSeekLLMService.Settings(model="deepseek-flash")`。
|
||||
|
||||
**失去本地 vLLM 降级的代价**:目前本机 27B 的 vLLM 已停用,fallback 实际上没有可用的
|
||||
后备目标。所以 Phase 3 先只用 DeepSeek 是合理的。将来若要恢复双后端,
|
||||
Pipecat 有 `LLMSwitcher` 可用,不必自写。
|
||||
|
||||
---
|
||||
|
||||
## 四、实测结果(7 项,9 个断言全通过)
|
||||
|
||||
```
|
||||
=== 2. DeepSeek 在 Flows 中流式输出中文 ===
|
||||
回复: '我是你的桌面陪伴小助手,平时就待在这儿陪你聊聊天、说说话。'
|
||||
分块数: 18
|
||||
[PASS] 有中文回复
|
||||
[PASS] 回复不含 Markdown
|
||||
[PASS] 7. token 级文本事件(>1 块) — 18 块
|
||||
|
||||
=== 3+4. 流式工具参数聚合 / 工具结果回灌 ===
|
||||
工具调用参数: [{'city': '北京'}]
|
||||
最终回复: '北京现在是下午三点半。'
|
||||
[PASS] 3. 工具被调用 — 1 次
|
||||
[PASS] 3. 参数聚合完整(无重复拼接) — {"city": "北京"}
|
||||
[PASS] 4. 工具结果回灌后有最终回答
|
||||
|
||||
=== 5. 取消 ===
|
||||
取消前 13 块 → 取消后 1.5s 内 13 块(又收到 0 块)
|
||||
[PASS] 5. 取消后停止输出
|
||||
|
||||
=== 6. system message ===
|
||||
context 消息角色序列: ['system', 'user', 'assistant']
|
||||
[PASS] 6. system 消息不超过 1 条
|
||||
[PASS] 6. 有 user 与 assistant 轮次
|
||||
```
|
||||
|
||||
补充观察:
|
||||
- **role_message 生效**:设"你是桌面陪伴助手"后,模型自称"桌面陪伴小助手",
|
||||
不再是通用的"DeepSeek AI 助手"
|
||||
- **工具调用只有 1 次**(修好 runner 之前会失败重试 2 次,见 §五)
|
||||
|
||||
---
|
||||
|
||||
## 五、暴露的问题(4 个坑,都已定位)
|
||||
|
||||
### 坑 1:`FlowManager.initialize` 是协程,漏 `await` 会静默失效
|
||||
|
||||
```python
|
||||
inspect.signature(FlowManager.initialize) # (self, initial_node=None) -> None
|
||||
inspect.iscoroutinefunction(...) # True ← 签名上看不出来
|
||||
```
|
||||
|
||||
漏 `await` 时**不报错**,只是整个 flow 从不初始化:role_message、task_messages、
|
||||
工具注册**全部没生效**,而 LLM 照样会回复(因为 `run_llm=True` 直接触发推理)。
|
||||
|
||||
现象极具误导性:会让人误判成"Flows 不支持这些特性"。实测中我一度得出
|
||||
"role_message 不生效"的结论,就是这个原因。
|
||||
|
||||
### 坑 2:必须用 `PipelineRunner` 跑 worker,不能直接 `worker.run()`
|
||||
|
||||
直接 `worker.run(...)` 时 runner 没挂上,**工具执行会失败**:
|
||||
|
||||
```
|
||||
Error executing function call [get_time]:
|
||||
Worker 'PipelineWorker#0': runner is not set; call attach() first.
|
||||
```
|
||||
|
||||
这个异常被 Pipecat 转成给 LLM 的通用错误串
|
||||
`"The function X failed and returned no result."`,**handler 从不执行**,
|
||||
日志里默认看不到真实原因。要开 DEBUG 才看得到。
|
||||
|
||||
正确写法:`await PipelineRunner().run(worker)`
|
||||
|
||||
### 坑 3:`respond_immediately` 默认 `True`,initialize 会立刻触发一轮回复
|
||||
|
||||
`FlowManager.initialize()` 会 queue 一个 `LLMRunFrame`,**在用户说话之前**
|
||||
就产生一轮 LLM 回复。
|
||||
|
||||
做测试/归因时如果不清空观察窗口,会把"节点初始化的自动回复"当成
|
||||
"对用户问题的回复"——我第一版 spike 就误判了第 2 项。
|
||||
|
||||
生产上这通常是想要的行为(节点进入即主动开口),但**必须知道它存在**。
|
||||
|
||||
### 坑 4:会产生两条 system 消息
|
||||
|
||||
实际发给 DeepSeek 的请求里:
|
||||
|
||||
```
|
||||
[system] '你是桌面陪伴助手……' ← role_message(走 system_instruction)
|
||||
[system] '当前任务:普通陪伴对话。' ← task_messages
|
||||
[user] '……'
|
||||
```
|
||||
|
||||
`role_message` 通过 `LLMUpdateSettingsFrame(system_instruction=...)` 下发,
|
||||
而 `task_messages` 里的 system 消息留在 context 里,**两者都进请求**。
|
||||
|
||||
交接文档 §九 Phase 1 第 6 项要求"context aggregator 不产生第二条 system message"——
|
||||
严格说,**context 里确实只有一条**(`task_messages` 那条),
|
||||
`role_message` 走的是 LLM settings 而非 context。但**发出去的请求有两条 system**。
|
||||
|
||||
风险:
|
||||
- 多数模型能处理多条 system,但**优先级不保证**(谁覆盖谁取决于实现)
|
||||
- 我们之前在**本机 vLLM** 上就吃过"多条 system 直接 400"的亏
|
||||
(见 `P0-本地LLM验证报告.md`)——如果将来把 LLM 切回本地 vLLM,这条会立刻炸
|
||||
|
||||
**建议**:Phase 3 集成时**只用一种**。要么把节点任务说明也写进 `role_message`,
|
||||
要么不用 `role_message`、全部走 `task_messages`。不要两个都用。
|
||||
|
||||
### 坑 5(环境):NLTK `punkt_tab` 本地也要装
|
||||
|
||||
Pipecat 启动会预热 NLTK 分句器,缺 `punkt_tab` 会尝试联网下载并被安全策略拦
|
||||
(`SSRF attempt to restricted IP`),setup 超时后管线卡住。
|
||||
|
||||
服务器上已装(`~/.local/share/ai-companion/nltk_data`),**本地开发机也要装**,
|
||||
否则 spike 跑不起来。`p1/flows_spike.py` 已加 fail-fast 检查与拷贝命令提示。
|
||||
|
||||
---
|
||||
|
||||
## 六、对现有功能的影响
|
||||
|
||||
| 现有功能 | 是否受影响 | 说明 |
|
||||
|---|---|---|
|
||||
| 分句 TTS | **不受影响** | Flows 输出 token 级 `TextFrame`(实测 18 块),自有分句逻辑照用 |
|
||||
| RTVI 网页显示 | **不受影响** | token 增量与完整文本都能拿到 |
|
||||
| 记忆 / 身份 / SQLite | 不受影响 | Flows 的 state 只存会话控制状态,长期数据仍走 `MemoryStore` |
|
||||
| SearXNG 搜索 | 需迁移 | 现在的 `web_search` 手写循环要改成 Flows 工具 |
|
||||
| **本地 vLLM fallback** | **会失去** | `FallbackProvider` 不能被 Flows 直接使用(见 §三) |
|
||||
| 两条 system 消息 | **需要注意** | 见坑 4;切回本地 vLLM 时会 400 |
|
||||
|
||||
---
|
||||
|
||||
## 七、建议的最小生产集成 diff(Phase 3 用)
|
||||
|
||||
不改动现有 `EngineProcessor` 的音频路径,只在**文字路径**上分流:
|
||||
|
||||
1. `app/config.py` 加 `AI_AGENT_RUNTIME=legacy|flows`,**默认 legacy**
|
||||
2. `app/agent/` 新增 `tools.py`(工具定义)、`memory_policy.py`(记忆策略)、
|
||||
`flows_runtime.py`(FlowManager 装配)
|
||||
3. `services/bot_server.py` 里,RTVI `send-text` 按 `AI_AGENT_RUNTIME` 分流:
|
||||
`legacy` → 现有 `EngineProcessor`;`flows` → 新建的 Flows 路径
|
||||
4. 语音路径**暂不动**,留到 Phase 5
|
||||
5. `requirements.txt` **不加** `pipecat-ai-flows`
|
||||
|
||||
关键注意点:
|
||||
- 用 `PipelineRunner().run(worker)`,不要直接 `worker.run()`
|
||||
- `await flow.initialize(node)`
|
||||
- `role_message` 与 `task_messages` 的 system 消息**二选一**
|
||||
- `DeepSeekLLMService.Settings(model="deepseek-flash")` 必须显式覆盖
|
||||
|
||||
---
|
||||
|
||||
## 八、下一步
|
||||
|
||||
spike 已给出结论,**可以进入 Phase 2**(抽出工具与策略层):
|
||||
|
||||
- 把现有 `web_search` 执行从 `ConversationEngine` 抽成可复用函数
|
||||
- 实现记忆工具,先用 fake store 单测
|
||||
- legacy 路径继续工作,Flows 不是唯一调用者
|
||||
|
||||
Phase 3(接网页文字路径)等 Phase 2 完成标准达成后再开始。
|
||||
|
||||
---
|
||||
|
||||
## 九、复现
|
||||
|
||||
```bash
|
||||
cd ~/workspace/ai-companion
|
||||
|
||||
# 探索脚本(带请求体抓取与逐项诊断)
|
||||
~/venvs/ai_assistant/bin/python p1/flows_spike.py
|
||||
~/venvs/ai_assistant/bin/python p1/flows_spike.py --only 3
|
||||
|
||||
# 断言版(接 CI 用)
|
||||
~/venvs/ai_assistant/bin/python app/tests/test_flows_spike.py
|
||||
|
||||
# 需要 DEBUG 日志排查时
|
||||
SPIKE_DEBUG=1 ~/venvs/ai_assistant/bin/python p1/flows_spike.py --only 3
|
||||
```
|
||||
@@ -0,0 +1,501 @@
|
||||
# P1 · Pipecat Flows Agent 化与记忆改造交接
|
||||
|
||||
> 日期:2026-09-16
|
||||
> 状态:方案已选定,等待实现
|
||||
> 实现仓库:`~/workspace/ai-companion`
|
||||
> 部署目录:`yonglin@cadadmin-server.taild0f91d.ts.net:~/ai-companion`
|
||||
|
||||
## 一、给 Claude Code 的任务摘要
|
||||
|
||||
在**不推翻现有实时语音链路**的前提下,渐进引入 Pipecat Flows,把当前手写的
|
||||
单工具循环升级为受策略约束的 Agent 工具层,并补齐通用长期记忆的确认、读取、
|
||||
修改和忘记流程。
|
||||
|
||||
必须遵守以下边界:
|
||||
|
||||
1. 保留现有 `MemoryStore`、SQLite 数据、身份隔离、DeepSeek fallback、SearXNG、
|
||||
RTVI 网页显示和流式 TTS。
|
||||
2. 先做 Pipecat Flows 与当前 `ConversationEngine` 的兼容性 spike;验证通过前,
|
||||
不得替换线上默认路径。
|
||||
3. 使用 `AI_AGENT_RUNTIME=legacy|flows` 保留可立即回滚的 legacy 路径。
|
||||
4. 模型不得决定或传入 `owner_user_id`;用户身份必须由服务端会话上下文注入。
|
||||
5. 用户明确要求记忆与模型推测必须分开。模型推测不得直接成为 confirmed。
|
||||
6. 不把 Pipecat Flow state 当成长期记忆;长期数据仍通过 `MemoryStore` 持久化。
|
||||
7. 不顺手实现唤醒词、硬件 AEC、人脸自动切换、向量数据库或 systemd。
|
||||
|
||||
## 二、开始前必须知道的仓库状态
|
||||
|
||||
### 代码和部署
|
||||
|
||||
- Git 已提交基线包含:
|
||||
- `c3d2c7f`:修复 Pipecat 实时链路并补齐网页测试
|
||||
- `719fc13`:修复网页对话并接入 SearXNG 搜索
|
||||
- 2026-09-16 的姓名档案修复、疑问句防误写和测试目前仍是**未提交改动**。
|
||||
- 线上服务已运行这些未提交改动,不等于服务器代码与 Git HEAD 相同。
|
||||
- 开始工作前必须运行 `git status --short` 和 `git diff`;不得 reset、checkout
|
||||
或覆盖现有改动。
|
||||
- 线上健康接口:`http://127.0.0.1:7860/health`。
|
||||
- 网页调试入口通过 SSH 隧道访问:`http://localhost:7860/client/`。
|
||||
|
||||
### 当前已验证能力
|
||||
|
||||
- SmallWebRTC 信令、ICE、data channel、音频输入输出已闭环。
|
||||
- 浏览器真人麦克风 → STT → DeepSeek → Qwen3-TTS → 浏览器播放正常。
|
||||
- 网页文字输入、用户文本、助手文本和语音回复正常。
|
||||
- DeepSeek 能自主调用 `web_search`,SearXNG 工具结果能回灌后继续回答。
|
||||
- 用户自报“我叫林”会同步写入 confirmed 姓名档案,disconnect/reconnect 后恢复。
|
||||
- 姓名疑问句不会再把“谁吗”之类内容覆盖为姓名。
|
||||
- 当前专项回归:
|
||||
- 姓名档案 22/22
|
||||
- 记忆存储 30/30
|
||||
- 身份 19/19
|
||||
- 搜索 12/12
|
||||
- 传输 4/4
|
||||
|
||||
### 线上姓名数据库备份
|
||||
|
||||
- 首次档案迁移前:
|
||||
`/home/yonglin/.local/share/ai_assistant/memory.db.before-name-profile-20260916`
|
||||
- 疑问句误写修复前:
|
||||
`/home/yonglin/.local/share/ai_assistant/memory.db.before-name-question-20260916-1140`
|
||||
|
||||
不要删除这些备份。后续若改 schema,必须另建带时间戳的备份。
|
||||
|
||||
## 三、当前系统到底是什么
|
||||
|
||||
当前不是完整通用 Agent,而是:
|
||||
|
||||
```text
|
||||
SmallWebRTC / RTVI
|
||||
↓
|
||||
EngineProcessor
|
||||
↓
|
||||
SimpleVAD → StreamingSTT
|
||||
↓
|
||||
ConversationEngine
|
||||
身份 → 自动记忆检索 → DeepSeek/搜索工具循环 → 分句 TTS
|
||||
↓
|
||||
WebRTC 音频与 RTVI 文本
|
||||
```
|
||||
|
||||
`ConversationEngine._stream_and_speak()` 手写了最多两轮的 `web_search` Function
|
||||
Calling 循环。模型能自主决定是否搜索,但还不能自主调用记忆工具。
|
||||
|
||||
Pipecat 当前主要负责传输、Frame、PipelineWorker 和 RTVI;STT、LLM、TTS 语义链路
|
||||
仍由自有引擎调度。
|
||||
|
||||
## 四、当前记忆决策与真实缺口
|
||||
|
||||
### 写入
|
||||
|
||||
| 输入 | 当前行为 | 实际是否可复用 |
|
||||
|---|---|---|
|
||||
| “我叫林” | 同步更新 `users.display_name`,写 confirmed 姓名 | 是 |
|
||||
| “记住我不吃香菜” | 回复后规则抽取,写 candidate | 否,默认检索排除 |
|
||||
| “我喜欢咖啡” | 回复后规则抽取,写 candidate | 否,默认检索排除 |
|
||||
| 普通聊天 | 只保留当前内存历史和 user turn | 重连后不可用 |
|
||||
|
||||
当前没有 candidate 的确认、晋升或管理入口。因此除姓名外,“写进数据库”和“助手会在
|
||||
以后使用”不是一回事。
|
||||
|
||||
### 读取
|
||||
|
||||
- 每条已确认身份的用户消息都会尝试检索,不是模型按需决定。
|
||||
- 默认只读取 confirmed、private/shared、未过期记忆。
|
||||
- 最多 5 条,超时 300ms 后无记忆继续回答。
|
||||
- 当前是中文字符/二元组字面相似度;记忆总量很小时,无命中会退回最近记忆。
|
||||
- 用户姓名作为档案固定注入,不依赖语义检索。
|
||||
|
||||
### 必须达到的目标语义
|
||||
|
||||
1. **稳定档案**:姓名、称呼、语言和明确的稳定偏好;连接开始时加载。
|
||||
2. **显式长期记忆**:用户明确说“记住/以后/别忘了”,经策略校验后 confirmed。
|
||||
3. **候选观察**:普通聊天中推断出的可能信息只能 candidate,不直接影响回答。
|
||||
4. **事件/摘要**:会话事件与逐字稿不冒充永久事实,可过期、可删除。
|
||||
5. **矛盾更新**:同 owner + kind + key 的 confirmed 新值覆盖或 supersede 旧值。
|
||||
6. **透明控制**:支持查看、确认、修改、忘记;模糊删除必须追问。
|
||||
|
||||
## 五、为什么选择 Pipecat Flows
|
||||
|
||||
### 候选比较
|
||||
|
||||
| 框架 | 优点 | 与本项目的主要问题 | 结论 |
|
||||
|---|---|---|---|
|
||||
| Pipecat Flows | 已在 Pipecat 生态;状态、节点、函数注册和校验;支持 DeepSeek 等 OpenAI-compatible 服务 | 期待 Pipecat `LLMService + context aggregator`,与当前自有引擎有接口差异 | **选择,渐进集成** |
|
||||
| Agno | 内置 user/session/memory,最接近轻量版 OpenClaw | 会与现有会话、历史和记忆层重叠 | 暂不采用 |
|
||||
| PydanticAI | 类型安全、依赖注入、流式工具事件、较轻 | 长期记忆策略仍要自建;与现有工具循环收益有限 | 备选 |
|
||||
| LangGraph | 持久状态、checkpoint、长任务和人工审批成熟 | 对当前实时陪伴链路过重 | 暂不采用 |
|
||||
| LiveKit Agents | 完整实时语音 Agent、工具、handoff | 相当于替换已跑通的 Pipecat/WebRTC;长期记忆仍需外接 | 不迁移 |
|
||||
| smolagents | 代码少、多步工具 Agent | 偏任务型,多步延迟高,API仍偏实验 | 不用于主链路 |
|
||||
| OpenClaw | 通用 Agent runtime、分层记忆和后台整理完整 | 运行时过重、默认个人 Agent 假设、实时语音和身份边界仍需定制 | 不整体迁移 |
|
||||
|
||||
官方资料:
|
||||
|
||||
- [Pipecat Flows Overview](https://docs.pipecat.ai/api-reference/pipecat-flows/overview)
|
||||
- [Pipecat Function Calling](https://docs.pipecat.ai/pipecat/learn/function-calling)
|
||||
- [Agno Memory](https://docs.agno.com/examples/memory/overview)
|
||||
- [PydanticAI Agents](https://ai.pydantic.dev/agents/)
|
||||
- [LangGraph Memory](https://docs.langchain.com/oss/python/langgraph/add-memory)
|
||||
- [LiveKit Chat Context](https://docs.livekit.io/agents/logic/chat-context/)
|
||||
- [OpenClaw Memory](https://docs.openclaw.ai/concepts/memory)
|
||||
|
||||
### 选型原则
|
||||
|
||||
选择 Pipecat Flows,不是为了追求“框架化”,而是为了:
|
||||
|
||||
- 复用现有实时媒体栈;
|
||||
- 把工具 schema、执行、结果回灌和状态切换从手写循环中抽离;
|
||||
- 给记忆确认/删除等多轮流程提供显式状态;
|
||||
- 保留产品特有的身份、隐私和低延迟策略。
|
||||
|
||||
## 六、最重要的集成风险
|
||||
|
||||
Pipecat Flows 的正常构造需要:
|
||||
|
||||
- `PipelineTask`
|
||||
- Pipecat `LLMService`
|
||||
- context aggregator
|
||||
- 由 Pipeline Frame 驱动的函数调用和消息更新
|
||||
|
||||
当前项目则是:
|
||||
|
||||
- `PipelineWorker` + 自定义 `EngineProcessor`
|
||||
- 自有 `FallbackProvider`,不是 Pipecat `LLMService`
|
||||
- 自有消息历史与 prompt 构造
|
||||
- 自有 STT/TTS 调用,不是标准 Pipecat service processor
|
||||
- RTVI 事件由 `EngineProcessor` 手工补发
|
||||
|
||||
因此不能直接“安装 Flows,然后包住 ConversationEngine”。如果忽略这一点,很容易出现:
|
||||
|
||||
- 两套消息历史同时增长;
|
||||
- 工具调用重复执行;
|
||||
- token 既被自有引擎又被 Pipecat aggregator 消费;
|
||||
- RTVI 文本重复或消失;
|
||||
- TTS 重复播报;
|
||||
- 取消只停止其中一套任务;
|
||||
- 身份切换后旧 context 泄漏。
|
||||
|
||||
## 七、目标架构
|
||||
|
||||
```text
|
||||
┌─────────────────────────────┐
|
||||
WebRTC / RTVI ─────▶│ Pipecat Pipeline │
|
||||
│ transport + turn + metrics │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────┐
|
||||
│ Flows Runtime(可切换) │
|
||||
│ node / state / tools │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
┌────────────────────────┼────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
LLMService 适配器 Memory Policy Web Search
|
||||
DeepSeek/Fallback + MemoryStore SearXNG
|
||||
│ │
|
||||
▼ ▼
|
||||
流式文本与 TTS SQLite(继续使用)
|
||||
```
|
||||
|
||||
### Flow state 只保存会话控制状态
|
||||
|
||||
建议字段:
|
||||
|
||||
- `session_id`
|
||||
- 服务端绑定的 `user_id`(内部使用,不展示给模型)
|
||||
- `display_name`
|
||||
- `identity_version`
|
||||
- `pending_memory_action`
|
||||
- `pending_confirmation`
|
||||
- 当前 node
|
||||
|
||||
不要把全部记忆内容复制进 Flow state,也不要把 state 当数据库。
|
||||
|
||||
### 初始节点
|
||||
|
||||
第一版只需要两个节点,不要设计庞大状态图:
|
||||
|
||||
1. `conversation`
|
||||
- 普通陪伴对话;
|
||||
- 可用只读搜索和受限记忆工具;
|
||||
- 默认节点。
|
||||
2. `confirm_memory_change`
|
||||
- 只在修改、删除目标不明确或敏感写入时进入;
|
||||
- 用户确认/取消后返回 `conversation`。
|
||||
|
||||
## 八、工具设计
|
||||
|
||||
### 全局工具
|
||||
|
||||
#### `web_search(query)`
|
||||
|
||||
- 复用 `app/search.py` 与 SearXNG。
|
||||
- 保留最多两次连续搜索限制。
|
||||
- 搜索失败返回工具错误结果,不让整轮异常退出。
|
||||
- 来源 URL 保留给网页,TTS 继续不朗读来源段。
|
||||
|
||||
#### `memory_search(query)`
|
||||
|
||||
- `owner_user_id` 从会话注入,禁止模型传入。
|
||||
- 只返回 confirmed、未过期、授权 scope。
|
||||
- 不确认身份时返回不可用,不搜索任何私人记忆。
|
||||
- 结果条数和超时沿用配置上限。
|
||||
|
||||
#### `memory_remember(kind, key, value)`
|
||||
|
||||
- 仅确认身份可用。
|
||||
- 只有当前用户明确表达“记住/以后/别忘了/我的偏好是”等意图时,才能 confirmed。
|
||||
- 模型推断或总结只能写 candidate。
|
||||
- `owner_user_id`、source session/turn、scope 由服务端填充。
|
||||
- 默认 private;模型不得扩大成 shared。
|
||||
- 同 key confirmed 写入保持幂等并更新旧值。
|
||||
- 姓名确定性快路径暂时保留,不立即交给模型。
|
||||
|
||||
#### `memory_forget(selector)`
|
||||
|
||||
- 仅确认身份可用。
|
||||
- 精确匹配一条时可返回待删除摘要;删除前应有明确用户意图。
|
||||
- 多条或模糊匹配进入 `confirm_memory_change`,不得猜测删除目标。
|
||||
- 删除必须同时匹配 owner;不得只凭 memory_id。
|
||||
- 返回“已删除/没有找到/需要确认”,不能把“请求已接收”说成“已经删除”。
|
||||
|
||||
#### `memory_update(key, value)`
|
||||
|
||||
- 用于用户明确纠正已有事实或偏好。
|
||||
- 更新必须保留来源和更新时间。
|
||||
- 若未来增加 superseded 状态,先做向后兼容 migration,不删旧 schema。
|
||||
|
||||
#### `profile_update(display_name)`
|
||||
|
||||
- 第一阶段可继续使用现有确定性姓名提取,不必立即暴露给模型。
|
||||
- 如果以后工具化,也必须经过同一疑问句和角色描述过滤。
|
||||
|
||||
### 记忆执行策略必须独立于 prompt
|
||||
|
||||
不要仅依赖“请不要误记”的 system prompt。新增类似:
|
||||
|
||||
```text
|
||||
app/agent/memory_policy.py
|
||||
```
|
||||
|
||||
策略层输入至少包含:
|
||||
|
||||
- 当前原始 user text
|
||||
- 工具请求
|
||||
- 已确认身份
|
||||
- 当前 session/turn
|
||||
- 已有同 key 记忆
|
||||
|
||||
策略层输出:
|
||||
|
||||
- allow_confirmed
|
||||
- allow_candidate
|
||||
- needs_confirmation
|
||||
- reject
|
||||
|
||||
模型可以提出动作,策略层拥有最终决定权。
|
||||
|
||||
## 九、分阶段实施
|
||||
|
||||
### Phase 0:保护基线
|
||||
|
||||
1. 运行 `git status --short`、保存 `git diff`。
|
||||
2. 运行当前相关测试。
|
||||
3. 记录网页文字、网页语音、搜索、姓名重连的基线延迟与行为。
|
||||
4. 不修改线上数据库 schema。
|
||||
|
||||
完成标准:现有未提交修复没有丢失,基线测试结果有记录。
|
||||
|
||||
### Phase 1:独立兼容性 spike
|
||||
|
||||
新增独立实验,不先改生产入口,例如:
|
||||
|
||||
```text
|
||||
p1/flows_spike.py
|
||||
app/tests/test_flows_spike.py
|
||||
```
|
||||
|
||||
需要验证:
|
||||
|
||||
1. 找到与 `pipecat-ai==1.10.0` 兼容的 `pipecat-ai-flows` 版本并精确 pin。
|
||||
2. DeepSeek OpenAI-compatible 接口能在 Flows 中流式输出中文内容。
|
||||
3. 流式 function-call arguments 能被正确聚合一次,不重复拼接。
|
||||
4. 工具结果回灌后能生成最终回答。
|
||||
5. 任务取消能终止 LLM 请求和后续工具执行。
|
||||
6. context aggregator 不产生第二条 system message。
|
||||
7. 能获得 token 级文本事件,满足网页显示和分句 TTS。
|
||||
|
||||
若任何一项不成立,停止生产集成,先记录结论;不得为了使用框架而绕过现有正确行为。
|
||||
|
||||
### Phase 2:抽出工具和策略层
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
app/agent/
|
||||
├── __init__.py
|
||||
├── tools.py
|
||||
├── memory_policy.py
|
||||
├── flows_runtime.py
|
||||
└── llm_service.py
|
||||
```
|
||||
|
||||
- 先把现有 `web_search` 执行从 `ConversationEngine` 抽成可复用函数。
|
||||
- 实现记忆工具,但先用 fake store 单测。
|
||||
- legacy 路径继续工作;不要让 Flows 成为唯一调用者。
|
||||
|
||||
完成标准:工具层可被 legacy 和 flows 两条路径复用,策略测试不依赖真实 LLM。
|
||||
|
||||
### Phase 3:先接网页文字路径
|
||||
|
||||
- 新增 `AI_AGENT_RUNTIME`,默认仍为 `legacy`。
|
||||
- 只让 RTVI `send-text` 在显式 `flows` 配置下走新路径。
|
||||
- 保持网页用户文本、token 增量、完整 assistant 文本和可选 TTS。
|
||||
- 语音路径暂时保持 legacy。
|
||||
|
||||
完成标准:同一服务可切换两条 runtime;legacy 回归无变化;flows 文字路径通过工具、
|
||||
姓名、记忆和搜索验收。
|
||||
|
||||
### Phase 4:补齐通用记忆闭环
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. “记住我不吃香菜” → confirmed。
|
||||
2. disconnect/reconnect → “我不吃什么”能召回香菜。
|
||||
3. “我今天吃了香菜”不得自动变成稳定偏好。
|
||||
4. “其实我现在吃香菜了”能明确更新旧偏好,而不是保留冲突 active 值。
|
||||
5. “忘掉我的饮食偏好”能删除;目标模糊时先确认。
|
||||
6. 访客无法读取或写入私人记忆。
|
||||
7. 用户 A 的记忆绝不出现在用户 B 的 prompt、工具结果或日志中。
|
||||
|
||||
### Phase 5:迁移语音路径
|
||||
|
||||
只有 Phase 3、4 稳定后才迁移语音:
|
||||
|
||||
- 保持 STT transcript 和原始 user text 一致;
|
||||
- 保持首句尽快进入 TTS;
|
||||
- 工具调用期间不得把工具 JSON 念出来;
|
||||
- 来源段不进入 TTS;
|
||||
- 用户打断时取消 Flows、LLM、工具和 TTS 的同一轮 execution;
|
||||
- `ConversationEngine` 中已经稳定的身份与记忆接口可以拆分复用,不要求一次删除旧类。
|
||||
|
||||
### Phase 6:打断控制链
|
||||
|
||||
当前 `SimpleVAD` 只做 RMS 话段切分。下一步先实现控制闭环,不在本阶段承诺真实硬件
|
||||
声学效果:
|
||||
|
||||
```text
|
||||
user-started-speaking
|
||||
├─ 发出 InterruptionFrame / 客户端停播信号
|
||||
├─ 取消当前 Flows/LLM/TTS execution
|
||||
├─ 清空未发送和客户端已缓冲音频
|
||||
└─ 保留新用户话语的 pre-roll 并继续收集
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 不等待 700ms 尾静音才停止助手;
|
||||
- 使用开始阈值、连续帧或正式 VAD,避免单个噪声帧误触发;
|
||||
- `_bot_speaking` 不能只代表服务端推帧结束,要考虑客户端仍在播放;
|
||||
- 文字请求期间的麦克风回采不得错误抢占;
|
||||
- 先做合成 Frame 回归;真实 AEC、扬声器回声和远场误触发留到硬件 P2。
|
||||
|
||||
### Phase 7:部署与清理
|
||||
|
||||
- flows 默认值只有在线上验收后才能从 legacy 改为 flows。
|
||||
- 更新 README、部署说明和本交接文档。
|
||||
- 在修改数据库前创建新备份。
|
||||
- 确认 `/health`、网页文字、网页语音、搜索和重连记忆。
|
||||
- 确认日志没有 API key、私人全文记忆或内部 ID 泄漏。
|
||||
|
||||
## 十、测试与验收矩阵
|
||||
|
||||
### 必须新增的单元测试
|
||||
|
||||
- 工具参数 schema 与非法参数。
|
||||
- 模型不能传 owner_user_id/scope 越权。
|
||||
- 未确认身份拒绝 private memory。
|
||||
- 明确“记住”写 confirmed。
|
||||
- 推断信息只写 candidate。
|
||||
- 同 key 更新幂等。
|
||||
- 模糊忘记返回 needs_confirmation。
|
||||
- 精确忘记只删除当前 owner。
|
||||
- Flows node 切换后不会重复执行工具。
|
||||
- 取消后不再写记忆或发送音频。
|
||||
|
||||
### 必须保留的现有测试
|
||||
|
||||
```bash
|
||||
.venv/bin/python app/tests/test_profile_memory.py
|
||||
.venv/bin/python app/tests/test_memory.py
|
||||
.venv/bin/python app/tests/test_identity.py
|
||||
.venv/bin/python app/tests/test_search.py
|
||||
.venv/bin/python app/tests/test_transport.py
|
||||
.venv/bin/python app/tests/test_engine_e2e.py
|
||||
```
|
||||
|
||||
### 真人网页验收
|
||||
|
||||
1. 文字:“记住我不吃香菜。”
|
||||
2. disconnect/reconnect。
|
||||
3. 文字:“我不吃什么?”
|
||||
4. 语音重复 1–3。
|
||||
5. 问实时问题,确认会搜索且显示来源。
|
||||
6. 助手播长回答时插话,确认音频停止、插话未丢失。
|
||||
7. 问“我是谁”,只能回答用户称呼,不能回答内部 ID。
|
||||
|
||||
### 性能验收
|
||||
|
||||
- 无工具普通对话不能因为 Flows 固定增加一次额外 LLM 调用。
|
||||
- 记忆检索仍有超时和失败降级。
|
||||
- 分别记录 VAD、STT、记忆检索、LLM 首 token、工具、TTS 首段和首音。
|
||||
- 对比 legacy/flows 的 P50/P95;无工具首音出现明显回退时不得切默认。
|
||||
|
||||
## 十一、明确非目标
|
||||
|
||||
本次不要做:
|
||||
|
||||
- 唤醒词和 20 秒连续对话窗口;
|
||||
- N5095 产品客户端、声卡 AEC 或阵列波束形成;
|
||||
- 人脸自动确认说话者;
|
||||
- Mem0、Chroma、Graphiti、LanceDB 等新存储;
|
||||
- OpenClaw、Agno、LangGraph 或 LiveKit 迁移;
|
||||
- 多 Agent、子 Agent 或复杂任务规划;
|
||||
- 米家/Home Assistant 工具;
|
||||
- systemd、自启动和公网暴露;
|
||||
- 清理或重写所有 legacy 代码。
|
||||
|
||||
## 十二、回滚要求
|
||||
|
||||
1. `AI_AGENT_RUNTIME=legacy` 必须能绕过全部 Flows 代码。
|
||||
2. 新数据库字段必须向后兼容,禁止 drop/rename 现有表或列。
|
||||
3. 工具执行要有幂等键或 source turn,避免重试重复写入。
|
||||
4. 新路径异常时,本轮可以无记忆继续,但不能自动切换后重复执行有副作用工具。
|
||||
5. 部署前保留数据库与上一版本代码;回滚不能依赖重新生成用户数据。
|
||||
|
||||
## 十三、建议 Claude Code 的第一轮动作
|
||||
|
||||
```bash
|
||||
cd ~/workspace/ai-companion
|
||||
git status --short
|
||||
git diff --check
|
||||
|
||||
# 先读这些文件,不要先改
|
||||
sed -n '1,240p' services/bot_server.py
|
||||
sed -n '240,560p' services/bot_server.py
|
||||
sed -n '180,500p' app/engine.py
|
||||
sed -n '1,220p' app/memory/base.py
|
||||
sed -n '220,380p' app/memory/sqlite_store.py
|
||||
```
|
||||
|
||||
然后只完成 Phase 1 spike,提交一份结论:
|
||||
|
||||
- 兼容的 flows 版本;
|
||||
- 选择复用 Pipecat OpenAI-compatible `LLMService`,还是实现
|
||||
`FallbackProvider → LLMService` 适配器;
|
||||
- token、tool-call、取消和 context 的实测结果;
|
||||
- 建议的最小生产集成 diff;
|
||||
- 是否会破坏现有分句 TTS、RTVI 和 fallback。
|
||||
|
||||
未经 spike 结论,不进入 Phase 3。
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
# SearXNG 私有搜索服务部署与使用
|
||||
|
||||
> 2026-09-16 记录。为 [[AI桌面陪伴助手方案]] 部署一个不绑定 LLM 厂商的联网搜索后端。服务运行在 `cadadmin-server`,只监听宿主机回环地址 `127.0.0.1:18080`,由同机助手调用,不对局域网或公网开放。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论与当前状态
|
||||
|
||||
当前方案是:**DeepSeek 负责判断何时搜索和组织回答,SearXNG 负责查询互联网。**
|
||||
|
||||
```text
|
||||
用户问题
|
||||
↓
|
||||
DeepSeek 判断是否需要实时资料
|
||||
↓ Function Calling: web_search(query)
|
||||
AI Companion 调用 SearXNG JSON API
|
||||
↓
|
||||
SearXNG 聚合 360 搜索 / 搜狗等上游结果
|
||||
↓ 标题、摘要、URL
|
||||
结果作为 tool 消息回灌 DeepSeek
|
||||
↓
|
||||
生成正文 + 来源 URL
|
||||
├─ 网页:显示正文和来源
|
||||
└─ TTS:只朗读正文,不读网址
|
||||
```
|
||||
|
||||
截至 2026-09-16:
|
||||
|
||||
| 项目 | 状态 |
|
||||
|---|---|
|
||||
| 容器 | `ai-companion-searxng`,运行中 |
|
||||
| 宿主机地址 | `http://127.0.0.1:18080` |
|
||||
| 暴露范围 | 仅服务器回环地址 |
|
||||
| 重启策略 | `unless-stopped` |
|
||||
| JSON API | 已开启 |
|
||||
| 可用上游 | 360 搜索、搜狗 |
|
||||
| 暂不可用 | DuckDuckGo、Brave、Wikipedia 超时;Bing 暂时返回空结果 |
|
||||
| 助手接入 | 已完成并通过真实工具调用验收 |
|
||||
| 镜像 | 固定到已验收的 OCI digest,未使用浮动 `latest` |
|
||||
|
||||
真实闭环验收结果:DeepSeek 主动调用 `web_search('DeepSeek 最新消息 2025')`,SearXNG 返回 5 条结果,结果回灌后模型生成带两个来源 URL 的最终回答。
|
||||
|
||||
---
|
||||
|
||||
## 二、SearXNG 是什么
|
||||
|
||||
SearXNG 是开源的**元搜索引擎**。它本身不维护网页索引,而是把查询转发给多个搜索服务和数据库,再统一整理结果。官方项目强调不追踪、不画像用户,代码采用 AGPL-3.0 许可证。
|
||||
|
||||
这意味着:
|
||||
|
||||
- 自建 SearXNG 不需要维护爬虫和海量网页索引;
|
||||
- 搜索质量、时延和可用性仍依赖上游搜索引擎;
|
||||
- 同一个配置换到不同网络环境,结果可能完全不同;
|
||||
- 自建的主要价值是统一 API、集中控制搜索源、减少对单一商业 API 的绑定,而不是“完全脱离外部搜索服务”。
|
||||
|
||||
官方资料:
|
||||
|
||||
- [SearXNG GitHub](https://github.com/searxng/searxng)
|
||||
- [容器部署文档](https://docs.searxng.org/admin/installation-docker.html)
|
||||
- [`settings.yml` 文档](https://docs.searxng.org/admin/settings/settings.html)
|
||||
- [搜索引擎配置](https://docs.searxng.org/admin/settings/settings_engines.html)
|
||||
- [Search API](https://docs.searxng.org/dev/search_api.html)
|
||||
|
||||
### 为什么选择它
|
||||
|
||||
| 方案 | 优点 | 缺点 | 本项目结论 |
|
||||
|---|---|---|---|
|
||||
| **SearXNG** | 开源、自建、统一 JSON API、可更换上游、无按次 API Key | 需要运维;上游会限流、验证码或封禁 | **采用** |
|
||||
| DeepSeek 官方搜索 | 模型接入简单 | DeepSeek API 当前没有可直接使用的内置网页搜索,仍需自行实现工具 | 不可单独解决 |
|
||||
| DDGS/DuckDuckGo 库 | 代码少、原型快 | 单一来源,服务器网络下 DuckDuckGo 实测超时 | 不采用 |
|
||||
| Tavily / Brave / Exa | API 稳定,面向 Agent | 需要 Key,可能按量收费,绑定厂商 | 可作为未来备用 |
|
||||
| Vane/Perplexica | 完整开源 AI 搜索应用 | 自带 UI、LLM 编排,与现有助手架构重叠,集成过重 | 只作参考 |
|
||||
|
||||
选择 SearXNG 的核心原因不是“绝对免费”,而是把搜索后端抽象成独立服务:以后更换 DeepSeek、本地模型或其他 LLM,不需要重写搜索层。
|
||||
|
||||
---
|
||||
|
||||
## 三、部署布局
|
||||
|
||||
代码仓库中的配置:
|
||||
|
||||
```text
|
||||
~/ai-companion/deploy/searxng/
|
||||
├── docker-compose.yml
|
||||
├── settings.yml.example # 可提交的模板,密钥是占位符
|
||||
└── settings.yml # 服务器实际配置,含随机密钥,不进 Git
|
||||
```
|
||||
|
||||
Docker 数据:
|
||||
|
||||
```text
|
||||
容器名:ai-companion-searxng
|
||||
配置挂载:./settings.yml → /etc/searxng/settings.yml:ro
|
||||
缓存卷:searxng_searxng-cache → /var/cache/searxng
|
||||
端口:127.0.0.1:18080 → 容器 8080
|
||||
```
|
||||
|
||||
官方文档列出的两个持久化位置是 `/etc/searxng` 和 `/var/cache/searxng`。本项目把配置作为只读单文件挂载,缓存放 named volume。配置必须备份,缓存丢失可以重建。
|
||||
|
||||
---
|
||||
|
||||
## 四、从零部署
|
||||
|
||||
### 1. 前提检查
|
||||
|
||||
```bash
|
||||
docker version
|
||||
docker compose version
|
||||
ss -ltn | grep ':18080 ' || true
|
||||
```
|
||||
|
||||
本次服务器环境:Docker 28.4.0、Docker Compose v2.39.4,`18080` 原先未占用。
|
||||
|
||||
### 2. Compose 文件
|
||||
|
||||
`deploy/searxng/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
searxng:
|
||||
# 固定到 2026-09-16 已通过真实搜索验收的镜像。
|
||||
image: docker.io/searxng/searxng@sha256:f1aca3f61c7ff4552b8d54c0ab7fb458e907b78f51c503210cf450c900adca2c
|
||||
container_name: ai-companion-searxng
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:18080:8080"
|
||||
volumes:
|
||||
- ./settings.yml:/etc/searxng/settings.yml:ro
|
||||
- searxng-cache:/var/cache/searxng
|
||||
environment:
|
||||
- SEARXNG_BASE_URL=http://127.0.0.1:18080/
|
||||
|
||||
volumes:
|
||||
searxng-cache:
|
||||
```
|
||||
|
||||
为什么不写 `0.0.0.0:18080:8080`:当前服务没有对公网用户设计认证和限流,只供同机助手调用。绑定 `127.0.0.1` 能从网络边界直接阻止其他机器访问。
|
||||
|
||||
为什么固定 digest:`latest` 会在下次拉取时静默改变版本,可能带来配置兼容或搜索引擎行为变化。应先在新镜像上完成 API 和真实工具调用验收,再更新 digest。
|
||||
|
||||
### 3. `settings.yml` 模板
|
||||
|
||||
```yaml
|
||||
use_default_settings:
|
||||
engines:
|
||||
keep_only:
|
||||
- bing
|
||||
- bing news
|
||||
- 360search
|
||||
- sogou
|
||||
|
||||
general:
|
||||
instance_name: AI Companion Search
|
||||
debug: false
|
||||
|
||||
search:
|
||||
safe_search: 1
|
||||
formats:
|
||||
- html
|
||||
- json
|
||||
|
||||
server:
|
||||
secret_key: "__SEARXNG_SECRET__"
|
||||
bind_address: "0.0.0.0"
|
||||
limiter: false
|
||||
public_instance: false
|
||||
image_proxy: false
|
||||
|
||||
engines:
|
||||
- name: bing
|
||||
disabled: false
|
||||
- name: bing news
|
||||
disabled: false
|
||||
- name: 360search
|
||||
disabled: false
|
||||
- name: sogou
|
||||
disabled: false
|
||||
```
|
||||
|
||||
关键解释:
|
||||
|
||||
- `use_default_settings`:继承官方完整默认配置,只覆盖本项目关心的部分,减少升级维护量;
|
||||
- `keep_only`:只加载指定引擎,避免每次等待当前网络必然超时的搜索源;
|
||||
- `disabled: false`:必须显式启用。**只写 `keep_only` 不等于启用引擎**,它会保留默认的禁用状态;
|
||||
- `formats: [html, json]`:JSON API 必须显式开放。官方文档说明,未启用的输出格式会返回 HTTP 403;
|
||||
- `safe_search: 1`:默认使用中等级别安全搜索;
|
||||
- `limiter: false`:仅因为宿主机端口只绑定回环地址且是单用户内部调用;如果对外暴露,不能照抄;
|
||||
- 容器内部 `bind_address` 为 `0.0.0.0`,是为了让 Docker 端口映射能访问;真正的外部边界由宿主机的 `127.0.0.1:18080` 控制。
|
||||
|
||||
### 4. 生成实际配置
|
||||
|
||||
不要把真实 `secret_key` 写进仓库:
|
||||
|
||||
```bash
|
||||
cd ~/ai-companion/deploy/searxng
|
||||
|
||||
search_secret=$(openssl rand -hex 32)
|
||||
sed "s/__SEARXNG_SECRET__/${search_secret}/" \
|
||||
settings.yml.example > settings.yml
|
||||
chmod 600 settings.yml
|
||||
```
|
||||
|
||||
仓库 `.gitignore` 已包含:
|
||||
|
||||
```gitignore
|
||||
deploy/searxng/settings.yml
|
||||
```
|
||||
|
||||
### 5. 启动
|
||||
|
||||
```bash
|
||||
cd ~/ai-companion/deploy/searxng
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
预期端口应显示为:
|
||||
|
||||
```text
|
||||
127.0.0.1:18080->8080/tcp
|
||||
```
|
||||
|
||||
首次拉取镜像在本次环境中比较慢,但下载一直有进度;不要因为几十秒没有完成就重复启动多个 `docker pull`。
|
||||
|
||||
---
|
||||
|
||||
## 五、验证与使用
|
||||
|
||||
### 1. 最小健康验证
|
||||
|
||||
SearXNG 没有必要为本项目额外暴露公开健康接口,直接查询 JSON API:
|
||||
|
||||
```bash
|
||||
curl -GsS --max-time 20 http://127.0.0.1:18080/search \
|
||||
--data-urlencode 'q=DeepSeek' \
|
||||
--data-urlencode 'format=json' \
|
||||
| jq '.results | length'
|
||||
```
|
||||
|
||||
返回大于零的数字才算真正可用。**HTTP 200 或容器处于 Up 状态不代表上游搜索可用。**
|
||||
|
||||
查看前三条结果:
|
||||
|
||||
```bash
|
||||
curl -GsS --max-time 20 http://127.0.0.1:18080/search \
|
||||
--data-urlencode 'q=DeepSeek 最新消息' \
|
||||
--data-urlencode 'format=json' \
|
||||
--data-urlencode 'language=zh-CN' \
|
||||
| jq -r '.results[0:3][] | .title + " | " + .url'
|
||||
```
|
||||
|
||||
官方 API 支持 `/` 和 `/search` 两个端点、GET/POST 两种方法;常用参数包括:
|
||||
|
||||
| 参数 | 用途 |
|
||||
|---|---|
|
||||
| `q` | 必填,搜索词 |
|
||||
| `format` | `json` / `csv` / `rss`,必须在配置中启用 |
|
||||
| `language` | 搜索语言,如 `zh-CN` |
|
||||
| `categories` | 搜索分类 |
|
||||
| `engines` | 临时指定引擎,逗号分隔 |
|
||||
| `pageno` | 页码 |
|
||||
| `time_range` | `day` / `month` / `year`,仅对支持的引擎有效 |
|
||||
| `safesearch` | `0` / `1` / `2` |
|
||||
|
||||
指定单一引擎诊断:
|
||||
|
||||
```bash
|
||||
curl -GsS http://127.0.0.1:18080/search \
|
||||
--data-urlencode 'q=DeepSeek' \
|
||||
--data-urlencode 'format=json' \
|
||||
--data-urlencode 'engines=sogou' \
|
||||
| jq '.results | length'
|
||||
```
|
||||
|
||||
### 2. 从其他电脑临时打开 Web UI
|
||||
|
||||
服务没有直接暴露到局域网。需要人工查看时,用 SSH 隧道:
|
||||
|
||||
```bash
|
||||
ssh -L 18080:127.0.0.1:18080 \
|
||||
yonglin@cadadmin-server.taild0f91d.ts.net
|
||||
```
|
||||
|
||||
然后在本机浏览器打开:
|
||||
|
||||
```text
|
||||
http://localhost:18080/
|
||||
```
|
||||
|
||||
不要为了偶尔看 UI 就把 Compose 改成 `0.0.0.0:18080:8080`。
|
||||
|
||||
### 3. Python 调用示例
|
||||
|
||||
```python
|
||||
import aiohttp
|
||||
|
||||
async def search(query: str):
|
||||
timeout = aiohttp.ClientTimeout(total=6)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(
|
||||
"http://127.0.0.1:18080/search",
|
||||
params={
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"language": "zh-CN",
|
||||
"safesearch": "1",
|
||||
},
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json(content_type=None)
|
||||
```
|
||||
|
||||
生产代码还应限制查询长度、结果数量、摘要长度,并只接受 `http` / `https` URL。本项目的实现见 `ai-companion/app/search.py`。
|
||||
|
||||
---
|
||||
|
||||
## 六、接入 AI Companion
|
||||
|
||||
### 配置
|
||||
|
||||
服务器 `~/.config/ai_assistant/env`:
|
||||
|
||||
```bash
|
||||
SEARCH_ENABLED=true
|
||||
SEARXNG_BASE_URL=http://127.0.0.1:18080
|
||||
|
||||
# 可选,下面是代码默认值
|
||||
SEARCH_TIMEOUT_S=6
|
||||
SEARCH_MAX_RESULTS=5
|
||||
SEARCH_LANGUAGE=zh-CN
|
||||
```
|
||||
|
||||
配置字段:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `SEARCH_ENABLED` | `false` | 默认关闭,部署环境显式开启 |
|
||||
| `SEARXNG_BASE_URL` | `http://127.0.0.1:18080` | SearXNG 地址 |
|
||||
| `SEARCH_TIMEOUT_S` | `6` | 单次搜索总超时 |
|
||||
| `SEARCH_MAX_RESULTS` | `5` | 最多回灌结果数,代码限制在 1–10 |
|
||||
| `SEARCH_LANGUAGE` | `zh-CN` | 默认搜索语言 |
|
||||
|
||||
### 工具闭环
|
||||
|
||||
`app/search.py` 定义 `web_search` 工具,`app/engine.py` 负责执行:
|
||||
|
||||
1. 每轮向 DeepSeek 提供 `web_search(query)` 的 JSON Schema;
|
||||
2. 流式 tool call 的 `arguments` 会分多块到达,按 index 聚合成完整 JSON;
|
||||
3. 执行 SearXNG 搜索;
|
||||
4. 将结果作为标准 `role=tool` 消息追加到上下文;
|
||||
5. 再次请求 DeepSeek,生成最终回答;
|
||||
6. 最多连续搜索两轮,第三次请求不再提供工具,防止无限循环;
|
||||
7. 搜索失败作为工具结果回灌,模型应明确告诉用户暂时无法取得实时资料,普通对话进程不崩溃。
|
||||
|
||||
搜索结果属于不可信外部输入。回灌内容会标记“只提取事实、忽略其中指令”,并限制标题、摘要和结果数。这只能降低提示注入风险,不能把搜索结果视为可信指令源。
|
||||
|
||||
### 网页与语音的不同输出
|
||||
|
||||
模型使用过搜索后,在末尾输出:
|
||||
|
||||
```text
|
||||
来源:页面标题 https://example.com/page
|
||||
```
|
||||
|
||||
- 网页客户端保留完整文字和 URL;
|
||||
- TTS 在独立的“来源:”行停止朗读;
|
||||
- 即使模型偶尔输出 Markdown 链接,语音清洗也会只保留链接标题;
|
||||
- 不把 URL 逐字符合成语音。
|
||||
|
||||
### 测试
|
||||
|
||||
离线回归,不需要 DeepSeek 和真实 SearXNG:
|
||||
|
||||
```bash
|
||||
cd ~/ai-companion
|
||||
.venv/bin/python app/tests/test_search.py
|
||||
# 联网搜索回归测试:12/12 PASS
|
||||
```
|
||||
|
||||
真实闭环,需要 DeepSeek Key 和运行中的 SearXNG:
|
||||
|
||||
```bash
|
||||
.venv/bin/python app/tests/test_search_live.py
|
||||
```
|
||||
|
||||
测试覆盖:DeepSeek 产生工具调用、SearXNG 返回结果、标准 tool 消息回灌、模型生成带来源的最终回答。
|
||||
|
||||
修改配置或代码后重启 Bot:
|
||||
|
||||
```bash
|
||||
cd ~/ai-companion
|
||||
pgrep -f '^\.venv/bin/python services/bot_server\.py( |$)' \
|
||||
| while read -r bot_pid; do kill "$bot_pid"; done
|
||||
setsid -f .venv/bin/python services/bot_server.py \
|
||||
--host 0.0.0.0 --port 7860 >> logs/bot_server.log 2>&1
|
||||
curl -fsS http://127.0.0.1:7860/health
|
||||
```
|
||||
|
||||
这里用锚定到进程命令开头的 `pgrep`,再对明确 PID 执行 `kill`。不要直接使用宽泛的
|
||||
`pkill -f 'services/bot_server.py'`:此次部署曾因同一条 SSH 命令后半段也包含这个
|
||||
字符串,导致 `pkill -f` 把远端 shell 一起结束。长期应改用 systemd 管理服务。
|
||||
|
||||
---
|
||||
|
||||
## 七、本次部署过程与踩坑
|
||||
|
||||
### 1. 初始状态:容器正常,但搜索结果为零
|
||||
|
||||
首次部署后:
|
||||
|
||||
```text
|
||||
container: Up
|
||||
HTTP API: 200
|
||||
results: 0
|
||||
```
|
||||
|
||||
日志显示默认引擎全部超时:
|
||||
|
||||
```text
|
||||
wikipedia: Timeout
|
||||
brave: Timeout
|
||||
duckduckgo: Timeout
|
||||
wikidata: Timeout
|
||||
google cse: Timeout
|
||||
```
|
||||
|
||||
结论:只检查容器状态和 HTTP 状态码会产生“服务正常”的假象,必须检查 `results` 数量及实际标题、URL。
|
||||
|
||||
### 2. 分宿主机与容器测试上游
|
||||
|
||||
宿主机实测:
|
||||
|
||||
| 上游 | 结果 |
|
||||
|---|---|
|
||||
| 百度 | 能连接,但搜索请求跳验证码 |
|
||||
| Bing 中国 | HTTP 200,但通过 SearXNG 暂时无结果 |
|
||||
| 360 搜索 | HTTP 200,SearXNG 有结果 |
|
||||
| 搜狗 | HTTP 200,SearXNG 有结果 |
|
||||
| DuckDuckGo HTML | 8 秒超时 |
|
||||
|
||||
容器内访问百度成功,说明不是 Docker bridge 完全断网,而是特定境外上游不可达或受限。因此没有改成 host 网络,而是调整搜索源。
|
||||
|
||||
### 3. `keep_only` 不会自动启用引擎
|
||||
|
||||
第一次只配置:
|
||||
|
||||
```yaml
|
||||
use_default_settings:
|
||||
engines:
|
||||
keep_only:
|
||||
- bing
|
||||
- 360search
|
||||
- sogou
|
||||
```
|
||||
|
||||
逐个指定 `engines=360search` 或 `engines=sogou` 时有结果,但不指定 `engines` 的聚合查询仍返回 0。
|
||||
|
||||
原因:`keep_only` 只限制加载范围,仍继承这些引擎在默认配置中的 `disabled` 状态。补上以下配置后聚合查询恢复:
|
||||
|
||||
```yaml
|
||||
engines:
|
||||
- name: 360search
|
||||
disabled: false
|
||||
- name: sogou
|
||||
disabled: false
|
||||
```
|
||||
|
||||
最终聚合测试先后返回 8、14、5 条结果;数量会随查询和上游状态变化,判断标准应是“大于零且内容合理”,不能写死固定数量。
|
||||
|
||||
### 4. JSON API 默认不一定开放
|
||||
|
||||
SearXNG 的默认输出格式通常只有 HTML。如果没有在 `search.formats` 中加入 `json`:
|
||||
|
||||
```text
|
||||
/search?q=test&format=json → 403 Forbidden
|
||||
```
|
||||
|
||||
这是配置行为,不是路由不存在。
|
||||
|
||||
### 5. 搜索源会动态失效
|
||||
|
||||
元搜索无法消除上游限制。常见变化:
|
||||
|
||||
- 某引擎开始返回验证码;
|
||||
- 服务器 IP 被限流;
|
||||
- 引擎页面结构变化,SearXNG 解析器暂时失效;
|
||||
- 境外域名在当前网络超时;
|
||||
- 语言参数或复杂搜索词导致特定引擎零结果。
|
||||
|
||||
所以必须保留多引擎、超时、日志和失败降级,不能假定“一次配置永久可用”。
|
||||
|
||||
---
|
||||
|
||||
## 八、日常运维
|
||||
|
||||
### 状态和日志
|
||||
|
||||
```bash
|
||||
cd ~/ai-companion/deploy/searxng
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 searxng
|
||||
docker stats --no-stream ai-companion-searxng
|
||||
```
|
||||
|
||||
确认端口没有意外对外暴露:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{(index (index .HostConfig.PortBindings "8080/tcp") 0).HostIp}}:{{(index (index .HostConfig.PortBindings "8080/tcp") 0).HostPort}}' \
|
||||
ai-companion-searxng
|
||||
# 必须是 127.0.0.1:18080
|
||||
```
|
||||
|
||||
### 重启与停止
|
||||
|
||||
```bash
|
||||
docker compose restart searxng
|
||||
docker compose stop searxng
|
||||
docker compose up -d searxng
|
||||
```
|
||||
|
||||
`docker compose down` 会删除容器和 Compose 网络,但默认不会删除 named volume;不要随意加 `-v`。
|
||||
|
||||
### 升级
|
||||
|
||||
当前 Compose 固定 digest。升级步骤:
|
||||
|
||||
```bash
|
||||
docker pull docker.io/searxng/searxng:latest
|
||||
docker image inspect docker.io/searxng/searxng:latest \
|
||||
--format '{{index .RepoDigests 0}}'
|
||||
```
|
||||
|
||||
把得到的 `searxng/searxng@sha256:...` 更新到 Compose,然后:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
|
||||
# API 验收
|
||||
curl -GsS --max-time 20 http://127.0.0.1:18080/search \
|
||||
--data-urlencode 'q=DeepSeek' \
|
||||
--data-urlencode 'format=json' \
|
||||
| jq '.results | length'
|
||||
|
||||
# DeepSeek 工具闭环验收
|
||||
cd ~/ai-companion
|
||||
.venv/bin/python app/tests/test_search_live.py
|
||||
```
|
||||
|
||||
两个测试都通过后再提交新的 digest。官方文档建议升级时检查 Compose 模板和环境变量变化;固定 digest 不意味着永不升级,而是把升级变成显式、可验证的操作。
|
||||
|
||||
### 备份与恢复
|
||||
|
||||
需要备份:
|
||||
|
||||
```text
|
||||
deploy/searxng/docker-compose.yml
|
||||
deploy/searxng/settings.yml.example
|
||||
服务器实际的 deploy/searxng/settings.yml(包含 secret,放安全备份)
|
||||
```
|
||||
|
||||
缓存卷不属于关键数据,可以重新创建。恢复时也可以从模板生成新的随机 secret,不要求保留旧值,因为当前没有外部登录会话依赖它。
|
||||
|
||||
---
|
||||
|
||||
## 九、故障排查
|
||||
|
||||
| 现象 | 可能原因 | 检查与处理 |
|
||||
|---|---|---|
|
||||
| 连接拒绝 | 容器没启动或端口错误 | `docker compose ps`、`ss -ltn | grep 18080` |
|
||||
| HTTP 403 | `json` 未加入 `search.formats` | 检查 `settings.yml`,重启容器 |
|
||||
| HTTP 200、结果 0 条 | 默认引擎禁用或全部上游失败 | 逐个传 `engines=` 测试,查看容器日志 |
|
||||
| 单引擎有结果,聚合为 0 | 只写了 `keep_only`,未写 `disabled: false` | 显式启用引擎 |
|
||||
| 查询很慢 | 配置中保留了当前网络必然超时的引擎 | 收紧 `keep_only`,设置合理超时 |
|
||||
| 百度跳验证码 | 服务器 IP 被风控 | 不作为当前主搜索源 |
|
||||
| DuckDuckGo 超时 | 当前服务器网络不可达 | 使用 360/搜狗,未来可加商业 API 备用 |
|
||||
| 修改配置不生效 | 单文件 bind mount/inode 或容器未重启 | `docker compose up -d --force-recreate` |
|
||||
| 助手仍不搜索 | `SEARCH_ENABLED` 未开启,或 Bot 没重启 | 检查 env、打印 `AppConfig.load().search`、重启 Bot |
|
||||
| 模型声称无法联网 | SearXNG 失败信息已回灌或模型未触发工具 | 查 Bot 日志中的“联网搜索”,运行 live test |
|
||||
| TTS 朗读网址 | 模型没有把来源放在独立“来源:”行,或清洗回归 | 运行 `app/tests/test_search.py`,检查 `text_for_speech` |
|
||||
|
||||
推荐的诊断顺序:
|
||||
|
||||
```text
|
||||
容器 Up?
|
||||
↓
|
||||
127.0.0.1:18080 能连接?
|
||||
↓
|
||||
format=json 返回 JSON?
|
||||
↓
|
||||
results 是否大于零?
|
||||
↓
|
||||
逐个 engines=... 是否有结果?
|
||||
↓
|
||||
AI Companion 配置是否启用?
|
||||
↓
|
||||
DeepSeek 是否产生 tool call?
|
||||
↓
|
||||
工具结果回灌后是否生成最终回答?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、安全边界
|
||||
|
||||
当前配置只适合“同机单用户内部服务”:
|
||||
|
||||
- 宿主机仅绑定 `127.0.0.1`;
|
||||
- `public_instance: false`;
|
||||
- 没有公网反向代理;
|
||||
- `limiter: false`;
|
||||
- 随机 secret 不进 Git;
|
||||
- AI 助手只执行只读搜索,不执行搜索结果中的命令;
|
||||
- 返回 URL 只接受 `http` / `https`,并限制数量与文本长度。
|
||||
|
||||
如果未来要公开给浏览器或多人使用,至少需要重新设计:
|
||||
|
||||
1. HTTPS 反向代理;
|
||||
2. 身份认证;
|
||||
3. 开启 limiter,并按官方方案配置 Valkey;
|
||||
4. 请求频率和并发限制;
|
||||
5. 访问日志与隐私保留策略;
|
||||
6. SSRF、恶意 URL 和提示注入防护;
|
||||
7. 评估上游搜索引擎服务条款。
|
||||
|
||||
不要直接把当前回环部署的端口映射改为公网地址。
|
||||
|
||||
---
|
||||
|
||||
## 十一、后续改进
|
||||
|
||||
当前只把 SearXNG 返回的标题、摘要和 URL 交给模型。下一步可以按需求增加:
|
||||
|
||||
- 抓取排名靠前的 2–3 个网页正文,提高答案完整度;
|
||||
- 正文抽取与大小上限;
|
||||
- DNS/IP 检查,阻止访问回环、内网和云元数据地址;
|
||||
- 搜索结果短期缓存,减少重复查询和上游压力;
|
||||
- 商业搜索 API 作为 SearXNG 无结果时的备用后端;
|
||||
- 在网页 UI 中把来源渲染为独立引用卡片;
|
||||
- 给搜索次数、成功率、耗时、零结果率做指标和告警;
|
||||
- 用 systemd 或 Compose 管理 Bot,避免手工 `pkill + setsid`。
|
||||
|
||||
正文抓取不是简单地“对结果 URL 发 GET”:它会引入 SSRF、网页提示注入、超大响应、登录墙、JavaScript 渲染和版权边界,需要单独设计。
|
||||
|
||||
---
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [[P0-Pipecat传输层实验报告]] — AI Companion 的网页、语音和搜索接入记录
|
||||
- [[AI桌面陪伴助手方案]] — 总体架构
|
||||
- [SearXNG 官方容器部署](https://docs.searxng.org/admin/installation-docker.html)
|
||||
- [SearXNG Search API](https://docs.searxng.org/dev/search_api.html)
|
||||
- [SearXNG `settings.yml`](https://docs.searxng.org/admin/settings/settings.html)
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-09-16*
|
||||
Reference in New Issue
Block a user