Files
ai-companion/README.md
T
ylhan 20cfaa6ef7 基线:姓名档案修复、疑问句防误写、搜索与传输测试
这是进入 Pipecat Flows 改造前的回滚点。
提交时已核对本地与线上代码 md5 一致(engine.py 7e97bad8、
bot_server.py 1cf543ae),所以这个提交就是当前线上状态。

内容(此前一直在线上运行但未提交的改动):
- 姓名档案:用户自报"我叫林"同步写 confirmed 姓名,重连后恢复
- 疑问句防误写:"谁吗"之类的疑问句不再被覆盖成姓名
- 新增 test_profile_memory.py(姓名档案专项回归)
- 新增 test_profile_live.py(需要真实 LLM 的现场测试)
- 搜索、身份、记忆测试的相应补充

背景:下一步要按 P1-Pipecat Flows Agent化与记忆改造交接.md
渐进引入 Pipecat Flows,交接文档明确要求先保护基线、不得
reset/checkout/覆盖现有改动。此提交即该基线。
2026-09-16 12:48:18 +08:00

178 lines
10 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/ 回归与真实服务验收脚本
deploy/searxng/ 私有联网搜索服务(仅监听服务器回环地址)
services/
├── tts_server.py Qwen3-TTS 服务(跑在 ~/tts-qwen,独立 venv)
└── bot_server.py Pipecat SmallWebRTC 服务端(实验,见下)
client/
└── test_client.py 自写 aiortc 客户端(实验用,也是 N5095 客户端的雏形)
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 # 30 项
.venv/bin/python app/tests/test_identity.py # 19 项
.venv/bin/python app/tests/test_engine_e2e.py # 16 项,端到端
.venv/bin/python app/tests/test_transport.py # 4 项,媒体时钟/VAD/静音/RTVI文本
.venv/bin/python app/tests/test_search.py # 12 项,搜索 API/工具回灌/TTS 链路过滤
.venv/bin/python app/tests/test_profile_memory.py # 22 项,自报姓名/断开重连/疑问句防误写
```
端到端测试**不需要麦克风、音箱、摄像头、ESP32**——
这是"服务器即助手"架构的直接好处:链路可以在任何有 CPU 的地方验证,
硬件到货后再接最后一段。跑完会把合成音频写到 `out/`,可以人工试听。
## 联网搜索
DeepSeek 负责判断何时需要实时资料,`app/search.py` 提供 `web_search` 工具并调用
服务器本机的 SearXNG。工具结果回灌模型后再生成最终回答;网页保留来源 URL,TTS
只朗读正文。SearXNG 仅绑定 `127.0.0.1:18080`,配置见 `deploy/searxng/`。
服务器实测 DuckDuckGo、Brave、Wikipedia 超时,当前启用 360 搜索和搜狗;Bing
保留但暂时返回空结果。真实服务验收:
```bash
.venv/bin/python app/tests/test_search_live.py
```
## 设计上几个刻意的选择
**接口按最坏情况设计。** `MemoryStore` 全是 async、可超时、可失败,
虽然现在背后是本地 SQLite(微秒级)。因为将来多设备共享记忆时要搬到服务器,
如果现在写成同步直连本地文件,那时要重写所有调用方。
**身份、会话、视觉追踪三者分开。** `user_id`(永久身份)、`session_id`(一次会话)、
`track_id`(画面里的一个目标)互不冒充。人脸认出来**不等于**确认对方在说话——
所以人脸只产生"建议",不自动切换身份。做错的后果是路人经过就把你的私人记忆念出来。
**内部 ID 与用户称呼分开。** `u_ylhan` 只用于数据库隔离,绝不是姓名。用户明确说
“我叫林”时,会同步写入 `users.display_name` 和一条 confirmed 姓名记忆;新连接创建
引擎时从档案恢复称呼。自报姓名只用于个性化,不提升身份信任等级。
**记忆查询强制带 `owner_user_id`。** 没有"按 memory_id 单独取"的口子:
猜到 id 就能读别人的记忆,是这类系统最容易犯的错。删除同理,必须同时匹配 owner。
**自动抽取的记忆是"候选",默认不参与检索。** 当前只有自报姓名走同步 confirmed
档案路径;“记住/我喜欢/我习惯”等通用规则仍统一写成 candidate,而且尚无候选确认/
晋升入口。因此除姓名外,内容可能已经落库却不会参与后续回答。这是下一阶段必须补齐
的功能,不应把“数据库里有记录”误报为“助手已经会使用这条记忆”。
**异步任务绑定创建时的身份。** 回复后写记忆的任务在创建时捕获 `user_id`,
不读"当前用户"——否则用户在这几百毫秒内切了身份,这条记忆会写到别人名下。
**引擎与传输解耦。** 不依赖 Pipecat、不依赖任何传输方式,只依赖四个接口。
好处:可以用文件音频端到端测试;接 WebRTC 时只需写薄适配层。
## Pipecat 实时链路:已闭环
2026-09-15 复查并修复传输层实验:
- ✅ 信令、ICE、data channel、双向媒体轨
- ✅ `PipelineWorker` 自动 `StartFrame`,自定义 `EngineProcessor` 正常收帧
- ✅ 文件音频 → STT → DeepSeek → Qwen3-TTS → WebRTC 非静音回复
- ✅ 保留 RTVI 和 `/client/` prebuilt 网页客户端,并提供其需要的 `/start` 入口
原来的“`process_frame` 调用次数为 0”并非管线接线或模型不兼容。真实根因是
Pipecat 启动预热 NLTK `punkt_tab` 时发生运行时下载,20 秒 setup 超时,worker
尚未发送 `StartFrame` 就退出。数据预装后帧路由正常,因此确定采用 A 方案:
Pipecat 管传输、帧与 RTVI,自有 `ConversationEngine` 管会话语义。
自写客户端另有两处测试缺陷也已修复:文件结束后的静音帧没有推进 PTS,导致 VAD
收不到 700ms 尾静音;接收端又把 WebRTC 自动静音误判为回复。现在成功条件是收到
非静音语音,并按实际 WebRTC 采样率保存 WAV。
2026-09-16 又通过 SSH 隧道用 `/client/` 做了真人浏览器测试:补齐 Playground
要求的 `/start` 后,麦克风 → STT → LLM → TTS → 浏览器播放正常,单轮服务端指标
约 3.0 秒。这个页面是调试工具而非产品 UI;当前连接期间由 VAD 检出每段话后都会
响应,**尚无唤醒门控**。正式唤醒仍按方案放在 P2 的 N5095 本地端,待机时不上传音频。
完整记录见代码仓库外的 `P0-Pipecat传输层实验报告.md`(在 Obsidian 笔记库里)。
### 当前打断边界(2026-09-16)
当前 `SimpleVAD` 是实验性的 RMS 能量判定:阈值 350、最短语音 300ms、尾静音
700ms。它能切分话段和触发下一轮,但不是完整的 barge-in。助手推送音频时检测到
用户声音目前只记日志;只有等话段结束,或收到外部 `InterruptionFrame`,才会取消
当前异步轮次。浏览器播放器虽有清空能力,服务端尚未把“用户开始说话”可靠地转换为
取消生成、清空服务端输出和清空客户端缓冲三个动作。现有测试只证明任务可取消,
未证明声学打断端到端闭环。
### 下一阶段 Agent 方向:Pipecat Flows
已决定不整体迁移 OpenClaw、Agno、LangGraph 或 LiveKit Agents,而是在现有 Pipecat
实时链路上渐进引入 `pipecat-ai-flows`。目标是把 `web_search`、`remember`、
`search_memory`、`forget` 等能力变成受策略约束的工具,同时保留现有
`MemoryStore`、身份隔离和低延迟语音链路。Flows 与当前自有 `ConversationEngine`
的接口并不直接兼容,必须先做独立兼容性实验,再通过 feature flag 灰度迁移;不得
直接重写已部署链路。实施交接见笔记库
`P1-Pipecat Flows Agent化与记忆改造交接.md`。
### 实验挖出的 10 个坑
每个都表现为"什么都没发生",极难排查:
1. Pipecat 客户端 SDK 不覆盖"Linux x86 盒子接 USB 阵列"这种形态 → 客户端得自己写
2. 客户端必须建 data channel 并定期发 `"ping"`,否则服务端一帧音频都不推
3. offer 里没有媒体时 aiortc **完全不收集 ICE 候选**(连连通性测试都要加静音轨)
4. ICE 收集完成前发 offer → SDP 里 0 个候选地址
5. `PipelineWorker.run()` 要 `WorkerParams(task_manager=...)`,不是 `PipelineParams`
6. `TaskManager()` 必须在事件循环内创建
7. **后台任务里的异常被静默吞掉**,日志里查不到
8. 信令回调里必须 `create_task`,不能 `await task.run()`
9. Pipecat 会预热 NLTK `punkt_tab`;生产部署必须预装,不能在首个会话临时下载
10. `PipelineWorker` 自动发送 `StartFrame`,手工再发会造成重复启动帧
## 踩过的坑
全部写在对应文件的注释里,这里只列索引,详见 `部署说明.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 的线程扩展方向相反