老虎国际后端实习一二面面经:虚拟线程、Agent 工具拦截与文档协作
- 轮次
- 一二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面(30 分钟)
- 介绍一下你的实习经历。
- 做这个 Coding Agent 的初衷是什么?
- 它是基于框架做的还是从零开始做的?
- 如果用户输入”修改用户昵称接口,把字数校验从 30 字改成 36 字”,你的 Agent 接下来会怎么做?
- glob、文件读取和文件修改这些工具也是从零做的吗?
- 多个 Agent 同时修改文件时怎么处理?
- Agent 写出的代码和你手写的代码相比,谁的质量更好?
- 日常使用什么版本的 JDK?Java 21 有什么比较重要或者好用的特性?
- 虚拟线程什么时候使用?
- “使用虚拟线程可以降低 RT、提高接口响应速度”,这句话对吗?
- 如果要 fan-out 六个并发查询用户的不同信息,再把结果汇总成一个最终响应,你通常会怎么做?
- 你使用过队列吗?使用过 MQ 吗?在什么场景下使用 RabbitMQ?
- 异步链路的一致性如何保证?失败如何兜底?
二面(37 分钟)
- 介绍一下实习经历。
- Agent 工具如何与外界交互?模型如何通过 Agent 与外部交互?
- Agent 错误调用了某些接口,有什么方法可以发现并及时制止?
- 从工具层面看,怎么识别和拦截模型因幻觉或理解偏差导致的错误操作?
- 场景题:人和 AI 一起编辑飞书文档,人修改了云端内容,但 AI 拿着本地旧快照执行全量替换,把人的修改覆盖了。编辑本身是正常操作,也不能直接禁用编辑接口,你会怎样解决这个问题?
- 如果直接通过数据库约束多人编辑,应该怎样设计?例如一个部门几百人同时填写休假时间和联系方式,写入非常密集,怎样避免互相覆盖?
- 如何避免重复消费?
- 支付场景:用户下单后扣款,即使消息重复,也只能扣一次,应该用什么方式保证?
- 假设你做号码生成器,不依赖外部,所有业务都向你申请 ID,存在高并发的情况,你如何设计?
- Java 内存管理是怎样的?垃圾回收机制是怎样的?讲一下垃圾回收里的 STW 机制。
- 你用 AI 做过哪些事情?接触过推荐算法吗?
- 如果让你做你完全不知道的领域,你如何配合 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 频繁要查是不是有内存泄漏、大对象或元空间膨胀。