面试问题-01

  1. 介绍一下传音的实时AI对话调度平台?

它的职责是把一次实时对话中的音频输入、语义处理、Agent调用和结果输出组织成可执行流程。核心有三层:编排层使用DAG Task表达依赖和并行关系;通信层使用WebSocket和gRPC流承载音频与事件;状态与观测层把会话状态外置到Redis,并用Trace、Metrics、Log跟踪跨服务链路。

我参与的重点是Taskflow的节点与取消链路、WebSocket S2S和barge-in场景、Redis会话迁移联调以及关键节点Trace埋点。

TaskFlow 用 DAG 管理一轮语音对话全流程任务(收音频→ASR 识别→Agent 调用→TTS 合成→下发音频);取消链路是用户挂断、插话、报错、超时时,让所有正在运行的任务、网络流、RPC 请求被安全终止,释放 goroutine、连接、内存资源,不会出现僵尸协程、残留播报、脏会话数据。

实现:context.Context做信号透传

整个DAG任务树基于上下文父子继承关系传递取消信号:一次对话初始化根上下文rootCtx,rootCancel := context.WithCancel(context.Background()),整个会话所有任务都派生自此根ctx。

DAG每个Task结构体内置独立上下文:

type Task struct {

taskID string

parentCtx context.Context

ctx context.Context

cancel context.CancelFunc

dagDepends \[\]string

runFunc func(ctx context.Context) error

}

父任务ctx取消->所有子任务ctx.Done()管道统一收到关闭信号,天然实现级联取消。

额外搭配context.WithTimeout给每个任务执行超时,兜底卡死的任务

任务内部统一的取消监听范式

所有任务的业务逻辑必须包裹select监听ctx关闭信号

func runTask(ctx context.Context) error {

for {

select {

case <- ctx.Done():

return ctx.Err()

case data := <-inputChan:

process(data)

}

}

}

DAG串行/并行任务的取消差异

串行节点:前序任务取消,后续任务不在调度执行

并行节点:父ctx取消,所有并行goroutine同时感知退出

资源兜底回收规则

所有cancel()函数必须搭配defer执行:任务正常结束、异常崩溃、被取消三种场景都调用cancel,杜绝goroutine泄露,长连接hanging

WebSocket S2S:各个后端服务之间(网关、ASR 服务、TTS 服务、编排服务)通过 WebSocket 传输二进制音频流 + 事件指令; barge-in:机器人正在播放语音回复时,用户中途说话,系统立刻停止播报、重置对话流程、接收用户新语音(语音打断)。

WebSocket S2S服务间音频流实现

基于gorilla/websocket库搭建服务端双向WS长连接,各个微服务之间建立点对点长流,区别于客户端对接网关的C2S WS

自定义二进制封装包格式:帧类型(音频帧/事件帧)+ 序列号 + 载荷数据,音频分片流式传输,附带时序编号防止乱序

每条WS读写goroutine绑定独立的ctx,复用TaskFlow的全局取消上下文:ctx取消时主动调用conn.Close()关闭ws连接,读写循环立刻退出;

增加重连、丢包检测、缓冲区限流逻辑,防止音频堆积造成延迟飙升

barge-in 整体链路与Go实现

ASR服务实时人声检测,识别到用户发色后,组装barge-in事件包,经由S2S WS发送给TASKFlow编排服务;

给编排服务解析到打断事件,主动调用会话顶层的rootCancel()触发整条流程的取消链路;

TTS播报任务被ctx信号终止,立刻关闭下发音频的WS流,清空待发送的语音缓存

增加防抖机制:过滤环境噪音、短促呼吸声,通过时间窗口过滤无效打断事件

会话所有运行数据(对话上下文、播报进度、打断开关、任务运行状态、Trace 信息)不存服务本地内存,全部持久化到 Redis 集群;多台服务实例负载均衡切换、机器故障重启时,新接管的实例从 Redis 读取会话数据,对话无感续上,这个过程就是会话迁移;联调就是打通编排、WS、语音服务、Redis 整套流程,解决切换后的各类 bug。

会话数据使用Protobuf序列化后存入Redis,以session:{sessionID}作为唯一Key,配置过期时间自动清理僵尸会话

封装Redis统一操作SDK:会话创建、状态更新(运行/打断/取消/结束)、会话读取、会话销毁方法;所有状态变更原子写入Redis;

会话迁移机制:客户端TCP连接被LB转发到其他服务实例,新实例拿到sessionID后第一时间拉去Redis完整会话数据,重新初始化TaskFlow流程、恢复WS流状态

高频更新场景使用Redis Hash结构存储细分字段,减少整段数据覆盖带来的性能损耗,并发更新场景使用Redis原子命令/分布式锁解决数据覆盖问题

一次对话横跨网关、TaskFlow 编排、ASR、Agent 大模型、TTS、Redis 多个服务,出现卡顿、超时、打断失败、取消不生效等线上问题时,依靠 Trace 链路追踪每个步骤的耗时、报错、执行状态,精准定位故障点;埋点就是在代码关键位置插入上报逻辑,采集链路数据。

基于OpenTelemetry做链路追踪,一次会话生成全局唯一TraceID,所有子任务、网络请求生成独立SpaceID,Trace ID全程放在context透传给所有服务

分层创建Span:

顶层Span:一次完整对话会话

子Span:每个DAG Task执行Span、WS收发数据Span、barge-in事件Span、Redis读写Span、任务取消Span

每个Span记录核心信息:执行耗时、执行结果(成功/取消/失败)、错误堆栈、关键参数、取消触发原因

配套Metrics指标打点:任务取消率、barge-in打断成功率、各节点平均耗时,结合结构化日志(携带Trace ID)做问题检索

相关推荐
朱容zr3331331 小时前
为什么推荐使用自增主键?使用UUID作为主键的优缺点是什么?
java·运维·数据库·后端·mysql·面试·性能优化
李剑一2 小时前
再见前端,你好AI
面试·求职
HeiSenBerg2 小时前
Android IPC 深度解析:Binder 机制与 AIDL 实战
面试
城管不管2 小时前
重生——第五次面试2026.8.1一面
java·数据库·后端·ai·面试·职场和发展·agent
代码代码快快显灵3 小时前
MYSQL—DAY1
面试·职场和发展
kyriewen17 小时前
面试官让我手写虚拟列表——AI生成的版本,快速滚动几下就白屏了
前端·javascript·面试
Revolution6118 小时前
React 组件重新渲染时,到底重新执行了什么
前端·react.js·面试
Revolution6120 小时前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
进击的程序猿~20 小时前
Go 并发底层原理面试学习指南
开发语言·面试·golang