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

15 KiB
Raw Blame History

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. 本阶段不改变 .envpyproject.tomlsrc/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 双环形缓冲

设计 CaptureRingBufferRenderReferenceRingBuffer。麦克风帧和播放帧都按 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: 统一状态机和事件总线

新增 FullDuplexAgentStateMachinePipelineEventBus。状态机只负责合法转移,事件总线承载终端、GUI 和测试可观察行为。

理由:

  1. 现有 turn-based 状态无法表达 tool_runninginterrupted
  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 输出 partialstable_partialfinal。终端可展示 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.searchmemory.saveshell.readonlyopeninterpreter.runbrowser.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_idtimestamp_monotonic_mssample_ratechannelssamples

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

ToolCallRequest:
  id: str
  name: str
  arguments: dict
  requested_by_turn_id: str
  natural_language_intent: str
  timeout_ms: int

Tool decision

ToolDecision:
  action: execute | reject | require_confirmation
  risk_level: low | medium | high | forbidden
  reason: str
  sanitized_arguments: dict
  confirmation_prompt: str | null

Tool result

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 或其他公共方案。