SOP-001:小樱桃语音交互体系¶
最后变更:2026-09-10 14:44
语音设备日常运维 · VAD 参数 · 音量增益 · PowerMem · 角色设定 · 故障排查
服务器拓扑¶
┌─────────────┐ WebSocket ┌──────────────────┐
│ ESP32 │ ─────────────────→ │ xiaozhi-esp32- │
│ (音箱) │ ws://:18000 │ server (docker) │
└─────────────┘ └────────┬─────────┘
│
┌──────┴──────┐
│ open-xiaoai │
│ -xiaozhi │
│ 桥接器 │
└──────┬──────┘
│
┌──────────────┐
│ DeepSeek API │
│ deepseek-flash│
│ (V4.1-Flash)│
└──────────────┘
小樱桃容器内还包含:
┌─────────────────────────────────────────┐
│ xiaozhi-esp32-server (container) │
│ ├─ app.py → WebSocket :18000 │
│ ├─ app.py → subprocess.Popen │
│ │ └─ powermem-server :8848 │
│ └─ powermem_dev.db ← 唯一记忆 DB │
└─────────────────────────────────────────┘
关键地址:
- 服务器 IP:192.168.31.27
- WebSocket 端口:18000
- HTTP 端口:18003
- Dashboard 端口:8847
- 容器名:xiaozhi-esp32-server
参数表¶
VAD(语音活动检测)¶
| 参数 | server | bridge | 说明 |
|---|---|---|---|
| threshold | 0.30 | 0.10 | 进入说话的门槛 |
| threshold_low | 0.20 | 0.01 | 退出说话的门槛(保持 hysteresis 差距) |
| min_speech_duration | — | 250ms | 最短语音触发时长 |
| min_silence | 1200ms | 1200ms | 静默多久算完(两端统一) |
| boost | — | 10(代码未读取,无效参数) | 配置中 +10,但 bridge 代码无任何引用 |
| frame_window_threshold | 3 | — | 连续几帧达到 threshold 才算开始说话(防误触) |
VAD 双阈值(hysteresis):
threshold > threshold_low,防止在边界处反复触发/关闭。 桥接器 VAD 默认值见/app/.venv/.../silero_vad/__init__.py,配置覆盖见/app/config.py。 服务器 VAD 默认值见silero.py:27(threshold=0.5, threshold_low=0.2, min_silence=1000),.config.yaml 覆盖为当前运行值。 ⚠️ server 修改方式:改.config.yaml→docker restart xiaozhi-esp32-server⚠️ bridge 修改方式:改config.py→docker restart open-xiaoai-xiaozhi
唤醒与超时¶
| 参数 | bridge | server | 说明 |
|---|---|---|---|
| 唤醒前动作 | "嗯!" + 0.5s 缓冲 |
— | bridge before_wakeup,KWS 唤醒时播放缓冲 |
| 小爱拦截 | abort_xiaoai + sleep 2.0s |
— | 小爱正在说话时拦截并等待 |
| 唤醒后动作 | "小小苏,拜拜" |
— | bridge after_wakeup,退出时播报 |
| 唤醒词 | 你好小樱桃/小樱桃你好/呼叫小樱桃/召唤小樱桃 |
— | 支持 4 个变体 |
| 唤醒超时 | 20s | — | 超过此时间未说话自动退出 |
| WS 超时 | — | 120s(close_connection_no_voice_time) |
无语音多久断连 |
| TTS 超时 | — | 15s(tts_timeout) |
TTS 生成超时上限 |
音量¶
| # | 位置 | 增益 | 实现方式 |
|---|---|---|---|
| 1 | 桥接器 codec.py → write_audio() |
audioop.mul(pcm_data, 2, 1.5) 约 +3.5dB |
PCM 采样值相乘 |
| 2 | 桥接器 config.py | boost: 10 |
无效参数(代码未读取此字段) |
| 3 | 服务器 util.py → audioop.mul(raw_data, 2, 3) |
3x = +9.5dB | PCM 采样值相乘 |
| 4 | 服务器 .config.yaml | gain: 10 |
CosyVoice2 API 增益参数(满值) |
#1、#3、#4 是同一组,一起调小樱桃和小爱的音量一致性。
2(boost)是无效参数,代码未读取,修改无任何效果。¶
模型配置¶
| 参数 | 值 |
|---|---|
| LLM (主) | deepseek-flash(DeepSeek V4.1-Flash)via DeepSeek api.deepseek.com(2026-09-10 起,原 deepseek-chat 别名 → 正式名,同一模型) |
| LLM (备用) | glm-4.5-air via 智谱 open.bigmodel.cn(原主,降级备用,2026-08-08 起) |
| Embedding | embedding-3 via 智谱 open.bigmodel.cn(1536 维,保持) |
| TTS | CosyVoice2-0.5B:anna via 硅基流动 siliconflow(gain=10, response=wav) |
| ASR | FunASR SenseVoiceSmall(容器内本地,模型 data/models/SenseVoiceSmall) |
| PowerMem | selected_module=powermem(启用量化记忆,向量库=sqlite) |
角色设定(小樱桃)¶
- 身份: 苏子桐的 AI 小伙伴(不是助手、不是老师)
- 年龄定位: 永远比苏子桐大 2 岁(她 6 岁我 8 岁,她 10 岁我 12 岁)
- 关系: 朋友/玩伴,不是管教者
- 语气: 童真、好奇、偶尔调皮
- 原则: 引导表达 > 直接给答案。不主动说教
- 知识边界: 通过 PowerMem 知道苏子桐的经历,但不假装全知
prompt 四段结构:模板规则 + few-shot + 动态上下文(时间/记忆) + 聊天历史
用户画像 — 双层注入¶
总体结构¶
每次对话时,系统向 LLM 注入两层用户画像:
<memory>
<stable_profile> ← 你维护的权威事实
[按话题匹配注入]
</stable_profile>
<dynamic_profile> ← PowerMem 自动学习
[AI 从对话中提取的印象]
</dynamic_profile>
优先级:stable 高于 dynamic,冲突时以 stable 为准
</memory>
稳定层(你维护)¶
- 来源:
stable_profile.txt(位于服务器数据目录) - 格式: 6 个
<topic>话题块 +_default兜底块 - 基本信息(年龄、身高、体重)
- 学习(RAZ、英语、游泳)
- 生活作息(睡眠时间)
- 兴趣爱好(动画角色)
- 社交(家人、手表、微信)
_default(简短概要,话题不命中时兜底)- 匹配方式: 用户消息关键词 → 命中对应话题块
- 聊 RAZ → 只注入"学习"块,省 token
- 聊叶罗丽 → 只注入"兴趣爱好"块
- 都不命中 → 仅注入
_default - 更新方式: 直接覆盖文件,下一句话立即生效(无需重启容器)
- 备份: 同目录
stable_profile.txt.bak - 路径:
/opt/xiaozhi-esp32-server/data/stable_profile.txt - 权限: 同目录其他配置文件一致
动态层(AI 自动)—— PowerMem 记忆系统¶
PowerMem 负责对话时的语义记忆搜索和对话记忆保存。容器内自洽运行,Dashboard 同容器内 :8848。
保存策略(save_memory)¶
对话完成后,只提取苏子桐的最后一句话,清洗后存入:
- 提取:只取最后一条 user 消息(不存小樱桃的回复)
- 清洗:去"苏子桐说:"前缀 → 去 emoji → 去末尾标点(。,!?)→ 去前后空白
- 过滤:退出意图(拜拜/再见/bye/88)、短消息(<3字)、无意义词(好的/嗯/哦)→ 不存
- 去重:MD5 去重,重复内容不写入
- 写入:
UserMemory.add(infer=False, metadata={source, user})→memories表 payload:完整 JSON(含 data 原文、hash、user_id、metadata)vector:JSON 数组字符串"[]"(SDK 自动维护 embedding)fulltext_content:清洗后的文本(供语义搜索召回)
搜索策略(query_memory)¶
用户说话时实时触发:
- 搜索当前用户消息的语义近邻记忆(limit=30)
- 同时注入
user_profiles表(用户画像动态层) - 搜索结果格式化后填入
<dynamic_profile>标签 → LLM prompt - 日志:
QUERY_DEBUG可见搜索结果数量、首条内容
Dashboard(Web 管理界面)¶
| 属性 | 值 |
|---|---|
| 地址 | http://192.168.31.27:8848/dashboard/ |
| 启动方式 | 容器启动时自动启动 —— app.py 中 subprocess.Popen(["powermem-server", "--host", "0.0.0.0", "--port", "8848"]) |
| 关联进程 | 容器内 powermem-server 进程(和 PowerMem SDK 同读一个 DB 文件) |
| 数据源 | 容器内 /opt/xiaozhi-esp32-server/data/powermem_dev.db(唯一 DB,无需同步到宿主) |
| API 端点 | /api/v1/memories / /api/v1/memories/stats / /api/v1/memories/search / /api/v1/memories/timeline |
| 检查项 | ① curl localhost:8848/api/v1/memories/stats 返回 total_memories ② 页面 timeline 有事件条 ③ 记忆条数 > 0 |
架构变化: v0.4 前 Dashboard 跑在宿主机,需 cron 同步 DB(因 ZOS FUSE 隔离 bind mount 不可行)。v0.4 起 Dashboard 移入容器,和 PowerMem 同读一个 DB 文件,零同步零延迟。旧宿主看门狗已停用。
持久化:powermem_dev.db¶
容器内 /opt/xiaozhi-esp32-server/data/powermem_dev.db 是唯一的 PowerMem 数据库。宿主同路径文件 /opt/data/xiaozhi-server/docker/xiaozhi-server/data/powermem_dev.db 是 Dashboard 的只读副本(容器重建后由 docker cp 初始化,后续同步仅通过日记 cron 的 docker cp 步骤)。
- 对话记忆:save_memory 直接写入容器内 DB
- 日记记忆:容器内
pm_diary_sync.py写入容器内 DB(每日 9:00 cron) - 搜索:PowerMem SDK 读取容器内 DB
- Dashboard:powermem-server 读取容器内 DB
- 宿主张贴板:仅在容器重建后需要
docker cp初始化
持久化说明¶
修改桥接器参数 → 直接改 /app/config.py + 重启容器
/app/config.py ← 桥接器参数(VAD/唤醒词),bind mount 挂载
/app/xiaozhi/services/audio/codec.py ← 音量增益 audioop.mul,直接改
服务器端:
/opt/xiaozhi-esp32-server/data/.config.yaml ← 服务器参数
/opt/xiaozhi-esp32-server/data/init_and_run.py ← 启动补丁(音量增益 + 模糊匹配 + 记忆保存)
/opt/xiaozhi-esp32-server/core/utils/util.py ← 音量增益(init_and_run 启动时打补丁)
/opt/xiaozhi-esp32-server/core/connection.py ← 模糊匹配 + 记忆保存(init_and_run 启动时打补丁)
/opt/xiaozhi-esp32-server/core/providers/memory/powermem/powermem.py ← save_memory 逻辑(pm_v12.py)
/opt/xiaozhi-esp32-server/app.py ← 主入口,启动时自动拉起 powermem-server (Dashboard :8848)
/opt/xiaozhi-esp32-server/data/pm_diary_sync.py ← 日记→记忆注入脚本(每日9:00 cron)
/opt/xiaozhi-esp32-server/data/obsidian/小樱桃日记.md ← 日记源文件(cron 从 Obsidian 同步到容器)
故障排查¶
| 症状 | 可能原因 | 检查 |
|---|---|---|
| 没声音 | VAD 门槛太高 / 音量太低 | 检查 threshold 和 gain |
| 破音 | 总增益过高 | 总增益 ≥ +18dB 时有削顶风险 |
| 回答太长被截断 | VAD min_silence 太长 | 检查 server 和 bridge 的 min_silence |
| 反复触发/频繁打断 | threshold == threshold_low | hysteresis 双阈值需保持差距 |
| 沉默不答 | TTS 超时 / ASR 超时 | 检查日志中的 timeout |
| 答非所问/说胡话 | PowerMem 污染 / model 不可用 | 检查 LLM 响应 + API key · → 见 powermem-save-fix skill |
| 名字念错/叫不对 | ASR 听歪 + 模糊匹配没兜住 | 缺的角色名加到 hotwords.txt(122条) |
| Dashboard 页面空白 | Dashboard 未启动 / 端口冲突 | |
| Dashboard Timeline 无事件 | 记忆为日记注入(直接 SQLite 写入),对话记忆才有 Timeline 事件 |
ASR 热词与模糊匹配¶
Hotwords + 拼音模糊匹配,两层兜底。
第一层:后处理模糊匹配(connection.py _fuzzy_correct_names)¶
ASR 完成后、LLM 处理前,接管文本做角色名修正:
| 策略 | 匹配条件 | 示例 |
|---|---|---|
| 精确匹配 | seg == hw | 海绵宝宝 → ✅ |
| 编辑距离 ≤ 1 | 2字名首字必须相同(防误匹配) | 赛咯 → 赛罗 ✅ / 和迪↛巴迪 ✅ |
| 拼音完全相同 | 编辑距离 ≥ 2 但拼音去声调后一致 | 赛箩 → 赛罗 ✅(同音) / 紫曰 → 紫悦 ✅ |
拼音匹配的额外收益: 多音字、口齿不清、同音别字全部覆盖。 防误匹配:2字名编辑距离=1时首字不同不匹配(防
和迪→巴迪)。
数据源:hotwords.txt¶
- 路径:
/opt/xiaozhi-esp32-server/data/hotwords.txt - 当前:122 个名字(奥特曼/小马宝莉/叶罗丽/迪士尼等角色)
- 特点:文件式动态加载,无需重启容器,改完即生效
- 更新方式:直接编辑文件或告诉我加角色名
文件位置¶
| 文件 | 用途 | 持久化方式 |
|---|---|---|
connection.py |
实时代码(_fuzzy_correct_names) |
init_and_run.py 启动补丁 |
init_and_run.py |
容器重启后补丁代码 | 挂载在容器内 |
hotwords.txt |
角色名列表(122条) | bind mount 挂载 |
变更日志¶
铁律:改参数必更新¶
任何涉及小樱桃代码、配置、参数的变更,必须同步更新本 SOP 对应章节。 不改 SOP-001 视为变更未完成。
检查清单: - [ ] VAD 参数变了? → 更新参数表 - [ ] 音量增益变了? → 更新参数表 - [ ] 模型/API变了? → 更新模型配置 - [ ] 角色设定变了? → 更新角色设定 - [ ] 排查流程新增了? → 更新故障排查 - [ ] 版本日志追加了一笔
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-07-06 | v1 | 初版,基线参数固化 |
| 2026-07-08 | v2 | 新增 ASR 热词拼音模糊匹配章节 |
| 2026-07-08 | v3 | 修正音量参数(1~4# 分组)、VAD 值对齐实际运行值、更新 TTS/CosyVoice2 模型配置、更新持久化文件路径 |
| 2026-07-08 | v4 | 桥接器 VAD 初调:threshold 0.10→0.20,threshold_low 新增0.15,min_speech 250→300ms |
| 2026-07-08 | v5 | 稳定状态:VAD 全部还原(0.10/0.01/250ms),仅留 min_silence=1000ms;唤醒改用"嗯!"+0.5s缓冲;TTS key 修正(硅基流动);prompt 讲故事不中断;SOP 记录 VAD 参数和唤醒流程 |
| 2026-07-09 | v6 | 用户画像重构:双层注入(stable_profile.txt + PowerMem),年龄定位修正为"永远比苏子桐大2岁",反哺机制从向量切片改为文件直接注入 |
| 2026-07-10 | v8 | PowerMem 三层加固:①PATCH7(去重+过滤无意义词+直写 SQLite)②中修(对话后刷新 user_profiles 表)③深修(修正 embedder 配置字段名 + 清洗嵌入文本去前缀表情 + 真实 1536 维向量写入)。语义搜索从空向量升级为智谱 embedding-3 全链路。 |
| 2026-07-11 | v9 | PowerMem 架构重构:Dashboard 移入容器内 :8848(app.py subprocess.Popen 自动启停),和 PowerMem SDK 同读一个 DB,零同步零延迟。save_memory 改为只存苏子桐消息(不存小樱桃回复),清洗+过滤退出意图。日记注入脚本容器内跑(pm_diary_sync.py,每日9:00 cron)。旧宿主看门狗停用。 |
| 2026-07-18 | v10 | 大修:#3 server PCM 2.0x→3.16x(+6→+10dB);#4 TTS gain 3→10(满值);#1 bridge PCM 补丁重加(1.5x);PowerMem 401 修(validation_alias 别名传 base_url);记忆去重 59→38;prompt 加【基本原则】「不知道就说不知道」防护;use_powermem 初始化失败改 False |
| 2026-08-08 | v11 | LLM 切换:主对话 LLM glm-4.5-air(智谱)→ deepseek-chat(DeepSeek),因智谱余额停用;PowerMem LLM 同步换 DeepSeek;embedding-3 保持智谱。已固化 xiaozhi-server:stable(sha 632c1a5) |
| 2026-09-10 | v12 | 模型名对齐:deepseek-chat(弃用别名)→ deepseek-flash(DeepSeek V4.1-Flash),两处(LLM.DeepSeekLLM.model_name + powermem.llm.config.model)已改并重启验证;embedding-3 保持智谱。已固化 xiaozhi-server:stable |
相关文档¶
| 方向 | 链接 |
|---|---|
| 🔗 输入 | SOP-004 融合画像更新(反哺来源) · SOP-003 雷达扫描与推送(日志归档) |
| 🔗 输出 | 最新画像(反哺目标) |
| 🔧 运维 | xiaozhi-ops skill(小樱桃全链路运维 · 修复历史 · 故障排查) |
| 🔧 系统 | digital-twin-ops skill(数字孪生系统全链路 · 健康检查 · SOP审计) |
| 🔧 修复 | powermem-save-fix skill(记忆保存失败/不可见排查) |
| 📝 变更 | 版本日志 |