AI面试之全双工语音交互项目(理论+实战)
项目介绍与主要职责:
项目名称:VoiceNexus - 企业级智能语音助手平台项目
周期:2024.03 - 至今项目角色:核心研发 / 技术负责人
项目描述:面向 B 端客户的全双工 AI 语音客服平台,集成 ASR/LLM/TTS 三大能力,支持实时语音打断与多轮对话。日均处理 50 万+ 通语音会话,端到端 P99 延迟 < 800ms,降低人工成本 65%,线索转化率提升 23%。
核心职责:
- 负责 WebSocket 实时通信层架构设计,基于 Netty 实现高并发长连接服务,单节点支撑 5000+ 并发,连接成功率 99.97%,通过线程池隔离和对象池复用将 Full GC 降至 0。
- 主导 RAG 增强问答系统建设,使用 Spring AI + Milvus 构建混合检索 Pipeline,结合 Rerank 精排,知识库问答准确率从 72% 提升至 91.3%,幻觉率降至 1.5%。
- 设计基于 RocketMQ 的异步解耦架构,将音频存储、线索评分等非核心操作异步化,主链路 P99 从 2.1s降至 780ms;使用 Redis Hash 管理多轮会话上下文,支持滑动窗口与自动过期。
- 构建 LLM 多层容错体系:Sentinel 熔断 + 输出三层校验 + 兜底话术库,系统 SLA 从 98.5% 提升至99.92%,用户无响应感知率降至 0.15%。
- 建设全链路可观测性,接入 SkyWalking 实现端到端 Tracing,通过 Streaming + 分句合成优化,首句响应从 600ms 降至 280ms。
本期换个角度去讲解,之前都是在讲理论,今天讲代码,层层递进,接口层、service、数据库交互、大模型交互,和为什么这么写,以及遇到一些问题怎么解决。
(WebSocket全双工语音交互时序图,请保存好时序图)
一、API接口层(重点)
聊天对话API:VoiceController
接口:创建会话、结束会话、获取会话状态、文本对话(非WebSocket)、获取系统状态、健康检查
1、创建会话接口:createSession
提问:为什么不直接连接WebSocket?
回答:因为客户端要先获取sessionId,才能建立有状态的连接
提问:为什么返回wsUrl?
回答:支持动态路由,生产环境可能有多个WebSocket节点
提问:为什么userId/tenantId可选?
回答:支持匿名用户咨询,后续可关联
2、结束会话:endSession
设计理由:
触发收尾逻辑:发送线索评分消息、保存对话记录
资源释放:清理Redis缓存、断开WebSocket连接
提问:为什么不自动结束?
回答:因为用户可能断网重连,需要显式结束才能触发业务逻辑(兜底策略是,几分钟无对话,自动断开连接)
3、获取会话状态:getSessionStatus
设计理由:
前端展示:显示对话轮次,会话时长
断线重连:判断会话是否还存在,否则要重新创建
运维排查:定位用户会话状态(DLE、LISTENING、PROCESSING、SPEAKING)
4、系统状态与健康检查
设计理由:
实施容量监控,是否需要扩容。Grafana面板,定期拉取数据生成监控图表。
K8s/Nginx探活,负载均衡定期调用,判断节点是否存活。快速响应,不做任何业务逻辑,直接返回OK。
二、Service层(模型交互)
完整链路:RAG 检索 -> LLM 生成 -> 流式返回
1、获取上下文与状态更新
// 从 Redis 获取会话(包含历史对话) SessionContext context = sessionManager.getSession(sessionId); // 更新状态为 PROCESSING,前端可显示"正在思考..."(流式对话典型回复) sessionManager.updateSessionState(sessionId, SessionContext.SessionState.PROCESSING); // 把用户这句话加到对话历史(滑动窗口保留最近 10 轮) sessionManager.addConversationMessage(sessionId, ConversationMessage.user(userInput));
2、RAG检索
// 调用 RAG 服务检索知识库
RagResult ragResult = ragService.retrieve(
RagRequest.of(userInput, VoiceConstants.RAG_TOP_K) // TOP_K = 5
);
// 获取合并后的上下文(带来源标注)
String ragContext = ragResult.isEmpty() ? "" : ragResult.getCombinedContextWithSource();
3、构建LLM请求
LlmRequest request = new LlmRequest()
.setUserInput(userInput) // 用户问题
.setContext(ragContext) // RAG 检索结果
.setTemperature(0.7) // 控制创造性
.setStream(true); // 开启流式
// 添加系统提示词
request.setSystemPrompt("你是一个专业客服...");
// 添加历史对话(多轮上下文)
for (ConversationMessage msg : context.getConversationHistory()) {
request.addMessage(msg.getRole(), msg.getContent());
}
4、流式调用与实时推送
// 使用 Reactor 流式调用 LLM
Flux<String> responseFlux = llmRouter.chatStream(llmRequest);
responseFlux.subscribe(
token -> {
// 1. 打断检测:用户说话了,立即停止生成
if (sessionManager.isInterrupted(sessionId)) return;
// 2. 累加响应
fullResponse.append(token);
// 3. 实时推送给前端(打字机效果)
sendLlmResult(sessionId, token, false);
// 4. 分句触发 TTS(每 20 字合成一次语音)
if (charCount.get() >= TTS_CHUNK_SIZE) {
triggerTts(sessionId, fullResponse.toString());
charCount.set(0);
}
},
error -> {
// 错误处理:返回兜底话术
handleLlmError(sessionId, error);
},
() -> {
// 完成:保存对话、更新状态
sessionManager.addConversationMessage(sessionId,
ConversationMessage.assistant(fullResponse.toString()));
}
);
5、为什么这么设计
RAG检索避免幻觉,回答是基于真实的企业知识库,混合检索,语义匹配和精准匹配互补,提高召回率,流失输出(非常关键),如果不用流失输出,那么每次接口返回时间很长,用户体感很差,流式输出可以一边生成一边显示,用户体感好。打断检测也是语音助手必被功能,打断及时停止,用户体验好,且不浪费token,分句TTS是为了降低延迟,同流式输出的原理,边生成边合成,降低延迟,加上兜底话术(兜底思维面试官经常会问,XXXX出问题了怎么办呀?)LLM超市或者报错不会没有响应。
三、数据库建模
同时选择了三种数据库:(不细讲了,大家可以看我有关向量库的帖子)
Milvus:RAG知识库,文档向量,语义检索,他是专业的向量数据库,更符合专人专事
MySQL:存储会话信息,用户信息,订单数据
Doris:数据分析,转化漏斗,线索分析
Doris其实可以做以上的工作,对于一些中小型公司的项目,但对于大型企业的项目,还是专人专事相率更高。
四、问题与优化
1、prompt改进
改进前,直接讲prompt提示词硬写进代码中,无法对比效果,调优效率低,不同场景难以复用,修改需要重新部署:
改进后,使用枚举定义,不同业务场景用不同的prompt,可以用配置文件、数据库、prompt管理平台进行管理prompt:
// 从配置加载系统提示词
if (promptConfig != null) {
request.setSystemPrompt(promptManager.getFullPrompt("customer_service", null));
} else {
// 兜底默认提示词
request.setSystemPrompt("你是一个专业的客服助手,请基于参考信息回答用户问题。");
}
同时加入热更新prompt接口,可以基于情况动态更新:
/**
* 热更新:重新加载所有 Prompt 配置
* 修改 YAML 文件后调用此接口即可生效,无需重启服务
*/
@PostMapping("/refresh")
public Result<String> refreshPrompts() {
promptManager.refresh();
log.info("Prompts refreshed via API");
return Result.success("Prompts refreshed successfully");
}
基本经历了这样几个步骤,代码中直接写prompt-》配置文件+API热更新-》定义接口持久化管理
2、WebSocket 高并发架构优化
这边讲一下概念和几个点:
1、ASR:文字转语音,语音转文字,100ms切一个片,为了返回生成语音是实时而非要用户等很久,跟流式输出一个原理
2、槽位抽取:提取关键信息,比如“我猜你去年买了个表”,时间槽位:去年,商品槽位:手表,这些信息会填充到prompt中的关键信息里。(比如下面的牛客prompt工程题的提取。)
3、心跳机制:定期打招呼,已保证链接未断,WebSocket 建连后,客户端每 30s 发一个 “心跳包”(空消息),服务端回复 “确认”,如果客户端断开连接,会主动重连,心跳机制对全双工语音很重要,防止聊着聊着就断连了。
4、结合链路的整体逻辑串讲
用户说话 → ASR 把语音转文字 → 系统先识别 “用户想干嘛(意图)”+“要什么信息(槽位)” → 去知识库(Milvus)找相关内容 → 把这些信息组装成 Prompt 给 LLM → LLM 生成文字回复 → TTS 把文字转语音推给用户。
目前项目还在持续优化中,进展我会阶段性的发送文章。
#AI求职实录##AI项目实战#
