京东 · 零售商品审核 Agent

后端开发工程师面经合集

完整信息

65 条问法 · 51 道题

本篇目录

面试官开口不是自我介绍,是「你这个 92.3% 是怎么算的」。我愣了两秒,就知道今天不好过。

时间:11.03 一面 60min,11.05 二面 85min 结果:二面完进池子

京东这两面给我最大的感受是,他们不聊"Agent 是什么",只聊"你这一个数是怎么来的"。一个数讲不清楚,后面二十分钟全在拆它。

一面:从图片和规则开始

  1. 自我介绍,说重点。
  2. 你这个审核系统,输入是什么?纯文本还是带图?
  3. 一张商品图里同时有文字、logo、水印、二维码,你怎么分别处理?
  4. OCR 识别错了,比如把"0"认成"O",后面链路会怎么样?
  5. 你会不会把 OCR 结果直接喂给模型?
  6. 不喂的话,中间那一层做了什么?
  7. 图和文冲突:图片写"纯棉",详情写"聚酯纤维",你信谁?
  8. 这个冲突你自己判过吗?还是靠规则?
  9. 规则引擎有多少条规则?谁维护?
  10. 规则之间打架了怎么办?优先级怎么定?
  11. 新增一条规则,怎么验证不会误伤历史商品?
  12. 你有没有跑过全量回归?跑一次多久?
  13. 模型和规则,谁先谁后?为什么这么排?
  14. 模型判"不确定"的时候,走人工还是走规则?
  15. 人工审核队列一天多少单?人均处理多少?
  16. 审核人疲劳之后准确率会掉,你怎么发现?
  17. 你怎么衡量一个审核人靠不靠谱?
  18. 误杀一个正常商品,商家损失是什么?你怎么算这个数?
  19. 漏放一个违规商品,平台损失是什么?
  20. 这两个损失不对等,你的阈值往哪边偏?
  21. 阈值 0.8 是谁定的?业务签字了吗?
  22. 调到 0.9 通过率掉多少?测过吗?
  23. 92.3% 是准确率还是召回率?分母是什么?
  24. 测试集多少条?谁标的?标注一致性多少?
  25. 这个数里有没有把"模型没判、规则兜住了"的算进去?
  26. 如果把规则那部分剔掉,模型单独多少?
  27. 你怎么证明提升是你的改动,不是换了模型?
  28. 商家申诉了,链路怎么走?申诉成功之后数据怎么回流?
  29. 回流的数据要不要重新标?谁标?
  30. 反问。

(第 26 题我答不上来。我们线上从来没单独测过模型,一直是整套一起算的。)

二面:往系统底下压

  1. 一面问了什么,你复盘一下。
  2. 这个系统的峰值 QPS 多少?什么时候?
  3. 大促期间量翻三倍,第一个撑不住的是哪?
  4. 图片上传和审核结果,中间是同步还是异步?
  5. 异步的话,商家页面显示什么?
  6. 商家刷新页面,你怎么保证不重复提交一次审核?
  7. 幂等键怎么设计的?业务单号还是内容哈希?
  8. 同一张图换了个文件名重传,内容哈希一样吗?
  9. 幂等记录存多久?存 MySQL 还是 Redis?
  10. 表数据量到千万级你怎么分?按什么分片?
  11. 分片键选商家 id 还是时间?各有什么问题?
  12. 按商家分片,一个大商家把某个分片打爆了怎么办?
  13. 商品检索你用 ES 吗?索引怎么建的?
  14. ES 和数据库的一致性怎么保证?
  15. 双写失败了一半,怎么修?
  16. 你用 MQ 了吗?哪一步用?
  17. 消费失败返回什么?返回成功会怎么样?
  18. 一条消息重复消费,审核结果会不会写两遍?
  19. 消息积压了先看什么?
  20. 审核服务线程池参数怎么定的?
  21. 图片 OCR 是 IO 密集还是 CPU 密集?线程数该往哪边调?
  22. 你用虚拟线程了吗?为什么没用?
  23. ThreadLocal 在虚拟线程里有什么问题?
  24. 线上接口 P99 突然从 200ms 涨到 2s,你怎么排查?
  25. 怎么区分是下游慢还是自己 GC 了?
  26. Full GC 频繁你会看哪些指标?
  27. 缓存命中率多少?怎么算的?
  28. 命中率掉了但 QPS 没变,可能是什么原因?
  29. 你给模型加了缓存吗?什么粒度?
  30. prompt 差一个字缓存就失效,你怎么设计 key?
  31. 反问。

