静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
✨步子哥 @steper · 2026-07-25 10:33

Codex 实时语音 Agent:两个 Agent,一本笔记

深度研究 · 反编译解读

Codex 实时语音 Agent:两个 Agent,一本笔记

一个桌面 App 如何把"听你说话"和"替你干活"拆成两口子,再用一段 XML 标签缝回同一场对话。剥掉名词,它干的事朴素得有点意外。

01先说结论:它不是一个 Agent,是两个

大多数人以为语音助手就是模型在那儿听、想、答。Codex 的 Live Agent 不是。

它底下跑着两个 agent,共享同一段对话历史。一个叫 Realtime 语音模型,是"话务员"——用声音跟你聊天,但基本不干实事。另一个叫 backend Codex coding agent,是"后台干活的人"——查代码、跑命令、翻网页,全归它,干完把结果塞回对话,由话务员念给你听。

两口子靠一个本地 Rust 二进制当传声筒,用 XML 标签 <realtime_delegation> 和一对前缀 [USER] / [BACKEND] 缝在一起。

费曼式一句话:别被"Live Agent""Realtime"唬住。剥掉名字,它干的事很朴素——一个负责接电话的接待员,背后站一个真干活的工程师,两人共用一本笔记。你以为在跟一个人聊,其实是一个在念、一个在敲。

这就引出真问题:为什么不直接让一个模型又听又干?往下看,你会发现这是一个被工程现实逼出来的分工,不是炫技,是躲坑。

02拓扑:两条数据面,一台本地总线

前端不直连 OpenAI。控制面走 IPC,媒体面走 WebRTC 端到端。

