这是一篇按「面试追问链」组织的轻内容。先给结论:Java 后端接 AI,难点不在选框架,而在把三层边界分清楚——检索链路是数据层、协议与编排是调用层、参数校验与幂等是业务层。模型输出是概率性的,确定性必须留在服务端。本文用一组站点实测和两组 JDK 17 最小实验把这三层钉住,所有代码可直接运行。版本前提:JDK 侧为 OpenJDK 17.0.20.1(Temurin / Windows,单文件源码模式);站点为 Spring Boot 3.2.11 + 原生 HTML/CSS/JS,AI 侧只写稳定概念;未搭建真实模型服务与向量库做端到端验证。
近半年后端面试里,「你们系统怎么接的大模型」已经快追上「事务为什么不生效」了。它高频不是因为考 API 背诵,而是因为一问就能分清两类人:一类把接 AI 说成「引入某个框架」,另一类能说清哪一层归谁管、失效时先查哪一段。
有个类比很好用:接 AI 很像当年接 Spring,都是先分清配置注入、代理拦截、事务边界各自归谁,再挑具体组件。这个类比不准确的地方也恰恰是关键:Spring 的调用是确定性的,同样入参同样结果;模型输出是概率性的,不能把事务、幂等那套确定性假设照搬过来。后面所有追问,几乎都从这个差异长出来。
面试官:说说
RAG是什么。 候选人:三步——先把资料切成片段并向量化,检索时把问题也向量化、取回候选片段,最后把片段作为给定材料交给模型生成。 面试官:就这?中间哪一段最容易坏?
这里要补的是分段:切分 → 向量化 → 召回 → 重排 → 生成,每段有各自的失败特征。切分粒度决定信息是否被拆散;向量化要求入库与查询用同一个 embedding 模型版本;召回是候选集合,不是答案;重排做二次排序;生成阶段要约束「只用给定材料」。
候选人在白板上写下的是这条链路,而不是某个框架的类名:
graph TD
A[Java 后端接 AI] -->|前置| B[Token 与上下文窗口]
A -->|前置| C[Embedding 与向量空间]
C -->|因果| D[相似度不等于事实]
A -->|组合| E[RAG 检索链路]
E -->|进阶| F[向量库与召回]
F -->|因果| G[混合检索与元数据过滤]
A -->|组合| H[Function Calling]
H -->|因果| I[服务端参数校验与幂等]
A -->|对比| J[MCP 接口协议]
A -->|对比| K[Agent 编排循环]
A -->|进阶| L[推理可观测与评测回归]面试官点评:把 RAG 说成「查向量库」只答了第三段。真正的考点是「按段定位」——用户说答得不对时,你能不能把问题落到具体某一段上。
先看三类成因:入库侧(切分后片段没写进索引、增量没重建)、查询侧(查询向量与入库向量用的不是同一 embedding 模型版本)、闸门侧(topK 太小或相似度阈值设太高)。工程上再加两道:混合检索(字面 + 语义)兜住专有名词,元数据过滤把权限与时效挡在召回之前。
陷阱在哪:一上来就调 topK。阈值和索引都可能是好的,问题在「这段资料根本没入库」。定位顺序应该是「索引里有没有 → 查询向量对不对 → 闸门是不是过紧」。
候选人容易把余弦相似度当成语义真值。下面这段 JDK 17 单文件实验要证明的是:几何距离只说明两个坐标挨得近,不说明内容正确或有效。向量是手工构造的示意值,第三条文本标注的是「已作废的旧规则」。
// JDK 17.0.20.1 单文件源码模式:java CosineLab.java
import java.util.List;
public class CosineLab {
record Doc(double x, double y) {}
static double cos(Doc a, Doc b) {
return (a.x()*b.x() + a.y()*b.y()) / (Math.hypot(a.x(),a.y()) * Math.hypot(b.x(),b.y()));
}
public static void main(String[] args) {
var d = List.of(new Doc(1.00,0.20), new Doc(0.98,0.25), new Doc(0.96,0.22));
for (int i = 1; i < d.size(); i++) System.out.printf("cos(0,%d) = %.4f%n", i, cos(d.get(0), d.get(i)));
}
}# 实测输出(OpenJDK 17.0.20.1 / Windows)
# cos(0,1) = 0.9986
# cos(0,2) = 0.9996作废的旧规则反而离得更近。陷阱在哪:召回阶段追求「相关」,事实性与时效性是评测集、引用溯源和元数据过滤的责任,不能指望一个距离分数全包。
这是 Java 工程师最该加分的一问。工具描述本质上是写给模型看的接口契约,模型返回的参数仍然只是一段文本,属于不可信输入。
// JDK 17.0.20.1 单文件源码模式:java ToolCallLab.java
import java.util.Map;
public class ToolCallLab {
record Args(Map<String, String> p) {}
static String checked(Args a) {
String s = a.p().get("amount");
if (s == null) return "REJECT missing amount";
try {
int v = Integer.parseInt(s);
return (v > 0 && v <= 10_000) ? "TRANSFERRED " + v + " CNY" : "REJECT amount out of range";
} catch (NumberFormatException e) { return "REJECT amount not an integer"; }
}
static String unchecked(Args a) { return "TRANSFERRED " + a.p().get("amount") + " CNY"; }
public static void main(String[] args) {
System.out.println("unchecked : " + unchecked(new Args(Map.of("amount", "-500"))));
System.out.println("checked : " + checked(new Args(Map.of("amount", "-500"))));
System.out.println("checked : " + checked(new Args(Map.of("amount", "abc"))));
}
}# 实测输出(OpenJDK 17.0.20.1 / Windows)
# unchecked : TRANSFERRED -500 CNY
# checked : REJECT amount out of range
# checked : REJECT amount not an integer不校验就执行,负数金额直接落到业务里:unchecked 那一路把 amount = -500 原样带进了业务,而 checked 那一路用范围与类型两道校验挡住了它。陷阱在哪:把校验放进「提示词里再强调一遍」。提示词是概率层的约束,参数校验、幂等键、失败回传、重试上限必须放在服务端确定层。
面试官:做一个订单查询助手,用户可以问「我上周那笔单到哪了」,系统要调内部订单接口。给你 10 秒,说下你会切几层。
先想 10 秒,再看参考切法:
面试官真正想听的是第 3 层。把「模型说了」当成「用户有权」,是这类系统最典型的越权入口。
考点拆解成三句:能不能分层、能不能定位失效、能不能把确定性留在服务端。下面把关联知识点逐个补上关系说明,避免对话体把关系冲淡:
Embedding 与向量空间:它是召回的前置——维度、归一化、模型版本任一不一致,相似度就失去可比性。RAG 检索链路:它与 Java 后端是组合依赖——Java 负责切分、入库、权限与重排调度,模型只负责在给定材料内生成。RAG 的进阶——近似索引、双重闸门、混合检索、索引增量维护都在这里落地。Function Calling:它与业务层是组合依赖——工具描述是契约,参数校验归服务端,两者不能合并。MCP 与 Agent:它们与框架选型是同级对比关系——一个是接口协议,一个是编排循环,不是同一个东西;固定流程优先于 Agent。Spring AI / LangChain4j 抽象层:它们与核心知识点是同一道题的两种解法——把模型调用收进统一客户端或链式编排,选型看团队对可观测与状态管理的需求,抽象层不包办业务。我把这套关系整理进了站内的知识地图。下面这组数字是本机直接请求 /api/health 与 /api/catalog 实跑出来的,不是估的:
# 站点健康接口与目录统计(2026-11-12 实测)
curl -s https://zhuawassn.xyz/api/health
# {"status":"UP","scenes":95,"domains":14,"linkReachable":95,"linkDead":0}
# /api/catalog -> 14 域 / 95 场景 / 546 节点
# ai-basics: 7 图 / 37 节点;ai-eng: 7 图 / 39 节点;合计 14 图 / 76 节点
# 95 张图的原文入口 95/95 可达,死链 0新增的 ai-basics / ai-eng 两域正好对应三条补课路径:
Embedding → 注意力机制 → Transformer → 预训练/微调/对齐 → 幻觉与评估 → 推理参数。Spring AI 抽象层 → LangChain4j 编排 → RAG 检索链路 → 向量库与召回 → Function Calling → MCP 与 Agent → 推理部署与可观测。Agent、什么时候不该微调(先试检索)。这 14 张图不配独立成稿,原文入口统一由站内要点讲义即时生成,所以 95 张图的原文才都是 200。
跨语言对照在这里最有杀伤力:Python 生态习惯把模型调用写在脚本里,Java 生态更习惯把外部依赖收进一个可注入、可拦截、可观测的客户端。后者在 AI 场景反而占便宜——
已有能力 | 迁移到 AI 工程 | 不迁移的代价 |
|---|---|---|
配置注入 / 统一客户端 | 模型调用收口,切供应商不改业务 | 密钥与参数散落在各业务类 |
参数校验 / 幂等键 | 工具调用的参数与副作用管控 | 负数金额、重复下单直接落到库里 |
事务边界意识 | 明确「模型输出不受事务保护」 | 把概率结果当确定性状态写入 |
链路追踪 / 日志脱敏 | token、延迟、召回命中按维度归因 | 出问题只能靠人肉串日志 |
结论:Java 工程师不需要重新学一套工程观,只需要承认一件事——模型那一层不提供确定性,确定性仍由服务端提供。
层 | 归谁管 | 典型失效特征 |
|---|---|---|
数据层(检索链路) | 切分、向量化、召回、重排 | 召回为空、命中作废规则 |
调用层(协议与编排) | 工具注册、接口协议、编排循环 | 把 MCP 当 Agent、固定流程被绕成循环 |
业务层(确定性兜底) | 校验、幂等、权限、失败回传 | 负数金额、越权查询、重复副作用 |
观测层(可观测) | token、延迟、链路、评测回归 | 只能凭感觉调参 |
我按三层切:检索链路管数据,工具协议与编排管调用,参数校验、幂等、权限和失败回传留在 Java 服务端,因为模型输出是概率性的;再用 token、延迟、召回命中和评测回归做观测。选型放到最后,先定边界再挑框架,固定流程能走完就不上 Agent。
三句话带走:先分层再选框架;模型参数一律不可信;没有观测就没有优化。
TransactionSynchronizationManager(对照「上下文绑定」)以上是一组最小复现与机制推演,不是真实生产事故记录。评论区交流,发现错误欢迎指正。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。