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