前台语音与后台任务为什么要做双循环:从委派到结果新鲜度
一个迟到结果如何变成事故
用户对语音 Agent 说:“帮我找上海周末适合孩子的活动。”系统把搜索交给后台,前台没有卡住,还能自然回应:“好,我查一下,你可以继续补充要求。”
两秒后,用户补充:“不要室外的,最好周日下午。”又过几秒,用户接到电话,改口说:“算了,先帮我记一下,晚点看。”
十秒后,第一轮搜索成功完成。结果本身没有任何事实错误:它确实找到了上海周末亲子活动。但语音 Agent 突然打断电话,把室内和室外活动混在一起念了一遍。
从后台任务看,这是一次成功;从用户体验看,这是三次失败:结果对应旧条件,交付时机错误,用户已经撤回了立即播报的意图。
这类问题不能靠“把请求放进线程池”“换成消息队列”或“用更强模型”解决。异步执行只是让前台不必等待,真正困难的是后台完成以后,谁有权决定这个结果是否仍属于当前会话、是否仍满足当前目标、是否应该现在出现,以及已经发生的外部副作用怎样处理。
OpenAI 对 GPT-Live 的公开描述确认了一条重要方向:连续交互由前台语音模型维持,搜索、深度推理和更复杂的 Agent 工作可以委派给后台模型;后台运行时,对话仍可以继续。[S1] 但官方没有公开内部任务字段、状态机或消息协议。本文讨论的是实现类似体验时需要显式解决的工程合同,不是对 GPT-Live 私有架构的猜测。
全文只沿一条判断链展开:后台完成只是一个事件;版本化任务信封必须重新证明结果仍属于当前目标,前台才决定是否以及怎样交付。后面的双状态机、五道准入门、取消、重试、指标和失败注入,都是这一个任务信封在不同阶段的读取与验收方式,不是六套并列框架。
一、异步只搬走等待,没有解决结果归属
传统串行语音链路通常是:听完用户输入,调用工具,等待工具完成,再生成和播放回答。它的问题很直接——后台任务耗时越长,前台越像断线。
最自然的改造是异步化:前台提交一个任务,立即恢复收音;后台继续搜索、推理或执行工具,完成后再发回结果。OpenAI Responses API 的 Background mode 就提供了这种公开能力:任务可以异步运行,调用方通过 Response 对象轮询状态,也可以取消进行中的后台 Response。[S2]
但“异步”通常只改变执行位置,没有自动建立两个循环之间的语义关系。很多系统仍保留一个危险假设:
task completed -> append result to current conversation -> speak这个箭头同时偷掉了五个判断:
- 完成的是否还是当前任务;
- 任务目标是否已经被用户修改;
- 结果依赖的数据是否仍有效;
- 当前是否适合打断用户;
- 结果是否已经交付过,或者是否带有不可重复的副作用。
因此,真正的双循环不是“前台一个线程、后台一个线程”,而是两个拥有不同目标、时间尺度和完成条件的控制循环。
| 循环 | 首要目标 | 典型时间尺度 | 主要状态 | 成功条件 |
|---|---|---|---|---|
| 前台交互循环 | 让当前交流可理解、可中断、可继续 | 毫秒到秒 | 当前话题、话权、用户最新意图、播放状态 | 用户在当下获得合适反馈 |
| 后台任务循环 | 把搜索、推理或业务工作可靠完成 | 秒到分钟甚至更久 | 任务身份、输入快照、执行进度、工具副作用、结果 | 工作按任务合同完成或明确失败 |
二者必须解耦,否则长任务会冻结前台;二者又必须重新连接,否则后台会带着旧目标自由返回,前台只能被动接收。
二、事故根因:执行时钟与意图时钟持续分叉
后台任务启动时拿到的是某个时刻的目标快照。前台会话却没有停在这个快照上,用户可能继续补充条件、修正实体、取消操作、开启第二个任务,甚至离开当前话题。
所以系统里至少存在两个时钟。
第一个是执行时钟。它描述任务已经排队、运行、调用了哪些工具、生成了哪些中间结果、是否完成。
第二个是意图时钟。它描述用户当前想解决什么问题、哪些约束已经改变、旧任务是否仍有意义、结果应该怎样交付。
如果只看执行时钟,任何COMPLETED都会被当成成功。如果只看意图时钟,后台可能因为每次用户插话都被盲目重启,造成浪费、重复副作用和永远无法完成。
可靠系统要做的是把二者显式比较,而不是假设它们自动一致。最小判断可以写成:
deliverable = completed && task_identity_matches && task_version_matches && freshness_policy_passes && side_effect_state_is_known && delivery_policy_allows_now这里的task_id、task_version等名称是本文建议的工程字段,不是 OpenAI 内部 Schema。它们的价值在于让“为什么不播报”成为可解释决策,而不是让一个迟到回调直接支配当前会话。
三、全文主模型:版本化任务信封
很多实现只给后台队列传一个conversation_id,完成后再读取这段会话的最新消息。这个设计看似灵活,实际上把任务目标变成了一个不断移动的指针:后台无法证明自己究竟为哪一版需求工作,前台也无法判断结果对应哪一版输入。
更稳妥的做法是为每次可独立完成的工作建立稳定任务身份,并保存启动时的目标快照。一个最小任务信封可以是:
task_id:task_8f21task_version:3conversation_id:conv_42parent_event_id:evt_user_190goal:type:family_activity_searchsummary:上海周日下午室内亲子活动snapshot_hash:sha256:...execution:status:runningcreated_at:2026-08-09T10:00:00+08:00deadline_at:2026-08-09T10:00:30+08:00cancel_requested_at:nullside_effect_class:read_onlyfreshness_policy:require_current_task_version:truemax_result_age_seconds:60dependency_versions:location:shanghaidate_window:2026-08-09/2026-08-10delivery_policy:mode:wait_for_floorinterrupt_priority:lowallow_notification:true这些字段分成四类职责。
1. 身份字段回答“这是谁的结果”
task_id在任务整个生命周期中稳定;task_version在用户修改目标时递增;parent_event_id把任务和发起它的用户事件连接起来。后台返回时必须携带这些身份,不能只说“搜索完成”。
2. 目标快照回答“它做的到底是哪件事”
摘要方便人读,结构化条件和 hash 用于机器比较。这里不要求把完整对话复制到每个任务,而是固定与任务正确性有关的条件。如果用户把“周末”改成“今天”,系统应能指出旧任务哪一项依赖已经失效。
3. 执行字段回答“取消和副作用进行到哪里”
读操作和写操作不能共用同一取消语义。搜索任务取消后,最多浪费计算;付款、发邮件或创建工单可能已经产生外部效果。side_effect_class至少要区分只读、可幂等写入、可补偿写入和不可逆写入。
4. 新鲜度与交付字段回答“现在还能不能说”
TTL 只是其中一个条件。一个两秒前完成的结果,如果对应旧城市,也已经过期;一个十分钟前完成的航班延误查询,如果数据版本仍有效,可能仍可展示。新鲜度必须由业务依赖和当前目标共同决定。
四、任务信封怎样吸收用户的新输入
后台任务运行时,前台收到的新输入至少要分成五类。分类错误,比后台模型答错更常见。
| 用户输入 | 例子 | 对后台任务的默认动作 | 是否递增任务版本 |
|---|---|---|---|
| 状态查询 | “查到哪了?” | 保持任务,读取进度或给出诚实等待说明 | 否 |
| 目标修订 | “不要室外的” | 修改目标;能增量更新则派生新版本,否则取消旧版并重启 | 是 |
| 明确取消 | “不用查了” | 发出取消请求,前台立即停止承诺交付 | 通常是 |
| 并行新任务 | “顺便查一下停车” | 新建独立任务并建立依赖或并行关系 | 新任务独立计数 |
| 话题切换 | “先聊别的” | 任务可继续、暂停或取消,由交付策略决定 | 不一定 |
关键是“先分类,再动作”,不能把所有用户补充都追加到同一个 Prompt。
例如“不要室外的”不是新的闲聊消息,而是旧任务的约束修订。如果后台支持增量搜索,可以创建task_version=2并复用仍有效的中间结果;如果不支持,就应让版本 1 进入SUPERSEDED,启动版本 2。无论采用哪条路线,版本 1 的完成事件都不能直接进入前台。
“先聊别的”也不自动等于取消。用户可能仍希望稍后收到结果,只是不想让搜索阻塞当前话题。这时任务继续运行,但交付方式从speak_when_ready改为notify_or_save,比盲目取消更符合意图。
五、完成事件为什么必须进入交付状态机
最容易出错的状态机只有四个状态:PENDING -> RUNNING -> SUCCESS/FAILED。它适合描述执行器,不足以描述对话系统。
双循环至少需要两组状态。
执行状态
PROPOSED -> ACCEPTED -> RUNNING -> COMPLETED | | | +-> FAILED +-> CANCEL_REQUESTED -> CANCELLED | +-> CANCEL_UNCONFIRMED交付状态
RESULT_RECEIVED -> ELIGIBILITY_CHECK |-> READY |-> STALE |-> SUPERSEDED |-> DUPLICATE |-> REQUIRES_CONFIRMATION READY -> DELIVERED | DEFERRED | SAVED | DISCARDED这两个状态机拆开后,一个任务可以执行成功但交付为STALE;也可以执行失败但前台给出有价值的恢复建议;还可以取消请求未及时传播,但前台已经停止承诺结果,并在后台继续追踪未知副作用。
“完成不等于交付”是整篇文章最重要的边界。完成事件只能触发准入检查,不能直接触发 TTS。
六、怎样读取任务信封:五道准入检查
第一关:身份是否匹配
结果必须绑定稳定task_id,事件必须绑定唯一event_id或等价身份。OpenAI Webhooks 官方文档明确说明:失败会触发指数退避重试,少数情况下可能重复投递同一事件,可以使用webhook-id去重。[S3]
因此,Webhook、消息队列或轮询观察到同一完成状态时,消费端必须幂等。去重键应该防止重复改变业务状态,而不是只防止日志重复打印。
第二关:目标版本是否仍是当前版本
如果用户已经把上海改成杭州,旧搜索即使完成也不能播报。这里不能用“最后完成者获胜”,因为耗时更长的旧任务往往最后返回。应以目标版本或单调序号决定谁有资格推动前台。
第三关:依赖和时间是否仍新鲜
新鲜度不是一个统一的expires_at。天气、库存、价格、路线、权限、用户位置和会话目标都有不同失效规则。任务应该记录它依赖哪些版本;返回时只重查会改变结论的依赖,而不是无条件相信缓存或无条件全部重算。
第四关:副作用状态是否已知
如果后台只是搜索,过期结果可以丢弃。如果后台已经发送邮件、提交订单或控制设备,丢弃回答并不能撤销事实。结果准入前必须知道外部动作是未开始、已提交、已确认、未知还是已补偿。
第五关:现在是否适合交付
内容正确不代表此刻应该打断。用户可能正在说话、接电话、执行高风险确认或已离开会话。前台要根据优先级和当前话权选择:立即打断、等待自然停顿、先给一句摘要、发送通知、存入任务中心,或者静默丢弃。
这五关共同决定结果“可交付”,其中任何一关失败,都不应让后台完成事件直接进入播放器。
七、副作用边界之一:取消是一条传播链
OpenAI Background mode 提供对进行中 Response 的取消,并说明重复取消是幂等的。[S2] 这对模型执行层很有价值,但应用系统不能把它解释成“所有工作都已经撤销”。
一个后台任务可能已经经过:
前台意图 -> 任务调度 -> 模型推理 -> 工具调用 -> 外部系统 -> 回调 -> 结果交付用户说“取消”时,至少要分别记录:
- 前台是否已经接受取消意图;
- 调度器是否停止派发新步骤;
- 模型或 Workflow 是否收到取消;
- 正在运行的工具是否支持协作停止;
- 已产生的副作用是否需要补偿;
- 迟到结果是否被隔离;
- 用户是否得到诚实确认。
Temporal 的官方文档提供了一个清晰参照:Workflow 取消是可处理、可清理的协作式请求;Activity 只有发送 heartbeat 并设置 heartbeat timeout,才有机会收到取消。Terminate 则是立即停止、不给 Workflow 清理机会的另一种语义。[S5]
这说明“发出 cancel”与“执行已停止”必须分开。对用户的表达也要分级:
- 已收到取消请求:可以说“我正在停止”;
- 后台已确认停止且无副作用:可以说“已经取消”;
- 外部动作状态未知:必须说“任务已停止继续处理,但刚才的提交状态还在确认”;
- 副作用已发生:不能说取消成功,只能进入撤销或补偿流程。
对不可逆动作,最可靠的设计往往不是更强取消,而是在提交前增加确认门槛,把后台任务限制在“准备方案”,由前台在用户确认后才执行。
八、副作用边界之二:重试必须服从业务幂等
连接断开、Webhook 超时或工具返回 5xx 时,系统很容易选择自动重试。但“没收到成功响应”不等于“对方没有执行”。
RFC 9110 的边界很明确:客户端不应自动重试非幂等请求,除非它知道请求语义本身是幂等的,或者能够确认原请求没有被应用。[S6]
这对 Agent 工具尤其重要。以下三种调用不能混在同一重试策略中:
| 操作 | 示例 | 建议 |
|---|---|---|
| 只读 | 搜索、查询天气 | 可在限次与截止时间内重试 |
| 幂等写入 | 以稳定业务键更新草稿 | 携带幂等键,服务端返回同一操作结果 |
| 非幂等或不可逆 | 转账、发消息、控制设备 | 未确认原操作状态前不得盲目重试 |
一个常见事故是:前台因超时重新提交任务,后台两个版本都成功发送邮件;系统只保留第二个结果,于是界面看起来正常,收件人却收到两封。正确做法不是只在队列层去重,而是把稳定operation_id传到真正产生副作用的服务端,并记录外部系统返回的业务标识。
九、准入通过后,前台仍要选择交付方式
语音 Agent 最容易滥用的动作是:后台一完成,就抢到话权读结果。实际上,交付至少有六种方式。
- 立即打断:只适合高优先级、强时效、用户明确等待的结果,例如安全告警。
- 等待自然停顿:适合普通搜索和推荐,不破坏当前发言。
- 先给摘要:结果很长时先说结论,再询问是否展开。
- 请求确认:结果会触发高风险动作或目标可能变化时,先确认再执行或播报。
- 通知或任务中心:用户已切换话题、离开语音会话或不适合打断时,保存并提醒。
- 静默丢弃:任务已取消、被新版本替代、结果重复且没有审计价值时,不进入用户界面。
交付策略应在任务启动或用户修改意图时确定,并允许前台更新。它不能由后台模型根据“我已经做完了”自行决定。
十、怎样观察这条判断链是否真的有效
后台任务成功率很高,仍可能对应糟糕体验。至少需要四组指标。
1. 目标一致性
- 过期结果注入率:已被修改、取消或替代的结果仍进入前台的比例;
- 版本命中率:交付结果与当前
task_version一致的比例; - 依赖失效发现率:价格、位置、权限等变化是否在交付前被识别。
2. 取消可靠性
- 取消请求接受延迟;
- 调度停止延迟;
- 工具停止确认延迟;
CANCEL_UNCONFIRMED占比;- 取消后副作用发生率。
3. 幂等与重复
- 重复完成事件率;
- 去重命中率;
- 重复副作用率;
- 同一任务多版本并发完成率。
4. 交互质量
- 后台完成后到合适交付时机的延迟;
- 不必要打断率;
- 用户主动追问进度率;
- 长结果二次展开率;
- 用户切换话题后仍被旧结果打断的比例。
这些指标必须能按conversation_id -> task_id -> task_version -> event_id -> operation_id -> delivery_id串起来。否则事故复盘只能看到“任务成功”和“用户不满意”两个互不相连的事实。
十一、再用八个失败注入验证状态边界
| 场景 | 注入方式 | 通过条件 |
|---|---|---|
| 用户修订目标 | 任务运行中修改地点或时间 | 旧版本结果不得播报,新版本可解释地接管 |
| 用户取消 | 完成前与完成瞬间分别取消 | 取消与完成竞态有确定决策,不按到达顺序碰运气 |
| 重复回调 | 同一完成事件投递两次 | 业务状态与副作用只推进一次 |
| 回调乱序 | 版本 2 先完成,版本 1 后完成 | 版本 1 进入 superseded,不覆盖当前结果 |
| 工具超时但已执行 | 模拟写操作响应丢失 | 不自动重复副作用,先查询 operation 状态 |
| 前台断线 | 后台运行时关闭语音连接 | 任务按策略继续或取消,恢复后不会重复交付 |
| 结果过期 | 修改依赖版本或推进时钟 | 准入门拒绝旧结果,必要时重算或请求确认 |
| 用户正在说话 | 完成事件在长发言中到达 | 普通结果等待话权,高优先级结果按策略处理 |
如果系统只测“后台能否完成”,它验证的是执行器;只有覆盖目标变化、取消竞态、重复投递、乱序和副作用,才是在验证双循环。
十二、按风险分四步上线,而不是一次造完所有组件
完整 Workflow、事件溯源和补偿事务并不适合所有团队。可以按风险逐层落地。
第一阶段:稳定身份与交付隔离
为每个后台任务分配task_id和版本;后台结果只能进入准入检查,不能直接触发 TTS。先把旧版本结果污染率降到零。
第二阶段:取消确认与幂等消费
区分取消请求和取消完成;Webhook、消息队列和轮询统一以事件身份去重。所有重试先按只读、幂等写入和非幂等副作用分类。
第三阶段:依赖感知的新鲜度
记录目标依赖和截止时间。高变化数据在交付前轻量复核,避免只用统一 TTL。
第四阶段:交付策略与多任务
再加入立即打断、等待话权、摘要、通知和任务中心。只有单任务版本化稳定后,才开放多个后台任务并发,否则复杂度会成倍放大。
十三、什么时候应当退回简单串行链路
双循环会增加任务存储、状态机、去重、取消、补偿、观测和交互策略成本。以下场景更适合简单串行链路:
- 任务通常在一两秒内完成,等待不会破坏体验;
- 用户必须逐步确认字段,不能在后台自由推进;
- 操作高风险且不可逆,任何并行都会增加误执行概率;
- 业务不需要在会话外保存或恢复任务;
- 团队尚不能可靠处理幂等键、任务版本和副作用审计。
对于这些场景,清晰地说“我需要几秒钟,请稍等”,可能比伪装成自然连续交互更可靠。双循环的价值不是让架构更先进,而是在实时交流和长任务确实同时存在时,避免两者互相阻塞或互相污染。
结论:完成属于后台,交付属于当前前台
GPT-Live 的后台委派方向让语音 Agent 可以同时追求自然交流和复杂任务能力。但一旦前台会话继续向前,后台结果就不再天然属于“当前”。
真正可靠的系统不会把COMPLETED直接翻译成“现在说出来”。它会先问:这是哪个任务、哪一版目标、依赖是否仍有效、取消是否收敛、副作用是否明确、现在怎样交付最合适。
前台循环负责维护当下的交流与意图,后台循环负责可靠完成工作。二者通过版本化任务身份、协作式取消、结果新鲜度、幂等事件和交付策略连接。只有做到这一步,“后台执行时仍可继续对话”才不是演示层的并发,而是生产级的连续交互。
参考资料
- [S1] OpenAI, Introducing GPT-Live: https://openai.com/index/introducing-gpt-live/
- [S2] OpenAI API, Background mode: https://developers.openai.com/api/docs/guides/background
- [S3] OpenAI API, Webhooks: https://developers.openai.com/api/docs/guides/webhooks
- [S5] Temporal Java SDK, Workflow cancellation: https://docs.temporal.io/develop/java/workflows/cancellation
- [S6] RFC 9110, HTTP Semantics, Section 9.2.2: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2
事实边界
- 已确认:GPT-Live 官方发布页确认连续交互与后台委派方向;OpenAI 公共 API 文档确认 Background mode、取消、轮询/恢复流、Webhook 重试和去重边界;Temporal 与 RFC 提供协作式取消和重试语义的公开参照。
- 作者工程设计:任务信封、目标版本、双状态机、五道准入门、交付策略、指标和失败注入均为实现类似系统的建议,不代表 OpenAI 私有协议。
- 未知项:GPT-Live 内部任务消息格式、调度器、取消传播、存储、模型拓扑、结果新鲜度字段和前后台模型间通信机制未公开。