接进现有客服系统前——Qwen-Audio-Agent 的会话入口要自己包一层

文章导读
把 Qwen-Audio-Agent 接进已有的工单与客服系统时,需要先分清哪些能力由框架提供、哪些必须自己写。通常,框架负责一轮会话中的模型推理和音频流转;而“这次对话属于哪张工单、哪个坐席、哪次来电”的编号,“两端音频编码能不能对上”,“连接中途断掉之后这通对话怎么收尾”这三件事,业务系统和框架之间没有现成协议承载,需要自建一层薄适配。判断标准也简单:凡是需要在工单页面看到、需要写进库、需要事
📋 目录
  1. 一 先定业务系统用什么标识一次会话
  2. 二 在边界处把音频编码格式统一
  3. 三 写一层薄适配把会话标识传给框架
  4. 四 断流和超时时给出可验证的回退行为
  5. 五 用一段录音回放验证整条链路
A A

把 Qwen-Audio-Agent 接进已有的工单与客服系统时,需要先分清哪些能力由框架提供、哪些必须自己写。通常,框架负责一轮会话中的模型推理和音频流转;而“这次对话属于哪张工单、哪个坐席、哪次来电”的编号,“两端音频编码能不能对上”,“连接中途断掉之后这通对话怎么收尾”这三件事,业务系统和框架之间没有现成协议承载,需要自建一层薄适配。判断标准也简单:凡是需要在工单页面看到、需要写进库、需要事后翻日志复盘的,都归适配层;框架不负责这些。

接入前先用三个问题自检:一次会话的编号由谁生成、在哪些环节透传、日志里能否一眼对上;业务侧录音与框架输入是否同为单声道固定采样率的 PCM;网络抖动或上游静默超时时,会话是卡住还是能按约定收尾。三项都能通过配置、日志或回放命令验证,再谈联调;否则联调阶段排查成本会明显放大。适配层建议写薄,只做标识、格式和回退,不把业务规则塞进去。

先定业务系统用什么标识一次会话

标识来源建议由业务系统产生,不要等框架返回 ID 后再回填。常见做法是“工单号 + 会话序号”,例如 ticket-8842-s03,或者一个自增 session_id 加一个 trace_id 用于跨服务串联。这样,一次转接、一次重连,都能在同一张工单下区分成不同会话,不会互相覆盖。

透传路径通常有三个位置可选:网关或 BFF 层统一注入 HTTP 头,例如 X-Session-Id;放进建连消息的元数据字段;随每一帧音频的分包消息携带。一般建议前两个位置必带、第三个位置可选冗余,因为音频分包频率高,每帧都塞长字符串会占带宽。需要结合环境确认网关是否会把自定义头剥掉,很多反向代理默认只透传已知头。

核对方式很直接:在适配层收到请求的第一行打一条日志,在和框架完成绑定后再打一条,在会话结束时打第三条。三条日志里的 session_id 必须逐字相同,否则说明中间有环节丢失。

# 收到上游请求时
log.info("session_in", session_id=sid, trace_id=trace, ticket=tk)
# 完成与框架绑定后
log.info("session_bound", session_id=sid, upstream=sid, backend_ref=ref)
# 会话收尾时
log.info("session_close", session_id=sid, reason=reason, dur_ms=elapsed)

在边界处把音频编码格式统一

两端格式不一致,表现出来往往不是报错,而是杂音、变速、只有半边声道或者解码直接失败,排查起来容易误判成模型问题。接入前先把业务侧录音和框架输入格式都查出来。

  • 业务侧文件:ffprobe -v error -show_streams -select_streams a:0 rec.wav,看 codec_name、sample_rate、channels、sample_fmt。
  • 流式来源:sox `--i` rec.wav 或 file rec.wav,快速看容器、采样率、位深。
  • 框架侧:先查配置项或接入说明里声明的输入格式;看不到就以单声道、16kHz、16bit PCM 这类通用格式做保守假设,再用一段短音频试通。

转换步骤建议固化成一条命令或一个函数,不要临时手敲:

ffmpeg -i input.wav -ac 1 -ar 16000 -c:a pcm_s16le -f s16le output.pcm
ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le -f wav output.wav

转换后必须回放验证,不能只看文件生成成功:ffplay -f s16le -ar 16000 -ac 1 output.pcm,或 aplay -f S16_LE -r 16000 -c 1 output.pcm。听人声是否自然、有没有咔哒声、总时长与原文件是否接近。时长偏差明显,通常是采样率参数写错。

