AI应用开发面试题总结(非八股文)
前端请求超过 3 秒怎么分析原因1.看前端和网络F12开发者模式去查看network首先判断是前端问题还是后端问题通过查看接口 Waiting 时间进行判断是后端响应时间太长还是说前端渲染问题2.给后端接口添加日志进一步定位后端问题3.如果是数据库问题则可能是慢查询问题查看慢查询日志定位是哪个查询语句出问题了explain对应的sql语句看看对应的索引和索引利用情况如果是索引方面是问题看看是不是索引缺失或者失效的问题4.看 Redis 问题常见原因大 key、连接池耗尽。5.看外部接口或第三方服务比如短信、支付、AI 模型、地图接口问题经常是没有设置超时时间。如果第三方接口卡住后端线程也会一直阻塞。解决方案第三方接口超过 1 秒直接失败返回默认结果避免拖垮整个接口。6.看线程池和连接池如果并发高请求超过 3 秒可能不是单个逻辑慢而是排队。比如Tomcat 线程池满了请求进来后没有线程处理只能排队。数据库连接池满了业务线程拿不到连接也会等待。Redis 不加限制会耗尽内存怎么解决Redis 是内存数据库如果不限制内存业务一直写 key最终可能导致内存被打满轻则 Redis 变慢重则被操作系统 OOM Kill。所以生产环境一定要做内存限制和淘汰策略。设置最大内存 maxmemory设置淘汰策略 maxmemory-policy给缓存 key 设置过期时间过期时间加随机值防止缓存雪崩避免大 key那淘汰策略有哪些呢不淘汰任何 key内存满后写入直接报错从所有 key 中淘汰最近最少使用的数据从所有 key 中随机淘汰优先淘汰剩余过期时间最短的 keyMyBatis 为什么 Mapper 接口不用实现是什么机制MyBatis 的 Mapper 接口不用我们手动写实现类本质原因是 MyBatis 在启动时通过 JDK 动态代理为 Mapper 接口自动生成了代理对象。我们调用 Mapper 方法时实际上调用的是代理对象而不是接口本身。如果只是小厂的话这样你也就够了但是如果你面试一些大厂可能就会衍生出一些问题动态代理是什么底层原理是什么动态代理还有哪些应用场景调用Mapper方法时候的流程是什么我们先回答问题2调用Mapper方法时候的流程是什么Spring扫描 Mapper 接口为接口创建 MapperProxyFactory专门生成 Mapper 代理对象生成代理对象Spring 容器里放的是 proxy调用mapper方法的时候拦截方法通过 SQL 唯一标识com.xxx.mapper.UserMapper.selectById找到对应的 MappedStatementMyBatis 启动时会把 XML 或注解 SQL 解析成MappedStatement调用 SqlSession 执行 SQLSqlSession 调用 ExecutorExecutor 调用 JDBC1获取数据库连接2预编译 SQLselect * from user where id ?3参数绑定4执行 SQL9.ResultSet 结果映射数据库返回ResultSetMyBatis 会根据resultType或者resultMap映射成 Java 对象。那引出问题什么是预编译数据库会1.SQL 语法解析数据库先检查SQL 是否合法表是否存在字段是否存在2. SQL 优化走哪个索引如何生成执行计划是否全表扫描3.生成执行计划数据库会缓存执行计划后面参数不同可以复用那我们是不是可以想起之前背八股文的时候学的#{}和${}的区别是什么#{} 是预编译参数例如select select * from user where id #{id} /selectMyBatis 最终select * from user where id ?然后ps.setLong(1,1L)支持预编译执行计划复用防 SQL 注入${}是字符串拼接例如select select * from user where id ${id} /select如果id1最终 SQLselect * from user where id 1如果id2SQL 会变成select * from user where id 2每次都需要重新解析重新优化重新生成执行计划更严重的问题SQL 注入这里顺带大家回顾一下第一个问题实则是我也忘记了什么是动态代理动态代理本质上是在程序运行期间动态生成一个代理对象通过代理对象去增强目标对象的方法而不需要手动编写代理类。动态代理底层原理是什么Java 动态代理主要有两种类型使用场景JDK 动态代理代理接口CGLIB 动态代理代理类JDK 动态代理目标对象实现接口CGLIB 动态代理有些类没有接口底层流程1创建代理对象Proxy.newProxyInstance()2方法被拦截调用proxy.login()实际上进入InvocationHandler.invoke()动态代理有哪些应用场景Spring AOPSpring 事务TransactionalMyBatis Mapper日志监控那基于此是不是又可以引出问题AOP是什么AOP的底层原理是什么AOP的实现方法有哪些Spring的事务是什么有哪些实现方式AOP 是什么AOP 底层原理是什么AOP 全称是面向切面编程它的核心思想是在不修改原业务代码的情况下对方法进行统一增强。比如日志记录、权限校验、性能统计、事务管理这些公共逻辑如果直接写在业务代码里会产生大量重复代码而 AOP 可以把这些逻辑抽离成切面统一织入到目标方法中。Spring AOP 的底层核心其实是“动态代理 反射”。Spring 不会直接把原对象交给我们而是会在运行期间为目标对象生成一个代理对象。我们调用方法时实际上先进入的是代理对象而不是原对象。代理对象会先执行增强逻辑比如打印日志、开启事务等然后 JVM 会把当前调用的方法封装成一个 Method 对象传给代理逻辑例如调用proxy.login()时JVM 会自动把 login 方法对应的 Method 传进来。接着代理对象会通过反射method.invoke(target,args)去调用真实业务对象的方法最后再执行方法后的增强逻辑比如提交事务、统计耗时等。所以可以理解为AOP 的核心目的是增强方法动态代理负责生成代理对象并拦截方法反射负责真正调用目标对象的方法。(proxy, method, args) - { // AOP增强 System.out.println(before); // 反射调用真实方法 Object result method.invoke(target,args); // AOP增强 System.out.println(after); return result; }反射是什么底层原理是什么反射本质上是 Java 在运行时动态获取类信息并动态操作类的一种机制。正常情况下我们是“编译时”就确定调用哪个类、哪个方法比如userService.login()而反射可以在程序运行过程中动态获取类、方法、属性的信息并动态创建对象、调用方法。比如可以通过Class获取类信息通过Method获取方法再通过method.invoke()动态执行方法。AOP的实现方法有哪些1.基于 XML 配置实现2.基于注解实现也是目前最常用的方式。通常会使用Aspect定义切面类使用Before、After、Around等定义通知再通过切点表达式指定拦截哪些方法。例如日志、权限、事务等功能通常都是这样实现的。3.基于自定义注解实现Spring的事务是什么有哪些实现方式Spring 事务本质上是 Spring 对数据库事务的一层封装用来保证一组数据库操作要么全部成功要么全部失败从而保证数据一致性。比如转账场景A 扣钱和 B 加钱必须同时成功如果其中一步失败就需要整体回滚否则数据就会出现问题。Spring 事务底层核心其实是 AOPSpring 会通过动态代理为目标对象生成事务代理在方法执行前开启事务执行成功后提交事务发生异常时回滚事务。Spring 事务主要有两种实现方式编程式事务和声明式事务编程式事务TransactionTemplate声明式事务Transactional注解实现mysql索引按存储方式划分MySQL 索引按照存储方式或者底层数据结构来划分主要可以分为 BTree 索引、Hash 索引其中 InnoDB 最核心、最常用的是 BTree 索引。BTree 属于多路平衡查找树特点是树高度低、磁盘 IO 次数少并且叶子节点之间通过链表连接因此不仅适合等值查询还非常适合范围查询、排序和分组RAG 技术的挑战点是什么它最难的地方不在“调用大模型”而在怎么把正确的知识找出来并且让模型只基于正确知识回答1.文档解析难企业文档不是纯文本可能是 Word、PDF、Excel、CSV、图片扫描件。不同格式结构不一样。比如 Word 里有标题、正文、表格Excel 里是行列结构PDF 可能有分页、页眉页脚、换行错乱。解析错了后面检索一定差。2.chunk 切分难。chunk 太大会把很多无关内容塞进上下文chunk 太小又会丢上下文3.召回难用户问法和文档写法不一样。如果只靠关键词“出差花的钱”和“差旅费用报销”不是完全一样可能召回不到所以要做 query rewrite、同义词、关键词召回 向量召回4.排序难召回出来的 topK 不一定最准确所以要 rerank.5.拒答难资料里没有答案时要拒答资料里有答案时要回答。这件事非常难。6.权限隔离难企业文档有权限A 部门不能看到 B 部门文档。这个不能只靠 Prompt必须在检索层做 filter。那么如何去解决这些挑战1. 文档解析难首先按文件类型走不同解析器Word → 解析标题、段落、表格 PDF → 解析页码、段落、表格处理页眉页脚 Excel/CSV → 按 sheet、行、列解析保留表头和字段关系 图片扫描件 → OCR 识别文字其次解析结果不要只保存纯文本还要保留结构信息例如word文档标题3.2 报销审批规则报销金额小于1000元由直属主管审批。报销金额大于等于1000元由部门负责人审批。{ title: 员工报销制度, section: 3.2 报销审批规则, level: 2, content: 报销金额大于等于1000元由部门负责人审批。, page: 5 }2. chunk 切分难不能只按固定长度切要做结构化切分。可以按标题 章节 段落 自然语义边界例如制度文档3.2 报销审批规则 报销金额小于1000元由直属主管审批。 报销金额大于等于1000元由部门负责人审批。切 chunk 时要保留标题标题3.2 报销审批规则 内容报销金额大于等于1000元由部门负责人审批。还可以加 overlap避免上下文断裂比如 chunk1 末尾和 chunk2 开头重叠一部分chunk1: ...小于1000元由主管审批。大于等于1000元... chunk2: 大于等于1000元由部门负责人审批...这里可以给每个chuck段添加一些额外信息比如属于哪个文档文档中的第几个chuck后面可以进行相邻chuck的merge3. 召回难1.做 Query Rewrite把用户问题改写成更适合检索的形式第一层是固定处理规则去标点 统一大小写 去掉口语填充词 空格归一化 章节编号归一化 中英文数字/符号归一化2.使用混合召回使用关键词向量召回关键词 topK 向量 topK → 融合去重 → 进入 rerank4. 排序难做 rerank 重排。重排时不能只看向量分数要结合多个信号关键词命中 标题命中 实体命中 字段命中 结构化命中 chunk 位置 文档权重 内容长度惩罚5. 拒答难难点是避免两种问题没答案却乱答 → 幻觉 有答案却说不知道 → 误拒答Answer Guard根据多个信号判断top1 分数 top1 和 top2 分差 关键词命中率 实体是否命中 字段是否命中 上下文覆盖度 结构化直接命中6. 权限隔离难第一文档入库时打权限标签。每个文档、每个 chunk 都带权限字段{ documentId: 1001, departmentId: B, acl: [dept:B, role:manager] }第二检索时强制加 filter。A 部门用户查询时只能召回dept:A public不能召回 B 部门文档。ES 查询里加{ terms: { acl: [dept:A, public] } }第三rerank/上下文构建阶段 前二次校验第四答案必须引用来源。ES 里存什么为什么用 ES而不是 Milvus 做向量数据库ES 存的不是“整个文档”而是“chunk 级别的检索索引”。ES 底层最核心的数据结构其实是“倒排索引ES 底层最核心的数据结构是倒排索引Inverted Index。传统数据库更像是“文档到内容”的正向存储而 ES 会建立“词项到文档”的映射关系。例如文档里有“李四分数88”ES 在写入时会先经过分词器把内容拆成“李四、分数、88”等 term然后建立李四 → doc1 分数 → doc1 88 → doc1这样用户搜索“李四”时就不需要全表扫描而是直接通过倒排索引快速定位文档所以检索效率很高。倒排索引里除了 term 和 docId还会保存 term frequency、position、offset 等信息用于 BM25 相关性计算、短语匹配和高亮显示。在我这个 RAG 项目里ES 不仅存倒排索引还存 embedding 向量所以本质上是倒排索引 向量索引的混合检索架构。倒排索引负责关键词、人名、编号、字段等精确检索向量索引负责语义相似召回最后再结合 rerank 做融合排序。Milvus 是专业向量数据库确实很适合大规模向量检索。但你的项目不是“纯向量检索系统”而是“企业 RAG 问答系统”。企业 RAG 需要的不只是向量还需要关键词检索 权限过滤 结构化字段 文档状态 调试解释 多条件过滤第一ES 能做混合检索BM25 关键词召回 dense_vector 向量召回 filter 权限过滤 字段查询第二ES 过滤能力更方便。比如 A 部门用户检索时可以直接加{ term: { departmentId: A } }第三ES 对调试更友好。你可以看命中了哪些关键词 score 是多少 哪个字段命中 召回了哪个 chunk 为什么排在前面Embedding 模型是什么维度为什么不用网格划分Embedding 维度取决于你用的模型。 比如有的模型输出 768 维有的 1024 维有的 1536 维。ES mapping 里的 dense_vector 维度必须和 embedding 模型输出维度一致。比如模型输出 1024 维ES 里就要建{ contentVector: { type: dense_vector, dims: 1024 } }“网格划分”适合低维空间比如地图经纬度。比如二维地图x轴经度 y轴纬度可以切成很多小格子A1 A2 A3 B1 B2 B3 C1 C2 C3查附近的人时只查附近几个格子。但 embedding 是高维向量比如 768 维、1024 维。如果维度降维可不可以可以但不能随便降。降维的本质是把高维向量压缩成低维向量比如1024维 → 256维 1536维 → 384维好处存储减少 索引更小 检索更快 内存占用更低坏处语义信息损失 召回准确率可能下降 相似度分布改变最近用 AI 做开发的具体需求如何思考设计并落地可以讲你做 Conversation Memory原问题RAG 问答一开始是单轮的。用户第一轮问张三在哪个部门系统回答张三在技术部。第二轮用户问那他的负责人是谁系统不知道“他”是谁也不知道上一轮的上下文。所以你要加会话记忆。设计时你考虑了几个点。第一必须有 conversationId。不然不同用户、不同会话会串。请求里加{ question: 那他的负责人是谁, conversationId: conv_123 }第二不做复杂长期记忆只做 Redis 窗口记忆。比如只存最近 5 轮user: 张三在哪个部门 assistant: 张三在技术部。 user: 那他的负责人是谁第三历史只参与生成不参与检索。这点很重要。如果把历史全部拼进检索 query可能会导致召回偏移。所以 保守设计retrieval 仍然只基于当前 question history 只放进 prompt 里帮助生成第四Prompt 里规定优先级当前参考知识 历史对话因为历史可能过期当前检索到的知识更可靠。第五缓存要改。如果缓存 key 只看 question那么那他的负责人是谁这个问题在不同会话里会有不同含义。所以缓存 key 要包含conversationId question history signature落地步骤QaAskRequest 增加 conversationId QaAskResponse 增加 conversationId、memoryUsed、historyCount 新增 RedisChatMemoryRepository 新增 ConversationMemorySupport ask 前读取历史 ask 后写入 user/assistant PromptBuilder 加入历史对话 缓存 key 加入 conversationId 和历史签名你用 AI 的方式可以这样讲你不是让 AI 直接瞎写代码而是先让 AI 帮我拆设计方案 → 我确定 V1 边界只做窗口记忆不做复杂指代消解 → 让 AI 生成类和接口修改草案 → 我审查缓存 key、Redis 结构、Prompt 优先级 → 最后用多轮问题联调验证多表聚合查询用户表、订单表、支付表题目查询 2026 年注册且在注册后 30 天内成功支付金额大于 1000 的用户 IDSELECT DISTINCT u.id FROM user u JOIN orders o ON o.user_id u.id JOIN payment p ON p.order_id o.id WHERE u.register_time 2026-01-01 AND u.register_time 2027-01-01 AND p.pay_status SUCCESS AND p.pay_time u.register_time AND p.pay_time DATE_ADD(u.register_time, INTERVAL 30 DAY) AND p.pay_amount 1000;