面灵AI→

老虎国际后端实习一二面面经:虚拟线程、Agent 工具拦截与文档协作

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

《面试题目》

一面(30 分钟)

  1. 介绍一下你的实习经历。
  2. 做这个 Coding Agent 的初衷是什么?
  3. 它是基于框架做的还是从零开始做的?
  4. 如果用户输入”修改用户昵称接口,把字数校验从 30 字改成 36 字”,你的 Agent 接下来会怎么做?
  5. glob、文件读取和文件修改这些工具也是从零做的吗?
  6. 多个 Agent 同时修改文件时怎么处理?
  7. Agent 写出的代码和你手写的代码相比,谁的质量更好?
  8. 日常使用什么版本的 JDK?Java 21 有什么比较重要或者好用的特性?
  9. 虚拟线程什么时候使用?
  10. “使用虚拟线程可以降低 RT、提高接口响应速度”,这句话对吗?
  11. 如果要 fan-out 六个并发查询用户的不同信息,再把结果汇总成一个最终响应,你通常会怎么做?
  12. 你使用过队列吗?使用过 MQ 吗?在什么场景下使用 RabbitMQ?
  13. 异步链路的一致性如何保证?失败如何兜底?

二面(37 分钟)

  1. 介绍一下实习经历。
  2. Agent 工具如何与外界交互?模型如何通过 Agent 与外部交互?
  3. Agent 错误调用了某些接口,有什么方法可以发现并及时制止?
  4. 从工具层面看,怎么识别和拦截模型因幻觉或理解偏差导致的错误操作?
  5. 场景题:人和 AI 一起编辑飞书文档,人修改了云端内容,但 AI 拿着本地旧快照执行全量替换,把人的修改覆盖了。编辑本身是正常操作,也不能直接禁用编辑接口,你会怎样解决这个问题?
  6. 如果直接通过数据库约束多人编辑,应该怎样设计?例如一个部门几百人同时填写休假时间和联系方式,写入非常密集,怎样避免互相覆盖?
  7. 如何避免重复消费?
  8. 支付场景:用户下单后扣款,即使消息重复,也只能扣一次,应该用什么方式保证?
  9. 假设你做号码生成器,不依赖外部,所有业务都向你申请 ID,存在高并发的情况,你如何设计?
  10. Java 内存管理是怎样的?垃圾回收机制是怎样的?讲一下垃圾回收里的 STW 机制。
  11. 你用 AI 做过哪些事情?接触过推荐算法吗?
  12. 如果让你做你完全不知道的领域,你如何配合 AI 去做?如何辨别 AI 给出的回答的错误?

《参考解析》

“虚拟线程能降低 RT”这句话不准确。虚拟线程解决的是吞吐与线程数量的问题,不是单次请求的延迟。它的价值在于:当线程大量阻塞在 IO 上时,不再需要为每个请求占用一个昂贵的平台线程,于是可以用更少的 OS 线程支撑更高的并发,减少线程池排队带来的等待。RT 主要由下游依赖、代码路径和锁竞争决定,虚拟线程本身不会让它变快。它还有明确的适用边界:适合阻塞式 IO 密集的场景,但遇到 synchronized 块内阻塞、JNI 调用、或者被 pinned 住的情况会失去调度优势;CPU 密集型任务用它没有收益。JDK 21 里它是正式特性,写法上就是 Thread.ofVirtual() 或 Executors.newVirtualThreadPerTaskExecutor(),但要注意别再叠一层固定大小的线程池,那会把虚拟线程的优势抵消掉。

fan-out 六个并发查询再汇总:用 CompletableFuture 配合自定义线程池(不要用默认的 ForkJoinPool.commonPool,容易被别的任务饿死),每个查询一个 future,用 allOf 汇总,再按需 join。关键细节是:超时(orTimeout 或 get(timeout),避免一个慢依赖拖死整体)、异常处理(allOf 在任一失败时会直接抛,要 handle 让失败的子任务降级为空值而不是整体失败)、线程池隔离与容量(不同下游用不同池,防止一个下游变慢把池占满)、以及结果顺序(用索引数组而不是完成顺序收集,保证汇总逻辑稳定)。

人机协作编辑的覆盖问题:本质是乐观并发控制。彻底的做法是放弃”全量替换”,改成基于操作或差分的合并:给文档维护一个版本号(或每个区块独立版本),AI 提交时带上它读到的版本号,服务端比对——版本没变才允许写入,变了就返回冲突,让 AI 重新拉取最新内容并做三方合并(base、本地修改、远端修改)。如果只能走全量替换,也要至少改成”仅替换实际发生变化的区块”,把冲突面从整篇缩小到局部。产品层还可以做软提示:AI 提交前检查云端版本是否变化,变化就暂停并提示”文档已被他人修改,是否重新读取”。

高并发写入的互相覆盖:几百人同时填休假和联系方式,用行锁会锁争用严重,用悲观锁更糟。可行设计有三条:按人拆行(每人一行,行级更新天然不冲突,这是最简单也最有效的解法);列级 / 区块级更新(只更新实际改动的字段,而不是整行覆盖,避免”两个人改不同字段也互相覆盖”);乐观锁(行上带 version,更新时 WHERE id=? AND version=?,失败就重查重试)。另外可以用 upsert 语义 + 分区降低热点,把写入按用户 id 散列到不同分片。要点是:先问清”冲突到底发生在什么粒度”,再选锁的粒度——很多覆盖问题根本不需要锁,只需要别整行覆盖。

幂等消费与”只能扣一次”:最可靠的做法是唯一键落库而不是靠内存判断。具体有几种:消费端建一张去重表,用业务唯一键(订单号 + 操作类型)建唯一索引,插入成功才执行业务,重复消息会撞唯一索引直接忽略;或者把”业务状态流转”本身做成幂等——扣款前用 CAS 把订单从”待扣款”改到”扣款中”,改成功了才去调支付,重复消息看到的已是”已扣款”状态就直接返回。支付场景还要和渠道侧对齐:调渠道时带业务方生成的请求号(幂等号),靠渠道侧的幂等保证即使我方重试也不会重复扣款。不要只用 Redis 的 setnx 做去重——它不是持久化的权威记录,key 过期或实例故障后重复消息就会穿透。

高并发号码生成器:不依赖外部、要顺序或趋势递增、要高并发——号段模式最合适。持久层只记录”当前已分配到的最大值”(比如一次分配 1000 个),应用启动或号段用尽时用一条 UPDATE ... SET max_id = max_id + 1000 原子地把新号段取到内存,之后所有发号在内存里自增,零网络开销。要考虑的点:服务重启会浪费剩余号段(可接受,或按需缩短号段长度)、多实例安全(原子 UPDATE 保证不重叠)、容灾(内存里缓存两段,一段用完时提前异步取下一段,避免取号时阻塞)。如果要求严格连续、全局有序,那就只能用中心化的原子自增,吞吐上限受单点约束——所以要先问清”是要唯一还是要连续”。

STW 与垃圾回收:Stop-The-World 指 GC 过程中暂停所有应用线程,暂停期间请求全部挂起,所以它是延迟毛刺的主要来源。分代收集下,年轻代用复制算法、回收频繁但停顿短(Minor GC);老年代用标记整理或标记清除,停顿更长。关键指标是停顿时间与吞吐量的权衡:G1 通过把堆划分成 Region、按回收收益排序(Garbage First)并设定目标停顿时间来平衡,ZGC 和 Shenandoah 则靠并发标记与转发表实现亚毫秒级停顿,代价是额外的内存与 CPU 开销。排查上,jstat -gcutil 看频率与耗时,-Xlog:gc* 看每次停顿,遇到 Full GC 频繁要查是不是有内存泄漏、大对象或元空间膨胀。