Files
Owner/openspec/changes/add-full-duplex-agent-voice-assistant/design.md
T

319 lines
15 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.
# Design: 完整全双工 Agent 语音助手架构
## Context
当前 Owner 项目已经具备真实语音桌宠的基础能力:本地唤醒词、麦克风采集、VAD/端点、实时字幕、final STT、云端 LLM、本地 TTS、播报、连续追问和初步打断规划。现有实现仍以 turn-based 轮次为中心:用户唤醒后系统录一段、识别、生成、播放,再回到待机或短暂追问窗口。
完整 Agent 语音助手需要更强的架构边界:
1. 音频输入和输出必须同时运行,而不是录完再播。
2. 播放音频必须回注给 AEC reference,避免助手自己的声音污染麦克风输入。
3. VAD、STT、LLM、TTS、工具执行和播放都必须能被统一取消。
4. 长期记忆和工具执行必须纳入安全策略,而不是让 LLM 随意执行文本命令。
5. 终端、未来 GUI 桌宠和自动测试必须消费同一事件流。
本设计仅用于 OpenSpec 规划。本阶段不修改 `src/`、不安装依赖、不下载模型、不运行外部工具、不接入 Open Interpreter,也不提交本地未跟踪的 `openinterpreter/`
## Goals / Non-Goals
**Goals:**
1. 定义全双工音频底座:麦克风 capture、扬声器 render reference、WebRTC APM AEC/NS/AGC、环形缓冲和时钟对齐。
2. 定义状态机:`idle/listening/thinking/speaking/interrupted/tool_running/recovering`
3. 定义并发任务模型:音频输入、STT、LLM、TTS、播放、工具执行相互独立但可被统一取消。
4. 定义低延迟打断:用户在 `speaking` 中说话时,VAD 快速触发 interruption,目标小于 200 ms。
5. 定义 Streaming STT/TTSpartial/final transcript、LLM token stream、句子切分、TTS chunk 播放。
6. 定义长期记忆:SQLite 保存文本和元数据,FAISS 保存向量索引,敏感内容默认不自动保存。
7. 定义 Tool Router:结构化工具调用协议、安全分类、确认策略、执行预算、防循环、结果脱敏回注。
8. 定义 Open Interpreter 外部 adapter:只作为 CLI/子进程后端候选,不复制外部仓库。
9. 定义 Playwright browser adapter 和未来 `ComputerControlProvider` 预留边界。
10. 定义测试策略:fake APM/VAD/STT/TTS/memory/tool、端到端模拟、性能指标和安全回归。
**Non-Goals:**
1. 本阶段不实现运行代码。
2. 本阶段不安装 WebRTC APM、Silero、Faster Whisper、SenseVoice、CosyVoice、FAISS、Playwright 或 Open Interpreter。
3. 本阶段不下载模型。
4. 本阶段不改变 `.env``pyproject.toml``src/``tests/``scripts/`
5. 第一版规划不做 GUI 点击、键盘、屏幕控制,只预留公共 provider。
6. 第一版不做长期主人声纹注册和多人身份鉴权。
7. 第一版不自动执行高风险电脑控制、上传、交易、删除、权限修改或账号操作。
## Decisions
### Decision 1: WebRTC APM 作为默认音频底座
采用 `WebRtcAudioProcessingStage` 作为全双工默认音频预处理层,启用 AEC、NS、AGC。
**理由:**
1. AEC 需要播放 reference 才能可靠消除助手自己的声音,音色门控只能做后验抑制。
2. NS 能降低风扇、环境声和麦克风底噪对持续 VAD/STT 的影响。
3. AGC 能减少用户远近变化导致的阈值不稳定。
**替代方案:**
1. 继续使用 GTCRN 降噪:能改善噪音,但不能处理扬声器回声。
2. 继续使用音色门控:实现成本低,但对不同扬声器、房间回声和 TTS 音色变化不稳定。
3. 只靠 VAD/STT 置信度:误触发风险高,不适合作为全双工底座。
### Decision 2: capture/render 双环形缓冲
设计 `CaptureRingBuffer``RenderReferenceRingBuffer`。麦克风帧和播放帧都按 monotonic timestamp 写入,APM 处理 capture frame 时读取相邻 render reference。
**理由:**
1. 全双工需要持续输入和持续输出,不能用同步读写阻塞。
2. AEC 对 reference 时序敏感,必须保留时间戳和 drift 监控。
3. Ring buffer 可为 STT、VAD、打断检测和测试提供一致帧来源。
**替代方案:**
1. 直接从 sounddevice callback 推到各 stage:耦合高,难测试,易阻塞。
2. 文件式临时 wav:延迟高,不适合全双工。
### Decision 3: 统一状态机和事件总线
新增 `FullDuplexAgentStateMachine``PipelineEventBus`。状态机只负责合法转移,事件总线承载终端、GUI 和测试可观察行为。
**理由:**
1. 现有 turn-based 状态无法表达 `tool_running``interrupted`
2. 终端和未来 GUI 必须共享事件,不应各自读取内部字段。
3. 测试可以断言事件顺序、耗时和错误恢复。
### Decision 4: Cancellation Graph 管理所有可中断任务
每个用户输入 turn 创建 root cancellation tokenLLM、TTS、playback、tool 子任务挂在 root 下。用户打断时 root token 广播取消。
**理由:**
1. 播放停止但 LLM 继续生成会浪费资源并污染上下文。
2. TTS 合成继续运行会造成卡顿和旧回复残留。
3. 工具执行必须区分可取消和不可安全取消。
### Decision 5: Streaming STT 分层输出
`StreamingSttProvider` 输出 `partial``stable_partial``final`。终端可展示 stable partialLLM 默认只接收 final。
**理由:**
1. 用户需要实时看到系统听到了什么。
2. partial 抖动不能直接写入上下文。
3. final 是唯一可靠的 LLM 用户输入。
**开发候选:**
1. `faster-whisper`:开发阶段优先,生态成熟。
2. `SenseVoice`:产品候选,中文和情绪能力更强,需要评估部署成本。
3. `sherpa-onnx`:保留当前兼容 adapter,降低迁移风险。
### Decision 6: LLM token stream 按句进入 Streaming TTS
LLM delta 进入 `SentenceSegmenter`。达到完整句子、语义停顿或最大等待阈值后,将净化后的文本交给 TTS。
**理由:**
1. 不等待完整回复能显著缩短首音延迟。
2. 句子级 TTS 比 token 级 TTS 更自然。
3. 现有 TTS sanitizer 可复用,避免表情包、emoji 被读出。
### Decision 7: 长期记忆使用 FAISS + SQLite
SQLite 保存记忆文本、类型、敏感度、来源、时间和 checksumFAISS 保存 embedding 向量。
**理由:**
1. SQLite 适合可审计元数据和删除。
2. FAISS 适合向量相似检索。
3. 两者分离便于一致性检查和索引重建。
**替代方案:**
1. 只用 SQLite FTS:部署简单,但语义召回弱。
2. 直接把历史对话全文塞进 prompt:隐私和成本都不可控。
3. 使用外部向量数据库:当前单机桌宠不需要额外服务复杂度。
### Decision 8: Tool Router 先安全工具后电脑控制
第一版只规划低风险工具:`memory.search``memory.save``shell.readonly``openinterpreter.run``browser.playwright`。GUI 电脑控制只预留。
**理由:**
1. 语音助手执行工具的风险高于文本聊天,必须先定义确认策略。
2. Open Interpreter 能节省电脑任务开发时间,但必须被当成外部受限后端。
3. GUI 点击/键盘/屏幕控制需要 Accessibility 权限和更复杂安全策略,不适合第一阶段自动执行。
### Decision 9: Open Interpreter 作为外部 CLI adapter
本地 `openinterpreter/` 是未跟踪新 Rust 版 Open Interpreter。Owner 不复制、不 vendoring、不提交它,只在后续实现中通过命令路径调用外部 CLI。
**理由:**
1. 避免把外部大型仓库混入 Owner。
2. 保持 Owner 的工具边界清晰。
3. 可以对 CLI 调用做超时、目录限制、输出截断和确认策略。
### Decision 10: Codex Computer Use 只参考安全策略
Codex App 的 Computer Use 能力可作为“需要确认、限制高风险操作、避免私自执行 GUI 动作”的安全策略参考,但不复制私有实现。
**理由:**
1. 用户希望从 Codex 控制电脑技能中复刻思路,而不是复制私有实现。
2. 公共落地路线应基于 macOS Accessibility、Playwright、trycua 等能力。
3. 第一版先不做 GUI 控制可降低安全和实现风险。
## Concurrent Task Model
### Runtime task groups
1. `audio_capture_task`:从麦克风读取帧,写入 capture ring buffer。
2. `audio_render_task`:从 playback queue 取 PCM,送扬声器并写入 render reference buffer。
3. `apm_task`:处理 capture frames,输出 cleaned frames。
4. `vad_interrupt_task`:对 cleaned frames 做 VAD 和打断检测。
5. `stt_task`:消费 cleaned frames,输出 partial/stable/final transcript。
6. `llm_task`:消费 final transcript、记忆和工具结果,输出 token stream 或 tool call。
7. `tts_task`:消费句子片段,输出 PCM chunks。
8. `tool_task`:执行已批准工具,输出脱敏结果。
9. `event_task`:聚合事件、指标和 reporter 输出。
### Backpressure
1. Ring buffer 有固定容量,超出容量丢弃最旧非关键帧并发出 `audio_buffer_overrun`
2. TTS queue 超限时暂停 LLM sentence enqueue 或请求 LLM stream 暂停/取消。
3. Tool output 超限时截断并返回 `truncated=true`
### Cancellation
1. `interrupt_detected` 触发 root cancellation。
2. LLM stream 立即关闭连接或停止读取。
3. TTS session 停止合成并释放资源。
4. Playback queue 清空未播放 chunks,保留已播放文本边界。
5. 可取消工具收到 token 后停止;不可取消工具标记为 pending cleanup。
## Audio Ring Buffer Design
### Frame format
1. 默认 16 kHz 或 48 kHz 内部采样率需在实现前确认。
2. 默认 mono capturerender reference 可 mono 或 stereo downmix。
3. 默认 frame size 为 20 ms。
4. 每帧包含 `frame_id``timestamp_monotonic_ms``sample_rate``channels``samples`
### Reference alignment
1. render frame 写入 reference buffer 时记录实际播放排队时间和预计播放时间。
2. capture frame 进入 APM 时按 timestamp 查找 reference window。
3. drift 超过阈值时发 `audio_reference_drift`,并降低打断置信度。
### Fallback
1. APM 不可用时,若 `OWNER_AUDIO_APM_REQUIRED=1`,全双工模式启动失败。
2. 若允许 fallback,则使用现有 GTCRN denoiser、assistant playback gate 和 conservative VAD,但必须标记 `apm_fallback=true`
## Tool Router Protocol
### Tool call request
```text
ToolCallRequest:
id: str
name: str
arguments: dict
requested_by_turn_id: str
natural_language_intent: str
timeout_ms: int
```
### Tool decision
```text
ToolDecision:
action: execute | reject | require_confirmation
risk_level: low | medium | high | forbidden
reason: str
sanitized_arguments: dict
confirmation_prompt: str | null
```
### Tool result
```text
ToolResult:
id: str
status: success | failed | cancelled | rejected | confirmation_required
output_text: str
output_truncated: bool
error_code: str | null
duration_ms: int
audit_summary: str
```
### First-version tools
1. `memory.search`:只读,低风险。
2. `memory.save`:低到中风险,敏感内容需要确认。
3. `shell.readonly`:低到中风险,只允许 allowlist 只读命令。
4. `openinterpreter.run`:默认中风险,第一版默认关闭;执行前必须经过目录、意图和风险校验。
5. `browser.playwright`:默认中风险,第一版默认关闭;账号、支付、提交、购买流程必须确认或拒绝。
## Memory Retrieval Chain
1. 用户 final transcript 进入 `MemoryQueryBuilder`
2. Query builder 结合当前任务、会话摘要和用户文本生成检索 query。
3. Embedding provider 生成 query vector。
4. FAISS 返回候选 ids。
5. SQLite 读取 metadata,过滤 disabled、sensitive、expired、low-confidence records。
6. Re-ranker 按相似度、类型、最近使用、用户显式偏好排序。
7. Top-K 以独立 `memory_context` 注入 LLM。
8. LLM 回复完成后,`MemoryWriteCandidateExtractor` 生成候选记忆。
9. Safety classifier 决定保存、丢弃或请求确认。
## Migration Plan
1. 保留现有 turn-based `run-live` 作为稳定路径。
2. `run-agent-live` 是真实全双工 Agent 入口;`--check-config` 只做配置检查,不带该参数时必须启动运行时。
3. 先落地 fake provider 和模拟端到端,再接真实 WebRTC APM。
4. STT/TTS provider 先用兼容 adapter 接现有能力,再替换为 streaming provider。
5. 长期记忆默认关闭或空库启动,完成删除/禁用/隐私文档后再默认开启。
6. Tool Router 默认只启用 `memory.search`;其他工具按风险逐步开放。
7. Open Interpreter 和 Playwright adapter 默认关闭,用户显式配置后才可用。
8. GUI 电脑控制另开 OpenSpec 变更,不混入第一版全双工音频和安全工具验收。
## Live Runtime Revision
第一版真实 `run-agent-live` 采用软件 render-reference gate,不新增 WebRTC APM 重依赖。播放队列把已播放 PCM chunk 写入进程内 render reference;后台麦克风监听持续读取 capture frames,先用 VAD 判断有效人声,再与近期 render reference 做相似度/能量门控。候选音频不像助手回放且持续达到最短人声时长时,立即设置 playback stop eventSTT 只用于打断后的用户文本识别,不再作为停止播放的前置条件。
`run-live` 保持旧 wake/turn-based 入口。`run-agent-live` 跳过唤醒词和 ACK,常驻 `listening -> thinking -> speaking -> interrupted/listening`,打断后把已确认的用户音频接到下一轮 capture,避免丢首字。长期记忆、Tool Router、Open Interpreter 仍默认关闭。
## Rollback Strategy
1. 若全双工音频不稳定,可通过配置回退 turn-based `run-live`
2. 若 APM provider 失败,可禁用 full-duplex mode 或启用 fallback 标记的旧链路。
3. 若长期记忆异常,可设置 `OWNER_MEMORY_ENABLED=0`,继续短期对话。
4. 若 Tool Router 风险过高,可设置 `OWNER_TOOL_ROUTER_ENABLED=0`,保留纯聊天。
5. 若 Open Interpreter 或 Playwright adapter 出错,只禁用对应工具,不影响语音主循环。
## Risks / Trade-offs
1. WebRTC APM 依赖复杂 -> 通过 provider 抽象、fake APM 测试和 fallback 降低风险。
2. 全双工并发复杂 -> 通过 bounded queue、cancellation graph、虚拟时钟测试和事件指标控制。
3. 流式 STT/TTS 模型较重 -> 分阶段实现,先 provider interface 和 fake tests,再接真实模型。
4. 长期记忆有隐私风险 -> 默认敏感不保存、可禁用、可删除、可审计。
5. 工具执行有安全风险 -> 安全工具优先,高风险确认或拒绝,输出脱敏和审计日志。
6. Open Interpreter 能力强但风险大 -> 默认关闭、低风险受限执行、外部 CLI adapter、禁止复制仓库。
7. Playwright 登录态风险 -> 第一版不默认使用用户登录态,敏感流程确认或拒绝。
8. 第一阶段 scope 大 -> 按音频、状态机、STT/TTS、记忆、工具拆分里程碑,每个里程碑独立提交。
## Open Questions
1. 全双工入口是否命名为 `run-agent-live`,还是通过 `run-live --mode full-duplex` 启用。
2. WebRTC APM 的具体 Python/macOS 绑定选择需要验证。
3. 内部采样率统一用 16 kHz 还是 48 kHz,需要结合 APM、STT、TTS provider 决定。
4. SenseVoice 和 CosyVoice 是否作为产品强依赖,还是只保留候选。
5. 长期记忆是否默认开启,需要用户确认隐私预期。
6. Open Interpreter 是否允许写操作;若允许,确认流程和 sandbox 需要单独设计。
7. Playwright 是否允许使用现有 Chrome 登录态。
8. 后续 ComputerControlProvider 是否采用 macOS Accessibility、trycua 或其他公共方案。