2026.09.07-上海小厂AI-Infra面经与复盘

2026.09.07-上海小厂AI-Infra面经与复盘

最后更新:


1759 字 约 9 分钟

项目

这里说了在HardSeat项目构建中,由于12306官方票价接口的流量限制,考虑从在线搜索转为离线搜索,提前量爬取车次数据,建立时间延展图进行搜索

然后面试官问:怎么解决爬取过程中的数据不重不丢,在考虑到可能需要多次请求的情况下

这里之前没有想过,现场想了一下,考虑是在数据库层面建立一个key,比如说出发时间+始发地+目的地+车次编号这些合并编为一个key,作为数据库的主键确保唯一性,然后写入的时候就可以看这个key有没有重复

随后面试官追问:好这是不重,那你怎么保证这次任务一定是写入了

这个我记得之前有看过类似的方案描述,但是一时想不起来叫什么,只好大概描述是稍作延迟再尝试读以确保写入

这里复盘一下,分别从不重和不丢两个方面来讲

  • 不重:这里的唯一索引确实没问题,但是用数据库硬扛高并发写入会造成极大的压力,需要从其他的角度来考虑

    首先讲一下布隆过滤器,它其实底层的数据结构就是一个二进制数组,在存入一个数据的时候,使用 kk 个哈希函数对其进行处理,得到 kk 个哈希值,然后在数组里对这 kk 个哈希值对应的位置全部打上1

    查询同样是这么算,但是注意相同的结果不一定代表这里一定有数据,毕竟哈希冲突也是存在的

    一般应用在Redis缓存中,可以隔绝大量的外部无效请求,大幅减少数据库压力

    所以这里显然可以应用布隆过滤器了

    同时在数据库层面,还有方法也就是幂等写入,即后来的重复数据只会覆盖原来的数据

  • 不丢:这里也是基本没有答上来的部分,整理一下大概有以下的方法:

    首先是 WAL(Write-Ahead Logging),也就是预写日志,在任务真正开始执行之前,先把任务列表写入日志或者持久化队列中,这样即使中途中断,也可以从日志处快速恢复

    当然这也是有缺点的,比如写放大问题,有可能写入一行数据,需要先写入WAL,写Undo Log,导致实际的写入量比业务所真正需要的大得多

    同时还有日志内存占用、设计复杂度陡增等问题,这里不作展开

    还有一种方法是ACK与死信队列,爬虫这边抓到数据之后,投递给消费者写入服务,数据成功落库并收到数据库的commit回执之后,才返回确认ACK给爬虫这边生产者

    如果持续一段时间没有ACK回应,就会尝试重发,重试次数达到上限之后就塞入死信队列,看交付给人工处理还是怎么样

    这个组合虽然经典,但是也是有缺陷的,比如带来实现和维护复杂度,然后要求消费端实现绝对幂等,以及抛入DLQ中的事件丧失了时序逻辑等等

    最后一种策略是两阶段状态机,在普通的任务执行流程设计中,可能通常会采用

    graph LR
      A[未开始] --> B[执行中]
      B --> C[完成]

    这样的状态流转设计,但是在分布式或者网络不稳定的场景中,会出现诸如进程猝死、假死与重复执行、未实际落库等问题,这也与我们这里的爬取任务较为贴近

    因此我们考虑设计两阶段状态机,它本质上是将任务拆分为两个执行阶段:阶段一是任务执行或者说抓取,阶段二是任务提交或者说写库

    具体地,画出整体状态流转图:

    stateDiagram-v2
      [*] --> UNASSIGNED
      UNASSIGNED --> RUNNING : 调度器派发任务
      RUNNING --> PREPARED : 数据抓取成功,写入<br>临时表/待提交事务
      PREPARED --> COMMITTED : 数据库事务成功 Commit,<br>接收到物理写回执
      COMMITTED --> [*]

    首先在第一阶段,调度器通过CAS或者数据库乐观锁,将任务分配到节点,并记录updated_at和worker_id

    然后worker去执行任务,比如这里的爬虫爬取数据,爬取到之后不直接写入业务表,而是存储到临时表或者说存储到主表但是标记状态为PREPARED

    这里一旦转为PREPARED状态,哪怕这个时候节点worker爆了,系统仍然知道这个时候数据已经准备好了,可以进入下一阶段

    第二阶段,数据库尝试从临时表写入业务表,或者将PREPARED状态转为VALID,在原子提交完成后,在同一个数据库事务内,将状态机状态转为COMMIT,写入完成,该返回ACK的返回ACK

    当然如果只有这些还是不行,比如如果任务在节点执行的过程中挂掉了,则需要拿回来重试,这个时候设计一个兜底的巡检线程Sweeper,动态监测超时,拿回任务并重新分配抢占

    再比如PREPARED状态超时,说明可能写入挂了,Sweeper触发补偿提交,如果这里没有完整写入,就尝试回滚状态,重置任务