手撕:按 key 串行、跨 key 并行,还要能取消

题目原话:实现一个执行器 submit(key, task)。相同 key 的任务按提交顺序串行,不同 key 可以并行。加两个条件:任务能被取消,服务关闭时正在排队和正在跑的任务要能优雅收尾,不许丢任务也不许卡死。

这题毒在"优雅收尾"。大多数人写到"每个 key 一个队列 + 一个 worker"就停了,取消和停机一加进来,前面的结构就得重来。

思路:

  • 每个 key 一条队列,队列上挂一个"是否已有 worker"的标志,没 worker 才起,避免一个 key 起一堆线程。
  • 取消要分两种:还在排队的直接摘掉,正在跑的只能置标志,靠任务自己让出。
  • 停机先关入口(不再收新任务),再等队列清空,最后才停线程池。顺序反了就会丢任务。
import threading, queue
from collections import defaultdict

class KeyedExecutor:
    def __init__(self, n_workers=8):
        self.pool = []
        self.lock = threading.Lock()
        self.queues = defaultdict(queue.Queue)   # key -> 任务队列
        self.active = set()                      # 已有 worker 的 key
        self.closed = False
        self.running = {}                        # task_id -> cancel flag

    def submit(self, key, task):
        with self.lock:
            if self.closed:
                raise RuntimeError("executor closed")
            t = {"id": task.id, "fn": task.run, "cancel": threading.Event()}
            self.running[t["id"]] = t["cancel"]
            self.queues[key].put(t)
            if key not in self.active:
                self.active.add(key)
                th = threading.Thread(target=self._drain, args=(key,), daemon=True)
                self.pool.append(th)
                th.start()
        return t["id"]

    def cancel(self, task_id):
        flag = self.running.get(task_id)
        if flag:
            flag.set()          # 正在跑的靠协作式取消
            return True
        return False

    def _drain(self, key):
        q = self.queues[key]
        while True:
            t = q.get()
            if t is None:                # 停机哨兵
                q.task_done()
                break
            if not t["cancel"].is_set():
                try:
                    t["fn"](cancel_flag=t["cancel"])
                except Exception:
                    pass
            self.running.pop(t["id"], None)
            q.task_done()
        with self.lock:
            self.active.discard(key)

    def shutdown(self, wait=True):
        with self.lock:
            self.closed = True           # 1. 先关入口
            keys = list(self.queues.keys())
        for k in keys:
            self.queues[k].join()        # 2. 等每个 key 排空
        for k in keys:
            self.queues[k].put(None)     # 3. 再放哨兵收 worker
        if wait:
            for th in self.pool:
                th.join(timeout=5)

追问四个:

  • 一个 key 堆积了一万条任务,其他 key 会不会被饿死?
  • 你用了一个 key 一个线程,key 有几万个的时候怎么办?
  • cancel 只置标志,任务里跑的是一个不带取消语义的第三方库,怎么停?
  • shutdown 的时候有任务正好在 submit 和 put 之间,会不会漏?

第三个我说"那就停不了,只能等超时"。他说"所以取消这件事得在任务协议层就定好,不是执行器能补的"。

这句我记下来了。

出来之后

京东这两面,我最大的问题不是不会,是没拆过自己的数。

92.3% 写了半年,从来没人问过我分母是什么。真被问到的时候,我才发现自己也不确定。

面完在楼下买了杯咖啡,把项目里所有指标翻出来重新算了一遍。算完出了一身汗。

各位写简历的时候,是不是也有一两个数字自己没拆过?评论区聊聊,我看看是不是只有我这样。

#秋招面试记录# #面试AI岗经常会被问到哪些问题?#

岗位、轮次与题面基于牛客近一周公开面经整理重排,细节有改写与合并,不指向任何具体个人。

本篇目录
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
    实现一个执行器 `submit(key, task)`。相同 key 的任务按提交顺序串行,不同 key 可以并行。加两个条件:任务能被取消,服务关闭时正在排队和正在跑的任务要能优雅收尾,不许丢任务也不许卡死。
  62. 62
  63. 63
  64. 64
  65. 65

同作者面经

展开其余 13 篇收起其余 13 篇

相关公司