面灵AI→

海信聚好看一面

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 如果从零开发一个 Agent,应该从哪些方面考虑?
  3. 举一个实际做过的 Agent 例子:怎么描述,用什么模型,怎么找到 Skill,Skill 里大概是什么逻辑?
  4. 让 AI 编码,只给大模型详细的设计规格书就够了吗?
  5. 有没有参与过设计知识库?介绍知识库的种类和用法。
  6. 你用 Redis 缓存是为了写数据还是为了查询场景?
  7. 你的项目限流是怎么做的?用了什么技术?
  8. 令牌桶的存储和初始值是怎么设计的?
  9. 令牌桶是自己写的还是引入的?为什么不用 Sentinel?
  10. 怎么做压测的?用了什么工具?压测的是读接口还是写接口?
  11. 压测时预期是缓存扛压力还是数据库扛压力?
  12. 数据库数据变化后,怎么更新缓存?
  13. 缓存方面有没有做一些防护措施?
  14. 一个类有父类,父类和子类都有静态变量、静态代码块、非静态变量和构造函数,加载顺序是怎样的?
  15. Synchronized 加在静态方法和非静态方法上有什么区别?
  16. ThreadLocal 和 Synchronized 有什么区别?
  17. 堆内存的结构是什么样子的?一个对象在堆内存中是怎么流转的?
  18. 运行时异常需要自己捕获吗?
  19. 有没有对设计模式、DDD 等有过学习和应用?

《参考解析》

从零设计一个 Agent,先回答四个问题。第一是边界:任务到底是「流程固定、只是步骤多」,还是「要按上下文动态决定下一步」。前者写成 Workflow,用代码把状态机钉死,稳定、可测、便宜;后者才值得上 Agent,因为它的价值就在于根据观察结果换策略。第二是工具面:Agent 的能力上限由工具决定,工具要「一个工具做一件事、入参出参结构化、失败有可读原因」,返回统一成 {ok, data, error},模型才能判断成败。第三是状态与记忆:短期上下文放最近几轮,长期记忆落库,每一轮进模型前把「已完成 / 待完成 / 已知事实」显式带上,否则模型只能靠猜,表现为反复重试同一个动作。第四是终止与兜底:最大步数、最大重试、单步超时、总 token 预算都是写死在代码里的硬参数,不是靠 prompt 劝说;触顶就走降级路径并如实说明未完成。补上可观测性(每步的输入、决策、备选方案、耗时留 trace),这套答法比背概念有说服力得多。

Skill 与「只给规格书够不够」。Skill 是面向场景的能力封装:底层是一个或几个工具调用,外面套一层业务语义、prompt 模板和输入输出契约。契约要稳定,参数发布后不能随便改名——真改了就要加版本兼容层,旧参数名映射到新参数名,否则调用方会一起挂。「只给详细设计规格书就够了吗」,答案是远远不够,规格书解决「做什么」,不解决三件事:事实来源(模型不知道你代码库里既有的约定、工具函数和数据结构的真实形状,容易凭空造一套)、验收标准(没有可执行测试,模型会停在「看起来写完了」)、异常路径(正常路径它写得不错,网络分区、超时、空输入、并发写要人先列清单再要求逐条覆盖)。所以规格书必须配三样东西:可参考的既有代码、可运行的验收用例、明确禁止改动的范围。

缓存的定位、更新与防护。Redis 在项目里绝大多数是查多写少的场景:读请求先查缓存、未命中回源并回填,回填的过期时间加随机偏移避免集体失效;写请求走 Cache-Aside,先更新数据库再删除缓存而不是更新缓存——删除更安全,因为并发下「更新缓存」可能把旧值写进去。防护就是三个经典问题:穿透用布隆过滤器或短 TTL 空值缓存挡住不存在的数据,击穿给热点 key 加互斥重建(只有一个线程回源),雪崩靠过期时间打散、Redis 高可用加本地缓存兜底。更彻底的做法是订阅 binlog 异步失效,把删除动作从业务代码里挪出去。至于压测,读接口和写接口要分开压:读压力靠缓存扛,写压力最终靠数据库扛,所以要分别压出缓存命中率的拐点和数据库连接数的拐点,工具用 JMeter 或 wrk 都行,关键是看 P99 而不只是平均值。

限流与 Sentinel 的取舍。令牌桶要能扛分布式,桶状态不能只放单机内存,通常用 Redis + Lua 做原子取令牌;初始容量等于允许的突发量,速率等于稳态 QPS,取不到就返回 429 或排队。自己写桶的好处是逻辑透明、可以按用户 / 接口 / 租户灵活组合,坏处是它只是 Sentinel 能力的一小块。Sentinel 同时提供流控、熔断降级、系统自适应保护和实时监控面板,规则可动态下发,生产上出问题改阈值不用发版。选型看需求:只要一把简单限流器就自己写,需要整套「限流 + 熔断 + 降级 + 控制台」就直接用 Sentinel。

类加载与初始化顺序。JVM 里「加载」和「初始化」是两件事,问顺序一般指初始化:父类先于子类,静态先于非静态,变量赋值与代码块按源码书写顺序执行,构造器最后。完整链路是父类静态变量与静态代码块(只执行一次)→ 子类静态变量与静态代码块 → 父类非静态变量与代码块 → 父类构造器 → 子类非静态变量与代码块 → 子类构造器。两个易踩点:静态方法里不能直接访问非静态成员,因为此时对象还不存在;静态代码块初始化抛异常会包成 ExceptionInInitializerError,之后访问该类直接报 NoClassDefFoundError。

Synchronized 与 ThreadLocal。synchronized 修饰非静态方法锁的是当前实例(this),修饰静态方法锁的是该类的 Class 对象,两者互不影响,可以并行。它解决互斥,代价是同一把锁上的线程串行化。ThreadLocal 解决隔离:每个线程各拿一份副本,典型用途是请求上下文(用户身份、traceId)和 SimpleDateFormat 这类非线程安全对象的线程私有化。二者互补而非替代,ThreadLocal 还有两个坑:线程池里线程会复用,用完不在 finally 里 remove 就会串数据,且 ThreadLocalMap 的 key 是弱引用、value 是强引用,key 被回收后 value 仍挂在 Entry 上造成泄漏;它也不会让共享对象变安全,往里放可变对象、多线程拿到同一份引用照样出问题。

堆内存结构与对象流转。堆按生命周期分代:新生代 Eden 与两个 Survivor 默认 8:1:1,新对象先在 Eden 分配(大对象直接进老年代,栈上分配与标量替换是 JIT 的额外优化),Minor GC 后存活对象在 Survivor 间复制并累加年龄,到阈值晋升老年代;老年代放长生命周期对象,靠 Major/Full GC 回收。每一跳都可能出问题:晋升阈值太小会让短命对象污染老年代,Survivor 太小会让对象提前晋升,大对象频繁分配直接压老年代;排查靠 GC 日志与堆转储,区分「新生代回收太频繁」(分配速率问题)和「老年代增长过快」(泄漏或缓存过大)。运行时异常则不是「必须捕获」的——空指针、非法参数属于编程错误,应在开发期暴露而不是 catch 掉;真正要捕获的是可恢复或需要转换语义的边界(外部调用、用户输入、资源释放),且捕获后必须打日志或转成有意义的异常抛出,绝不能空 catch 吞掉。DDD 同理,它是划分边界的方法(限界上下文、聚合、领域事件、防腐层),不是目录规范,业务规则复杂到值得单独建模型时才引入。