Codex 实时语音 Agent:两个 Agent,一本笔记
一个桌面 App 如何把"听你说话"和"替你干活"拆成两口子,再用一段 XML 标签缝回同一场对话。剥掉名词,它干的事朴素得有点意外。
大多数人以为语音助手就是模型在那儿听、想、答。Codex 的 Live Agent 不是。 它底下跑着两个 agent,共享同一段对话历史。一个叫 Realtime 语音模型,是"话务员"——用声音跟你聊天,但基本不干实事。另一个叫 backend Codex coding agent,是"后台干活的人"——查代码、跑命令、翻网页,全归它,干完把结果塞回对话,由话务员念给你听。 两口子靠一个本地 Rust 二进制当传声筒,用 XML 标签 这就引出真问题:为什么不直接让一个模型又听又干?往下看,你会发现这是一个被工程现实逼出来的分工,不是炫技,是躲坑。01先说结论:它不是一个 Agent,是两个
<realtime_delegation> 和一对前缀 [USER] / [BACKEND] 缝在一起。
前端不直连 OpenAI。控制面走 IPC,媒体面走 WebRTC 端到端。 为什么绕这一道?因为 OpenAI 的 key、tool 执行、权限审批全收在本地 host 手里,前端只是个出声的壳。安全上这叫"把攻击面留在自己地盘"。02拓扑:两条数据面,一台本地总线
人格被拆成三份可远程改写的文本——经 Statsig 下发,bundle 里只是兜底值。 给语音模型的,核心三句铁律:绝不提 backend(把后台干的当自己干的呈现);绝不拒绝(一切转派);不复述后台已可视化的内容(表格/diff/代码块默认不念)。一个"有脾气但没实权的接待员"。 给 backend 的,真正的"大脑",在三种模式里选:03三份提示词:话务员、协调员、记忆
话务员守则
协调员指令
脑子风暴、澄清、轻量规划,留当前 thread 聊。
小快查(看分支、扫 PR),结果立刻帮上忙就原地做。
慢活、多步活、要浏览/实现——派给 worker thread。
每个 worker 派活 prompt 必须自带"干完或卡住就回一句话"。否则协调员瞎等。记忆与续接
continuityPrompt 恢复暂停会话时要求模型完全闭嘴等用户先开口;memorySummaryPrompt 注入长期记忆同样"静默"。语音场景下"模型自己突然说话"很诡异,所以特意用硬指令压住。
话务员只能用"会话级"小工具;真正的重活工具在 backend 那侧。04工具集:门面一套,后台一套
归属 工具 作用 语音端 capture_screen_context看前台 App 截图 + 无障碍树(macOS+Appshots) 语音端 get_app_state读 Codex 自身页面/侧栏状态 语音端 end_realtime_voice_call结束语音聊天 语音端 send_realtime_voice_feedback提交反馈 语音端 speak_to_user让语音模型开口念一段 backend list_projects / create_thread / send_message_to_thread派活三件套 backend wait_threads阻塞等 worker 最多 120s,非轮询 backend read / fork / handoff / list_threads / set_thread_*thread 编排 deferLoading:true。工具 schema 不在 session.update 时全量下发,模型第一次碰到才拉——给语音会话的初始 payload 减肥。
共享同一 threadId,靠 role + 前缀区分谁在说话。 还有个内部状态频道 05桥接协议:两口子怎么对话
[STATUS] / [ATTENTION] / [COMPLETE]——话务员可以"说"这些 tag 触发内部行为,但不显示给用户。相当于两口子在家用的暗号,不当客人的面讲。
很多人想成"听→想→说"的 ReAct。Codex 不是。 启动路径上有个巧思:四件事并行 bootstrap——拉声音、建 WebRTC、读历史、读记忆,全 06Loop 拆解:真正的 agent loop 在后台
oai-events 推事件,前端更新 transcript、orb 动画、音频。它不跑 tool 循环。wait_threads 阻塞等 → 回注。Promise.all 同时发。更绝的是 WebRTC 在拿到 host 互斥锁之前就先"预热"了一次(catch(()=>{}) 静默 fire-and-forget),目的是尽早开始 ICE 协商、压低"点开始到出声"的感知延迟。realtimeVoiceHostClaim.claim() 保证同一时刻只有一个窗口持语音会话,别的窗口抢就报 busy。防两个窗口同时抢麦克风/会话的硬保险。
对照 OpenAI 官方文档,把源报告的声称分三档。 语音传输、事件频道、音频格式、模型命名,全是 OpenAI 公开能力;真正私有的是那层"本地 host 总线 + 双 agent 桥接 + Statsig 远程改写提示词"——也就是把公开零件拼成语音助手的"胶水"。 出处:OpenAI Realtime WebRTC / conversations 指南、GPT-Realtime 模型页、Realtime API 参考事件表。07官方交叉验证:哪些公开,哪些私有
对照 Agents SDK / Anthropic / LangGraph / AutoGen / ChatGPT Tasks。08编排范式对比:它和别人有什么不同
方案 怎么连 共享上下文? 与 Codex OpenAI Agents SDK handoff 转移控制权 转移后共享 Codex 是持久共享同一 threadId Anthropic orchestrator-subagent 刻意不共享 Codex 反其道,靠前缀隔离 LangGraph supervisor StateGraph 图状态共享 Codex 用显式 wait_threads 阻塞 joinAutoGen GroupChat 对等 共享对话 ≈ 协调员+worker 对等感 ChatGPT Tasks 调度/接数据 — ≈ worker thread / 后台数据层
对照 Pipecat / LiveKit / ElevenLabs。 OpenAI 在浏览器/移动端强制 WebRTC。根因(Pipecat 讲得最透):TCP 有队头阻塞,丢包要重传、把后面全堵住;音频里一个丢包宁可扔掉也不要等。WebRTC 还自带回声消除、噪声抑制、抖动缓冲、ICE 重连。 Codex 麦克风管线是 09语音实时工程:为什么这么折腾音频
WebRTC vs WebSocket
pre-roll:客户端和服务端都在偷偷多录开头
buffering → replaying → live 状态机:30 秒环形缓冲,静音阈值 0.003,开口先回放 100ms pre-roll 再转 live。这跟 OpenAI 服务端 prefix_padding_ms(默认 300ms)是同一件事的客户端镜像——都为了补 VAD 确认那点延迟,否则你第一个字被切掉。wait_threads vs 轮询
wait_threads(阻塞 ≤120s + afterCursor 抑制重复)是"阻塞读 + 游标"模式,对比早期 Assistants 忙等 GET run 轮询——空闲开销更低,在 agent 编排里算新颖。
给用户:收紧屏幕录制权限、敏感操作暂停会话、关掉"改进模型"开关、检查 localStorage override 键、定期清数据。出处:OWASP / Oligo / OpenAI 社区隐私帖 / arXiv:2505.18471。10安全与隐私:这层"胶水"也是攻击面
capture_screen_context 回传截图 + 无障碍树(含文本)。社区已报告截图失败后静默回退全屏截取且无告知;银行密码界面可能被传上去。
一、命名 ≠ 理解。剥掉"Live Agent""Realtime",它干的事很朴素:一个接待员,背后一个工程师,共用一本笔记。 二、不是炫技,是躲坑。语音模型要即时回话、轻、不断流;写代码要慢慢想、跑工具、可能卡很久。塞一个循环里,要么语音卡顿,要么代码急躁。拆开,各自按节奏走——按物理约束做的分工。 三、演示 > 论证。 四、现实优先于叙事。Anthropic 说"共享上下文不适合多智能体"——对,但 Codex 用"门面共享 + 重活 fork 隔离"绕过去了。没违背物理定律,只是把共享限定在用户看得见的那层。 五、别骗自己。这套设计最大隐患不是技术,是透明度:backend 被藏、提示词能远端改、屏幕能静默截。体验更顺的名义下,你得知道自己交出了什么。11费曼拍板:为何如此设计
wait_threads 是个 10 秒讲清的演示:等慢活,隔几秒问"好了没"是轮询空耗;跟它说"完了叫我"是阻塞 join。Codex 选了后者。就这么回事。
12复刻契约:想 1:1 照做,守这十条
wait_threads,不轮询——最多 8 目标、120s、afterCursor 抑制重复。requestApproval / requestUserInput 从 loop 透传,话务员不代答。speak_to_user——backend 文本不被自动读出。
参考:OpenAI Realtime 文档 · OpenAI Agents SDK · Anthropic 多智能体指南 · LangGraph supervisor · Pipecat 传输文档 · OWASP Prompt Injection · arXiv:2505.18471