AI面试之全双工语音交互项目(理论+实战)

项目介绍与主要职责:

项目名称:VoiceNexus - 企业级智能语音助手平台项目

周期:2024.03 - 至今项目角色:核心研发 / 技术负责人

项目描述:面向 B 端客户的全双工 AI 语音客服平台,集成 ASR/LLM/TTS 三大能力,支持实时语音打断与多轮对话。日均处理 50 万+ 通语音会话,端到端 P99 延迟 < 800ms,降低人工成本 65%,线索转化率提升 23%。

核心职责:

  1. 负责 WebSocket 实时通信层架构设计,基于 Netty 实现高并发长连接服务,单节点支撑 5000+ 并发,连接成功率 99.97%,通过线程池隔离和对象池复用将 Full GC 降至 0。
  2. 主导 RAG 增强问答系统建设,使用 Spring AI + Milvus 构建混合检索 Pipeline,结合 Rerank 精排,知识库问答准确率从 72% 提升至 91.3%,幻觉率降至 1.5%。
  3. 设计基于 RocketMQ 的异步解耦架构,将音频存储、线索评分等非核心操作异步化,主链路 P99 从 2.1s降至 780ms;使用 Redis Hash 管理多轮会话上下文,支持滑动窗口与自动过期。
  4. 构建 LLM 多层容错体系:Sentinel 熔断 + 输出三层校验 + 兜底话术库,系统 SLA 从 98.5% 提升至99.92%,用户无响应感知率降至 0.15%。
  5. 建设全链路可观测性,接入 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项目实战#
全部评论
转化率提升 23%是牛逼啊,对业务实打实的啊
1 回复 分享
发布于 03-19 11:05 上海
话说幻觉率具体咋降的
点赞 回复 分享
发布于 03-19 11:06 广东
心跳相应机制做的厉害
点赞 回复 分享
发布于 03-19 11:05 北京
段哥这是全面往AI转型了呗
点赞 回复 分享
发布于 03-18 18:31 北京
流式输出确实省token
点赞 回复 分享
发布于 03-18 18:28 北京

相关推荐

招Agent研发工程师(后端、前端、测试和客户端等)、Agent优化工程师(算法)【团队介绍】淘天集团服务千万商家的核心技术引擎,曾打造千牛工作台、生意管家、淘宝开放平台、聚石塔等标杆产品。目前正在突破性跃迁:从&quot;交付工具&quot;到&quot;交付Agent&quot;。【技术方向】1.&nbsp;Agent&nbsp;OS构建:端云一体的千牛Agent产品,支持本地代理+云端沙箱2.&nbsp;Skill生态:万级Skill应用市场,客服、数据分析、内容生成等场景3.&nbsp;A2UI:自然语言指令到经营界面的精准映射与秒级生成4.&nbsp;算法优化:记忆压缩、任务规划、决策推理、RAG优化等【岗位要求】💻&nbsp;AI&nbsp;Agent研发方向●&nbsp;本科及以上学历,计算机、软件工程等相关专业●&nbsp;扎实的计算机基础,精通Java/Go/Python至少一门语言●&nbsp;了解分布式系统、高并发架构●&nbsp;熟悉Agent技术栈(RAG、Prompt&nbsp;Engineering等)优先🧠&nbsp;AI&nbsp;Agent优化方向●&nbsp;本科及以上学历,人工智能、机器学习等相关专业●&nbsp;精通Python,深入理解Agent核心技术栈●&nbsp;具备大模型应用算法经验:熟悉SFT、RLHF/DPO、模型蒸馏等●&nbsp;在NLP/CV/多模态方向有项目实践【你将获得】●&nbsp;亿级真实业务场景锤炼●&nbsp;前沿大模型+充足算力●&nbsp;1对1导师带教●&nbsp;技术×商业双视野【工作地点】杭州【投递方式】私信或评论区留言
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务