Skip to content

feat(intelligence): 接入端到端实时多模态模型(只读查询 + 语音追问) #197

Description

@LUPENGHAN

动机 / 用户故事

  • 说完就想听见回应。 用户说「明天下午三点在 203 开会」,希望立刻听到助手确认,而不是盯着屏幕等。
  • 一句话说不全的事,希望助手会问。 用户说「那个会几点开始」,助手不该猜一条,该问「是今天的早会还是明天的项目周会」。
  • 补一句就该接着办。 上一句被问了,用户只答「周会」,助手要知道这是在回答刚才那个问题,不是新请求。
  • 团队要在两条路之间做选择。 级联(ASR→LLM→TTS,实现 ASR、LLM Agent 与 TTS 流式处理能力 #183)和端到端两条链路都在做,需要在同一套协议下跑出真实延迟数字,而不是凭供应商宣传。

共同需求(一句话):让语音链路真的能听、能说、能反问、能记住上一句——并把它跑在自己的协议上,数字才可比。

目标用户

App 使用者(语音交互的直接体验者);项目团队(做技术选型的依据)。

现有做法及其不足

#172 建好了传输层,#196 建好了 AgentPort / ResultSink 的一进五出边界。但边界后面挂的是 FakeAgent——它回一条固定结果,不转写、不理解、不出音频。

现状是:管子通了,里面没有智能,也听不到声音。 用供应商 playground 手工试能看到单点效果,但不过本项目的协议与端口,测不出集成后的真实延迟,也验证不了 function calling 在我们的工具形状下够不够可靠。

本期范围与明确不做

本期做

# 内容
1 实时模型会话的端口 + 千问 Qwen-Audio 适配器(音频进、文本与音频流式出)
2 把模型接到 #196 的五个出口上,跑通「说话 → 听见回应」
3 Function Call 查日程(按时间范围 / 标题 / 地点筛选)
4 语音追问:模型信息不足时反问,并在下一轮认出用户的回答

明确不做

不做 为什么
写操作(创建 / 修改 / 删除日程) 需要架构设计 §5.7 的二次确认落点,且 ScheduleAgentService 五个方法目前全是 NotImplementedError)。
错误出口 模型失败时客户端收不到任何东西(只记服务端日志)。已知缺陷,本轮按「假设成功」推进,单独一个 PR 补。
ASR 部分转写(「你在说……」实时上屏) 纯体验优化。且实测转写可能出错而回答正确,提前显示错的转写可能更糟
重连与退避 网络抖动即整轮失败,属容错那一轮。
打断 push-to-talk 下打断该由我们做,但架构设计 §5 没定义打断消息,需先定协议。优先跑完全部功能
真实日程数据 用种子数据(8 条相对今天生成)。查询条件是真的在过滤,所以能验证模型有没有把「明天」理解对。换成真 service 是改一行

关键决策与依据

一、只读先行

#166@gac0812)分析了用实时多模态模型替换级联链路的可行性,建议「先完成只读工具 POC,通过验收后灰度替换,旧链路保留为降级通道」。本轮照此执行。

二、模型型号

候选 取舍
qwen-audio-3.0-realtime-plus(采用) 阿里云百炼实时 API 文档列出的可用型号,能力最全
qwen-audio-3.0-realtime-flash 同上,对延迟与成本更敏感时可换。未实测,保留备选

三、push-to-talk,不让模型判断轮次

厂商支持自动轮次检测,但我们的协议已经用 voice.stream.start / end 界定了轮次。让模型也来判断等于两套机制打架。turn_detection 设为 null,且它只能在第一帧音频前设置。

后续可以使用,语音对话的形式作为新的 PR 提交。

四、会话按 conversation_id 复用(追问能成立的前提)

模型的上下文存在厂商会话里(50 轮 / 300 秒上限)。若每轮开新会话,追问问出去了、用户答「周会」,落进一个从没问过问题的会话里。

协议本来就是这么设计的(架构设计 §5.4:客户端「直接再次发送 voice.stream.start,并携带上一次的 conversation_id」),且 VoiceStreamStartPayload.conversation_id 早就存在,网关层一行不用改

这是本轮第一个跨请求持有状态的组件,四件事都要处理:

