第 39 讲:Realtime/Voice 语音实时对话架构

Codex 源码解析 · 第 39 讲

Codex 源码解析系列 第 39 讲:Realtime/Voice 语音实时对话架构 基于 OpenAI Codex 源码 · 2026-11-02 💡 本讲一句话: Codex 的语音对话不是"主进程里开个音频线程",而是把麦克风、扬声器和 GStreamer 管线全部关进一个独立子进程 codex-voice-host——父进程只通过一条长度前缀 JSON 管道发命令。读完你能带走三样东西:一套跨进程音频控制协议的设计(状态机 + 全量快照保序)、一条 60ms 延迟预算的 OPUS 播放管线、以及"隐私命令永远插队"的抢占式调度。 一、三层架构:为什么音频必须住进独立进程 语音实时对话横跨三个 crate: core/src/realtime_conversation.rs (2638 行,会话状态机)、 realtime-webrtc (1437 行,协商门面 + 子进程客户端)、 voice-host (5137 行,真正碰音频设备的守护进程)。三者分属两个操作系统进程: 层 进程归属 职责 core/realtime_conversation.rs codex 主进程 会话状态机、文本/handoff/音频三路复用、服务端事件分发 realtime-webrtc(session/client) 主进程 voice-session 线程 门面:有序控制队列、SDP 协商编排、50ms 电平巡检 voice-host(codex-voice-host) 独立子进程 独占麦克风/扬声器与 GStreamer 管线,OPUS 编解码 + RTP 收发 为什么这样设计? 音频栈是整条链路里最"脏"的部分:GStreamer 原生库、PulseAudio/PipeWire/DBus 系统服务、设备热插拔回调——任何一处崩溃或卡死都可能拖垮主进程。把这一切关进子进程后,主进程只持有几条管道和一个退出码接收器;helper 崩了,通话断掉但 agent 活着。更关键的是安全边界:子进程的环境变量被白名单过滤(PATH 都不传),stderr 直接丢弃——音频权限、系统插件目录、未分类的原生错误文本,全部隔离在进程墙之外。 二、父子进程的"契约":长度前缀 JSON 协议 两个进程之间没有共享内存、没有 socket,只有一条 stdin/stdout 管道。协议定义在 realtime-webrtc/src/protocol.rs ——注意文件头注释:"Bounded same-build controls and redacted signaling; audio never crosses this pipe"(有界的同构建控制与脱敏信令; 音频数据从不经过这条管道 )。 📄 codex-rs/realtime-webrtc/src/protocol.rs (第 9-107 行,节选) pub const MAX_FRAME_BYTES: usize = 128 * 1024; // 单帧上限 128KB:超限直接拒绝,防止管道被撑爆 [derive(Debug, Deserialize, PartialEq, Serialize)] // derive:serde 序列化 + PartialEq——回包必须精确匹配,错一个字段就报错 [serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)] // tag=type 自描述 + 拒收未知字段:状态机严格,不静默兼容 pub enum Message { // 父子进程间的全部合法消息(严格状态机) Hello { protocol: u32, build_commit: String }, // 握手:父进程声明协议版本与构建 commit Ready {}, // 子进程确认就绪、可接收命令 InitializeRuntime {}, // 初始化打包的 GStreamer 运行时(不碰设备) RuntimeReady {}, // 运行时就绪 StartTransport {}, // 启动 WebRTC 传输并收集本地 offer Offer { sdp: SessionDescription },…

阅读全文:Codex 源码解析 第 39 讲