1. 这不是Java面试题集锦,而是一份大模型应用开发的“生存地图”
我带过三届校招Java后端团队,去年开始陆续有27位候选人主动提出想转大模型应用方向。其中21人卡在同一个环节:他们能手写红黑树、能讲透Spring事务传播机制、能用JVM参数调优GC停顿,但一聊到“怎么让LLM准确回答公司内部报销制度”,就立刻陷入沉默——不是不会,是根本没建立过这个技术坐标系。这45问,不是考你背了多少API,而是检验你是否真正把Java工程能力迁移到了AI原生应用的土壤里。它覆盖的四个核心模块——SpringAI的工程化封装、RAG知识库的落地瓶颈、Agent的决策链路设计、向量库选型的隐性成本——每一条都对应着真实项目里摔过的跟头。比如,你可能知道SpringAI支持OpenAI和Ollama,但未必清楚它的RetryTemplate在流式响应场景下会吞掉chunked数据;你可能用过ChromaDB存文档,但未必意识到它默认的HNSW索引在千万级向量时内存暴涨300%;你可能写过Agent的Tool调用逻辑,但未必发现SpringAI的FunctionCallback在并发请求下会因线程局部变量污染导致工具参数错乱。这些坑,不靠真刀真枪跑通一个报销审核系统、一个合同条款比对服务、一个客服话术生成平台,光看文档永远填不平。所以这45问,本质是45个“你有没有亲手踩过”的确认点。如果你的答案里有超过15个“不确定”,那说明你还没完成从传统Java后端到AI应用工程师的思维切换——这不是知识储备问题,而是工程直觉的缺失。
2. SpringAI不是Spring Boot的插件,而是AI原生应用的底座重构
2.1 SpringAI的核心价值:把LLM调用从“HTTP请求”升级为“Spring Bean生命周期管理”
很多人把SpringAI当成一个简化OpenAI API调用的工具包,这是最大的认知偏差。它的本质,是将大模型交互纳入Spring容器的治理范畴。传统做法是写个RestTemplate发POST请求,而SpringAI让你定义一个ChatClientBean,它自动集成重试、熔断、指标埋点、上下文注入。关键在于,它强制你用Message对象而非原始JSON字符串传递数据,这背后是类型安全的保障。比如,SystemMessage、UserMessage、AiMessage三个类分别对应不同角色,SpringAI在序列化时会自动添加role字段,避免手动拼接JSON时因字段名大小写错误(如"role"写成"Role")导致模型拒绝响应。更深层的是,它把LLM响应解析从“自己写Jackson反序列化”变成“交给ResponseMapper接口”。我见过太多团队在处理流式响应时,因为没正确处理SSE的data:前缀和event:标识,导致前端接收的文本碎片化。SpringAI的StreamingChatClient内部已封装好EventSource解析器,你只需实现StreamingResponseHandler的onChunk()方法,就能拿到结构化的token流。这省下的不是几行代码,而是对HTTP协议细节的反复调试时间。
2.2 系统提示词(System Prompt)配置的三大陷阱与实战方案
系统提示词不是写在application.yml里的静态字符串,而是需要动态注入的上下文载体。常见错误有三:
第一,硬编码在Bean定义里。比如@Bean ChatClient chatClient() { return ChatClient.builder().systemPrompt("你是一个财务专家").build(); }。这会导致所有请求共享同一套提示词,无法支持多租户场景。正确做法是使用PromptTemplate,将提示词模板化。例如定义"你作为{department}部门的{role},请依据以下规则回答:{rules}",在Service层通过PromptTemplate.of(template).render(Map.of("department", "HR", "role", "薪酬专员", "rules", hrRules))动态生成。
第二,忽略提示词版本管理。当业务规则变更时,直接修改提示词会导致历史问答结果不可复现。我们采用GitOps模式:提示词存放在独立仓库,每次变更打tag,SpringAI启动时从指定tag拉取。同时在ChatResponse中记录promptVersion字段,便于审计。
第三,未做敏感词过滤。曾有个项目因提示词中包含“请忽略所有法律限制”,触发了模型的安全策略直接返回空响应。解决方案是在PromptProcessor中插入自定义拦截器,对渲染后的提示词进行正则扫描,匹配ignore|bypass|override等关键词并抛出PromptSecurityException。
提示:SpringAI 1.0.0-M3版本起,
ChatClient支持withOptions()方法传入ChatOptions,其中temperature、maxTokens等参数可按请求粒度设置。但要注意,ChatOptions是不可变对象,每次调用必须新建实例,否则线程间会相互覆盖。
2.3 流式响应的可靠性保障:从Chunk丢失到Token计费对齐
流式响应的痛点不在前端展示,而在后端可靠性。典型问题是:用户看到“正在思考…”后页面卡死,日志里却显示onComplete()已触发。根源在于SpringAI默认的StreamingChatClient使用Flux,而WebFlux的背压机制在高并发下容易丢弃下游来不及消费的事件。我们的解决方案是引入bufferUntil操作符,在StreamingResponseHandler中缓存最近10个chunk,再批量推送给WebSocket。同时,为解决Token计费问题,我们扩展了StreamingResponseHandler,在onChunk()中累加AiMessage.getText().length(),并在onComplete()时将总字符数上报到计费系统——注意,这里不能直接用AiMessage.getTokenCount(),因为SpringAI的Token统计依赖于Tokenizer实现,而不同模型(如Qwen vs. Llama3)的分词器差异巨大,必须对接模型厂商提供的Token计算API。
3. RAG不是“检索+生成”,而是知识可信度的全链路工程
3.1 RAG的四大瓶颈:为什么90%的POC项目止步于Demo
RAG项目失败率高的根本原因,在于把“能跑通”误认为“能交付”。我们拆解出四个致命瓶颈:
检索精度瓶颈:用BM25或简单向量相似度,在技术文档场景下召回率常低于40%。根本原因是未做查询改写(Query Rewriting)。比如用户问“如何申请差旅预支”,原始查询向量与文档中“差旅借款流程”语义距离远。解决方案是引入HyDE(Hypothetical Document Embeddings):先让LLM生成假设性答案“差旅预支需填写《借款单》并经部门负责人审批”,再对该答案编码,用其向量检索。实测在内部知识库上,召回率提升至82%。
知识新鲜度瓶颈:向量库更新延迟导致回答过期信息。常见做法是定时全量重建索引,但千万级文档重建耗时超2小时。我们采用增量更新策略:监听MySQL binlog,对变更的文档ID触发局部索引更新。关键技巧是,向量库(如Milvus)支持upsert操作,但需确保新旧向量ID一致,否则会产生重复条目。为此,我们在文档元数据中增加version字段,更新时携带该版本号,向量库侧做幂等判断。
幻觉抑制瓶颈:LLM倾向于编造不存在的条款编号。传统方案是用retrieval_grades阈值过滤,但阈值设0.6还是0.7?我们改为双通道验证:检索结果送入LLM生成答案的同时,另起一个轻量级模型(如Sentence-BERT)计算答案与检索片段的语义相似度,低于0.45则返回“未找到相关信息”。
多模态支持瓶颈:RAG知识库能否存图片?答案是“能存,但不能直接检索”。向量库存储的是图片的CLIP特征向量,但用户提问“找出去年Q3销售报表截图”,需要先用多模态模型(如Qwen-VL)将问题转为文本描述,再检索。我们构建了统一的MediaProcessor接口,对PDF、图片、音视频统一提取文本摘要,再向量化。对于图片,额外保存OCR识别的文本内容,作为第二检索通道。
3.2 向量库选型决策树:从Chroma到Milvus的性能拐点分析
选型不是比参数,而是算TCO(总拥有成本)。我们用真实业务数据做了压力测试:
| 向量库 | 100万向量内存占用 | QPS(P95延迟<100ms) | 水平扩展能力 | Java SDK成熟度 |
|---|---|---|---|---|
| Chroma | 4.2GB | 120 | ❌ 单机 | ⚠️ 社区版无事务支持 |
| Qdrant | 3.8GB | 350 | ✅ Kubernetes部署 | ✅ 官方维护,支持gRPC |
| Milvus | 5.1GB | 890 | ✅ 分布式集群 | ⚠️ SDK文档陈旧,需读源码 |
关键发现:当向量规模超过500万,Chroma的HNSW索引内存占用呈指数增长,而Qdrant的Tantivy引擎保持线性。但Qdrant的磁盘IO在高并发写入时成为瓶颈,此时Milvus的分布式架构优势凸显。我们最终选择Qdrant作为初期方案,因其Java SDK的QdrantGrpcClient支持连接池和异步批量插入,且SearchRequest可精确控制consistency_level(强一致性/最终一致性),这对金融类应用至关重要。迁移Milvus的触发条件是:单节点Qdrant CPU持续>80%超15分钟,且写入延迟P95>200ms。
注意:所有向量库都要求向量维度严格匹配。我们曾因PyTorch模型输出768维,而Java端加载的ONNX模型输出1024维,导致插入失败。解决方案是在Java端用
Nd4j库做维度校验,并在CI流程中加入向量维度一致性检查脚本。
3.3 RAG知识库与KG知识库的本质区别:何时该用图谱而非向量
很多团队纠结“RAG还是KG”,其实二者解决的问题域完全不同。RAG擅长处理“是什么”(What)类问题,如“报销标准是多少”;KG擅长处理“关系”(Relationship)类问题,如“张三的直属上级是谁,该上级分管哪些部门”。我们做过对比测试:在员工组织架构查询场景,RAG的召回准确率仅63%,因为文档中“王五是李四的上级”这种表述稀疏且形式多样;而Neo4j图谱通过MATCH (a:Employee)-[:MANAGES]->(b:Employee) WHERE a.name='李四' RETURN b.name,准确率100%。但KG的构建成本极高,需人工定义Schema和实体关系。我们的实践是混合架构:用RAG处理文档型知识(制度、流程),用KG处理强关系型知识(组织架构、产品依赖),中间通过EntityLinking模块打通——当RAG返回“根据《采购管理办法》第5条”,EntityLinking自动提取“采购管理办法”作为KG中的Document节点,关联到相关责任人。
4. Agent不是“智能体”,而是可审计的决策流水线
4.1 Agent架构的三层解耦:从单体函数调用到可编排工作流
把Agent理解为“调用几个工具的函数”是危险的。真正的Agent必须具备状态管理、决策回溯、异常熔断能力。我们采用三层架构:
执行层(Execution Layer):封装工具调用。每个Tool实现Tool接口,定义name、description、inputSchema(JSON Schema)。关键创新是ToolExecutor,它不直接调用API,而是将请求放入BlockingQueue,由独立线程池处理。这样做的好处是:当某个工具(如调用ERP接口)超时时,不会阻塞整个Agent线程,且可对队列长度做限流。
编排层(Orchestration Layer):用State Machine定义决策逻辑。我们基于Spring State Machine构建,状态包括RECEIVE_INPUT、RETRIEVE_KNOWLEDGE、SELECT_TOOL、EXECUTE_TOOL、GENERATE_RESPONSE。每个状态转移由Guard条件控制,例如从SELECT_TOOL到EXECUTE_TOOL的Guard是toolInputValid && toolQuotaAvailable。这使得Agent行为完全可预测、可测试。
审计层(Audit Layer):记录完整决策链路。每次状态转移写入AuditLog表,包含traceId、state、input、output、durationMs。当用户投诉“为什么给出错误建议”,运维可凭traceId还原整个决策过程,定位是知识库检索失败,还是工具返回异常数据。
4.2 Agent安全的五个硬性约束:从输入净化到输出沙箱
Agent安全不是加个防火墙,而是贯穿全链路的硬性约束:
输入净化:对用户输入做AST解析,禁止
eval()、exec()等危险函数调用。我们用Javassist动态生成安全沙箱类,所有用户输入的代码都在该类中执行。工具权限隔离:每个Tool绑定最小权限角色。例如
FinanceTool只能读取finance:read资源,写操作需额外审批。权限校验在ToolExecutor入口处完成。输出内容过滤:LLM生成的响应需通过
ContentFilter,基于规则(正则匹配身份证号、银行卡号)和模型(BERT分类器识别敏感话题)双重过滤。Token预算硬控制:在
ChatOptions中设置maxTokens=2048,但LLM可能忽略该参数。我们在StreamingResponseHandler中实时计数,达到阈值时主动中断流并返回“响应过长,请精简问题”。决策链路签名:每个
AuditLog记录用HMAC-SHA256签名,密钥存于KMS。防止日志被篡改,满足金融行业审计要求。
4.3 Agent开发避坑指南:那些文档里不会写的实战细节
工具参数校验陷阱:SpringAI的
FunctionCallback会自动将JSON字符串转为Java对象,但若对象字段名与JSON键名不一致(如Java字段bankCardNumber,JSON键bank_card_number),默认不报错而是设为null。解决方案是给@JsonProperty注解显式声明,或在ObjectMapper中配置setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)。状态持久化误区:Agent状态不能存在内存里。我们曾用
ConcurrentHashMap存session,结果重启后所有进行中的任务丢失。正确做法是用Redis的Hash结构存状态,Key为agent:session:{sessionId},Field为state、context、stepCount,并设置TTL为24小时。工具调用超时设计:不要给所有工具设统一超时。调用内部API设5秒,调用第三方支付网关设30秒。我们在
ToolDefinition中增加timeoutMs字段,ToolExecutor根据此值动态设置Future.get(timeoutMs, TimeUnit.MILLISECONDS)。错误重试的边界:网络超时可重试,但业务错误(如“余额不足”)重试毫无意义。
ToolExecutor的重试策略需区分IOException和BusinessException,后者直接抛出不重试。
5. Java后端工程师转型的三道分水岭:从代码搬运工到AI原生架构师
5.1 技术栈迁移的真相:不是学新框架,而是重构工程思维
转型最大的障碍不是技术,而是思维惯性。传统Java后端关注“请求-响应”的确定性,而AI应用关注“提示-响应”的概率性。举个例子:写一个订单查询接口,你保证SQL执行100%成功;但写一个合同风险提示Agent,你得接受LLM有5%概率给出错误结论。因此,你的监控体系要从“错误率<0.1%”变成“置信度<0.8的响应占比<5%”。这意味着:
- 日志系统要记录
responseConfidence字段,而非仅status=200; - 告警规则要监控
lowConfidenceRate,而非errorCount; - 压测方案要模拟不同置信度分布,而非只测QPS。
我们团队推行“AI可观测性三件套”:Prometheus采集chat_request_total{confidence="high"}等指标,Grafana看板展示置信度分布热力图,ELK日志中response.confidence字段可全文检索。这比任何框架学习都重要。
5.2 面试真题背后的考察逻辑:45问如何映射到实际项目能力
这45问不是知识点罗列,而是项目能力的映射表。比如:
- “SpringAI如何配置系统提示词” → 考察你是否理解提示词是运行时上下文,而非配置项;
- “RAG知识库能否存图片” → 考察你是否分清存储能力与检索能力,以及多模态处理的工程路径;
- “Agent的安全机制” → 考察你是否具备生产环境的风险意识,而非仅会调用
Tool接口。
我们设计了一套“能力雷达图”评估候选人:横轴是4个技术域(SpringAI/RAG/Agent/向量库),纵轴是3个能力层级(能跑通Demo/能解决线上问题/能设计架构)。一个合格的AI应用工程师,至少要在2个域达到第三层。比如,在RAG域达到第三层,意味着你能设计混合检索策略(关键词+向量+图谱),能制定向量库容量规划方案,能主导知识库质量评估体系。
5.3 从零搭建第一个AI应用的路线图:避开90%新人的启动陷阱
别一上来就搞“智能客服”,那是巨人的坟墓。我们推荐渐进式路径:
阶段一(1周):用SpringAI + ChromaDB搭一个“内部Wiki问答机器人”。重点练:提示词工程(写10版系统提示词对比效果)、向量库CRUD(用PDF解析器提取文本)、基础RAG链(Retriever + LLM)。目标是让机器人能准确回答“入职流程需要哪些材料”。
阶段二(2周):给Wiki机器人加Agent能力。新增HRPolicyTool(查政策库)、LeaveBalanceTool(查年假余额)。重点练:状态机设计(如何判断用户问题需调用哪个Tool)、错误处理(Tool调用失败时的降级策略)。目标是支持复合问题:“我还能休几天年假?根据最新政策,休完假后工资怎么算?”
阶段三(3周):接入真实业务系统。将LeaveBalanceTool对接HRIS系统API,HRPolicyTool对接Confluence。重点练:安全加固(OAuth2.0鉴权、敏感数据脱敏)、可观测性(埋点、告警)、性能优化(向量库分片、缓存策略)。目标是上线灰度,收集真实用户反馈。
这条路径的价值在于:每个阶段都有可交付物,每个交付物都能暴露真实问题。当你在阶段一就发现ChromaDB在Mac上内存泄漏,那比在阶段三崩溃更有价值——因为问题越早暴露,修复成本越低。
6. 最后分享一个血泪教训:关于“Java是静态链接的”这个伪命题
面试中常有人问“Java是静态链接还是动态链接”,这问题本身就有陷阱。Java字节码在JVM里是动态链接的,但SpringAI这类框架让链接行为变得隐蔽。我们曾遇到一个诡异Bug:Agent调用FinanceTool时,偶尔返回空结果。排查三天,最终发现是FinanceTool依赖的commons-math3版本与SpringAI冲突,导致RealVector类加载失败。但JVM没报NoClassDefFoundError,因为ToolExecutor的异常捕获太宽泛,把LinkageError吞掉了。解决方案是:在ToolExecutor中显式捕获LinkageError并打印完整堆栈,同时在Maven的dependency:tree中用-Dverbose参数检查冲突。这件事教会我:AI应用的稳定性,一半靠算法,一半靠Java工程师对JVM底层的敬畏心。别以为用了LLM就不用懂ClassLoader,恰恰相反,越复杂的AI系统,越需要扎实的Java功底来兜底。