319 lines
15 KiB
Markdown
319 lines
15 KiB
Markdown
# 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/TTS:partial/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 token,LLM、TTS、playback、tool 子任务挂在 root 下。用户打断时 root token 广播取消。
|
||
|
||
**理由:**
|
||
|
||
1. 播放停止但 LLM 继续生成会浪费资源并污染上下文。
|
||
2. TTS 合成继续运行会造成卡顿和旧回复残留。
|
||
3. 工具执行必须区分可取消和不可安全取消。
|
||
|
||
### Decision 5: Streaming STT 分层输出
|
||
|
||
`StreamingSttProvider` 输出 `partial`、`stable_partial` 和 `final`。终端可展示 stable partial;LLM 默认只接收 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 保存记忆文本、类型、敏感度、来源、时间和 checksum;FAISS 保存 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 capture;render 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 event;STT 只用于打断后的用户文本识别,不再作为停止播放的前置条件。
|
||
|
||
`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 或其他公共方案。
|