写一层薄适配把会话标识传给框架

适配层的作用是解耦:业务系统只认自己的工单模型,框架只认自己的入参,中间由适配层做翻译和兜底。下面是一个不依赖任何未公开接口的通用骨架,参数名按实际 SDK 替换即可。

接进现有客服系统前——Qwen-Audio-Agent 的会话入口要自己包一层
class SessionAdapter:
    def __init__(self, backend, cfg):
        self.backend = backend          # 框架客户端,任意实现
        self.cfg = cfg                  # 采样率、声道、超时等配置

    def open(self, req):
        sid = req.session_id or make_sid(req.ticket_id)
        ctx = {
            "session_id": sid,          # 业务侧可读编号
            "trace_id": req.trace_id,   # 跨服务排查用
            "format": self.cfg["audio"],# 输入格式声明
            "meta": {"ticket": req.ticket_id, "agent": req.agent_id},
        }
        self.h = self.backend.connect(ctx)
        log.info("session_bound", session_id=sid, backend_ref=ctx["session_id"])
        return sid

    def on_audio(self, sid, chunk):
        chunk = self.normalize(chunk)   # 采样率/声道对齐
        self.stats[sid]["bytes_in"] += len(chunk)
        self.h.send(chunk)

    def close(self, sid, reason):
        self.h.close(reason=reason)
        log.info("session_close", session_id=sid, reason=reason)

参数上有几点要注意:session_id 一旦生成就不要在会话中途变更,重连时沿用同一编号,另起序号表示新会话;format 建议显式声明而不是依赖框架推断,避免两端默认值不同;normalize 里做重采样和声道合并,不要在这层做降噪或增益,那是业务侧音频处理的事。

断流和超时时给出可验证的回退行为

让一次网络抖动不至于卡死整通对话,关键是提前定义“什么算断”和“断了做什么”,并且这两件事都要能在日志里看到。

触发条件通常有几类:上游连续一段时间没有音频数据、收到的音频持续为静音且超过阈值、连接层心跳丢失、单轮响应等待超时。阈值按业务场景定,可以先取保守值再压测调整,不要照搬别人的数字。

# 伪代码:每个会话一个监控循环
while session.alive:
    if now() - session.last_audio_ts > T_SILENCE:
        fire(session, reason="upstream_silent")
    elif now() - session.last_frame_ack > T_ACK:
        fire(session, reason="backend_stall")
    elif session.turn_start and now() - session.turn_start > T_TURN:
        fire(session, reason="turn_timeout")

fire(session, reason):
    session.state = "fallback"
    notify_business(session.sid, reason)      # 坐席端提示或占位
    session.half_close()                      # 先停写入,等尾部数据
    session.flush_remaining(T_DRAIN)
    session.close(reason=reason)
    log.warn("session_fallback", session_id=session.sid,
             reason=reason, bytes_in=session.bytes_in,
             elapsed_ms=session.elapsed, state=session.state)

回退动作建议按顺序:先把半截结果落库并标记为不完整,再通知业务侧(坐席端弹提示、工单上写一条事件),最后释放连接。不要静默重连并复用同一个会话编号,那样事后无法区分正常结束和异常收尾。日志里至少固定输出 session_id、state、reason、bytes_in、elapsed_ms 五个字段,用 grep session_fallback 就能拉出全部异常会话。

用一段录音回放验证整条链路

联调最后一步不要只靠实时说话验证,用一段固定录音按实时节奏推流,结果可复现,也方便对比两次改动。

# 转成适配层的输入格式后,按 20ms 一包推给本地适配服务
ffmpeg -re -i sample.wav -ac 1 -ar 16000 -f s16le - | \
  python tools/push_chunks.py `--endpoint` localhost:8080 `--session` ticket-8842-s03 `--chunk-ms` 20

预期日志序列应当是:session_in → session_bound → fmt_normalized → 若干 chunk_in → session_close。其中 fmt_normalized 打印的实际采样率和声道要与配置一致;chunk_in 累计字节数应接近录音时长乘以每秒字节数,差太多说明适配层吞了音频或者分包参数写错。

业务侧的结果也要看:工单页面是否出现这次会话记录、时长是否与录音接近、坐席端是否收到完整回复而不是半句。三处都对上,说明标识透传、格式统一、回退分支都没有把数据吃掉。如果日志齐但业务侧看不到记录,问题通常在落库那一步,不在适配层。