场景题

面试官:假设你有一个系统,每天有10亿条用户登录的log,和Agent聊天的这些记录,然后你现在要对这些log进行热词统计,比如说取top 100,你会怎么设计这个方案

一开始答了将这些数据分成多个小顶堆处理,然后根据log更新动态更新记录,分配的话就是取模数看分到哪个小顶堆,然后逐级往上多路归并这样

然后面试官追问这个过程中可能对某些词会少计算,这个怎么解决

考虑的是使用哈希表,但是具体细节不知道怎么设计了

其实要解决这个问题,分两个视角设计

首先是离线批处理视角,也就是你需要在一天结束后再统一对这一天的数据日志进行处理,要想数据不丢,就要使用Map-Reduce思想

Map阶段:先将十亿条log分配到map节点,做文本分词

Shuffle阶段:这里不能单纯地取模计算,而是做哈希取模,保证每个相同的词语都汇集到一个Reduce节点中

Reduce阶段:每个Reduce节点内部维护局部哈希表统计以及小顶堆,选出该节点内部的top 100

Global Merge阶段:最后再总体合并,上层再维护一个小顶堆,从下层 N×100N \times 100 个词中选出最终的top 100,没有任何遗漏

但是如果要采用在线实时流式视角的话,内存完全没办法放下这么多的key,就要使用其他的方法了

我们考虑引入CM-Sketch算法,考虑在内存中维护一个 D×WD \times W 的二维矩阵,其中 DD 为深度,WW 为宽度

同时引入 DD 个哈希函数,分别对应矩阵的 0,1,2,...,D−10, 1, 2, ... , D - 1 行,其中每个哈希函数最终的处理结果都会落入 0,1,...,W−10, 1, ... , W - 1 内,也就是对应了每一列

对于一个词 ss ,依次使用 DD 个哈希函数进行处理,最终根据结果在每一行的对应列上那个位置的计数+1,因此插入就是 O(D)O(D) 的时间复杂度,且常数极小

然后查询的时候,获取词 ss 的频次,同样也是用 DD 个哈希函数进行计算,最终取得 DD 个位置的结果,取其中最小值作为近似频次

为什么取最小值?因为哈希冲突可能导致其他的词的计算结果也落入相同的位置,所以最终频次计数器的结果只会高不会低,这时候取其中的最小值,就是最贴近真实值的结果

光有这个还不够,因为我们只能获取频次而不能取得top k热词,这个时候再维护一个小顶堆即可

这里还有更多其他的算法,就不一一阐述了


设计题

面试官:如果让你设计一个AI聊天的一个接口,你设计这个接口都要考虑什么东西,尽可能地细

我当时答了是流式输出、请求体设计包括system_instruction、Tools配置、definition序列化模板化,前端WebSocket连接、API调用到后端,后端再调用LLM的一个完整链路,然后前端呈现做流式文档

追问是token limit这样,然后我就答了在请求之前和收到LLM响应之后用像tiktoken这样的库去计算token,然后考虑小模型预先Compact或者暴力滑动窗口做截断

整理一下,首先这个连接方式应该用SSE连接,WebSocket全双工长连接成本高,而且难做负载均衡,SSE适合这种单向流式文本输出的场景

然后请求体应该说得清楚一点,像是model_config、context_stratgy以及tools_schema这些

然后响应体定义SSE的event,分成meta、delta、tool_call这些事件

上下文压缩还有择优留存和异步摘要策略也可以采用,实在到上限的时候再触发400告诉用户切换对话

同时客户端保存session_id,具体的对话历史在服务器后端持久化

评论

评论会在审核后展示

正在加载人机验证...

查看原图