英语流利说秋招笔试题解析:从音素分割到高并发语音评测架构

发布时间:2026/9/1 20:26:36
英语流利说秋招笔试题解析:从音素分割到高并发语音评测架构 从拿到英语流利说2019秋招技术类笔试题的那一刻起我的第一反应是这公司是真把业务揉进笔试里了。市面上大多数技术笔试都是教科书味的算法题一套模板走天下但流利说的卷子明显不同字符串编辑距离、语音评测链路、弱网优化这些考点全都围着“AI教育”这个核心场景转。如果你正准备投这类AI教育公司或者对语音方向的技术栈感兴趣这篇文章值得你花20分钟读完。我会从考察逻辑、典型算法题推导、系统设计思路、基础考点和准备策略五个方面把这份卷子拆开揉碎讲一遍。1. 这份笔试题的考察逻辑为什么这么出题1.1 技术栈与岗位画像英语流利说的业务核心是“AI老师”用户对着手机读一段英文系统要实时打分、纠音、给出反馈。这决定了它的技术岗位画像不是纯粹的算法研究员也不是只会CRUD的后端开发而是“懂算法落地、懂高并发、懂音频处理”的复合型候选人。笔试题目设计有两条隐含主线一是考察你能否把经典算法应用到非经典场景二是考察你是否有全局视野能从一次用户录音请求出发看到后端全链路。如果你只刷LeetCode不关注业务场景做这份卷子会明显觉得别扭。1.2 题型分布与时间分配的博弈我印象里整张卷子分为四块选择题/填空题、算法编程题、系统设计题、计算机基础简答题。时间一共120分钟题量不算少。这里有个容易被忽视的点算法题占分比没有想象中高系统设计和基础题才是拉开差距的地方。我的建议是拿到卷子先用3分钟扫一遍全部题目给每道题预估一个时间上限。算法题如果15分钟内想不出最优解立刻写暴力解保分。千万别在一道DP题上死磕40分钟导致后面的系统设计题只能写两行字。我当年就吃过这个亏算法题写爽了系统设计题草草收尾最后面试时被追问得很难受。2. 算法题实战复盘从暴力解到最优解的完整推导2.1 动态规划变形音素序列分割问题原题的大意是一段语音被切分为N个音素片段每个片段有个“流畅度得分”由于音素边界检测存在误差选取的片段之间至少需要间隔K个片段求能获得的最大总得分。这道题本质是“打家劫舍”的变体但多了间隔K的限制。第一眼看到它我脑子里跳出来的是DP但状态转移方程需要仔细推。定义dp[i]表示从前i个片段中能获得的最大总得分那么对于第i个片段有两种选择不选第i个片段dp[i] dp[i-1]选第i个片段dp[i] dp[i-K-1] score[i]取两者的较大值。初始状态dp[0]0dp[1]score[1]如果i-K-1小于0说明前面没有可选片段直接取score[i]。def max_score(scores, K): n len(scores) dp [0] * (n 1) for i in range(1, n 1): # 不选当前片段 dp[i] dp[i - 1] # 选当前片段前一个可选位置是 i-K-1 prev i - K - 1 if prev 0: dp[i] max(dp[i], scores[i - 1]) else: dp[i] max(dp[i], dp[prev] scores[i - 1]) return dp[n]注意这里的边界处理片段下标从1开始方便表示scores[i-1]是第i个片段的得分。如果K1就是“不能选相邻片段”的经典打家劫舍如果K0那就是“选或不选”的0/1背包了。这道题还有个进阶问法如果片段总数N达到10^6O(N)的DP已经是最优但如果你用Python的递归加备忘录很可能会因为递归深度爆栈所以必须用迭代写法。2.2 字符串处理带权重的音素编辑距离第二道编程题让我印象深刻因为它直接映射了语音评测里的“发音纠错”功能。题目给出一段标准音素序列和一段用户实际发音的音素序列允许三种操作插入、删除、替换但每个操作的代价不同比如替换的代价是2插入和删除的代价都是1求最小编辑代价。经典编辑距离大家都刷过但带权重之后状态转移方程需要微调。dp[i][j]表示标准序列前i个音素和用户序列前j个音素的最小编辑代价如果A[i] B[j]dp[i][j] dp[i-1][j-1]否则替换dp[i-1][j-1] cost_replace插入在标准序列中插入一个音素dp[i][j-1] cost_insert删除删除标准序列中的一个音素dp[i-1][j] cost_deletedef min_edit_cost(A, B, cost_insert1, cost_delete1, cost_replace2): n, m len(A), len(B) dp [[0] * (m 1) for _ in range(n 1)] for i in range(n 1): dp[i][0] i * cost_delete for j in range(m 1): dp[0][j] j * cost_insert for i in range(1, n 1): for j in range(1, m 1): if A[i-1] B[j-1]: dp[i][j] dp[i-1][j-1] else: dp[i][j] min( dp[i-1][j-1] cost_replace, dp[i][j-1] cost_insert, dp[i-1][j] cost_delete ) return dp[n][m]这里有个很实用的优化如果cost_replace cost_insert cost_delete那么替换永远不划算这时候可以用插入删除组合代替替换。很多同学在笔试时忽略了这一点导致结果偏大。更深一层这道题还能引导你思考真实语音评测里编辑距离只是最底层的对齐工具实际还要结合发音得分、重音、连读等特征但如果连编辑距离都写不对后面的优化就无从谈起。2.3 数据结构设计支持区间统计的在线榜单第三道算法题是纯数据结构设计设计一个类支持三个操作——添加一个用户分数、删除一个用户分数、查询分数在[low, high]区间内的用户数量。分数范围是0到10^9操作次数是10^5。第一个直觉是用有序数组或链表但删除和查询都是O(N)肯定超时。第二个直觉是平衡二叉搜索树但手写红黑树在笔试里不现实。正确解法是用树状数组Fenwick Tree做离散化。把所有出现过的分数收集起来排序去重映射到1到M的连续下标然后树状数组维护每个分数段的人数。查询区间[low, high]时先二分找到low和high对应的离散化下标再调用前缀和函数。class FenwickTree: def __init__(self, n): self.n n self.bit [0] * (n 1) def add(self, idx, delta): while idx self.n: self.bit[idx] delta idx idx -idx def sum(self, idx): res 0 while idx 0: res self.bit[idx] idx - idx -idx return res def range_sum(self, left, right): if left right: return 0 return self.sum(right) - self.sum(left - 1)这道题考察的不只是树状数组模板还有离散化的意识。当时有同学直接用有序字典或者优先队列删除操作复杂度就崩了。笔试时选择数据结构一定要先分析每个操作的时间复杂度上限再倒推合适的结构。3. 系统设计题语音评测服务的高并发架构3.1 需求拆解从录音上传到评分回传的全链路系统设计题的大背景是英语流利说的核心功能“跟读评测”用户朗读一段英文App录音上传后端进行语音识别、发音评分、流利度分析最后把分数和纠音建议推回App。请你设计一个支持百万级日活用户的后端架构。这道题是典型的“大而全”设计题关键不是堆组件而是展示你如何拆解需求、识别瓶颈。完整链路可以拆成五个环节客户端录音、文件上传、消息异步处理、AI推理打分、结果存储与推送。很多人的第一个错误是只画了一个简单的“客户端-服务器-数据库”架构图然后开始谈MySQL分库分表。但实际上语音文件的上传和AI推理是最大的瓶颈。录音文件通常是几MB到几十MB不能直接存MySQL也不能用同步HTTP请求等AI推理完成后再返回因为一次推理可能要几百毫秒到几秒用户的请求早就超时了。正确的拆解方式是客户端录音结束后先把音频文件上传到对象存储上传成功后服务器只返回一个“上传成功”的确认并生成一个task_id服务器把task_id和音频元信息写入消息队列后端的AI推理Worker从队列里拉取任务执行语音识别和评分推理完成后将结果写入结果存储并通过长连接或推送通知把结果回传客户端。这个链路的核心是“异步化”把耗时操作从请求主路径中剥离出来。3.2 存储选型对象存储缓存分库分表存储层面至少需要三类存储对象存储存放原始音频文件和评测报告文件按日期分目录文件名用task_id或用户ID做哈希散列避免单目录文件过多。缓存用Redis缓存热点用户的最近评测结果方便用户回看历史记录。key的设计建议是eval:result:{userId}:{taskId}过期时间设置5天。关系型数据库存储用户基本信息、任务状态、成绩汇总。这里才用到分库分表按userId哈希分16个库再按时间按月分表。我特意强调一下不要在系统设计里把所有数据都塞进一个组件。很多候选人在笔试里写“用Redis存所有东西”这是大忌。Redis适合做缓存和轻量级队列不适合做持久化主存储因为内存成本高、数据可靠性不足。关于分库分表有一个常见的面试追问分表键怎么选如果按userId分表那么“查询某用户最近的评测记录”这个操作就非常快但如果运营人员要按时间维度统计全平台的评测量就需要扫描所有分表。这是典型的分布式系统trade-off你要在卷面上明确说出你的取舍依据。3.3 异步处理消息队列削峰与结果回调消息队列是这道题的灵魂。为什么不用同步HTTP调用AI服务因为AI推理服务是CPU密集型并发能力有限而且语音评测有明显的波峰波谷——晚上8点到10点是使用高峰期如果所有请求都直接打到AI服务服务必定被打垮。引入消息队列后生产者上传服务和消费者AI Worker之间解耦队列天然具备削峰填谷的能力。选择哪个消息队列笔试题不会限定技术栈你写Kafka、RocketMQ、RabbitMQ都可以但要说清楚理由。我个人推荐写Kafka或RocketMQ因为它们的吞吐量大、支持消费者组扩展。尤其是RocketMQ支持延迟消息和事务消息在评测结果回传场景下很好用。还有一个关键细节结果回调。AI推理完成后怎么把结果推送到客户端两种主流方案客户端定时轮询每秒请求一次“任务状态”接口实现简单但有延迟且浪费服务器资源WebSocket长连接客户端建立长连接后服务器主动推送结果体验更好。在笔试里你应该选择WebSocket方案并说明原因移动端网络环境复杂轮询在弱网下表现差而且频繁轮询会产生大量无效请求。如果担心WebSocket连接断开可以设计一个补偿机制——客户端在断线重连后根据本地缓存的task_id主动查询一次结果。4. 计算机基础题藏在八股文里的真实考点4.1 操作系统进程线程与协程在AI推理中的取舍基础题里有一道让我印象很深在语音评测的AI推理服务中为了提高并发能力应该用多进程、多线程还是协程为什么很多人看到这道题直接写“用协程因为协程轻量”但这是不完整的。AI推理的底层通常依赖深度学习框架如TensorFlow、PyTorch这些框架的推理操作是CPU密集或GPU密集的而且很多底层库是C实现的内部有自己的线程池。在Python里用协程如果遇到CPU密集的计算协程并不会自动让出控制权反而可能因为GIL的存在导致其他线程阻塞。合理的方案是“多进程 少量线程”。多进程可以充分利用多核CPU每个进程内部署一个模型副本进程间互不干扰。线程池用于处理I/O密集型操作比如读取音频、HTTP请求等。协程更适合用在高并发I/O场景比如网关层、代理层。这道题考察的其实是“技术选型要结合场景”而不是背概念。回答案例时要讲清楚Python的GIL决定了纯计算任务用线程是伪并行GPU推理通常受CUDA流和显存限制也不能无限开线程。4.2 网络弱网环境下的传输优化另一道网络题是用户在移动网络环境下进行口语评测如何保证录音文件上传的稳定性和成功率这道题可以从三个层面答传输层使用HTTP/2或QUIC协议支持多路复用和更好的拥塞控制减少TCP队头阻塞。应用层分片上传把录音文件切成2MB左右的块每个分片独立上传、独立重试最后服务端合并。断点续传也是这里的关键词。客户端策略根据当前网络状态动态调整录音质量和采样率。比如Wi-Fi下用48kHz采样4G网络用16kHz采样2G/3G网络降级为8kHz优先保证上传成功率评分模型可以兼容不同采样率。这里有一个我实际踩过的坑分片上传时如果每个分片都创建一个独立的HTTP连接握手开销会很大。正确做法是复用连接并且用并发数为3~5的线程池控制上传速度避免把用户的上行带宽全部占满影响其他业务。4.3 数据库索引失效场景与慢查询排查数据库题考察了一个很常见的场景评测记录表有userId、created_at、score三个字段查询语句是SELECT * FROM evaluation WHERE user_id ? AND created_at BETWEEN ? AND ? ORDER BY created_at DESC LIMIT 10请说明如何设计索引并列举至少两种索引失效的场景。正确做法是建立联合索引(user_id, created_at)顺序不能颠倒。因为查询条件中user_id是等值匹配created_at是范围匹配联合索引可以同时过滤两个条件避免回表。索引失效的常见场景要记住三条对索引列使用函数比如WHERE DATE(created_at) 2024-01-01会导致索引失效应该改为created_at ? AND created_at ?隐式类型转换比如user_id字段是varchar查询时传入数字MySQL会触发隐式转换索引失效左模糊查询LIKE %abc无法使用索引而LIKE abc%可以用索引。我在笔试时额外提了一条如果查询量巨大可以考虑用覆盖索引把score字段也放进索引里这样可以直接从索引中返回数据避免回表IO。这些细节虽然是小点但能体现你真的处理过慢查询问题。5. 准备建议与踩坑提醒从我实际笔试经历中提炼5.1 时间分配策略先保分还是先攻坚整张卷子做完我最深的体会是笔试不是竞赛是“在有限时间内展示最大价值”。如果你在一道题上卡了20分钟果断跳过把能拿的分先拿到。我当时的策略是填空选择题限时30分钟算法题每道最多35分钟系统设计题留40分钟基础题最后20分钟收尾。系统设计题很容易被低估很多同学先做算法题最后只剩15分钟写设计题结果只画了架构图没有说明存储选型和消息队列的细节分数直接被砍掉一大截。如果你时间紧张宁可写一个完整的“简化版”系统设计——一条清晰的主链路加关键组件——也不要东写一句西写一句。5.2 代码规范细节面试官真正在看什么编程题不光看答案对不对还看代码风格。笔试平台一般支持选择题和代码题代码题如果AC了面试官会看你的解题思路如果没有AC但代码结构清晰、注释到位也能获得一些同情分。有几个细节我能提醒就提醒一下变量命名要有意义不要用a、b、c这类无意义名称边界条件一定要处理比如数组为空、K为0、分数范围越界复杂度分析要写在注释里或提交前最后一段文字中让面试官快速了解你的思路。我当时在编辑距离那道题里虽然代码AC了但没写复杂度分析面试时被追问才补充。其实笔试时在代码注释里写上“O(n*m)时间和O(m)空间滚动数组优化”是很加分的。5.3 复盘方法笔试结束后的二次价值笔试结束后别急着丢到一边。我自己会把每道题的解题思路重新整理一遍尤其是那些没做出来的题花时间去查最优解、写一遍完整代码。这样做的好处是面试时如果问到类似问题你能立刻调出清晰的思路而且流利说这种公司面试题往往和笔试题高度关联笔试里出现的系统设计题面试里大概率会继续深挖。我当年笔试后的复盘笔记里专门把“音素序列分割”和“编辑距离”两道题归类为“业务算法题”总结出这类题目的通用解法框架先抽象业务场景的数据结构再套经典算法模板。这个框架后来对我在其他AI公司的面试也很管用。最后再分享一个真实感受流利说这套笔试题与其说是在筛选“刷题机器”不如说是在筛选“能用技术解决实际问题的人”。如果你备考时只是埋头刷LeetCode可以理解业务场景的技术需求可能不够但如果你也花时间研究过语音评测、移动端弱网优化、异步消息架构这些实际工程问题答起来会顺手很多。准备时把视野放宽一点多做跨知识域的串联比单纯刷题更值。