面灵AI→

哔哩哔哩AI应用开发实习一面面经复盘

轮次
实习一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 你做过的最复杂的 Agent 项目是什么?完整讲一遍
  2. 这个 Agent 的架构怎么设计的?核心模块有哪些
  3. 如果 Agent 需要处理千万级视频,你怎么设计处理方案
  4. 视频 Agent 中,FFmpeg 随机 seek 为什么需要定位关键帧
  5. OCR 关键帧的场景检测怎么做
  6. 你怎么判断 ASR、OCR 的结果是否正确
  7. 如果让你构建 ASR、OCR 的评测集,你会怎么做?怎么保证场景覆盖
  8. 如果不同场景下算法效果差异很大,怎么设计评测集和指标
  9. 如果把 Agent 项目移植到 B 站、要处理千万量级请求,架构怎么改?你会怎么重新设计?
  10. Agent 的停止条件是什么?怎么判断分析完成
  11. 如果 Agent 陷入死循环,怎么检测和处理
  12. 你的 Skill 怎么设计和实现?在大规模场景下(1000 个 Skill、每个描述 500 token)怎么做渐进式加载
  13. 如果模型不遵循 Skill 文档,高合规场景怎么保证不越权
  14. 高并发下 Agent 系统怎么扛?1000 个沙箱同时运行,资源怎么隔离
  15. 如果容器重启,会话状态怎么恢复
  16. 从技术层面讲线程池配置,核心参数怎么设置
  17. 什么情况会导致 OOM?怎么排查?频繁 GC 怎么优化
  18. Java 内存模型讲一下,volatile 怎么实现可见性
  19. 进程、线程、协程的区别?各适用什么场景?Goroutine 调度怎么实现
  20. 你的 Agent 系统支持多租户吗?数据和资源怎么隔离
  21. 如果 Agent 执行到一半用户退出,任务怎么处理?状态怎么保存
  22. 你最近用过什么新的 AI 工具?有没有觉得不错的
  23. 你觉得 AI 编程工具最大的瓶颈是什么?希望下一代解决什么
  24. 如果让你设计一个视频内容理解 Agent,你会怎么做
  25. 视频中的多模态信息怎么融合?音频、视觉、文本怎么对齐
  26. 如果让你做视频推荐理由生成,怎么保证吸引人
  27. 你怎么评估视频 Agent 的效果?有哪些指标
  28. 如果让你设计一个「视频审核 Agent」,怎么处理违规内容
  29. 多模态违规怎么识别?比如暴力、色情、政治敏感
  30. 如果让你做「视频摘要 Agent」,怎么保证摘要质量
  31. 长视频怎么处理?分段还是整体
  32. 如果让你设计一个「弹幕互动 Agent」,实时回复弹幕,你会怎么做
  33. 弹幕场景对延迟要求极高,怎么优化
  34. 如果弹幕内容违规,怎么实时拦截
  35. 你怎么看 AI 在视频社区的应用前景
  36. 如果让你设计一个「UP 主创作助手」,帮 UP 主生成标题、封面、文案,你会怎么做
  37. 你最近关注 AI 领域的哪些新技术
  38. 手撕:实现一个函数,给定一个字符串,将其中的空格替换为 %20

《参考解析》

FFmpeg 随机 seek、关键帧与 -ss 的正确用法:视频是帧间压缩,P 帧参考前面的 I/P 帧、B 帧双向参考。解码器必须先拿到所在 GOP 的 I 帧,才能解出中间的帧;少了参考帧,解出来就是绿屏、花屏或大块马赛克。所以「随机 seek」在工程上永远是两步:先在容器索引里跳到目标时间点之前的最近关键帧(demuxer 级 seek),再从关键帧开始解码到目标点。写命令时这两步由 -ss 的位置决定:ffmpeg -ss 00:01:30 -i in.mp4 -frames:v 1 -q:v 2 out.jpg 是输入 seek,跳关键帧后再解码丢弃到 90s,代价上界只有一个 GOP,现代 ffmpeg 默认就是精确的(-accurate_seek 默认开);ffmpeg -i in.mp4 -ss 00:01:30 -frames:v 1 out.jpg 是输出 seek,从 0 解码到 90s,精确但长视频基本不可接受。要特别说清的是 -noaccurate_seek 的作用:它不是「修复花屏」的开关,而是关掉「跳关键帧后再解到目标点」这一步。ffmpeg -ss 90 -noaccurate_seek -i in.mp4 -frames:v 1 out.jpg 会直接把关键帧那一帧交给你,速度最快,但你请求的 90s 与输出帧的真实 PTS 可能差最多一个 GOP——关键帧间隔 10 秒就偏 10 秒。所以它是「接受关键帧对齐」的显式取舍,不是精度修复。真正导致抽帧花屏的常见原因是另外三种:用 -c copy 在非关键帧处切,文件头部几帧的参考帧被切掉;硬解拿到不完整 GOP;以及 seek 不精确却仍按请求时间戳命名输出帧,画面没坏但帧和时间戳错位了,对下游 OCR/融合是致命的。排查顺序建议固定下来:先 ffprobe -v error -select_streams v -show_frames -show_entries frame=key_frame,best_effort_timestamp_time -of csv in.mp4 | head 看关键帧分布,确认 GOP 有多长;再看抽出的帧真实 PTS 是否等于你记下的时间戳。GOP 太大(10 秒以上)既慢又不准,工程上的标准解法是入库时转一遍 mezzanine:-c:v libx264 -crf 18 -g 30 -keyint_min 30 -sc_threshold 0 -force_key_frames "expr:gte(t,n_forced*1)",每秒一个闭合关键帧,之后所有随机访问的解码成本上界固定为 1 秒。只要关键帧可以直接 -skip_frame nokey -i in.mp4 -vsync 0 -frame_pts 1 kf_%06d.jpg。场景检测抽帧则用 -vf "select='gt(scene,0.3)*gte(t-prev_selected_t\,2)',showinfo" -vsync 0 frames/%06d.jpg,乘上最小间隔条件是为了避免滚动字幕、闪光、镜头抖动把帧抽爆。

千万级视频的异步分层与背压:千万级不可能同步处理,也不该指望一个队列包打天下,核心是三件事——分档、算得清的水位、以及入口敢拒绝。第一步先定 SLO 分档:P0 实时档(用户正在页面等着看结果,秒级)、P1 普通档(后台分析,分钟级)、P2 批量档(离线挖掘,小时级)。三档各自独立队列、独立消费组、独立资源池,物理隔离,否则一条长尾批量任务就能把实时请求饿死。数据面上,消息里只放对象存储 URI、任务元数据和 trace_id,绝不搬视频字节;Kafka 按 video_id hash 分区,分区数就是消费并行度的上限,消费者数量超过分区数纯属浪费。背压的关键是从「队列条数」升级成「预计排队时长 = 积压量 ÷ 消费速率」:设阈值(例如 P1 档预计排队超过 10 分钟,或 consumer lag 超过 5 万条)就对入口限流——要么直接拒绝并给客户端退避重试的建议,要么按视频长度和优先级降级链路(只做 ASR 不做 OCR、抽帧降到 1/5 帧率、先返回草稿摘要)。对用户要说人话:「当前排队较多,预计等待 X 分钟」永远好过静默转圈。伸缩用 KEDA 按 Kafka lag 或 Redis 队列长度做 HPA 指标,比 CPU 利用率贴近真实压力,但扩容同时受分区数和下游配额(ASR/LLM 的 QPS、GPU 卡数)硬约束,所以入口限流必须和下游配额一起设计,否则扩容只是把压力原样传导到下游再雪崩。每个阶段要幂等:以 task_id + stage 作唯一键,产物写 <video_id>/<stage>/<version>/,重跑覆盖或写新版本再原子切指针,失败只重跑单阶段。成本侧靠内容 hash 去重命中间产物 + 按热度分级路由长尾视频,盯的指标是元/千分钟处理,而不是笼统的「机器利用率」。可观测上必须盯:端到端 P50/P95、各阶段 P95、consumer lag、重试率、死信量。原帖里「上传后几小时还没出结果」这种体验问题,本质就是没有 lag 告警、也没有背压。

Agent 的模块解耦与数据契约:接入层、调度层、执行层、存储层的四层划分只是部署边界,真正决定这套系统能不能长期演进的是数据契约。原帖踩的坑很典型:抽帧器输出「图片路径列表」,OCR 模块期望「图片二进制流」,中间靠加一层转换才通——这种修法只解决了这一次,下一个模块接进来还会再撞一次。正确做法是让每个 stage 都是 manifest -> manifest 的纯函数:输入一个 manifest(引用上游资产、时间基准、schema_version、统计信息),输出一个新 manifest 和若干新资产,资产本体进对象存储,模块之间只传 manifest 的 URI 加版本号。谁生产谁负责写契约字段,谁消费谁只依赖契约里显式声明过的字段,其余一概不看。契约要版本化:schema_version 必填,只增字段、不改已有字段语义、不删字段(删除必须先经过一个双读窗口);消费端解析时忽略未知字段;Protobuf 的 field number 一旦发布就不能复用,JSON 方案则靠 CI 上的 schema 校验卡住破坏性变更,破坏性变更必须升 major 并配迁移脚本。落地手段是契约测试:把上游的样例产出固化成 fixture,在下游的 CI 里跑一遍,跨语言管线尤其需要。另外,时间语义也是契约的一部分——时间戳单位、起始偏移、以及「这一段没有 OCR 结果」到底是空数组还是字段缺失,都得写死,否则融合阶段必然对不齐。做到这几条之后,每个模块都能独立扩缩容、独立重跑、独立灰度,失败时只重跑坏掉的那一段,而不是整条链路重来一遍。

抽帧 / OCR / ASR 的时间戳对齐:对齐的前提是全链路只有一套时间基准。建议统一到媒体 timeline 的毫秒,并注意 ffprobe 报出来的 start_time 可能不为 0(TS 流里很常见),要先减掉起始偏移再落库;可变帧率视频更不能拿「帧号 ÷ fps」推时间,必须用真实 PTS——抽帧时加 -copyts 配合 -frame_pts 1 命名,或者用 showinfo 打印 pts_time,把「文件名 → 时间戳」写成 sidecar 一起落盘。第二件事是把信息区间化而不是点化:OCR 出来的字幕在时间上是一段区间(出现到消失),ASR 出来的也应该是区间(句级,需要词级就开 word_timestamps),两者统一表达成 [start, end, text, source, confidence],再做区间交叠 join,而不是拿单点时间戳比距离。第三件事是窗口聚合:按固定窗口把同一窗口内的关键帧、OCR 文本、ASR 文本聚成一个 multimodal chunk 再送模型,chunk 内保留各自的时间戳;窗口太小会把一句话拦腰截断,太大则几个事件混在一起、摘要开始串味,实践上 1 秒是齐平各模态的栅格粒度、515 秒是语义窗的常用起点。原帖遇到的「音频 16kHz、视频 30fps 直接拼维度对不上」,本质也是采样率不同,正确做法是各自先抽成定长特征再按统一时间栅格对齐(视觉降到 1fps、音频按 1 秒 hop 取特征),而不是硬拼维度。最后是漂移校正:跨模态偏差主要来自 VAD 把首字吞掉或句尾拖长、以及字幕相对语音天然提前 100300ms,可以借场景切换、静音段这类共同锚点做一次互相关校正;上线前至少人工抽 30 条视频量一遍时间偏差,把「偏差大于 0.5 秒的比例」做成常驻指标,比事后靠肉眼看融合结果错位靠谱得多。

沙箱隔离与会话恢复:1000 个沙箱同时跑,第一件事是承认「安全等级不同、隔离手段就该不同」。Docker + seccomp 只是同一内核上的命名空间隔离,逃逸面依旧存在;跑模型生成的不可信代码时,gVisor(用户态内核、拦截 syscall)或 Firecracker 这类 microVM(独立内核、毫秒级冷启动)要安全一个档次,代价是 syscall 密集任务变慢、冷启动更贵。工程解法是分池:高风险任务进 microVM/gVisor 池,可信模板或纯计算任务进 runc 池,池子预创建、任务借还,避免每个任务都付一次冷启动。限额必须逐项落实:CPU quota、内存上限(要让它超限被 OOM-kill,而不是拖垮整台宿主机)、PID 数量、磁盘 IO、临时盘大小;网络默认全拒,只放白名单出口,这既是安全要求也是防数据外泄的必要条件。回收侧要有硬超时(单沙箱 30 秒级)+ 空闲回收 + 定期对账「分配表 vs 实际进程」,防止沙箱泄漏;调度上用反亲和性把沙箱打散到不同节点,单节点水位控制在七成左右留突发余量,否则一台机器上几百个沙箱会互相抢内存带宽。会话恢复的正确姿势是「状态不在沙箱内存里」:每完成一个子任务就写 checkpoint(状态 + 版本号 + 幂等键)到 Redis,写入要原子(单条 Lua 或 MULTI/EXEC),别写一半;容器重启后从最近 checkpoint 续跑,DAG 里已完成的子任务直接跳过。版本号不匹配就丢弃旧状态重放,避免新代码读到旧结构。要强调的是重放必须幂等——工具调用的结果要有去重缓存(相同 tool + 参数在时间窗内直接返回缓存结果),否则恢复之后就是重复下单、重复写库这类线上事故。

线程池参数与 OOM / GC 排查:线程池参数不该背公式,应该由「目标 QPS × 单任务耗时」倒推并发度,再被下游配额卡一道——下游 ASR/LLM 有 QPS 上限时,池子开太大只会把下游打挂。IO 密集 2N、CPU 密集 N+1 只是压测的起点,最终参数看活跃线程数、队列长度、拒绝次数和任务 P99 来定。队列必须有界,无界队列的后果是任务堆到 OOM 而不是快速失败;CallerRunsPolicy 在入口线程是业务线程时能形成天然背压,但要注意它会让调用线程同步跑任务,调用方若是 RPC/IO 线程反而会拖慢上游,而可丢弃的旁路任务更适合 DiscardPolicy + 拒绝次数进监控。线程要命名、线程池要有指标暴露,流量波峰波谷明显的场景再考虑 allowCoreThreadTimeOut 和动态调参。OOM 定位的第一件事是看报错类型:Java heap space、Metaspace、Direct buffer memory、unable to create native thread 指向四个完全不同的方向。堆内先 -XX:+HeapDumpOnOutOfMemoryError 兜住现场,再 jmap -dump:live,format=b,file=/tmp/heap.hprof <pid> 用 MAT 的 dominator tree 找「谁在持有」;堆外看 -XX:MaxDirectMemorySize,必要时开 NMT 用 jcmd <pid> VM.native_memory summary;元空间则去查动态类生成(CGLIB、反射、脚本引擎)。ThreadLocal 泄漏的机理值得讲透:线程池线程长期存活,ThreadLocalMap 的 key 是弱引用、value 是强引用,线程不死 value 就跟着活,修复就是在 try/finally 里 remove(),并在 Filter 或装饰器里统一收口。频繁 GC 也要先分类:年轻代 GC 频繁多半是分配速率太高或 Eden 太小,方向是减少热点路径上的临时对象和字符串拼接;Full GC 频繁说明老年代被长期对象或大对象占满,先找泄漏,用 -XX:+HeapDumpBeforeFullGC 抓前后对比,换 G1/ZGC 永远放在最后一步——有泄漏时换收集器只是让 OOM 晚来一会儿。