图片加文字提问和语音连续对话被塞进同一套调用逻辑里,最先坏掉的通常不是模型能力,而是参数:图文链路需要一次把图和多轮文本拼好、等一个完整回复;语音链路需要边收音频边出字、随时准备被打断。把两者的超时、重试、上下文裁剪和流式处理混在一起,常见结果是语音被长超时拖住、图文被短超时切断。
同一模型可以同时承载图文问答和实时语音,但两条链路的输入形态、等待容忍度和失败代价不同,建议拆成两套调用逻辑:图文按整包请求、可整段返回,语音按分片流式、按轮次管理上下文。判断方式是自己分别记录耗时分解和轮次节奏,用日志与页面行为验证后再定参;不要用一套超时、重试和上下文裁剪策略同时管两边,这是最常见的参数漂移来源。
先写下两条链路各自要满足的输入形态和响应节奏
先把两边的需求写成能观测的指标,再决定调用方式;写不出来,说明场景还没分清。
图文链路
- 输入构成:一次请求里的图片数量、单图分辨率与体积、图片以链接还是 base64 传入、配套文本长度、是否多图对比。这些都能在请求日志里数出来。
- 可接受的等待时间:用户点发送到看到第一个字的时间,以及看到完整答案的时间。图文问答通常是整段阅读,首字稍晚可以接受,但整体拖太久就要给进度提示。
- 失败代价:重发一次成本低,用户能接受稍后重试。
语音链路
- 输入构成:音频采样率与声道、每帧或每包的时长、静音检测放在客户端还是服务端、单轮最长录音时长。
- 轮次节奏:用户停止说话到系统开始出声的间隔、系统说话时被打断能否立刻停、连续多轮之间保留多少轮上下文。这几个指标在客户端埋点里都能拿到。
- 失败代价:一次没接住,用户会重复说一遍,体验损失比图文大,所以语音更依赖短超时和快速降级。
对照两条链路的请求字段与返回处理方式
同一模型下,两类请求的结构差异主要在模态字段、流式开关和会话管理,其余多数是共通的。下表用于对齐你实际用到的字段,具体字段名以你对接的接口定义为准,未确认的一律按待核实处理,不要凭印象写死在代码里。
| 字段或能力 | 图文问答链路 | 实时语音链路 | 备注 |
|---|---|---|---|
| 模型标识、鉴权头 | 需要 | 需要 | 两条链路都要,建议放同一份配置 |
| 消息或内容数组 | 文本块加图片块 | 文本块加音频块,或走独立音频通道 | 结构相近,模态块不同 |
| 流式开关 | 可开可关,整段返回更省事 | 基本必须开启 | 语音靠流式才能边收边出 |
| 图片分辨率、体积上限、压缩方式 | 需要 | 不涉及 | 仅图文 |
| 音频采样率、声道、编码 | 不涉及 | 需要 | 仅语音 |
| 音频分片或帧长 | 不涉及 | 需要 | 直接影响首帧延迟 |
| 静音检测或断句方式 | 不涉及 | 需要 | 在客户端还是服务端做,需按实际实现确认 |
| 打断控制 | 不涉及 | 需要 | 仅语音 |
| 输出长度上限、随机性参数 | 需要 | 需要 | 语音一般取更短的输出 |
| 会话标识 | 可选 | 建议必需 | 语音多轮靠它串上下文 |
| 超时与重试 | 需要 | 需要 | 值不同,语义也不同 |
| 具体字段名、是否支持某模态 | 待核实 | 待核实 | 以你实际对接的接口与返回为准 |
落到代码上,两条链路最好共用同一个请求封装,只把差异字段当入参传进去,避免两套拼装逻辑各自演化。
{
"chain": "vision_qa 或 voice_realtime",
"model": "同一模型名",
"messages": [],
"stream": false,
"timeout_ms": 20000,
"trace_id": "用于日志回捞"
}校验时按 chain 分支:图文请求缺图片就报错,语音请求缺音频格式或采样率就报错,不做隐式兜底。
分别测一次端到端耗时并记录采样点
不要靠感觉慢去选链路。把一次请求拆成四段记录:上传(客户端到服务端的图片或音频传输)、排队(服务端开始处理的等待)、生成(首字或首帧到结束)、返回(下行传输与渲染)。四段相加与总耗时的差值,就是网络和客户端开销,值得单独看一眼。
下表是记录模板。同一链路至少各测三次,尽量保证输入大小接近、网络环境一致,否则结论不成立。空格按实际日志填写,不要用估计值。
| 链路与分段 | 第 1 次 | 第 2 次 | 第 3 次 | 记录位置 |
|---|---|---|---|---|
| 图文 · 上传 | 待填 | 待填 | 待填 | 客户端发包到服务端收全 |
| 图文 · 排队 | 待填 | 待填 | 待填 | 服务端入队到开始推理 |
| 图文 · 生成 | 待填 | 待填 | 待填 | 首字到结束 |
| 图文 · 总耗时 | 待填 | 待填 | 待填 | 用户点击到看到完整答案 |
| 语音 · 首帧延迟 | 待填 | 待填 | 待填 | 用户停说到系统出声 |
| 语音 · 单轮总时长 | 待填 | 待填 | 待填 | 一轮开始到结束 |
| 语音 · 打断后重新出声 | 待填 | 待填 | 待填 | 打断事件到新回复首个音频帧 |
判断方式:图文卡在生成段,多半是输出太长,可以先限制输出长度或分批出结果;卡在排队段,再看并发或配额。语音单轮间隔里首帧耗时波动大,通常先查客户端采集与上传,而不是先调模型参数。这些结论都要用自己的采样点验证,别人的数据不能直接套用。
给失败场景各定一条降级路径
两条链路的降级动作不一样,不要共用一套重试逻辑。
图文链路的降级动作
- 超时后只重试一次,且只对幂等请求重试;仍失败则把图片降到更低分辨率,或提示改为单图重发。
- 仍不可用时退化为纯文本回答,并在页面上明确说明图片未参与分析,不要把文本答案伪装成看图结果。
- 用户可感知的变化:等待变长、可能只得到文字解释、需要重新上传图片。
语音链路的降级动作
- 单轮超时或流中断时不要整段重放,先让客户端保留这一轮音频,提示用户再说一次,由用户决定是否重说。
- 连续多次失败则切换为录一段发一次的半双工模式,牺牲打断能力换取可用性。
- 仍不可用时退到文字输入,并明确提示当前是文字模式。
- 用户可感知的变化:打断不再即时、轮次变慢、需要手动结束录音。
两条链路在服务端保留同一个请求标识,便于按 trace 回捞失败样本,验证降级触发条件是否合理。
把跨链路共用的部分抽成同一份配置
能共用的抽成一份,减少参数漂移;差异大的保持独立,别为统一而强行合并。
可共用的配置项
- 接入地址与鉴权方式,同一套密钥轮换策略。
- 日志字段:请求标识、链路标识、耗时分解字段、错误码。命名统一后两张表能拼到一起看。
- 超时的外层上限、重试次数上限、退避写法。值可以不同,结构保持一致。
- 开关与灰度:按用户或场景决定哪条链路生效。
- 图片与音频的隐私留存策略。
不建议共用的项及原因
- 音频采样率、编码、分片长度、静音检测阈值:语音专属,放进通用配置只会让图文链路读到无用项。
- 图片分辨率与压缩策略:只对图文有意义。
- 流式处理与打断逻辑:语音需要随时中断,图文按整段处理更简单,混在一起状态机会很难维护。
- 上下文裁剪策略:语音按轮次裁剪,图文按图片数量和文本长度裁剪,规则本就不同。
{
"shared": {
"endpoint": "统一入口",
"timeout_ms": 20000,
"max_retry": 1,
"log_fields": ["trace_id", "chain", "latency_total_ms"]
},
"vision_qa": { "image_max_side": 1536, "stream": false },
"voice_realtime": { "audio_sample_rate": 16000, "vad": "client", "barge_in": true }
}把这份配置放在网关或统一客户端里,两条链路各读自己的块;改超时或日志字段时只改 shared 一次,避免一条链路已改、另一条还在用旧值。合并配置后要再用两边的耗时采样点复测一次,确认没有把语音的首帧拖慢。