快手后端一二面:Agent 执行链路与 SSE 推送、RAG 检索判断、索引下推与引用类型
- 轮次
- 一二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 介绍一下你自己。
- 讲一下 Agent 的完整执行链路。
- Agent 最大执行 20 步和 RAG 最多补查 3 轮有什么区别?
- Agent 异步执行后,如何接收执行结果并通过 SSE 推送到前端?
- 请求线程直接返回后,为什么还能继续推送模型的输出?
- 不同用户之间如何实现隔离?不同用户的会话如何区分?
- topK 是如何设置的?有什么依据?
- 检索结果能不能回答用户的问题,由谁来判断?
- RAG 的完整执行流程是什么?
- 你的向量数据库目前有多大规模?检索相关性如何判断,用的是欧式距离吗?
- Agent 的思考与执行模式基于什么思想?
- 这个项目是自己做的还是基于网上项目改造的?第二个项目你改造了哪些部分?
- 将用户信息从 ThreadLocal 改为 RequestScope,有什么区别?
- 如果有几亿用户使用这个系统,每次提问和回答都存数据库吗?你会如何优化?
- 如果只把会话上下文放在缓存中,用户过一段时间回来继续聊天,缓存已经过期了怎么办?
- MySQL 的事务隔离级别有哪些?分别解决了什么问题?MVCC 的原理是什么?
- 缓存穿透、缓存击穿和缓存雪崩有什么区别?分别如何解决?
- HashMap 的底层原理是什么?线程池的核心参数有哪些?
- 线上出现慢 SQL,你会如何排查和解决?什么情况下索引会失效?
- 手撕:力扣 128 最长连续序列。
- 二面:Agent 知识库项目如何防止恶意会话请求、避免用户绕过权限?
- 稠密向量和稀疏向量有什么区别?
- Java 中的强引用、软引用、弱引用和虚引用有什么区别?
- 谈谈你对 ThreadLocal 的理解。
- 什么是索引下推?
- Lock 和 synchronized 有什么区别?synchronized 除了修饰方法还可以怎么用?
- 公平锁和非公平锁有什么区别?ReentrantLock 如何实现?非公平锁具体如何抢锁?
- 手撕:力扣 560 和为 K 的子数组。
《参考解析》
「20 步上限 vs 补查 3 轮」考的是你分不分得清两种控制流:Agent 步数是整个「思考-调用工具-观察」循环的全局预算,防的是无限循环与成本失控;RAG 补查轮数是检索子流程内部的局部预算,防的是「检索不到就一直换 query」的死循环。前者超限要终止任务并给用户交代,后者超限要带着已有证据作答或明确说「资料里没有」。两者都要有单调递减的计数器和可观测的日志。
SSE 推流为什么能「请求线程返回了还在推」:HTTP 响应是一个流,Servlet 3 的 AsyncContext 或 Spring 的 SseEmitter/ResponseBodyEmitter 把请求线程释放回容器,但连接和输出流仍然活着,后台线程(或响应式流)继续往里写;关键是超时与清理——设置 emitter 超时、客户端断开时移除监听、写出失败要吞掉并清理资源。用 WebFlux / Flux 更自然,因为整条链路是非阻塞的。若前端在网关后面,还要关掉代理缓冲(X-Accel-Buffering: no)、设置 text/event-stream 与心跳。
用户隔离与几亿用户的存储:会话按 userId + conversationId 分区,向量库按租户字段过滤(metadata filter),缓存 key 带用户前缀,权限校验在检索前而不是生成后。海量问答不要全量入库热库——冷数据归档到对象存储 / ClickHouse,热会话放 Redis 并设 TTL,正文与向量分表;写入走异步队列批量落库。缓存过期后「用户回来接着聊」要靠落库的最近 N 轮对话重建上下文,而不是假设缓存还在。
topK 的依据要说成实验与预算共同决定的:先用 20 测召回率,再看上下文 token 预算与延迟,最终取 5~10 并配 rerank;相关性判断可以是分数阈值 + 让模型在生成前先判断「资料是否足够」,不足则明确拒答而不是硬编。
索引下推(ICP):MySQL 5.6 起在存储引擎层就用索引中的列做条件下推,减少回表次数——例如 where name like '张%' and age = 20 在联合索引 (name, age) 上,age 的过滤在引擎层完成再看是否需要回表,EXPLAIN 的 Extra 里出现 Using index condition。四种引用:强引用(不回收)、软引用(内存不足时回收,适合缓存)、弱引用(下次 GC 必回收,ThreadLocalMap 的 key 用它)、虚引用(只为回收时收到通知,配合 ReferenceQueue 管理堆外内存)。ThreadLocal 每个线程一个 ThreadLocalMap,key 是弱引用的 ThreadLocal 本身,value 强引用,所以线程池复用时必须 remove() 防内存泄漏与串号。公平锁靠 hasQueuedPredecessors() 判断队列里有没有更早的等待者,非公平锁在 lock() 时先 CAS 抢一次再入队,这正是它吞吐更高的原因(减少线程唤醒的上下文切换),代价是可能饿死。