Qwen Live 工具延续与权限恢复机制解析:语音会话响应仲裁与权限生命周期设计

Qwen Live 工具延续与权限恢复机制解析:语音会话响应仲裁与权限生命周期设计 Qwen Live 工具延续与权限恢复机制解析语音会话响应仲裁与权限生命周期设计【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读本文以 docs/design/2026-09-01-qwen-live-tool-continuation-permission-resume.md 设计文档为主体结合 qwen-live 包的源码实现深入解析 Qwen Live 在 DashScope Realtime 语音链路上如何处理工具调用结果后的会话延续与跨会话权限请求的恢复两大问题。读完本文你将掌握为何 provider 的自动响应创建必须被关闭、直接响应如何与用户语音仲裁、continuesResponse工具延续的触发条件、pre-ack 替换窗口的竞态处理以及 daemon-session 范围的 PermissionBroker 如何让权限询问在 Live 调用重启后依然可重放、可幂等恢复。背景语音链路上的三个生命周期缺口Qwen Live 的语音客户端通过 WebSocket 连接 DashScope 的 qwen-omni realtime 服务对应实现见 packages/qwen-live/src/realtime/realtime-session.ts同时通过 ACP/适配器与后端 Agent 会话如 qwen-acp、qodercli交互。设计文档指出该架构存在三类生命周期错配工具结果后对话静默DashScope Realtime 在收到function_call_output后不会自动继续生成回复。旧版 Live 客户端只提交工具输出、不请求新一轮响应导致携带结果的工具如权限确认、命令执行回执完成后对话一直静默直到用户再次开口。用户语音所有权冲突如果对每个工具结果都立即创建续接响应又会与用户正在说话的轮次竞争。后端结果在用户说话期间到达时必须被保留并折叠进对该话语的回答而不是与 provider 的直接响应竞速、或打断用户语音。response.done之后的播放尾部provider 响应已经结束时Host 端可能仍有缓冲音频在播放这是第二个生命周期缺口。而权限请求还存在一个更深层的第二生命周期不匹配ACP 会话的存活时间长于一次 Live 调用Live call 可能被重启且一段注入的语音询问也可能在用户侧被打断或忽略但后端请求仍挂起——因此语音已投递并不能证明权限已被裁决。设计总览两条主线的统一方案整个设计围绕两条主线展开响应仲裁线让服务端 VAD 负责检测与提交话语但禁用其自动响应创建由客户端在输入提交回调同步排空后端上下文后创建恰好一个直接响应并将工具结果与后端语音请求合并进该响应。权限生命周期线将权限 Broker 的所有权上提到daemon-session 作用域让它在 Live 调用重启后重建事件泵、幂等重放权限事件并持续报告waiting_for_permission状态。两条主线在语音注入窗口和可重放的权限询问处交汇最终由一组 realtime 单元测试与 orchestrator 集成测试兜底验证。服务端 VAD 提交与恰好一个直接响应仲裁关闭 provider 的自动响应创建会话建立时客户端发送session.update其中turn_detection被配置为semantic_vad且create_response: false、interrupt_response: true见 realtime-session.ts{ type: session.update, session: { modalities: [text, audio], input_audio_transcription: { model: qwen3-asr-flash-realtime }, turn_detection: { type: semantic_vad, create_response: false, // 关键VAD 只提交话语不自动创建响应 interrupt_response: true }, tool_choice: auto } }这意味着 VAD 仍然负责检测话语结束并提交输入但提交后自动生成回复这一步改由客户端显式控制为后面的合并式仲裁腾出空间。两种提交事件被当作同一事件的幂等形式DashScope 对一次输入提交可能以两种事件之一确认conversation.item.created携带 VAD 输入 item或input_audio_buffer.committed。在 realtime-session.ts 中两者都会进入同一个commitInputItem()处理函数conversation.item.created仅当 item 为message类型、包含input_audio内容且该 itemId 在待提交集合pendingSpeechItemIds中时才视为输入提交input_audio_buffer.committed直接携带item_id提交。commitInputItem()内部realtime-session.ts先做幂等去重若 itemId 已提交或已消费则标记为duplicate_event并忽略。随后推进speechGeneration语音世代计数器用于判定后续事件是否过期并通过onInputCommitted回调通知上层。直接响应只创建一个提交回调的关键行为是同步排空排队的后端上下文后只创建一个直接响应。if (activeDirectResponse) { // 用户话语已绑定到进行中的直接响应不创建新响应 directResponsePending false; responseToolCapabilities.set(activeResponseId, direct); } else if (directResponsePending) { // 没有活跃直接响应创建恰好一个 requestResponseCreate(direct, undefined, itemId); }在 requestResponseCreate 中可以看到整套仲裁规则当authority ! direct且directResponsePending为真用户话语的响应尚未被 provider 接受时新的后端语音请求如[SPEAK_TO_USER]不会另起响应而是以[MERGE_WITH_USER]前缀作为对话 item 注入等待合并进那一个直接响应若已存在pendingResponseCreate或activeResponseId新请求进入responseCreateQueue队列排队每个非直接响应还受两档超时保护NON_DIRECT_RESPONSE_CREATED_TIMEOUT_MS 15_000等待response.created与NON_DIRECT_RESPONSE_DONE_TIMEOUT_MS 120_000等待response.done超时即上报response.created/response.done超时错误见 realtime-session.ts 与 armResponseCreatedTimer / armResponseDoneTimer。pre-ack 替换窗口合并必须发生在响应创建之前竞态的关键窗口是response.create已离开 socket但 provider 的response.created确认尚未返回。此时若合并内容后端结果到达直接取消未确认请求并排队替换才能保证合并 item 落在响应上下文中而不是变成第二段播报。源码注释明确写道realtime-session.ts// The direct request has left the socket but has not been accepted by // the provider yet. Cancel and replace it so the merged item, which is // ordered after that first request on the wire, is guaranteed to be in // the response context instead of becoming a second spoken turn. if ( pendingResponseCreate?.authority direct !pendingResponseCreate.cancelled ) { pendingDirect.cancelled true; pendingDirect.cancellationReason superseded; responseCreateQueue.unshift({ /* 原样重建 direct 请求 */ }); }取消的响应保持活跃直到response.done被替换的响应不能立即释放响应槽位。设计约定被取消的响应保持活跃直到 DashScope 确认response.done才释放槽位并创建替代响应。response.created分支realtime-session.ts会处理provider 在同一轮次拆分多个连续响应的情况若新响应并非来自客户端的response.create且没有新的未绑定输入则把上一个响应的输入 item 与对话文本转移给新响应splitResponseInputItemId/splitDialogueText保留真正的麦克风能力。response.done分支realtime-session.ts则负责回收状态并通过queueMicrotask(flushResponseCreate)触发队列中下一个响应的发送。新语音使旧输入作废如果另一个话语先开始则退役旧的排队输入并忽略其迟到的 ASR 完成事件。input_audio_buffer.speech_started分支realtime-session.ts会推进speechGeneration并置speechCommitPending true、directResponsePending true清空responseCreateQueue对队列中带speechMessage的请求以[MERGE_WITH_USER]前缀回注为对话 item将pendingResponseCreate若为 direct标记为user_interrupted取消对因打断而过期的输入 item 调用consumeInputItem()消费若存在活跃响应触发onBargeIn回调并标记取消user_interrupted。随后在conversation.item.input_audio_transcription.completed分支中已消费consumedInputItemIds的 itemId 的迟到最终转录会被当作stale_input忽略realtime-session.ts保证健康的调用不会被误判为协议违例。工具延续continuesResponse与响应权威类型工具声明的两个语义标记实时会话的工具声明由 RealtimeToolDefinition 描述除了 OpenAI 风格的 function schema 外还有两个本地语义标记capturesTranscript标记 handoff 型工具。模型调用它时会话捕获实时转录尾部供 orchestrator 打包进后端提示词并把该响应标记为委托使其不进入直接回答的转录收集。continuesResponse标记收据型工具其结果需要再生成一轮模型响应工具延续。异步 handoff 收据则不设置此标记因为其后的后端事件本身就是用户可见的结果不需要冗余确认。工具延续的触发链路工具调用的生命周期是response.function_call_arguments.done/response.output_item.done→dispatchFunctionCall()realtime-session.ts→ 调度器经onFunctionCall回调交给 orchestrator → 后端执行后经submitFunctionOutput()以收据形式回传 →sendFunctionCallOutput()发送conversation.item.create类型function_call_output。发送完工具输出后maybeRequestToolContinuation 检查该响应是否声明了continuesResponse记录在toolContinuationStates中若声明且语音世代未过期则发起authority: tool_continuation的response.createconst maybeRequestToolContinuation (responseId: string): void { if ([...pendingCalls.values()].some((call) call.responseId responseId)) { return; // 仍有未完成的调用不续接 } const continuation toolContinuationStates.get(responseId); if (!continuation) return; toolContinuationStates.delete(responseId); if (continuation.speechGeneration ! speechGeneration) return; // 语音世代过期 requestResponseCreate(tool_continuation, undefined, continuation.inputItemId, undefined, continuation.toolCapability); };注意两个细节权限投票也走工具延续权限确认工具声明continuesResponse后Live 只有在收据已投递之后才通过延续响应确认成功从而把语音已播放与权限已裁决解耦。不冗余确认异步收据handoff型工具的收据不触发延续后续后端事件本身就是用户可见的结果避免产生多余的播报。特殊工具remain_silent与未知工具REMAIN_SILENT_TOOL_NAME remain_silentrealtime-session.ts模型请求静默时客户端直接以空字符串作为输出提交不产生任何播报。未知工具配置中从未声明的工具被调用时客户端以错误收据回复Unknown tool: ...让模型能以语音恢复而不是永远等待一个不会有 handler 完成的调用realtime-session.ts。非直接响应禁止调工具若响应的工具能力toolCapability不是direct工具调用会被以RESPONSE_TOOL_REJECTION_OUTPUT拒绝This response is not authorized to call tools.防止后台注入的轮次越权执行工具。响应权威类型会话内部用 RealtimeResponseAuthority 标注每个响应的来源它是整个仲裁机制的可观测锚点type RealtimeResponseAuthority | direct // 用户话语的直接回答 | tool_continuation // 工具结果后的续接 | backend_speech // 后端请求的语音 | proactive // 主动推送 | proactive_repair;// 主动修复注入窗口与播放尾部管理后端事件回灌由 packages/qwen-live/src/orchestrator/injector.ts 负责。其头部注释明确了注入窗口injection window关闭的三个条件injector.ts用户正在说话VAD 进行中realtime 响应在途response in flightHost 播放已开始但尚未完成playback started but not completed。设计文档对注入窗口做了两处收紧窗口从speech_started一直关到输入提交input commit而不只是到speech_stopped。speech_stopped只表示用户停顿提交前仍有 ASR 尾巴此时注入仍可能与用户话语竞争。开始新语音会结束旧的播放静默间隔估计即使音频本身已经播完playbackCompleted一旦新语音开始旧的 quiet-gap 估计即作废。相关实现见 injector.tsplaybackInProgress false; playbackCompletedAt 0与QUIET_GAP_MS 800的静默间隔常量injector.ts。若语音在 Host 估计的播放尾部仍挂起时开始即使 provider 已发出response.done也必须清除该尾部估计。orchestrator 侧的playbackStarted/playbackCompleted收据packages/qwen-live/src/orchestrator/live-session.ts配合 Host 播放协议提供这一判定依据playbackSuppressed标记用于忽略被显式静音清除的输出收据。注入内容本身遵循 spoken/detail 分离每个 item 都以静默上下文注入模型可据此回答追问语音价值的 item 额外触发一段简短逐字播报MAX_SPOKEN_CHARS 280、MAX_CONTEXT_CHARS 6_000。权限类 itemkind: permission通过requestId让远端裁决可以撤回已排队的语音询问injector.ts。权限恢复daemon-session 作用域的 PermissionBroker生命周期不匹配的根源权限请求的生命周期横跨两层ACP/后端会话长生命周期与Live 调用短生命周期可被重启。旧的实现把权限状态绑定在单次 Live call 内于是出现语音询问被注入并播报但用户没回应、或 Live 调用被重启后端请求仍挂着——投递语音 ≠ 解决权限。Broker 的设计要点packages/qwen-live/src/permissions/permission-broker.ts 在文件头注释中明确了两个设计点permission-broker.ts始终允许allow always绝不作为持久授权到达后端。协议投票永远是一次性 allow常驻规则standing rule保存在 Broker 本地带 TTL通过静默自动回答相似请求来生效。在其他地方如 WebShell解决的请求会撤回已排队的语音询问。具体机制包括作用域化 requestIdscopedRequestId(backend, requestId)将适配器局部的 requestId 与后端句柄绑定避免两个 Live 后端撞 key 导致误撤回permission-broker.ts。常驻规则参数DEFAULT_RULE_TTL_MS 30 * 60_00030 分钟 TTL、MAX_RULES 64permission-broker.ts。规则键 工具名 完整规范化 detailtrim 后折叠空白但保留大小写只按第一个冒号分割保证始终允许只覆盖用户听到并批准过的精确命令。挂起权限模型PendingPermission 携带requestHandle供状态工具引用、requestId、backend、sessionHandle、jobRef后端作业引用、title人类可读标题、options与createdAt。重连、重放与幂等Broker 的所有权在 daemon-session 作用域orchestrator 在 live-session.ts 构造PermissionBroker。当新的 Live call 启动时重建事件泵为之前观察过的每个后端会话重新连接事件泵排空缓冲的 ACP 事件只问仍需用户裁决的决策进行中的常驻规则投票不重复询问幂等重放重放的权限事件通过 PermissionAskEvent 的alreadyPending标记识别——若该 ask 已在等待投票则不重复提问。这样重订阅永远不会把同一个未解决问题问两遍。可重放的询问、提醒去重与撤回未解决的权限询问保持可重放用户语音打断输出时未决询问被重新入队直接响应未能成功投出投票时同样重新入队排队的提醒reminder去重避免同一问题重复播报本地或外部裁决如 WebShell 侧的request.action permission都会撤回已排队的语音询问orchestrator 通过broker.resolveHandle(request.requestHandle)解析并撤回见 live-session.ts。作业引用与状态报告权限事件保留后端作业引用jobRef会话级状态可以报告任何挂起的投票但作业专属监控只有在投票属于该确切作业时才报告。状态工具从waiting_for_permission状态live-session.ts、live-session.ts中携带请求句柄与人类可读标题供上层回答该问题。orchestrator 在 Live 调用重启后通过重放待决 ask 恢复权限中继源码注释 Suppress asks until buffered backend events have drained on resume 表明恢复时先抑制询问、待缓冲事件排空后再重放。语音契约与内部句柄脱敏内部句柄requestHandle、jobRef 等保留在模型上下文与工具收据中它们是状态工具回答权限问题所必需的但设计同时要求强化语音契约与主动摘要使这些句柄不被朗读出来。即句柄可以进入模型上下文供其引用但注入语音时必须过滤或改写避免把perm-1、UUID 之类内部标识念给用户听。直接转录去重与中断保留直接回答direct response的完整转录按responseId/inputItemId去重collectedDirectResponseIds/collectedDirectInputItemIds集合避免同一轮对话被重复收集与此同时响应被中断前已经听到的部分输出必须保留——因为被中断的响应可能永远收不到规范的response.done回调其部分输出partial output仍应进入对话记录见collectDialogueResponse(responseId, true)在打断路径中的调用realtime-session.ts。验证矩阵测试覆盖与回归清单设计文档列出了明确的验证要求源码仓库中均有对应落点Realtime 单元测试packages/qwen-live/src/realtime/realtime-session.test.ts覆盖手动直接响应仲裁、单工具与多工具调用、provider 的两种输入提交形式conversation.item.created与input_audio_buffer.committed、选择性延续与合并式延续、pre-ack 替换窗口、过期延续、重复语音取代排队输入、未知工具、remain_silent。Orchestrator 集成测试packages/qwen-live/src/orchestrator/live-session.test.ts覆盖waiting_for_permission挂起状态如第 6326 行附近的状态断言、调用重启、重启后的权限中继、输入提交注入门控、播放尾部打断、日志不重复、无句柄的口头完成。工具延续的集成场景同样有覆盖如第 2046 行附近tool_continuation权威类型断言以及延迟修复通过排队工具延续并在新语音到达时丢弃的用例。发布前流程先运行 qwen-live 包测试、构建与类型检查再使用qwen-acp与qodercli两个适配器做人工 Host 重测。小结Qwen Live 的工具延续与权限恢复机制本质上是一套以用户语音为最高优先级的实时对话仲裁协议关闭 provider 自动响应客户端在输入提交后只创建一个直接响应后端结果以合并 item 形式折叠其中通过 pre-ack 替换窗口、response.done后才释放槽位、语音世代speechGeneration过期判定消除创建续接与用户说话之间的竞态continuesResponse让收据型工具含权限投票在输出投递后获得确认性延续而异步 handoff 收据不做冗余播报注入窗口从speech_started持续关闭到输入提交并清除过期的播放尾部估计权限 Broker 提升到 daemon-session 作用域通过重连事件泵、幂等重放、可重放询问与去重撤回让权限裁决的可靠性不再依赖单次 Live 调用的存活时间。这套设计对应的全部实现细节均可回溯到 packages/qwen-live/src/realtime/realtime-session.ts、packages/qwen-live/src/orchestrator/injector.ts、packages/qwen-live/src/orchestrator/live-session.ts 与 packages/qwen-live/src/permissions/permission-broker.ts是理解 Qwen Live 语音链路并发模型的重要入口。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考