Files
ai-companion/README.md
T
ylhan 35e107c290 助手本体:会话引擎、LLM 路由、记忆、身份、语音适配
从 Obsidian 笔记库剥离为独立仓库。规划文档与 P0 报告留在笔记库,
代码(助手本体、TTS 服务、P0 基准脚本、ESP32 固件)在此。

架构:服务器是助手,本地是客户端
- 助手本体跑在 cadadmin-server,N5095 只做 USB 外设桥与网络桥接
- 起因:实测 N5095 无 AVX2,跑不动可用的语音模型

当前技术栈
- LLM:DeepSeek deepseek-flash(云端,必须关思考)
- TTS:Qwen3-TTS 1.7B(服务器 3090,独立 HTTP 服务)
- STT:Zipformer 中英双语 int8(CPU,中文识别率未测)

实测:端到端 16/16 通过,首音延迟 1545ms
(对比:本地 LLM + vits-zh-ll 是 179ms,但音色不满意——音质与延迟
在此硬件上此消彼长)

设计要点
- MemoryStore 接口按"可能远程"设计(全 async、可超时、可失败)
- user_id / session_id / track_id 三者分开,人脸只作候选不自动切身份
- 记忆查询强制带 owner_user_id,无按 id 单独取的口子
- 自动抽取的记忆是"候选",默认不参与检索
- 异步写记忆绑定创建时的身份,不读"当前用户"
- 引擎与传输解耦,只用文件音频即可端到端测试

模型与音频产物不进版本管理(1.1GB),由 download_models.sh 恢复
2026-09-15 11:05:09 +08:00

91 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI 桌面陪伴助手 · 代码说明
**架构一句话**:服务器是助手,本地是客户端。
助手本体(会话引擎、记忆、身份、STT/TTS 调度)跑在 cadadmin-server;
N5095 只做 USB 外设接入和网络桥接。理由与实测见 `P0-语音基准报告.md`。
运行方式与配置见 `部署说明.md`;总体方案见 `AI桌面陪伴助手方案.md`。
## 目录结构
```
app/ 助手本体(部署到服务器)
├── config.py 配置。全部可用环境变量覆盖——部署时不改代码
├── prompts.py 提示词构造。单条 system 每轮重建
├── engine.py 会话引擎:身份 → 记忆 → LLM → TTS 的核心回路
├── llm/ LLM 抽象与后端
│ ├── base.py 接口 + SSE 解析 + 消息归一化
│ ├── local_vllm.py 本机 vLLM(Qwen3.8-27B,已停用)
│ ├── deepseek.py DeepSeek 云 API(当前主用)
│ └── __init__.py 工厂 + 降级路由
├── memory/ 记忆
│ ├── base.py 接口。**按"可能远程"设计**——全 async、可超时、可失败
│ └── sqlite_store.py SQLite 实现
├── identity/ 身份
│ ├── base.py 接口。人脸只提供候选,**不自动切身份**
│ └── manual.py 首版实现:显式切换 + 访客兜底
├── voice/ 语音
│ ├── stt.py sherpa-onnx 流式 Zipformer(CPU)
│ ├── tts.py 本地 vits-zh-ll(sherpa-onnx)
│ └── remote_tts.py Qwen3-TTS 远端服务适配(当前主用)
└── tests/ 4 个测试,共 88 项断言,全部无需硬件
services/
└── tts_server.py Qwen3-TTS 服务(跑在 ~/tts-qwen,独立 venv)
p0/ P0 阶段的验证与基准脚本(保留用于复现)
├── verify_deepseek.py DeepSeek 四条路径
├── verify_local_llm.py 本机 Qwen3.8-27B
└── stt_tts_bench/ STT/TTS 基准、模型下载脚本、线程扫描
```
## 测试
```bash
# 服务器上(完整)
cd ~/ai-companion
.venv/bin/python app/tests/test_llm.py # 27 项
.venv/bin/python app/tests/test_memory.py # 28 项
.venv/bin/python app/tests/test_identity.py # 17 项
.venv/bin/python app/tests/test_engine_e2e.py # 16 项,端到端
```
端到端测试**不需要麦克风、音箱、摄像头、ESP32**——
这是"服务器即助手"架构的直接好处:链路可以在任何有 CPU 的地方验证,
硬件到货后再接最后一段。跑完会把合成音频写到 `out/`,可以人工试听。
## 设计上几个刻意的选择
**接口按最坏情况设计。** `MemoryStore` 全是 async、可超时、可失败,
虽然现在背后是本地 SQLite(微秒级)。因为将来多设备共享记忆时要搬到服务器,
如果现在写成同步直连本地文件,那时要重写所有调用方。
**身份、会话、视觉追踪三者分开。** `user_id`(永久身份)、`session_id`(一次会话)、
`track_id`(画面里的一个目标)互不冒充。人脸认出来**不等于**确认对方在说话——
所以人脸只产生"建议",不自动切换身份。做错的后果是路人经过就把你的私人记忆念出来。
**记忆查询强制带 `owner_user_id`。** 没有"按 memory_id 单独取"的口子:
猜到 id 就能读别人的记忆,是这类系统最容易犯的错。删除同理,必须同时匹配 owner。
**自动抽取的记忆是"候选",默认不参与检索。** 只有用户显式"记住"或工具调用
写入的才是"已确认"。对应方案 §8 的"模型推测不当作确认事实"。
**异步任务绑定创建时的身份。** 回复后写记忆的任务在创建时捕获 `user_id`,
不读"当前用户"——否则用户在这几百毫秒内切了身份,这条记忆会写到别人名下。
**引擎与传输解耦。** 不依赖 Pipecat、不依赖任何传输方式,只依赖四个接口。
好处:可以用文件音频端到端测试;接 WebRTC 时只需写薄适配层。
## 踩过的坑
全部写在对应文件的注释里,这里只列索引,详见 `部署说明.md` §已知的坑:
- 本机 vLLM 只接受一条 system 消息(多条 400)
- 思考字段名 vLLM 用 `reasoning`、DeepSeek 用 `reasoning_content`
- DeepSeek 默认开思考,多轮/工具回灌会 400;且思考会吃掉 `max_tokens`
- `max_num_sentences=1` 静默截断多句输入
- VITS 非自回归,首音 = 整句合成耗时
- Qwen3-TTS 短句 RTF 反而最差(固定开销约 1 秒)
- qwen-tts 与 vllm-omni 依赖互斥,必须分 venv
- STT 与 TTS 的线程扩展方向相反