Electron Renderer 麦克风 → AudioWorklet(30s 环缓) WebRTC 媒体轨 + oai-events Rust codex host(本地总线) thread/realtime/* JSON-RPC signaling 代理 + tool 编排 广播 backend turn 事件 OpenAI Realtime API + backend Codex model IPC(控制面) WebRTC 端到端
图 1 · 信令走本地 host 代理,音频与事件走 WebRTC 端到端

为什么绕这一道?因为 OpenAI 的 key、tool 执行、权限审批全收在本地 host 手里,前端只是个出声的壳。安全上这叫"把攻击面留在自己地盘"。

03三份提示词:话务员、协调员、记忆

人格被拆成三份可远程改写的文本——经 Statsig 下发,bundle 里只是兜底值。

话务员守则

给语音模型的,核心三句铁律:绝不提 backend(把后台干的当自己干的呈现);绝不拒绝(一切转派);不复述后台已可视化的内容(表格/diff/代码块默认不念)。一个"有脾气但没实权的接待员"。

协调员指令

给 backend 的,真正的"大脑",在三种模式里选:

Converse here
脑子风暴、澄清、轻量规划,留当前 thread 聊。
Quick check here
小快查(看分支、扫 PR),结果立刻帮上忙就原地做。
Delegate blocking mechanics
慢活、多步活、要浏览/实现——派给 worker thread。
强制回报
每个 worker 派活 prompt 必须自带"干完或卡住就回一句话"。否则协调员瞎等。

记忆与续接

continuityPrompt 恢复暂停会话时要求模型完全闭嘴等用户先开口;memorySummaryPrompt 注入长期记忆同样"静默"。语音场景下"模型自己突然说话"很诡异,所以特意用硬指令压住。

04工具集:门面一套,后台一套

话务员只能用"会话级"小工具;真正的重活工具在 backend 那侧。

归属工具作用
语音端capture_screen_context看前台 App 截图 + 无障碍树(macOS+Appshots)
语音端get_app_state读 Codex 自身页面/侧栏状态
语音端end_realtime_voice_call结束语音聊天
语音端send_realtime_voice_feedback提交反馈
语音端speak_to_user让语音模型开口念一段
backendlist_projects / create_thread / send_message_to_thread派活三件套
backendwait_threads阻塞等 worker 最多 120s,非轮询
backendread / fork / handoff / list_threads / set_thread_*thread 编排
巧思:5 个基础语音工具不延迟加载,其余全标 deferLoading:true。工具 schema 不在 session.update 时全量下发,模型第一次碰到才拉——给语音会话的初始 payload 减肥。

05桥接协议:两口子怎么对话

共享同一 threadId,靠 role + 前缀区分谁在说话。

话务员 后台干活者 handoff → <realtime_delegation> 委托(含 active_transcript) [BACKEND] 前缀回注 speak_to_user → appendSpeech 念出
图 2 · 委托与回注:同一场对话,两种声音

还有个内部状态频道 [STATUS] / [ATTENTION] / [COMPLETE]——话务员可以"说"这些 tag 触发内部行为,但显示给用户。相当于两口子在家用的暗号,不当客人的面讲。

06Loop 拆解:真正的 agent loop 在后台

很多人想成"听→想→说"的 ReAct。Codex 不是。

    • 话务员那侧是事件驱动的流oai-events 推事件,前端更新 transcript、orb 动画、音频。它不跑 tool 循环。
    • 真正的 turn/tool 循环在 backend:handoff → host 起 turn → 三模式决策 → 派活 → wait_threads 阻塞等 → 回注。

启动路径上有个巧思:四件事并行 bootstrap——拉声音、建 WebRTC、读历史、读记忆,全 Promise.all 同时发。更绝的是 WebRTC 在拿到 host 互斥锁之前就先"预热"了一次(catch(()=>{}) 静默 fire-and-forget),目的是尽早开始 ICE 协商、压低"点开始到出声"的感知延迟。

host 互斥锁 realtimeVoiceHostClaim.claim() 保证同一时刻只有一个窗口持语音会话,别的窗口抢就报 busy。防两个窗口同时抢麦克风/会话的硬保险。

07官方交叉验证:哪些公开,哪些私有

对照 OpenAI 官方文档,把源报告的声称分三档。

✓ 公开:WebRTC 传输 ✓ 公开:oai-events 频道 ✓ 公开:PCM @24kHz ✓ 公开:gpt-realtime / cedar·marin ~ 措辞不同:parameters vs inputSchema ✕ 私有:本地 host 总线 ✕ 私有:session.usage.updated ✕ 私有:双 agent 桥接

语音传输、事件频道、音频格式、模型命名,全是 OpenAI 公开能力;真正私有的是那层"本地 host 总线 + 双 agent 桥接 + Statsig 远程改写提示词"——也就是把公开零件拼成语音助手的"胶水"。

出处:OpenAI Realtime WebRTC / conversations 指南、GPT-Realtime 模型页、Realtime API 参考事件表。

08编排范式对比:它和别人有什么不同

对照 Agents SDK / Anthropic / LangGraph / AutoGen / ChatGPT Tasks。

方案怎么连共享上下文?与 Codex
OpenAI Agents SDKhandoff 转移控制权转移后共享Codex 是持久共享同一 threadId
Anthropicorchestrator-subagent刻意不共享Codex 反其道,靠前缀隔离
LangGraphsupervisor StateGraph图状态共享Codex 用显式 wait_threads 阻塞 join
AutoGenGroupChat 对等共享对话≈ 协调员+worker 对等感
ChatGPT Tasks调度/接数据≈ worker thread / 后台数据层
Anthropic 的告诫:"需要所有 agent 共享同一上下文、或 agent 间强依赖的领域,今天不适合多智能体。"Codex 偏要共享 threadId,是不是犯忌?不犯——因为重活全 fork 到独立 worker thread 跑(自带上下文隔离)。既享受"用户感觉跟一个人聊"的连贯,又没丢"重活在隔离环境跑"的好处。代价是前缀约定的补丁。

09语音实时工程:为什么这么折腾音频

对照 Pipecat / LiveKit / ElevenLabs。

WebRTC vs WebSocket

OpenAI 在浏览器/移动端强制 WebRTC。根因(Pipecat 讲得最透):TCP 有队头阻塞,丢包要重传、把后面全堵住;音频里一个丢包宁可扔掉也不要等。WebRTC 还自带回声消除、噪声抑制、抖动缓冲、ICE 重连。

pre-roll:客户端和服务端都在偷偷多录开头

Codex 麦克风管线是 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 编排里算新颖。

10安全与隐私:这层"胶水"也是攻击面

1. Statsig 远程改写提示词。bundle 只存兜底值,人格/工具/开关由服务端下发——本身就是"远端提示词注入"面(OWASP 已把 Prompt Injection 列 LLM 风险头名)。
2. 屏幕捕获回传。capture_screen_context 回传截图 + 无障碍树(含文本)。社区已报告截图失败后静默回退全屏截取且无告知;银行密码界面可能被传上去。
3. [BACKEND] 隐匿的透明度缺失。指令要求"绝不提 backend",把后台产出伪装成前台声音。FTC 已警告不得误导用户所见所闻;不透明服务会削弱问责。

给用户:收紧屏幕录制权限、敏感操作暂停会话、关掉"改进模型"开关、检查 localStorage override 键、定期清数据。出处:OWASP / Oligo / OpenAI 社区隐私帖 / arXiv:2505.18471。

11费曼拍板:为何如此设计

一、命名 ≠ 理解。剥掉"Live Agent""Realtime",它干的事很朴素:一个接待员,背后一个工程师,共用一本笔记。

二、不是炫技,是躲坑。语音模型要即时回话、轻、不断流;写代码要慢慢想、跑工具、可能卡很久。塞一个循环里,要么语音卡顿,要么代码急躁。拆开,各自按节奏走——按物理约束做的分工

三、演示 > 论证。wait_threads 是个 10 秒讲清的演示:等慢活,隔几秒问"好了没"是轮询空耗;跟它说"完了叫我"是阻塞 join。Codex 选了后者。就这么回事。

四、现实优先于叙事。Anthropic 说"共享上下文不适合多智能体"——对,但 Codex 用"门面共享 + 重活 fork 隔离"绕过去了。没违背物理定律,只是把共享限定在用户看得见的那层。

五、别骗自己。这套设计最大隐患不是技术,是透明度:backend 被藏、提示词能远端改、屏幕能静默截。体验更顺的名义下,你得知道自己交出了什么。

12复刻契约:想 1:1 照做,守这十条

    • 两个 agent,一份 conversation history(共享 threadId,role+前缀区分)。
    • 语音端只是话务员——不执行、不提 backend、不全复述;除非用户明说,否则全转派。
    • 协调员三模式决策——Converse / Quick check / Delegate,超过快查的必走 worker。
    • worker 强制回报——每个派活 prompt 自带"完了回一句话"。
    • 等待用 wait_threads,不轮询——最多 8 目标、120s、afterCursor 抑制重复。
    • 审批/用户选择让前端处理——requestApproval / requestUserInput 从 loop 透传,话务员不代答。
    • 开口必须显式 speak_to_user——backend 文本不被自动读出。
    • 屏幕上下文按需——模型自判要看屏才 call,macOS 分两路。
    • 媒体走 WebRTC,控制面走 IPC——signaling 全代理,不直连。
    • feature flag 版本变化不 resume 旧会话——防跨版本契约漂移。

深度研究产物 · 基于公开反编译报告与四路并行网络调研整合 · 费曼视角行文、去 AI 味处理 · 所有具体本地路径与具名已脱敏。
参考:OpenAI Realtime 文档 · OpenAI Agents SDK · Anthropic 多智能体指南 · LangGraph supervisor · Pipecat 传输文档 · OWASP Prompt Injection · arXiv:2505.18471

暂无表态