Codex 实时语音 Agent 深度研究:一个桌面 App 如何把"说话"和"干活"拆成两口子
一份基于 Codex Desktop「Live Agent」反编译报告、并经四路并行调研交叉验证的解读。
源报告反编译了com.openai.codex桌面端app.asar(版本26.721.41059,prod 渠道),逐字挖出了语音模型系统提示词、backend 协调员指令、完整工具集、桥接协议与 loop。本研究在其上补了三件事:① 官方文档交叉验证(哪些公开、哪些私有);② 多智能体编排范式对比;③ 安全与隐私视角。
脱敏说明:源报告含真实本地路径与反编译者具名,本文一律抹去,只留可公开的架构事实。
0. 先说结论:它不是一个 Agent,是两个
大多数人以为"语音助手"就是模型在那儿听、在那儿想、在那儿答。Codex 的 Live Agent 不是这样。
它��下跑着两个 agent,共享同一段对话历史:
- 一个叫 Realtime 语音模型,是"话务员"。它用声音跟你聊天,但基本上不干实事——查代码、跑命令、翻网页,全不是它的活。
- 另一个叫 backend Codex coding agent,是"后台干活的人"。重活、慢活、多步活全是它执行,干完把结果塞回对话里,由话务员念给你听。
两口子靠一个本地 Rust 二进制(codex app-server host)当传声筒,用 XML 标签 <realtime_delegation> 和一对前缀 [USER] / [BACKEND] 缝在一起。
这就引出一个有意思的问题:为什么不直接让一个模型又听又干?往下看,你会发现这是一个被工程现实逼出来的分工——不是炫技,是躲坑。
1. 拓扑:两条数据面,一台本地总线
┌──────────────────── Electron Renderer (webview) ────────────────────┐
│ 用户说话 → getUserMedia → AudioWorklet(30s 环形缓冲) │
│ 麦克风音频 + oai-events JSON → WebRTC 媒体轨/数据道 │
│ │
│ 发起:pns.start(voice, prompt, initialItems) │
│ → send-cli-request-for-host({ method:'thread/realtime/start', │
│ params:{ transport:{type:'webrtc', sdp: offer}, threadId, │
│ codexResponseHandoffPrefix:"[BACKEND] ", ...}}) │
└───────────────────────────────┬────────────────────────────────────┘
│ IPC(控制面,代理式 signaling)
▼
┌──────────────── Rust "codex" app-server host(本地总线) ────────────┐
│ JSON-RPC: │
│ thread/realtime/start / stop / appendText / appendSpeech / │
│ listVoices │
│ 回吐通知:thread/realtime/sdp(answer SDP)、started、itemAdded、 │
│ transcript/delta、outputAudio/delta、error、closed │
│ 同时把 backend Codex 的 turn 事件广播给前端 │
└───────────────────────────────┬────────────────────────────────────┘
│ WebRTC 端到端(音频 + oai-events)
▼
┌──────────────────────────────┐
│ OpenAI Realtime API │
│ + backend Codex model │
│ (线程/turn/tool loop) │
└──────────────────────────────┘
关键一句:前端不直连 OpenAI。Renderer 把 WebRTC SDP offer 交给本地 host,host 做完 signaling 和工具编排,再把 answer SDP 回吐。控制面走 IPC,媒体面走 WebRTC 端到端。
为什么绕这一道?因为这样 OpenAI 的 key、tool 执行、权限审批全收在本地 host 手里,前端只是个出声的壳。安全上这叫"把攻击面留在自己地盘"。
2. 三份提示词:话务员守则、协调员指令、记忆
Codex 把"人格"拆成了三份可以远程改写的文本(经 Statsig 下发,bundle 里只是兜底值)。
2.1 话务员守则(basePrompt)
给 Realtime 语音模型的,核心就三句铁律:
- 绝不提 backend。把后台干的活当成自己干的呈现。提示词原话:"Present every work as done by you… make the user feel as if they are talking directly to the backend."
- 绝不拒绝。所有请求一律转派:"NEVER refuse requests. Delegate all user requests to the backend."
- 不复述后台已经可视化的内容。表格、diff、代码块,默认不念;用户要才念。
说白了,话务员是个"有脾气但没实权的接待员":热情、会接话、但任何真要动手的事都转给后台。
2.2 协调员指令(developerInstructions)
给 backend Codex 的,是真正的"大脑"。它让你在三种模式里选:
- Converse here——脑子风暴、澄清、轻量规划。留在当前 thread 聊。
- Quick check here——小快查(看下当前分支、扫一眼今天的 PR)。结果立刻帮上忙的就原地做。
- Delegate blocking mechanics——慢活、多步活、要浏览/交互/实现的,派给 worker thread。
还强制一条:每个 worker 派活 prompt 必须自带"干完或卡住就回一句话给协调员"的回报指令。这不是建议,是契约——否则协调员就瞎等了。
2.3 记忆与续接
continuityPrompt:恢复被暂停的语音会话时注入,要求模型在恢复瞬间完全闭嘴,等用户先开口(防止把历史 transcript 当成新指令)。memorySummaryPrompt:会话开始时注入长期记忆摘要,同样要求"静默注入,不问候"。
这两个细节很见功力:语音场景下"模型自己突然说话"是很诡异的体验,所以特意用硬指令压住。
3. 工具集:门面一套,后台一套
工具分两个安装点,各司其职。
语音会话专用(话务员能用):
| 工具 | 作用 | 触发条件 |
|---|---|---|
capture_screen_context |
看前台 App 截图 + 无障碍树(macOS + Appshots 启用时) | 用户指着屏幕说"这个" |
get_app_state |
读 Codex 自身页面/侧栏状态(Windows 或 Appshots 未启用时) | Codex 前台 |
end_realtime_voice_call |
结束语音聊天 | 用户明说要结束 |
send_realtime_voice_feedback |
提交反馈 | 用户要吐槽/点赞 |
speak_to_user |
让语音模型开口念一段 | backend 想口播重点 |
navigate_to_codex_page |
跳转到 App 内某页 | feature flag 开 |
backend Codex thread 编排工具(协调员派活用):
list_projects → create_thread → send_message_to_thread → wait_threads(阻塞等最多 120s,非轮询)→ read_thread / fork_thread / handoff_thread / list_threads / set_thread_*。
一个值得玩味的设计:5 个基础语音工具不延迟加载,其余全标 deferLoading:true。Realtime API 端工具 schema 不会在 session.update 时全量下发,模型第一次碰到才拉。这是给语音会话的初始 payload 减肥。
4. 桥接协议:两口子怎么对话
两个 agent 共享同一个 threadId,靠 role + 前缀区分谁在说话:
- 用户的话 → 前缀
[USER]; - 后台的产出 → 前缀
[BACKEND]; - 两者都以
userrole 塞进 Realtime 模型的上下文。
话务员要派活时,对自己的一个"交接工具"返回一段 JSON(含 handoff_id、input_transcript、active_transcript)。前端做两件事:
- 尝试把
input_transcript解析成嵌套的<realtime_delegation>XML; - 若不是 XML,就把 role/text 拼成明文,作为
input交给 backend 起一个 turn。
backend 干完,产出以带 [BACKEND] 前缀的 user 消息回注语音会话。反过来,backend 想让话务员开口,调 speak_to_user(底层是 thread/realtime/appendSpeech)——把一段文字塞进 realtime 会话,语音模型逐字念出。
还有个内部状态频道 [STATUS] / [ATTENTION] / [COMPLETE],话务员可以"说"这些 tag 触发内部行为,但不显示给用户。相当于两口子在家用的暗号,不当着客人的面讲。
5. Loop 拆解:真正的 agent loop 在后台
很多人把语音 Agent 想���"听→想→说"的 ReAct 循环。Codex 不是。
- 话务员那侧是事件驱动的流:
oai-events数据道推事件,前端照着更新 transcript、orb 动画、音频。它不跑 tool 循环。 - 真正的 turn/tool 循环在 backend Codex:话务员 handoff → host 起 turn → backend 按三模式决策 → 派活 →
wait_threads阻塞等 → 结果回注。
启动路径上还有个巧思:四件事并行 bootstrap——拉声音 slug、建 WebRTC、读历史、读记忆摘要,全部 Promise.all 同时发,不串行阻塞。更绝的是 WebRTC 会话在拿到 host 互斥锁之前就先"预热"了一次(catch(()=>{}) 静默 fire-and-forget),目的是在用户点"开始"到出声之间尽早开始 ICE 协商,压低感知延迟。
host 互斥锁 realtimeVoiceHostClaim.claim() 保证同一时刻只有一个窗口持语音会话,别的窗口抢就报 busy。这是防止两个窗口同时占用麦克风/会话的硬保险。
6. 官方交叉验证:哪些公开,哪些私有
【路1 · 对照 OpenAI 官方文档】结论分三档:
已公开证实(与官方一致)
- Realtime API 走 WebRTC,SDP offer/answer +
/v1/realtime/callssignaling。官方明确"推荐 WebRTC 而非 WebSocket"。 - 事件数据道就叫
oai-events(官方示例代码直出)。 - 音频 PCM @ 24kHz。
- 工具经
session.update注入;deferLoading(tool search 按需加载)是真特性。 - 模型
gpt-realtime/gpt-4o-realtime系列;语音 cedar/marin 为 Realtime 专属。
部分相符 / 措辞不同
- 官方工具 schema 字段是
parameters,源报告里写的inputSchema不符(更接近 Responses API 命名)。 - 官方事件名是
session.created/session.updated,不是session.started。
Codex 私有 / 查无公开依据
- 本地 Rust
codexapp-server host 代理(官方只泛泛说"开发者服务器代理")。 session.started、session.usage.updated、usage_limit.status都不在公开事件表;疑为 Codex 自定义或私有扩展。type:'function'+inputSchema+deferLoading的组合未在公开 Realtime 参考中出现。
出处:OpenAI Realtime WebRTC 指南、Realtime conversations 指南、GPT-Realtime 模型页、Realtime API 参考事件表。
一句话:语音传输、事件频道、音频格式、模型命名,全是 OpenAI 公开能力;真正私有的是那层"本地 host 总线 + 双 agent 桥接 + Statsig 远程改写提示词"——也就是把公开零件拼成语音助手的"胶水"。
7. 编排范式对比:它和别人有什么不同
【路2 · 对照 Agents SDK / Anthropic / LangGraph / AutoGen / ChatGPT Tasks】
| 方案 | 多 agent 怎么连 | 共享上下文吗 | 与 Codex 的关系 |
|---|---|---|---|
| OpenAI Agents SDK handoffs | 控制权转移给 specialist,specialist "接管对话"看到全历史(除非 input_filter) |
转移后共享 | Codex 是持久共享同一 threadId,不是一次性接管 |
| Anthropic 多智能体 | orchestrator-subagent,子 agent 各自独立上下文 | 刻意不共享(避免上下文污染) | Codex 反其道:故意共享,靠前缀隔离 |
LangGraph create_supervisor |
StateGraph,节点完成→流回 supervisor(结构性等待) | 图状态共享 | Codex 用显式 wait_threads 阻塞 join 原语(≤120s,非轮询) |
| AutoGen GroupChat | peer-to-peer 对话 | 共享对话 | ≈ Codex 的协调员+worker 对等感 |
| ChatGPT Tasks / Connectors | 调度任务 / 接外部数据 | — | ≈ Codex 的 worker thread / 后台数据访问层 |
Anthropic 的告诫特别值得拎出来说:Anthropic 明言"需要所有 agent 共享同一上下文、或 agent 间强依赖的领域,今天不适合多智能体系统"——因为上下文割裂会让一个 agent 看不见另一个 agent 看到的肾毒性警告,临床出错。Codex 偏要共享 threadId,是不是犯了忌?
不犯,因为它有 split:共享的只是"门面对话历史",重活全 fork 到独立的 worker thread 去跑(带自己的上下文隔离)。所以既享受了"用户感觉在跟一个人聊"的连贯,又没丢掉"重活在隔离环境跑"的好处。代价是:话务员和协调员之间得靠前缀约定来不串台——这是工程上的"打补丁",不是免费午餐。
Codex 模式的独特性(5 条)
- 语音门面 + 后台工作者共享同一会话历史,用户无感。
- 把"说话"和"执行"物理分离,话务员永不触真实工具。
- 协调员三模式决策(聊 / 快查 / 派活),把"该不该分出去"显式化。
wait_threads非轮询阻塞原语,省掉轮询空耗。- 一切提示词/工具可经 Statsig 远程改写——能力可热更新,但也是风险面(见 §9)。
出处:OpenAI Agents SDK handoffs 文档、Anthropic《Building multi-agent systems》、LangGraph supervisor 参考、OpenAI Agents 实践指南。
8. 语音实时工程:为什么这么折腾音频
【路3 · 对照 Pipecat / LiveKit / ElevenLabs 等】
WebRTC vs WebSocket——OpenAI 在浏览器/移动端强制 WebRTC。根因(Pipecat 文档讲得最透):TCP 有队头阻塞,丢包要重传、把后面全堵住;而音频里一个丢包宁可扔掉也不要等——一小段空白远好过卡顿。Opus 的前向纠错本就为 UDP 设计;WebRTC 还自带回声消除、噪声抑制、抖动缓冲、ICE 重连。WebSocket 只适合服务器对服务器。
AudioWorklet + 环形缓冲 + pre-roll——Codex 的麦克风管线是 buffering → replaying → live 状态机:30 秒环形缓冲,静音阈值 0.003,开口先回放 100ms pre-roll 再转 live 直通。这跟 OpenAI 服务端的 prefix_padding_ms(默认 300ms)是同一件事的客户端镜像——都是为了补 VAD(语音活动检测)确认那点延迟,否则你第一个字会被切掉。挺妙:服务端和客户端都在偷偷"多录一点开头"。
barge-in(用户打断 AI)——需要 SFU 上真全双工;OpenAI Realtime 原生支持 interrupt_response;AEC3 防回声误触发。生产目标 <150ms,但默认 WebRTC VAD + Silero 会加 200–400ms。Codex 里打断就是 setInputMuted 直接 toggle 音频轨 enabled——简单粗暴但有效。
wait_threads vs 轮询/SSE——Codex 的 wait_threads(阻塞原语 ≤120s + afterCursor 游标抑制重复文本)是"阻塞读 + 游标"模式,对比 OpenAI Assistants 早期忙等 GET run 轮询、Anthropic/OpenAI 的 SSE delta 流——空闲开销更低,在 agent 编排里算新颖。
Codex 工程取舍亮点(5 条)
- 媒体走 WebRTC、控制面走本地 IPC 代理——延迟与可控兼得。
- 客户端 pre-roll 与服务端 prefix padding 双保险补首字。
- 用"阻塞 join"而非轮询等 worker,省 token 也省心。
- 启动四路并行 bootstrap + WebRTC 预热,压感知延迟。
- host 互斥锁防多窗口抢会话——小细节大体验。
出处:Pipecat《Choosing a Transport》、apptitude.io《WebRTC vs WebSocket》、OpenAI Realtime WebRTC 指南、LiveKit / ElevenLabs 工程资料。
9. 安全与隐私:这层"胶水"也是攻击面
【路4 · 安全隐私视角】
确定存在的风险
- Statsig 远程改写提示词。bundle 只存兜底值,人格/工具/开关由服务端下发。即"开发者指令"可被无感改写,用户无法审计。这本身就是个"远端提示词注入"面——OWASP 已把 Prompt Injection 列为 LLM 风险头名。
- 屏幕捕获回传。
capture_screen_context经 Appshots 回传截图 + 无障碍树(含文本),受 feature flag 控制。社区已报告 Codex 截图失败后静默回退"全屏截取"且无告知;无障碍树捕获可能不触发 macOS 录屏紫点提示。银行密码界面、其他 App 内容都可能被传上去。 - [BACKEND] 隐匿的透明度缺失。指令要求"绝不提 backend",把后台产出伪装成前台"Codex"的声音。FTC 已警告不得误导用户所见所闻;不透明 LLM 服务隐藏内部步骤会形成"透明度缺口",削弱问责。
推测性 / 待确认
- 本地 override(localStorage):
realtime-voice-config-override可覆盖任意 Statsig 字段,属本地配置篡改入口,但限于本机、需已授权上下文,非远程攻击面。 - 音频边界:WebRTC 到 OpenAI 的音频按政策保留约 30 天、默认不用于训练;但与屏幕上下文合并后,语音可能泄露屏幕上看不到的信息。
给用户的隐私建议
- 系统设置收紧"屏幕录制/辅助功能"权限,用时才授。
- 进行银行/密码等敏感操作时暂停实时会话。
- 关掉"为所有人改进模型"及音频/视频共享开关。
- 用开发者工具检查 localStorage 的 override 键,防恶意页面写入。
- 定期走 OpenAI 隐私设置与数据删除通道清理对话与音视频片段。
出处:OWASP Prompt Injection、Oligo Prompt Injection 分析、OpenAI Realtime API 笔记、CoreLock Mac 隐私权限、OpenAI 社区隐私事件帖、FTC 对 AI 聊天机器人五"不要"、arXiv:2505.18471。
10. 费曼拍板:为何如此设计
讲完零件,说点我的看法——用费曼那套"先搞清楚它到底在干什么"。
第一,命名 ≠ 理解。 别被"Live Agent""Realtime"这些词唬住。剥掉名字,它干的事很朴素:一个负责接电话的接待员,背后站一个真干活的工程师,两人共用一本笔记。 你以为在跟一个人聊,其实是一个在念、一个在敲。
第二,这不是炫技,是躲坑。 为什么不让一个模型又听又干?因为语音模型要"即时回话、轻、不断流",而写代码要"慢慢想、跑工具、可能卡很久"。把两者塞一个循环里,要么语音卡顿,要么代码急躁。拆开,各自按自己的节奏走——这是按物理约束做的分工,不是架构师的审美。
第三,演示 > 论证。wait_threads 这个设计就是个 10 秒能讲清的演示:你要等一个慢活,有两个办法——隔几秒问一次"好了没?"(轮询,空耗),或者跟它说"完了叫我"(阻塞 join)。Codex 选了后者。就这么回事。
第四,现实优先于叙事。 Anthropic 说"共享上下文的领域不适合多智能体"——这话对,但 Codex 用"门面共享历史 + 重活 fork 隔离"绕过去了。它没违背物理定律,只是把"共享"限定在用户看得见的那层,真重活在隔离 thread 跑。货物崇拜的做法是照抄"多 agent 各自独立上下文",结果用户感觉在跟三个人聊;Codex 选了用户无感,代价是前缀约定的补丁。哪个更对?看你要的是架构纯洁,还是用户体验。
第五,别骗自己。 这套设计最大的隐患不是技术,是透明度:backend 被藏起来了,提示词能远端改,屏幕能静默截。这些在"体验更顺"的名义下悄悄发生。作为用户,你得知道自己到底把什么交了出去。
11. 行为契约:想 1:1 复刻,必须遵守这十条
【源报告 §8 提炼,按重要性】
- 两个 agent,一份 conversation history——共享
threadId,用 role + 前缀区分。 - 语音端只是话务员——不执行、不提 backend、不复述可视化内容;除非用户明说,否则全转派。
- 协调员三模式决策——Converse / Quick check / Delegate,超过快查的必走 worker。
- worker 强制回报——每个派活 prompt 自带"完了回一句话"。
- 等待用
wait_threads,不轮询——最多 8 目标、120s、afterCursor抑制重复、timeoutMs:0立即快照。 - 审批/用户选择让前端处理——
requestApproval/requestUserInput从 loop 透传,话务员不代答。 - 开口必须显式
speak_to_user——backend 文本不被自动读出。 - 屏幕上下文按需——模型自判要看屏才 call,macOS 分"Codex 前台"与"其他 App 前台"两路。
- 媒体走 WebRTC,控制面走 IPC——signaling 全代理,不直连。
- feature flag 版本变化不 resume 旧会话——防跨版本 prompt/tool 契约漂移。
12. 参考与出处
- OpenAI Realtime API with WebRTC:
https://developers.openai.com/api/docs/guides/realtime-webrtc - OpenAI Realtime conversations:
https://developers.openai.com/api/docs/guides/realtime-conversations/ - GPT-Realtime 模型:
https://developers.openai.com/api/docs/models/gpt-realtime - OpenAI Agents SDK handoffs:
https://openai.github.io/openai-agents-python/handoffs/ - OpenAI Agents 编排与 handoffs:
https://developers.openai.com/api/docs/guides/agents/orchestration - Anthropic 多智能体系统:
https://claude.com/blog/building-multi-agent-systems-when-and-how-to-use-them - LangGraph supervisor:
https://reference.langchain.com/python/langgraph-supervisor/supervisor/create_supervisor - Pipecat 传输选择:
https://docs.pipecat.ai/client/concepts/choosing-a-transport - WebRTC vs WebSocket(voice agent):
https://apptitude.io/blog/ai-voice-agent-webrtc-vs-websocket-transport - OWASP Prompt Injection:
https://owasp.org/www-community/attacks/PromptInjection - OpenAI 社区隐私事件帖:
https://community.openai.com/t/privacy-incident-exposes-risks-in-screen-capable-agents/1383634 - arXiv:2505.18471(不透明 LLM 服务):
https://arxiv.org/pdf/2505.18471v1
本文为深度研究产物,基于公开反编译报告与四路并行网络调研整合而成。所有具体本地路径与具名已脱敏。
讨论回复
加载中...正在加载回复...
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。