恒生电子AI应用开发AI面试:JVM、InnoDB加锁与RAG切分
- 轮次
- AI面试
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一、JVM 类加载与双亲委派模型
- JVM 的类加载完整过程以及双亲委派模型的核心逻辑是什么?
- 双亲委派的层级委托逻辑下,自定义一个与核心类库完全同名的类(如 java.lang.String),会被 JVM 加载吗?
- JVM 通过什么具体机制保证核心类库不被自定义类加载器篡改?
二、MySQL InnoDB 事务与锁机制
- InnoDB 执行 update 语句,条件分别是主键、唯一键、普通非索引字段时,加锁逻辑有什么区别?
- 普通非索引字段作为 update 条件会扫描全表加聚簇索引记录锁,在可重复读隔离级别下,会额外锁住哪些不存在的间隙范围?
- InnoDB 全表扫描更新时,为什么不会做锁优化、无法只精准锁定符合条件的记录?
三、大模型 Function Calling 核心机制
- Function Calling 如何实现大模型调用外部 API?模型本身无法执行函数,在整个机制中承担的核心作用是什么?
- 用户需求需要连续调用多个不同工具时,模型在多轮工具调用过程中具体承担什么核心工作?
- 如何解决大模型生成的函数调用参数不符合后端 API 规范、参数错误、格式非法的问题?
四、RAG 系统设计与检索优化
- 标准 RAG 从用户输入到最终答案输出的完整架构流程是什么?每个核心环节的作用是什么?
- 针对跨章节、逻辑强关联的超长文档,如何设计分块策略避免语义割裂?
- 语义感知加层级多粒度分块,对比传统固定长度分块,会给检索召回环节带来哪些全新挑战?
- 固定长度切分在投研、财报这类专业文档场景下,会出现哪些典型失效问题、割裂业务逻辑?
- 语义感知层级多粒度分块的具体方案是什么?如何识别专业文档的逻辑边界?不同粒度的分块如何做关联绑定?
- 向量召回容易召回语义相似但业务口径不匹配的片段,如何量化验证原有召回方案的缺陷?用到哪些评测指标和测试样本?
- 什么是口径匹配率?该指标的具体定义、计算方式、解决的核心问题是什么?
五、多智能体(Multi-Agent)系统架构
- 单体 Agent 存在能力瓶颈、无法处理复杂任务时,如何设计多智能体解决方案?多 Agent 的通信、协作、调度机制怎么设计?
- 多智能体系统中,两个执行 Agent 对同一业务事实输出矛盾结果,如何设计合理的仲裁机制?
- 多个 Agent 依赖的原始参考资料本身存在信息冲突时,系统依据什么优先级原则判定采信规则?
- 从单体 Agent 升级为职责分离多 Agent 加黑板调度方案,需要放弃哪些优化路线?最终选型的核心原因是什么?
六、AI 工程落地与 Spring AI 框架
- 主流 AI 应用开发框架的核心价值是什么?以 Spring AI 为例,其核心设计巧思、解决的行业痛点、落地适用场景有哪些?
- 没有 Spring AI 时,Java 生态对接大模型、向量库的核心适配痛点是什么?
- Spring AI 针对大模型工具调用,核心抽象层的设计原理是什么?如何屏蔽各家大模型的 API 差异?
- Spring AI 在复杂 RAG、多 Agent 场景下存在哪些原生短板?实际落地会踩哪些核心坑?
《参考解析》
类加载过程与双亲委派:类加载分加载、验证、准备、解析、初始化五个阶段(之后还有使用与卸载)。加载阶段由类加载器读取字节码生成 Class 对象;验证检查字节码合法性与安全性;准备为静态变量分配内存并赋类型零值;解析把符号引用替换为直接引用;初始化才真正执行静态代码块与静态变量赋值。双亲委派是「收到加载请求先交给父加载器,父加载器无法完成才自己加载」:Bootstrap → Extension/Platform → Application → 自定义。它的价值是保证同一个类在各类加载器视野里只有一份、避免核心类库被替换,也避免了类的重复加载与类型冲突。
自定义 java.lang.String 会被加载吗:写一个同名的 java.lang.String 编译能过(编译期是允许的),但运行时不会生效。JVM 对以 java. 开头的类做了包级保护:双亲委派会先把请求交给 Bootstrap,Bootstrap 在 rt.jar/jrt 里已经找到了真正的 java.lang.String,直接返回,自定义类加载器根本没有机会加载;即使绕过双亲委派(重写 loadClass 不委派)强行加载,defineClass 也会因为「禁止加载 java.* 包下的类」抛 SecurityException。
靠什么机制保证核心类库不被篡改:三重防线。一是双亲委派模型本身,让核心类永远由最顶层的 Bootstrap 加载,应用类加载器拿不到定义权。二是 ClassLoader.preDefineClass 里的包名与保护域检查,显式拒绝以 java. 开头的类名(JVM 层还有相应的 native 校验),这是真正的硬闸。三是保护域(ProtectionDomain)与签名校验——核心 jar 带证书签名,加载时会校验,篡改字节码会导致签名不匹配。要讲清楚的一点是:双亲委派是「约定 + 默认实现」,可以被重写绕过,真正兜底的是 JVM 层对 java.* 的定义禁令。
InnoDB 按不同条件 update 的加锁差异:条件命中主键(或唯一索引且值存在)时走唯一索引等值查询,只需在聚簇索引上给这一行加记录锁(X 锁),不加间隙锁,锁范围最小。条件命中唯一索引但值不存在时,会退化成间隙锁(Gap 锁)锁住该值所在的间隙防止并发插入。条件走普通二级索引时,会先在二级索引上加记录锁与间隙锁,再回表给聚簇索引上加记录锁——注意这里有两套锁,隔离级别为 RR 时二级索引上还会用 Next-Key 锁(记录锁 + 前面间隙的间隙锁)来解决幻读。条件是完全没有索引的字段时,只能全表扫描聚簇索引,扫描路径上遇到的所有记录都会被加锁,且 RR 下每条记录还会连带其前面的间隙,效果等同于锁全表——这正是「线上用非索引字段做 update 容易把库锁死」的原因。
全表扫描为什么不能只锁符合条件的行:因为加锁发生在扫描阶段而不是过滤阶段。InnoDB 是边扫描边加锁的:读到的每一行都先加锁,再去判断是否符合 WHERE 条件,不符合的虽然会被 MySQL 层提前释放(semi-consistent read / 条件下推的优化在 RC 下生效),但在 RR 隔离级别下这些锁要保留到事务提交,所以扫描路径上的所有行都会被锁住。根本原因是「不做锁优化」才安全——如果跳过某些行不加锁,那些行在事务提交前被并发修改,就可能出现同一事务内两次读到不同结果(不可重复读)或幻读,破坏 RR 的语义保证。
Function Calling 的实现原理:模型本身不执行任何函数,它承担的职责是「把自然语言意图翻译成结构化的调用请求」——在工具定义(名称、描述、参数的 JSON Schema)被注入上下文后,模型在推理时输出一段符合该 Schema 的结构化 JSON(函数名 + 参数),运行时(Agent 框架)解析这段输出、校验参数、真正发起 HTTP/RPC 调用、拿到结果后再以工具消息的格式回填给模型,模型据此继续推理或生成最终回答。所以分工是:模型负责「选哪个工具、填什么参数」这种语义决策,程序负责执行与安全边界。模型能做得好的前提是工具描述足够清晰——描述本质上就是给模型的 prompt,工具命名模糊、描述含糊是选错工具的首要原因。
连续调用多个工具时模型做什么:在多轮工具调用循环里,模型承担的是「规划与状态推进」:每一步根据已有的工具返回结果判断目标是否达成、下一步该调哪个工具、参数如何基于前面的结果推导。所以它同时在做三件事——任务分解(把复合需求拆成有依赖顺序的子步骤)、参数传递(后一个工具的入参来自前一个工具的输出,需要从结果里正确抽取字段)、以及终止判断(决定继续调用还是给出最终答案)。工程上要注意的是这个循环必须有最大轮数上限与重复调用检测,否则模型可能在两个工具之间来回震荡。
参数不符合 API 规范怎么办:分四层治理。第一层在 Schema 上做约束:把参数类型、枚举取值、必填项、数值范围、格式(如日期用 format: date)都写进 JSON Schema,让模型有明确的形状可依;能枚举就不要用自由文本。第二层在服务端做严格校验:拿 Schema 做一次真实校验(不是靠模型自述),不合法就带着具体的错误信息(哪个字段、期望什么、收到什么)打回给模型重生成,同时限制重试次数。第三层做参数的规范化与容错:类型可安全转换的(字符串 “5” 转数字 5)、大小写与空白的差异、常见别名的映射,在网关层直接纠正而不必占用一轮模型调用;但涉及金额、权限、目标对象这类关键参数一律不做猜测式纠正。第四层是让模型少犯错:给 few-shot 示例展示正确调用、把容易混淆的参数在描述里写清区别、必要时拆成多个职责单一的工具而不是一个参数众多的巨无霸工具。生产上还要给每个写操作带上幂等标识,防止重试造成重复执行。
标准 RAG 的完整流程:分索引段与查询段。索引段:文档解析(PDF/HTML/Word 抽正文,处理表格与图片)、清洗去噪、分块、embedding、写入向量库并保留元数据(来源、页码、章节路径、时间)。查询段:查询理解与改写(纠错、消歧、多角度扩展)、检索(向量召回 + 关键词召回并行)、融合(RRF 等)、重排序(Cross-Encoder 精排取 Top-K)、上下文构建(去重、按相关性排序、必要时压缩或摘要)、生成(带引用约束的 prompt)、以及后置的事实校验与引用回填。每个环节的价值不同:分块决定召回的上限,检索决定召回率,重排决定精确率,上下文构建决定模型能不能用上,生成侧的约束决定幻觉率。
跨章节强关联文档怎么分块:固定长度切分在这种文档上必然失效,所以要按文档自身的结构切。做法是分层:先按标题层级(章、节、小节)切出逻辑单元,再在每个单元内部按段落切;跨章节的强关联(比如「见第三章的假设」「以上口径适用于全部报表」)要靠元数据与引用关系保留——给每个块打上章节路径、前后块 id、以及显式引用链接,检索时命中一块就把它的兄弟块与引用块一起带出来。更完整的方案是父子结构:子块(段落级)用于检索,父块(小节或章节级)用于生成,命中子块后返回父块内容。另外对表格要单独处理(整表保留并转成结构化文本)、对公式与术语表要额外建一张术语到定义的映射,避免「语义相似但口径不同」的召回。
语义感知多粒度分块给检索带来的新挑战:主要四条。一是块长度分布极不均匀,长块会稀释向量语义、短块又信息不足,同一个 query 对不同长度块的相似度不可比——解法是召回时对不同粒度分别检索再融合,或给相似度做长度归一。二是块数量与层级变多导致检索目标变模糊,需要决定在哪一层检索、命中后如何向上向下扩展,处理不好会召回大量冗余的邻近块。三是父子/兄弟关系的维护成本:文档更新时要同步更新层级关系,否则出现悬空的父块引用。四是评测变复杂,传统的 Recall@k 不再能直接衡量效果,要按粒度分别评估并额外看「召回块是否覆盖了回答所需的全部证据」。指标上除了 Recall@k、MRR、nDCG,还要补一个「证据覆盖率」和口径一致性的指标。
固定长度切分在投研财报场景的失效:典型问题有四类。一是切断语义单元——把一张财务表格、一段风险披露、一条会计政策从中间截断,检索到的片段无法独立理解。二是割裂口径与前提——财报里的数字往往依赖「本期/上期」「合并/母公司」「调整前/调整后」这些限定,被切开后模型可能拿错口径的数。三是打断因果与条件链——「若某项达到 X 则计入 Y」这类规则被拆散,模型只看到后半句就会误用。四是丢失结构位置——同一句话在「管理层讨论」和「审计意见」里的含义完全不同,固定长度切分丢掉了章节归属。这四类问题的共同后果是召回片段看起来语义相关,但业务口径不匹配,最终答案数字对不上——这也是必须引入口径匹配率这类指标的原因。
口径匹配率:它是为「向量相似度衡量不了业务口径」这个缺口设计的补充指标。定义上,口径匹配率 = 召回片段中「与问题所需口径一致」的比例,除以召回总数——分子分母都要由人工标注或规则判定「口径标签」是否一致,口径标签包括时间区间(本期/去年同期)、报表范围(合并/母公司)、币种与单位、调整状态、统计维度(区域/产品线)等。计算方式一般是建一个有标准口径标注的评测集,对每条 query 跑一遍召回,逐条比对召回片段的元数据与 query 期望口径,统计匹配比例。它解决的核心问题是:把「语义相似但口径错」和「语义与口径都对」区分开——前者在纯向量指标里得分很高,但会直接导致答案数值错误,是专业文档场景最危险的失败模式。实务上它要和 Recall@k 一起看:Recall 高而口径匹配率低,说明检索该加结构化过滤(时间、报表范围等元数据过滤)而不是换 embedding 模型。
多智能体的通信、协作与调度:结构上分三类角色:规划者(Orchestrator/Planner)负责拆解目标与编排流程,执行者(Worker)负责各自领域的具体动作,评审者(Critic/Verifier)负责校验结果。通信有三种模式——共享黑板(所有 Agent 读写同一块结构化状态,适合需要全局视野的任务)、点对点消息(Agent 之间直接请求,适合依赖关系明确的流水线)、事件总线(发布订阅,适合松耦合与并行);生产上通常是黑板上放共享事实、消息队列承载任务投递。调度要解决三件事:依赖解析(把任务建成 DAG,无依赖的并行、有依赖的按拓扑序)、资源分配(并发上限、超时与重试、把长任务异步化)、以及失败处理(某个 Worker 失败是重试、降级还是整体回滚)。工程上必须有的护栏是最大轮数/最大步数、全局超时、以及每个 Agent 的成本预算。
两个 Agent 输出矛盾结果怎么仲裁:按成本从低到高排一个仲裁阶梯。第一层是规则裁决:如果矛盾可以归因于数据版本不同(一个用了旧快照),就统一到同一数据快照重算,这类矛盾根本不该由模型拍板。第二层是证据比对:把两个结论各自的支撑证据拉出来,谁引用的是权威源(原始数据库、有版本号的文件)谁胜出,这比让模型「再想想」可靠得多。第三层是加权投票:给每个 Agent 按历史准确率、任务相关度、以及当前置信度赋权,权重可配。第四层才引入独立仲裁者(一个专门做裁判的 Agent 或人工),并且要求它必须给出理由和引用,不能只给结论。无论用哪一层,都要把矛盾本身落库做为评测样本——反复出现的同类矛盾说明是流程或缺省规则的问题,不是模型的问题。
原始资料本身冲突时的采信优先级:要有一套写死的、可解释的优先级规则,而不是每次让模型自己权衡。常见顺序是:权威性(官方发布/合同原文 > 内部解读 > 第三方转述)→ 时效性(有生效时间戳的取最新,历史版本仅在追溯场景使用)→ 特定性(专门针对该业务场景的口径优先于通用口径)→ 可验证性(能追溯到原始出处的优于无法溯源的)。同时系统要做两件事:一是给每个文档片段带上这些元数据(来源等级、生效时间、适用范围),让排序可自动化;二是当冲突无法按规则消解时,不要静默选一个,而是把两种说法都呈现给用户并标明出处与差异——在金融、合规这类场景里,暴露不确定性比给一个错误的确切答案更负责。
单体 Agent 升级到多 Agent 加黑板要放弃什么:主要是三样东西。一是「全局最优的单次推理」——多 Agent 拆开后每个 Agent 的信息都是局部的,协调过程本身有信息损耗,可能出现各自都对但合起来不对的情况;二是低延迟与低成本——多一次 Agent 就多一次模型调用和上下文传递,总延迟与 token 成本都会上升;三是可预测性——单体 Agent 的执行路径容易复现和调试,多 Agent 的调度依赖运行时状态,同一个输入两次跑可能走不同的分工路径。选它的核心原因是可维护性与可扩展性:职责分离后每个 Agent 的工具集与权限都能收窄(安全性提升)、可以独立评测与迭代(改一个不牵动全局)、并且能突破单次上下文窗口的限制。判断标准是「任务的复杂度是否真的超过了一个 Agent 加一套好工具的承载能力」——没有这个前提,多 Agent 只是把复杂度从代码搬到了调度上。
AI 应用框架的核心价值与 Spring AI:框架的核心价值是「把与具体模型厂商无关的通用流程收敛成抽象」——模型调用、prompt 模板、对话记忆、向量库接入、工具调用、结构化输出、可观测性。Spring AI 的设计巧思在于它把 Java 生态既有的抽象习惯(类似 JdbcTemplate 的 Template 模式、ChatClient 的流式 API、自动装配与配置属性)搬到 AI 场景,让 Java 团队不必切换到 Python 也能接大模型;它解决的痛点是 Java 侧过去要针对每家厂商各写一套 HTTP 客户端与参数拼装、还要自己维护向量库 SDK,现在换模型只改配置。适合的场景是企业内部已有 Spring 技术栈、需要与既有鉴权与事务体系深度集成的系统;反过来,如果业务需要最快跟进模型侧最新能力(新的多模态、新的 reasoning 参数),Java 生态的跟进速度通常慢于 Python,这一点要提前认。
没有框架时 Java 生态的适配痛点:主要是四类重复劳动。一是每家厂商的协议都不同:OpenAI 兼容格式与各家自有格式(鉴权方式、消息结构、流式返回的 SSE 分帧、错误码)都要各写一套,换模型等于重写。二是流式返回的处理:SSE 分帧解析、粘包处理、背压、以及流式与结构化输出的冲突,手写容易出 bug。三是向量库的适配:不同向量库的 SDK、集合与索引管理、过滤条件语法、批量写入的限流都不一样。四是公共能力的缺失:重试与降级、token 计数与成本统计、prompt 模板管理、调用链路的可观测性——这些每个项目都要重造一遍。框架的本质价值就是把这些横切关注点收敛到一层,让业务代码只表达「要什么」。
Spring AI 屏蔽厂商差异的抽象设计:核心是两层抽象。第一层是模型调用抽象(ChatModel 接口),把「对话请求」定义成厂商无关的对象(消息列表、模型参数、工具定义),各厂商提供自己的实现(OpenAI、Anthropic、DashScope 各一个),调用方只依赖接口;进阶能力(流式、多模态、结构化输出)也统一在接口上,实现类内部做协议转换。第二层是工具调用的抽象,把方法用注解标记成工具(类似 @Tool),框架启动时反射扫描出方法签名并自动生成 JSON Schema 定义,调用时再把模型返回的参数反序列化成方法入参并执行——开发者写的是普通 Java 方法,框架负责 Schema 生成、参数绑定与结果序列化。屏蔽差异的关键在于「归一化到最小公共能力集 + 各实现补齐差异」这个模式:公共能力走统一接口,厂商独有能力通过实现类特有的方法暴露,避免抽象层出现「为了兼容所有人而谁都用不好」的宽接口。
Spring AI 在复杂 RAG 与多 Agent 场景的短板:RAG 侧,它的向量库抽象和文档读取器覆盖的是「标准流水线」,一旦要做语义感知分块、父子块索引、混合检索加自定义 rerank、按元数据做复杂过滤,就需要大量绕过框架直接操作底层;上下文构建与压缩策略也几乎没有可插拔的空间。Agent 侧,它的工具调用是「单轮的一次调用」范式,缺少原生的多轮规划循环、状态机编排、检查点与人工介入(interrupt),做复杂 Agent 基本要自己实现调度层。工程上常见的坑:流式输出与工具调用同时开启时的顺序处理容易出问题;版本迭代快、API 有破坏性变更,升级要仔细看 release notes;模型特有的参数(思考等级、缓存控制)在统一抽象里表达不了,只能下探到实现类,等于部分放弃抽象收益。结论是:简单问答/标准 RAG 用框架省事,复杂的 Agent 编排更适合自己控制主循环、只把框架用在模型接入这一层。