做法
300 秒过期 到 240 秒就换新会话——厂商到点直接丢,表现为「这一轮什么都没有」。提前换会丢记忆,但丢记忆是差答案,静默是没答案
50 轮上限 到 40 轮就换
失败 / 断线 直接丢弃不复用——半句话的状态没人推理过
并发 每个 conversation 一把锁。会话是 append→commit→respond 的序列,两轮叠上去会把一个人的音频折进另一个人的回答

外加空闲会话主动回收:用户拿到答案就放下手机,那条会话没有下一轮,会一直占着一个到厂商的连接。

五、追问用工具声明,不靠猜文本

模型什么时候在「问」而不是在「答」?从文本判断不可靠。给它一个 ask_user 工具,四个参数正好是 §5.4 payload 的四个字段,复用已跑通的 Function Call 机制。两处服务端把关:

  • question_kind 不在四个枚举里就拒掉——转发模型自己编的值,等于把客户端没有分支的枚举放上线
  • 提问的音频 purpose 标成 dialogue_question,客户端才知道这轮不是结束、可能要继续开麦

六、分层:适配器不许 import 对话层

架构测试 A.4 禁止 infrastructure 依赖任何产品层。所以适配器自己声明一个同形状的 Observer 协议,对话层也自己声明 TurnObserver,两边互不 import。这一条是被架构测试拦出来的——初版适配器直接 import 了 timeflow.intelligence.realtime_ports,测试报错后重构。

基本概念与信息结构

音频 ──► AgentPort(#196)──► RealtimeAgent ──► 厂商会话(按 conversation_id 复用)
                                   ▲                    │
                                   │                    ▼
                             ResultSink(#196)◄── 五种模型事件

厂商事件到五个出口是五进四出,不是一对一

厂商事件 走哪个出口 出去的消息
用户转写完成 deliver_transcript voice.asr.completed
回复文字增量 deliver_reply_text voice.dialogue.reply
工具调用完成 → find_schedules deliver_result voice.command.result
工具调用完成 → ask_user deliver_question voice.dialogue.question
回复音频增量 deliver_audio voice.tts.start → 帧 → end
失败 无出口(见「明确不做」)

新增两处目录,与级联方案(#189 已用 intelligence/conversation/)不重叠:

  • intelligence/realtime/ —— 端到端方案的对话层实现
  • infrastructure/external/realtime/ —— 厂商适配器

验收标准

以下均已在真机跑通,作为验收的具体例子:

一、能听见回应
说「明天有什么安排」,依次收到 voice.asr.completedvoice.command.resultvoice.tts.start → 音频帧 → voice.tts.end,并能听到中文回答。首帧音频延迟:直连模型 1089ms,经我们的 WS 1329ms,带一次工具调用 2339ms。

二、查询条件真的在过滤
说「明天有什么安排」只返回明天那条,不是全部 8 条。问「203 有什么会」能匹配到存储为「203」的日程(地点双向包含)。

三、会反问,且候选可渲染
说「那个会几点开始」→ 模型先查(voice.command.result 2 条)→ 再问(voice.dialogue.questionquestion_kind=ambiguous_targetcandidates 带 2 条),口播「您说的是今天的早会,还是明天的项目周会?」,音频 purpose=dialogue_question

四、记得上一句
接上一轮,只说「周会」(带同一个 conversation_id)→ 模型答「明天的项目周会下午三点开始」,且不再查库——从会话上下文直接答出来,说明跨轮记忆生效。

五、时间与时区正确
模型知道今天是哪天(当前本地时间注入 instructions,每轮重建)。协议消息里时间一律 UTC;只有给模型看的 JSON 额外附一个本地时刻字符串——否则它会把 01:30:00Z 念成「凌晨一点半」。

PR 拆分

两个 PR,按开发流程递进,每个都能独立评审。PR 合并后在此勾选,本 Issue 的进度一眼可见:

原计划拆四个,合成两个:端口的消费者是 RealtimeAgent,分开提的话第一个 PR 里的端口文件没有任何调用方;而追问本身就是一个工具,和查询住在同一个 ToolBox 里,拆开等于刚交付就打开重改。

两项全部勾选即本 Issue 可关闭。补错误出口作为后续独立 PR,不并入本 Issue。

Metadata

Metadata

Assignees

No one assigned

    Labels

    FullSpec完整规格提案:影响面较大,需要写清楚动机、范围、不做、备选方案、接口/数据结构、原型、验收标准proposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions