怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际图文社区的服务端,Java + Spring Cloud。核心场景是笔记的评论与回复关注关系下的内容分发内容与评论的安全审核。用户遍布多个国家,有本地法规和语言差异。
为什么选这三个模块 社区业务的三个真正难点:树形数据的存储与分页(评论)、关系链导致的扇出问题(分发)、不确定的外部依赖与人工环节(审核)。这三个都不是套模板能做出来的,而且每一个都有清晰的容量边界可以讨论。

模块一:评论与回复体系

  1. 评论与回复体系(两级结构 + 分页稳定性 + 热评排序)★★★
    简历这样写 评论与回复体系(Spring Boot + MySQL 分表 + Redis + 游标分页):把无限层级的评论树收敛为「一级评论 + 平铺回复」两级结构,一级评论按热度排序、回复按时间正序并预取前几条;分页改为游标式解决新评论插入导致的错乱,计数与热度分走缓存。万级评论笔记的详情页评论区接口 P95 由约 860ms 降至 180ms 上下,单次请求的数据库查询次数由随层级增长降为固定 3 次
    展开完整拆解
    为什么要这么设计

    第一版按「无限层级」建模:每条评论有 parent_id,展示时递归查子评论。这个设计在数据量小的时候看不出问题,评论多了之后全面崩坏。

    一是查询次数不可控。递归查询每层一次,一个有几百条评论、嵌套五六层的笔记,一次请求打出上百条 SQL。二是分页压根做不了——树形结构没法自然分页,只能全查出来在内存里组装,笔记有一万条评论就要把一万条全查出来。三是深层嵌套的展示体验很差,手机屏幕上缩进四层之后一行只能显示几个字。

    关键判断是:无限层级是技术上的自由,但不是产品需要的。看主流社区产品,几乎都是两级——一级评论,以及针对一级评论的回复(回复的回复也平铺在同一层,只是文案上带「回复 @某人」)。把数据模型从树收敛成两级,是这个模块最重要的决策,它让分页、排序、计数全部变得简单。

    整体链路
    数据模型(收敛后) comment 表 ├─ id ├─ note_id ← 笔记 ID(分表键) ├─ root_id ← 一级评论 ID;自己是一级则等于 id ├─ reply_to_id ← 直接回复的评论 ID(仅用于展示 "回复 @X") ├─ reply_to_user ← 被回复人(冗余,省一次关联查询) └─ like_count / reply_count / status / created_at 一级评论:root_id = id 所有回复:root_id = 所属一级评论 ID(不管回复的是谁) → 一个 root_id 下的回复全部平铺,天然可分页 请求链路(评论区首屏) │ ├─ 查一级评论(1 次) │ where note_id=? and root_id=id and status=正常 │ order by 热度分 desc, id desc limit 20 (游标:分数+ID) │ ├─ 批量查这 20 条的前 3 条回复(1 次,窗口函数或 union) │ ├─ 批量查用户信息(1 次,走缓存;缺失的批量回源) │ └─ 组装返回(点赞状态从 Redis 批量判断) 固定 3 次数据库查询,与评论总量、嵌套深度无关
    分步拆解
    1. root_id 是整个设计的核心字段。一级评论的 root_id 等于自己的 id,所有回复的 root_id 都指向所属的一级评论。这样「查某条一级评论下的所有回复」就是一个简单的等值查询加分页,不需要递归。reply_to_id 只用于展示,不参与查询逻辑。
    2. 被回复人的昵称要冗余存一份。展示「回复 @张三」时如果去关联用户表,20 条评论就是 20 次关联。冗余存下来省掉这一步。代价是用户改昵称后历史评论里的昵称是旧的——这个代价可以接受(很多产品就是这么做的),或者在读取时用缓存里的最新昵称覆盖显示。
    3. 首屏只预取每条一级评论的前几条回复。用户点「查看更多回复」才拉剩下的。预取用一次批量查询完成(窗口函数按 root_id 分组取前 N,或者 20 个小 union)。不要在循环里查,那就是 N+1 问题。
    4. 热评排序用缓存里的热度分。热度分由点赞数、回复数、时间衰减算出来,存在 Redis 的 ZSet 里,定时刷新。不要在 SQL 里实时计算带时间衰减的分数——那会导致排序字段不可索引,必然全表扫描加排序。
    5. 游标分页解决插入错乱。用 offset 分页时,翻页期间有新评论插入到前面,第二页会重复出现第一页的内容。游标里存「上一页最后一条的排序分和 ID」,下一页查「分数小于它 或者 分数相等但 ID 更小」的。排序字段必须有 tie-breaker(这里用 ID),否则分数相同的评论顺序不稳定,翻页还是会乱。
    6. 评论数按笔记维度用缓存计数。不要每次 count(*)。写评论时 INCR,定时以数据库为准校准。这和计数场景的通用做法一致。
    7. 分表键选 note_id因为查询几乎都是「查某个笔记的评论」,按 note_id 分表能保证单次查询落在一张表上。如果按 comment_id 分表就会导致每次查评论区都要扫所有分表,这个选择要能讲清依据——分表键跟着最高频的查询维度走。
    关键决策与取舍

    放弃无限层级的代价。确实丢失了表达能力——理论上用户可能想针对某条回复展开一个独立的子对话。但实际观察下来这种需求极少,而且产品上用「回复 @某人」的文案已经能表达出对话关系。用产品数据支撑技术决策,比单纯说「技术上更简单」有说服力。如果面试官追问「那如果产品坚持要无限层级呢」,答案是:可以用路径枚举(存一个 path 字段记录祖先链,比如 1/5/23)实现,一次查询就能取出整个子树并按 path 排序,但分页依然困难,而且深度有上限(path 字段长度限制)。

    热评排序和分页的冲突。按热度排序时,用户翻页期间某条评论的热度分变了,可能导致它在第二页重复出现或者被跳过。彻底解决要把排序结果快照下来(第一次请求时生成一个有序列表存起来,翻页从快照里取),但那增加了存储和一致性成本。我们的选择是接受偶发的重复——热度分的刷新周期是分钟级,用户翻页通常在几秒内完成,实际撞上的概率很低。说清「为什么这个不完美是可接受的」比假装没有问题好。

    踩过的坑:删除一级评论导致回复变孤儿。作者删掉一条一级评论后,它下面的几十条回复的 root_id 指向一个不存在的记录,查询时这些回复既不属于任何一级评论也不会被展示,但还占着评论计数。修法是删除改为逻辑删除并级联标记:一级评论被删时把 root_id 相同的回复一起标记为不可见,同时修正笔记的评论计数。这里的教训是:有父子关系的数据,删除逻辑必须和查询逻辑一起设计。

    数字是怎么测的

    接口 P95:造一个有一万条评论(含回复)的笔记,压测拉评论区首屏。改造前约 860ms(大部分耗在几十次数据库往返和内存组装树),改造后 180ms 上下。要说清测试数据的构造方式——评论的分布(有多少条一级、平均每条几个回复)会显著影响结果,用「每条一级评论平均 5 条回复」这种描述让数字可复现。

    数据库查询次数:开慢查询日志或者用 APM 看单次请求的 SQL 数量。改造前随评论数和嵌套深度增长(几十到上百次),改造后固定 3 次。这个「固定」是最有说服力的部分——它说明复杂度从 O(n) 降到了 O(1),比单纯的耗时数字更能体现设计质量。

    不要报「评论发布成功率」之类的指标。评论是个简单的写入,成功率本来就该接近全部,报这个数字没有信息量,反而暴露对指标的选择能力不足。

    面试追问
    Q:如果产品一定要无限层级,你会怎么设计? A:主要有三种模型。邻接表(就是 parent_id)最简单但查子树要递归;路径枚举存祖先链(1/5/23),一次 like '1/5/%' 就能取整个子树,按 path 排序天然是树的前序遍历,缺点是 path 长度有上限、移动节点要批量更新;闭包表额外建一张祖先后代关系表,查询最灵活但存储放大明显(一条深度 n 的评论要存 n 条关系)。评论场景我会选路径枚举,因为评论不会移动位置,path 一旦生成就不变,最大的缺点用不上。但分页问题依然存在——树形结构的分页没有好的通用解法,这也是为什么主流产品都选择收敛层级。
    Q:一条笔记有百万条评论(比如明星笔记),你的方案还撑得住吗? A:读取撑得住,因为查询是「按 note_id 加排序分取前 20」,走索引后和总量关系不大。真正的问题在写入和热度排序:几百万条评论集中在一个 note_id 下,分表后这一张表的这一个分片就是热点,写入会集中;热度 ZSet 也会变成一个几百万成员的大 key。处理办法是对超热点笔记做二次拆分——按评论 ID 哈希再分若干桶,读取时并行查各桶再归并;热度榜只维护 Top N(比如前 1000 条),超出的按时间序展示,因为用户压根不会翻到第 50 页。核心思路是:极端热点要用和普通场景不同的策略,不要试图用一套方案覆盖所有量级。
    Q:评论的点赞状态(当前用户有没有赞过这条评论)怎么查?20 条评论要查 20 次吗? A:不能查 20 次。用批量判断:Redis 里存「用户对评论的点赞集合」,用一次 pipeline 或者 Lua 批量判断 20 个评论 ID。更省的做法是把某个用户点赞过的评论 ID 存成一个集合(按笔记维度分片),一次取出来在内存里比对。要注意集合的大小——用户点赞过的评论可能很多,全量取出不合适,所以按笔记维度分片存,只取当前笔记相关的那部分。
    Q:评论区的实时性怎么保证?别人刚发的评论我能马上看到吗? A:分两种情况。自己发的评论必须立刻可见,做法是发布成功后客户端本地插入到列表顶部,不依赖重新拉取(否则热度排序下新评论可能压根不在第一页,用户会以为发失败了)。别人发的评论走正常的拉取,不做推送——评论区不需要实时,用户下拉刷新或者重新进入时看到就够了。硬做实时推送会带来长连接成本,而收益很低。这里还有个细节:自己发的评论要「置顶展示」但不能影响真实排序,实现上是客户端本地维护一个「我刚发的」列表叠加在拉取结果之上。

模块二:关注关系与内容分发

  1. 关注关系与内容分发(推拉结合 + 大 V 特殊处理 + 异步扇出)★★★
    简历这样写 关注流内容分发(Spring Boot + Redis + 消息队列 + 推拉结合):关注流从发布时同步写扩散改为推拉结合——普通用户走异步写扩散进粉丝收件箱,粉丝量超阈值的账号不做扩散、由读时拉取合并;扇出任务分片并行、支持断点续跑。大粉丝量账号的发布接口 P95 由约 5.4s 降至 130ms 上下,单次发布的同步数据库写入由数十万条降为 1 条
    展开完整拆解
    为什么要这么设计

    第一版是纯写扩散(推模式):用户发布内容时,遍历他的粉丝列表,给每个粉丝的收件箱插一条记录。这个设计对普通用户很好——读的时候直接查自己的收件箱,一次查询搞定。

    问题出在粉丝量的分布是极度不均的。普通用户几十个粉丝,扇出几十条记录,几毫秒完成;但一个有 50 万粉丝的账号发一条内容,要插 50 万条记录。同步做的话接口要几秒甚至几十秒,用户以为发布失败反复重试;而且这几十万次写入会把数据库打满,影响所有其他业务。

    更麻烦的是浪费——50 万粉丝里当天活跃的可能只有几万,给不活跃用户的收件箱写记录,他们压根不会来看。

    所以设计成推拉结合:按粉丝量分流,普通用户继续推(读得快),大 V 改成拉(不扩散,读时实时查)。这是典型的按数据分布特征分级处理,也是社区系统里最经典的取舍。

    整体链路
    发布内容 │ ├─ 写内容表(1 次同步写)→ 立即返回成功 │ └─ 发扇出事件 ──▶ 队列 │ ├─ 判断作者粉丝量 │ ├─ 粉丝量 < 阈值(普通用户) │ └─ 分片遍历粉丝 → 批量写收件箱 │ 每片 1000 个粉丝,独立可重试 │ 记录进度(断点续跑) │ 只写活跃粉丝(近 N 天登录过) │ └─ 粉丝量 >= 阈值(大 V) └─ 不做扩散,只更新「大 V 最新发布」缓存 读关注流 │ ├─ 查自己的收件箱(推来的,普通关注对象的内容) │ ├─ 查自己关注的大 V 列表(缓存) │ └─ 并行拉各大 V 的最新内容(缓存,命中率高) │ ├─ 按时间归并两部分 + 去重 │ └─ 游标分页返回(游标同时编码收件箱位点和时间边界)

    大 V 走拉模式之所以可行,是因为大 V 的数量少、内容会被大量用户读取,缓存命中率极高。一个大 V 的最新内容列表被缓存后,几万个粉丝读的是同一份缓存,成本极低。这和写扩散给几十万人各存一份形成鲜明对比。

    分步拆解
    1. 发布接口只做一次同步写。写内容表然后立刻返回,扇出通过消息异步做。这是发布接口从 5.4s 降到 130ms 的全部原因——把不必要的工作移出请求链路,这比任何微观优化都有效。
    2. 扇出任务分片。50 万粉丝不能一个任务做完(执行时间太长、失败重试代价大、消息可见性超时)。按每片 1000 个粉丝拆成 500 个子任务,各自独立执行和重试。要记录进度,任务中断后从断点继续而不是重头开始。
    3. 只给活跃粉丝写收件箱。近 N 天没登录过的粉丝跳过。他们下次登录时走「拉」的路径补齐(查关注列表的近期内容)。这一步能把扇出量减少一大半,是成本优化的关键。
    4. 阈值怎么定。不是拍脑袋,是看粉丝量的分布——统计所有账号的粉丝量分位数,找到一个「超过这个数的账号只占极少数,但他们的扇出量占总扇出量的大部分」的点。通常在几千到几万这个量级。阈值要可配置,因为随着业务发展分布会变。
    5. 大 V 的内容缓存要主动更新。大 V 发布时立刻刷新「该作者最新内容列表」缓存,而不是等缓存过期。因为这份缓存会被几万人读,如果失效瞬间几万个请求同时回源就是缓存击穿。主动更新 + 不设短过期是这类超热点数据的标准做法。
    6. 读时归并要处理去重。如果一个作者的粉丝量刚好在阈值附近波动,可能出现「一部分内容推过了、一部分没推」,归并时会重复。按内容 ID 去重即可。阈值变化时的边界情况必须考虑到,这是设计里容易漏的地方。
    7. 取关和删除要清理。用户取关某个人后,收件箱里那个人的历史内容应该消失(或者至少不再展示)。不要同步删除几万条记录,做法是读取时按当前关注关系过滤,收件箱的脏数据由定时任务慢慢清理。
    关键决策与取舍

    为什么不全部走拉模式(省掉收件箱)。纯拉模式下读关注流要「查关注的 N 个人,各查他们的最新内容,归并排序」。关注 500 人的用户,一次读要发起 500 次查询——读的成本转移到了每一次读,而读的次数远多于写。社区场景是读多写少,把成本压在写侧(而且可以异步)显然更划算。所以普通用户必须走推。

    推拉结合的复杂度代价。要维护两套路径、要处理归并去重、游标要同时编码两种位点、阈值变化时有边界问题。代码复杂度明显高于单一模式。这个复杂度值不值得,取决于粉丝量分布是不是真的极度不均——如果所有用户粉丝量都差不多,就不该引入这个复杂度。面试时主动说「如果分布均匀我不会这么做」,能体现你是按数据决策而不是套架构。

    踩过的坑:扇出消息堆积导致关注流延迟。活动期间大量用户同时发布,扇出任务在队列里积压,粉丝要几分钟后才在关注流看到新内容。修法是把扇出队列按优先级拆开——粉丝量小的任务(占绝大多数、执行快)走高优先队列,粉丝量大的走低优先队列慢慢跑。这样普通用户的体验不受少数大账号影响。教训是:同一类任务如果执行成本差几个数量级,就不该共用一个队列。

    没做的部分:没做收件箱的定长裁剪。理论上收件箱应该只保留最近的 N 条(比如 1000 条),更早的从关注列表实时拉。我们是无限累积加定时清理,长期会有存储浪费。

    数字是怎么测的

    发布接口 P95:造一个有 50 万粉丝的测试账号,压测发布接口。改造前约 5.4s(同步扇出),改造后 130ms 上下(只写内容表加发消息)。粉丝量必须说出来——「发布接口从 5.4s 降到 130ms」如果不说是多少粉丝的账号,这个数字就没有意义(普通用户压根没有 5.4s 的问题)。

    同步写入次数:从数十万降为 1。这是个结构性的数字,比耗时更能说明问题——它直接说明「同步链路的工作量与粉丝量解耦了」。

    扇出完成时间:要额外报这个,否则会被追问「你只是把慢转移到了后台」。实测 50 万粉丝的扇出在分片并行下几十秒内完成,粉丝侧感知的延迟在可接受范围。主动给出异步链路的耗时,说明你知道异步不等于免费。

    面试追问
    Q:大 V 走拉模式,用户关注了 100 个大 V,读关注流不就要拉 100 次吗? A:确实要拉,但成本可控,原因有三。一是缓存命中率极高,大 V 的最新内容列表是超热点数据,几乎必然命中缓存,一次拉取是亚毫秒级的缓存读而不是数据库查询;二是可以并行加批量,用 pipeline 一次批量取 100 个 key,不是 100 次往返;三是关注大量大 V 的用户是少数。真要进一步优化,可以给「关注了很多大 V 的用户」单独做一份合并缓存,或者退回到给这部分用户做写扩散——因为这类用户少,扩散成本可控。这就是推拉结合的灵活性:可以按用户维度再分一次流。
    Q:只给活跃粉丝写收件箱,不活跃的粉丝回来之后怎么补齐内容? A:登录时触发一次补齐任务:查他关注的人在他离线期间发布的内容,写入收件箱(或者直接在读时归并,不落收件箱)。要限制补齐的时间范围和数量,比如只补最近 7 天的前 200 条——用户离线半年,压根不需要把半年的内容全补上。这里有个体验判断:不活跃用户回来看到的应该是「最近的精选」而不是「按时间倒序的半年积压」,所以补齐时可以按热度而不是纯时间取。
    Q:一个用户在扇出过程中取关了作者,会怎样? A:会给他的收件箱写入一条本不该有的记录(因为扇出任务用的是发布时刻的粉丝快照)。这是可接受的不一致,因为读取时会按当前关注关系过滤,用户不会看到。反过来的情况是「扇出过程中新增了粉丝」,这个粉丝拿不到这条内容——他会通过「新关注后拉取该作者近期内容」的逻辑补上(关注成功后通常会预填一部分该作者的历史内容到关注流)。要点是:异步扇出必然基于快照,所以读侧必须有过滤和补齐机制,不能假设收件箱是精确的。
    Q:收件箱存在哪?MySQL 还是 Redis? A:看容量和成本。Redis 的 ZSet 最合适(按时间打分、天然有序、分页方便),但全量用户的收件箱放内存成本很高。我们的做法是分层:活跃用户的收件箱在 Redis 并定长裁剪(只保留最近若干条),冷用户的收件箱只在 MySQL,登录时按需加载到 Redis。纯 MySQL 方案也能做,但要注意收件箱表会非常大(用户数乘以人均条数),必须按用户 ID 分表,而且写入量大的时候索引维护成本高。选哪种取决于活跃用户规模和预算,这个权衡要说出来而不是直接给一个答案。

模块三:内容安全审核链路

  1. 内容安全审核链路(先发后审 + 机审人审串联 + 分级处置)★★★
    简历这样写 内容安全审核链路(Spring Boot + 消息队列 + 三方机审 + 状态机 + 人工审核队列):设计「先发后审 + 分级可见」的审核链路,机审结果分为通过 / 拒绝 / 待人审三档,待人审内容限流量可见(仅作者与粉丝可见);机审依赖三方服务,做超时降级、重试与结果幂等回写。发布到公开可见的中位延迟约 6 秒,机审服务不可用时发布链路不阻塞(转人工队列),审核状态变更全程可追溯。
    展开完整拆解
    为什么要这么设计

    最初的方案是「先审后发」:用户发布后进审核队列,审核通过才可见。这个方案安全但体验极差——用户发完看不到自己的内容,以为失败了,反复重发;机审排队时延迟到几十秒甚至几分钟;机审服务抖动时发布功能等于瘫痪。

    另一个极端是「先发后审」:立刻可见,审核不通过再删。体验好但违规内容有一个暴露窗口,在有本地法规要求的市场是不能接受的。

    最后的方案是两者结合的「先发后审 + 分级可见」:内容立刻对作者可见(作者体验和先发后审一样好),机审通过后才进入公开分发。关键洞察是「可见」不是一个布尔值,而是有范围的——仅自己可见、仅粉丝可见、公开可见是三个不同的状态。把这个维度打开之后,安全和体验的矛盾就有了解空间。

    整体链路
    发布 │ ├─ 写内容,status = 待审核,visibility = 仅作者可见 ├─ 立即返回成功(作者刷新自己主页能看到,带「审核中」标记) │ └─ 发审核事件 ──▶ 队列 ──▶ 审核服务 │ ├─ 本地前置:关键词库、黑名单、频率限制 │ └─ 命中高危词 → 直接拒绝,不调三方 │ ├─ 调三方机审(文本 / 图片 / 视频抽帧) │ ├─ 超时 / 熔断 → 转人工队列(不阻塞) │ └─ 重试用同一 requestId(幂等) │ └─ 结果分三档 ├─ 通过 ──▶ visibility = 公开 ├─ 拒绝 ──▶ status = 驳回 + 原因通知作者 └─ 不确定 ─▶ visibility = 仅粉丝可见 + 进人工队列 │ ├─ 人工通过 ──▶ 公开 └─ 人工拒绝 ──▶ 下架 + 通知 + 可申诉 结果回写:以 contentId + 审核轮次 做幂等,重复回调只生效一次 状态流转:条件更新(where status = 期望值),非法流转直接拒绝 全程留痕:每次状态变更写审核日志(谁/什么时候/依据什么)
    分步拆解
    1. 本地前置过滤先跑。关键词库、用户黑名单、发布频率限制这些在本地做,命中高危规则的内容直接拒绝,不调三方。这一步能拦掉一部分明显违规的,省掉三方调用费用和时间。关键词匹配用 AC 自动机之类的多模式匹配,几万个词的匹配也是微秒级。
    2. 机审结果必须是三档而不是两档。「通过 / 拒绝」的二分法会导致机审在不确定的情况下被迫做选择——判通过则漏放违规内容,判拒绝则误杀正常内容。加一个「不确定」档转人工,把机器不擅长的判断交给人。三方机审服务通常会返回置信度,按置信度分档。
    3. 「不确定」的内容限范围可见。这是这个设计的精髓——不是完全不可见(那和先审后发一样伤体验),而是只对作者和粉丝可见,不进公开分发。这样即使真的违规,扩散范围也被限制在小圈子;如果是误判,作者的粉丝仍然能看到,作者的核心体验没有受损。
    4. 三方依赖必须做熔断降级。机审服务不可用时不能阻塞发布链路。做法是超时后直接把内容转人工队列,标记「机审未完成」。这样发布功能仍然可用,只是人工审核压力上升。要有告警,让运营知道要临时加人。
    5. 回调幂等。三方的异步回调可能重复(他们也会重试)。用「内容 ID 加审核轮次」做幂等键,重复回调直接忽略。状态流转用条件更新where status = '待审核'),非法的状态流转(比如已经人工通过了,机审的迟到回调又来判拒绝)要被拒绝而不是覆盖。
    6. 人工队列要分优先级。不能先到先审。按内容热度、作者等级、疑似违规类型排优先级——一条正在快速传播的疑似违规内容必须优先审,一条没人看的可以慢慢排。这个优先级逻辑是审核系统的核心价值之一。
    7. 全程留痕和申诉。每次状态变更写日志(时间、操作者、依据、原始机审结果)。作者对驳回不服可以申诉,申诉进独立的复审队列。留痕不只是为了合规,也是为了排查误杀和优化规则——没有日志压根不知道机审在哪些类型上误判率高。
    关键决策与取舍

    「分级可见」不是所有内容都适用。对于高危类别(涉及法规红线的),必须先审后发,不能有任何暴露窗口。所以实际是按内容类型分流:普通图文走「先发后审 + 分级可见」,涉及特定类别(比如带商品链接的、涉及医疗健康的)走「先审后发」。用一套统一策略覆盖所有内容是错的,安全等级不同的内容要走不同的链路。

    误杀和漏放的取舍方向。机审的阈值调松则漏放多,调紧则误杀多。我们的选择是向「宁可多转人工」倾斜——因为误杀会直接伤害创作者(他们会流失),而多转人工只是增加运营成本。但这个方向不是绝对的,在高危类别上必须反过来(宁可误杀)。说清「取舍方向随内容类别变化」,比给一个统一答案更专业。

    踩过的坑:机审回调迟到覆盖了人工结论。一条内容机审超时转了人工,人工审核通过并公开了;十分钟后三方的机审回调迟到,结果是「拒绝」,直接把内容下架了。作者投诉。修法是状态机加严格的流转约束——人工结论的优先级高于机审,已经人工终审的内容,机审回调只记日志不改状态。这个坑的本质是「多个来源都能改同一个状态」,必须定义清楚优先级和终态。

    没做的部分:没做机审规则的效果回归。理论上应该定期用人工标注的样本回测机审准确率,指导阈值调整。我们只做了误杀的个案分析,没有系统性的评估机制。

    数字是怎么测的

    「发布到公开可见的中位延迟约 6 秒」:从审核日志里取「内容创建时间」到「visibility 变为公开的时间」的差值,取中位数。用中位数而不是平均值,因为转人工的那部分会拉高平均值到分钟级甚至小时级,平均值反映不出大多数用户的体验。被追问时要能给出分位数分布——中位 6 秒、P90 多少、转人工的占比多少。只报一个中位数容易被认为在藏问题。

    不要报「违规内容拦截率」或「审核准确率」。这两个数字都需要「真实违规总量」作为分母,而那个数字压根不可知(没被发现的违规内容不在统计里)。这类分母不可知的比率是最容易被问穿的。可以报的是绝对数:每天机审处理量、转人工量、人工驳回量、申诉成功量。

    「机审不可用时不阻塞」怎么验证:做故障演练——把机审服务的地址指向一个不可用的端点,验证发布功能仍然正常、内容正确转入人工队列、告警正常触发。这类可用性声明必须有演练支撑,说「我们做了降级」但没验证过,一问「你怎么知道它有效」就答不上来。

    面试追问
    Q:先发后审有暴露窗口,法务或者监管不接受怎么办? A:那就按类别分流,高危类别走先审后发。实际上这也是必须的——不同市场的法规要求不同,同一套内容在某些国家可以先发后审,在另一些国家必须先审后发。所以审核策略要做成可配置的、按「内容类别 × 市场」维度的规则表,不能硬编码。这也解释了为什么审核系统的复杂度主要来自配置和规则管理,而不是技术实现。
    Q:视频内容怎么审?总不能把整个视频都送去机审吧。 A:抽帧加音频转文字。视频按固定间隔(比如每秒或每几秒)抽帧做图像审核,音轨转成文字做文本审核,加上标题、描述、封面单独审。抽帧间隔是成本和覆盖率的取舍——间隔太大会漏掉一闪而过的违规画面,太小则费用和耗时都上去了。进阶做法是关键帧优先加变化检测:画面变化大的地方多抽几帧,静态画面少抽。另外封面必须重点审,因为封面是分发时唯一展示的图,违规封面的影响面比视频内部的画面大得多。
    Q:人工审核队列积压了几十万条,怎么处理? A:先看积压的原因。如果是机审服务不可用导致的,恢复后重新走机审就能消掉大部分。如果是机审阈值太紧导致转人工过多,调整阈值。如果是真的量上来了,那就要在策略上做取舍:按优先级审(热度高的先审,长尾的可以等);对低风险内容放宽(比如老账号、历史无违规的作者,其内容可以先公开后抽检);极端情况下对特定类别临时收紧发布。绝对不能做的是「为了消积压批量通过」——那等于没有审核,而且一旦出事无法追溯。这里的原则是:可以降低覆盖率,但不能伪造审核结论。
    Q:作者的内容被误杀了,申诉链路怎么设计? A:申诉要走独立的队列和独立的审核人——不能让做出原判定的人复审自己的结论,那基本不会改。申诉队列的优先级要高(作者在等),并且申诉成功的案例要回流到规则优化,作为机审误判的样本。还有两个细节:驳回原因要具体(只说「违反社区规范」作者压根不知道改什么,也无法有效申诉);申诉次数要限制,否则会被恶意刷。另外从产品角度,申诉期间内容的可见状态怎么处理是个判断——保持下架更安全,但如果误杀率高会持续伤害作者,我们的做法是保持下架但把申诉的处理时效压得很短。

没有匹配的内容,换个关键词试试。

项目拆解 · 社区互动与消息(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据