AI图鉴

AI图鉴首页 › AI 资讯 › 面试官让你把 LLM、RAG、Agent、MCP 串起来讲清楚,90% 的 Ja

面试官让你把 LLM、RAG、Agent、MCP 串起来讲清楚,90% 的 Java 工程师当场露馅

来源:掘金 · 发布时间:11-04 · 由 AI图鉴每小时自动聚合

面试官高频题:串讲 LLM、RAG、Agent、MCP。以电商客服链路落地,附 Java 生产代码、余弦相似度数学细节、五层架构,梳理 16 个踩坑点与面试问答,讲清 Java 工程师 AI 工程化护

面试官让你把 LLM、RAG、Agent、MCP 串起来讲清楚,90% 的 Java 工程师当场露馅

📌 本文导读(建议先花 60 秒)

说句扎心的:LLM、RAG、Agent、MCP、向量、余弦相似度……这些词你个个都听过,但面试官让你“串起来讲一遍”的时候,是不是瞬间就卡壳了?

别慌,这恰恰说明你缺的不是知识点,而是一张全景图。

这篇文章不堆概念、不背定义,用一条真实的电商客服请求做主线,带你从“用户问:我的订单为什么还没发货”一路走到最后流式输出的答复,中间每个环节用到的技术、写的 Java 代码、踩过的坑,全都摆到台面上。

读完这篇,你能带走什么?

- 🧭 一句话隐喻串起所有概念:LLM 是发动机,Embedding 是坐标系,余弦相似度是尺子,向量库是图书馆索引卡,RAG 是外挂资料室,Tool 是手脚,MCP 是 USB-C 插座,Skills 是岗位手册,Agent 是那个会自己干活的数字员工

- 🏗️ 五层架构 + 依赖关系两张图:搞清每一层是谁的活,顺便把“为什么招 Java 而不是招算法”这句话说到位

- 🧮 余弦相似度的三个数学关系:归一化后余弦 == 内积、余弦越大 L2 越小、排序等价 ≠ 阈值通用——面试答不出来基本就露馅的那几个点

- 💻 可以直接抄的生产级代码:float 存储 double 累加防精度污染、零向量短路防 NaN、JDK Vector API 上 SIMD、RRF 融合、带四道防护的 ReAct 循环、三分钟把 Spring Boot 服务变成 MCP Server

- ⚡ 反直觉但救命的结论:RAG 的瓶颈在检索不在生成;Skills 别塞进 System Prompt 要用渐进式披露;工具集要从 28 个收敛到 5~8 个;MCP 的 HTTP 端点默认无鉴权,裸暴露就是裸奔

- 🕳️ 16 个生产踩坑对照表 + 13 个面试连环追问速答:建议直接收藏当检查清单,改分块、换模型、调阈值之前先过一遍

这篇适合谁? 想转 AI 方向的后端、正在做企业知识库或智能客服的 Java 研发、以及下周就要面试、现在有点慌的朋友。

这篇不适合谁? 想学怎么训练大模型、调 LoRA 的——这里一个梯度都不推。本文只讲一件事:怎么把不确定的模型能力,装进确定的工程系统里。

⏱️ 全文约 1.5 万字,建议先收藏、再细看,通勤路上刷不完的。

看到最后有个小彩蛋:文末问了大家“做 RAG 或 Agent 落地时踩过最疼的坑”,评论区聊两句,我看到会挨个回。觉得有用的话,点个赞、点个在看,别让这篇在收藏夹里吃灰 👇

--

一个请求串起 LLM、RAG、Agent、MCP:Java 工程师的 AI 工程化落地全景

面试里有道高频题:把 LLM、Agent、Skills、MCP、RAG、向量、余弦相似度串起来讲清楚。大多数人单个概念都能背,一连起来就露馅。这篇文章用一条真实的客服请求做主线,结合我们在电商客服、企业知识库项目里的落地经验,讲透每个概念的原理、Java 实现和踩过的坑。适合想转 AI 方向的后端朋友,建议先收藏再细看。

一、先立全景:一句话 + 两张图串起所有概念

学 AI 概念最大的痛点是"每个都听过,串不起来"。别急着一头扎进细节,先花三分钟建立全局认知,后面怎么学都不会迷路。

先用一句话把所有概念放进同一个隐喻体系:

LLM 是发动机;Embedding 是语言的坐标系;余弦相似度是尺子;向量库是图书馆的索引卡;RAG 是给模型配的"外挂资料室";Tool 是模型的手脚;MCP 是统一的 USB 插座;Skills 是岗位行为手册;Agent 是那个会自己拆任务、干完还会复盘的数字员工。

而 Java 工程师的价值,从来不在"造发动机",而在造整车、修高速、建加油站。

再来一张"人话版"速查表:

1.1 五层架构:搞清楚每一层是谁的活

把概念放进分层架构里看,职责一目了然——这和咱们熟悉的 MVC 分层是一个思路:

💡 技术亮点(面试加分句):这五层里,①②是买来的,③④是框架给的,⑤才是企业自己的护城河。这一句话,基本就把"为什么招 Java 而不是招算法"说透了。

1.2 概念依赖关系:它们是怎么咬合的

一句话串完:向量和余弦相似度是语义计算的地基,RAG 给大模型装知识库,Skills 和 MCP 给大模型接业务手脚,Agent 把所有能力组装成能自主完成任务的智能体。

接下来自底向上逐个拆。先从不那么性感、但决定一切的地基讲起。

二、LLM:别神化,它就是个"下一个 token 预测器"

很多人把大模型当百科全书用,这是所有工程问题的根源。先把三个关键认知钉死:

- 它是概率生成器,不是数据库。LLM 本质是函数 f(文本上下文) → 下一个 token 的概率分布,它生成的是最像样的文本,不是最真实的文本;

- 知识冻结:训练完那一天,参数里的知识就冻住了。你上周发的产品文档,它天然不知道;

- 幻觉的根因:模型优化的是流畅度而不是真实性,不知道又必须答的时候,它会用概率分布"编一个最像样的"。

再加两条:它没有记忆,所谓多轮对话,全靠我们把历史消息一遍遍塞进 messages 数组;它不联网、不查数据库,强在语言理解与生成,弱在精确计算、实时数据和私域知识。

想明白这些,Java 研发的定位就清楚了:

💡 一句话记忆:大模型在 Java 世界里,本质上就是另一个能聊天的第三方 REST 服务——只是它特别慢、特别贵、还偶尔胡说八道。我们要做的不是转行搞算法,而是当一名 LLM Application Engineer:像治理任何"不可靠依赖"一样,做超时、重试、熔断、降级、可观测。

2.1 Token、上下文、Temperature:都是预算

上下文越长越贵,也越容易"迷失中间信息"(Lost in the Middle)。所以别把上下文当硬盘用,它是稀缺资源——这个观点后面讲 Skills 时还会再用到。

2.2 别把 LLM 当数据库

这是我踩坑之后总结的分工原则,实际开发中非常管用:

事实数据:查数据库 / 知识库;流程操作:走业务接口 / MCP;语言表达:才交给 LLM。

LLM 擅长推理和组织表达,不擅长长期记忆和精确事实。让它干它不擅长的事,就是幻觉的开始。

模型本身这么多毛病,怎么补?答案是给它配外挂——先外挂知识(RAG),再外挂手脚(Tool/MCP)。但在那之前,得先解决一个数学问题:机器怎么理解"意思"?

三、向量 + 余弦相似度:把"语义"变成可计算的数字

3.1 为什么需要向量

传统 SQL 只擅长精确匹配和模糊匹配,like '%car%' 永远匹配不到 automobile。关键词检索匹配的是字,而我们要匹配的是意思。

Embedding 模型把一段文本压成一串高维浮点数(常见 768 / 1024 / 1536 / 3072 维),语义越近的文本,在向量空间里距离越近:

"如何申请年假" → [0.012, -0.231, 0.548, ...] ┐ 夹角很小 → 语义几乎等价

"年假怎么请" → [0.009, -0.198, 0.561, ...] ┘

"如何申请离职" → [-0.34, 0.087, -0.112, ...] → 夹角很大 → 语义不同

几个关键工程认知,都是血泪换来的:

- 不用关心每一维代表什么,只关心"距离能不能用";

- 向量是模型给的,不是算出来的——换 Embedding 模型,整个向量库必须全量重建,这是迁移成本的地雷;

- 入库和查询必须用同一个 Embedding 模型,否则坐标系都不一样,算出来的相似度是纯噪声;

- 生产上 Embedding 通常离线批量跑 + 增量幂等重放,别把它放进用户请求的同步链路。

维度怎么选?维度越高语义表达越细,但存储和计算成本线性增长。float 存储下 1536 维 × 4B ≈ 6KB/条,企业级常见选 1024 维再配 PQ 量化压缩。

3.2 余弦相似度:为什么不用欧氏距离

公式很简单,值域 [-1, 1]:

cos(θ) = (A·B) / (‖A‖ × ‖B‖) = Σ(aᵢbᵢ) / (√Σaᵢ² × √Σbᵢ²)

分数怎么解读,心里要有杆秤:

为什么文本场景默认选余弦而不是欧氏距离?

文本向量的模长受文本长度影响——"今天天气很好"和"今天天气很好很好很好"意思一样,欧氏距离却会变大;余弦只看方向,对长度不敏感。而且高维空间里欧氏距离有维度灾难,距离趋同,区分度越来越差。

3.3 三个数学关系:答不出来基本就露馅

⭐ 面试必答:

- 归一化后,余弦 == 内积。做 L2 归一化使 ‖a‖=‖b‖=1,代入公式即得 cosθ = a·b。工程价值:查询时只需一次乘法累加,省掉每次开方和除法。FAISS 里 IndexFlatIP 配归一化向量,算的就是余弦。

- 归一化后,余弦越大 ⇔ L2 距离越小。‖a-b‖² = 2 - 2cosθ = 2(1-cosθ),三种度量的排序结果完全等价,只差一个分数刻度。

- 排序等价 ≠ 阈值通用。如果拿余弦分数做绝对阈值过滤(比如"低于 0.6 判定无答案"),度量选择和归一化状态就至关重要——混用归一化/未归一化向量,阈值会直接失效。

3.4 三个容易被追问的坑

3.5 生产级代码:三个细节比公式本身更重要

public final class CosineSimilarity {

private CosineSimilarity() {}

/**

* 通用余弦相似度,三个工程细节:

* ① 用 double 累加——1536 维 float 连续累加会丢精度,污染 Top-K 排序;

* ② 零向量必须短路——否则 NaN 一路污染到排序结果,Comparator 行为不可预测;

* ③ 维度校验——入库与查询的坐标系必须一致。

*/

public static double cosine(float[] a, float[] b) {

if (a.length != b.length) {

throw new IllegalArgumentException("向量维度不一致: " + a.length + " vs " + b.length);

}

double dot = 0.0, na = 0.0, nb = 0.0;

for (int i = 0; i < a.length; i++) {

dot += (double) a[i] * b[i]; // 提升到 double 再累加

na += (double) a[i] * a[i];

nb += (double) b[i] * b[i];

}

if (na == 0.0 || nb == 0.0) { // 零向量短路,防 NaN

return 0.0;

}

return dot / (Math.sqrt(na) * Math.sqrt(nb));

}

/** 入库前 L2 归一化:之后检索退化为内积,省掉 2 次开方 + 1 次除法 */

public static float[] l2Normalize(float[] v) {

double n = 0.0;

for (float x : v) n += (double) x * x;

if (n == 0.0) return v;

double inv = 1.0 / Math.sqrt(n);

float[] out = new float[v.length];

for (int i = 0; i < v.length; i++) out[i] = (float) (v[i] * inv);

return out;

}

/** 归一化向量之间的内积 == 余弦相似度(检索热路径用这个) */

public static double dot(float[] a, float[] b) {

double s = 0.0;

for (int i = 0; i < a.length; i++) {

s += (double) a[i] * b[i];

}

return s;

}

}

💡 技术亮点:float 存储省一半内存与带宽,但累加用 double 防精度丢失;零向量返回 0 而不是 NaN——NaN 会让 Comparator 排序结果不可预测,这是真实踩过的坑。另外"写入时归一化、查询时退化为点积"这一招,实测百万级向量打分性能提升 2~3 倍,千万级批量检索能提升约 40%。

批量点积还可以交给 SIMD:

import jdk.incubator.vector.FloatVector;

import jdk.incubator.vector.VectorOperators;

import jdk.incubator.vector.VectorSpecies;

/**

* JDK Vector API(jdk.incubator.vector,孵化阶段,需 --add-modules):

* 让 JIT 生成 SIMD 指令,批量点积性能可达标量循环的数倍。

* 定位:离线建库/重排序阶段的自研算子;在线检索一律交给向量库。

*/

public final class SimdDotProduct {

private static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

public static double dot(float[] a, float[] b) {

int i = 0;

int upperBound = SPECIES.loopBound(a.length);

FloatVector acc = FloatVector.zero(SPECIES);

for (; i < upperBound; i += SPECIES.length()) {

FloatVector va = FloatVector.fromArray(SPECIES, a, i);

FloatVector vb = FloatVector.fromArray(SPECIES, b, i);

acc = acc.add(va.mul(vb));

}

double sum = acc.reduceLanes(VectorOperators.ADD);

for (; i < a.length; i++) { // 尾部不足一个向量宽度,标量兜底

sum += (double) a[i] * b[i];

}

return sum;

}

}

有了向量和尺子,下一个问题很现实:数据量上来之后,难道真的一条条 for 循环去算?

四、向量库与 ANN 索引:为什么不能自己 for 循环

先算一笔账:100 万条 1024 维向量,每条查询要 1024 次乘加,单次查询约 20 亿次浮点运算;光把 4GB 向量从内存读一遍就够喝一壶。100 并发?CPU 排队到天亮。

破局思路:允许自己"不算那么准"——ANN(近似最近邻)用少量召回率换数量级速度。

理解 HNSW 一句话就够:跳表解决"跳得远",小世界图解决"跳得准"——从高层入口大步跳,逐层下降、每层贪心走向更近的邻居,用 O(log N) 的访问路径逼近目标。

生产上用 recall@K(ANN 结果与暴力结果的 Top-K 重合比例)验收,一般调到 95%~99%,同时把延迟从秒级压到毫秒级。

4.1 Java 侧选型对照表

⚠️ 踩坑提醒:不要自己去实现 ANN 索引。亮点应该是"怎么选索引、怎么调 nprobe / efSearch、怎么用 recall@K 验收",而不是手写 HNSW。千万级向量下检索延迟超 1s,标准解法是 HNSW 索引 + 热门向量本地缓存(Caffeine)+ 冷热数据分库。

pgvector 场景下,计算直接下沉到数据库,避免把全量向量拉到 Java 内存:

-- <=> 是 pgvector 的余弦距离操作符,1 - 距离 = 余弦相似度

SELECT id, content, 1 - (embedding <=> :qvec) AS score

FROM doc_chunk

WHERE tenant_id = :tenantId

ORDER BY embedding <=> :qvec

LIMIT 5;

Spring Data JPA 里用原生查询包一层即可:

@Query(value = """

SELECT id, content, 1 - (embedding <=> :queryVector) AS similarity

FROM document_chunks

WHERE 1 - (embedding <=> :queryVector) > :threshold

ORDER BY similarity DESC

LIMIT :topK

""", nativeQuery = true)

List<DocSimilarity> findTopKByCosine(@Param("queryVector") float[] queryVector,

@Param("threshold") double threshold,

@Param("topK") int topK);

Milvus 场景的声明式配置,注意维度必须和 Embedding 模型严格对齐:

@Configuration

public class MilvusVectorConfig {

@Bean

public VectorStore milvusVectorStore(EmbeddingModel embeddingModel) {

return MilvusVectorStore.builder()

.embeddingModel(embeddingModel)

.host("milvus-internal-host")

.port(19530)

.collectionName("knowledge_base")

.dimension(1536) // 与 Embedding 模型维度严格对齐

.indexType(IndexType.HNSW) // 高性能向量索引

.metricType(MetricType.COSINE) // 余弦度量

.build();

}

}

地基打完了,正式进入应用层。先讲面试第一关——RAG。

五、RAG:开卷考试,但瓶颈在检索不在生成

先纠正一个高频错误答案:

RAG 不是"向量库 + 大模型",而是"外部知识管理 + 检索系统 + 生成模型"的一整套协同机制。 它解决四个问题:知识不新、私有知识不知道、容易编(幻觉)、答案不可追溯。

5.1 完整链路:两阶段,必须按步骤讲

5.2 高分答法:先讲"朴素 RAG 为什么不行"

把解法攒起来,就是 2026 年的生产级 RAG 公式:

Production RAG = 高级解析(OCR / 版面还原)+ 混合检索(BM25 + Dense,RRF 融合)+ 神经重排(Cross-Encoder)+ Agentic 路由(按问题复杂度动态决定检索深度)+ GraphRAG(全局/多跳问题)+ 持续评测(RAGAS / TruLens,量化 recall@k、忠实度、幻觉率)

📌 这里有个分水岭级别的认知:RAG 的瓶颈在检索,不在生成。朴素管道在检索这一步大约 40% 的概率会失败,检索入口错了,后面 rerank 和生成再优化都白搭。

顺带回答一个高频疑问:现在上下文都 1M 了,还要 RAG 吗?要。三个理由:成本与延迟(每次全量灌入不可接受)、注意力衰减(塞得多不代表记得住)、实时性与权限粒度(知识库按条更新、按租户过滤)。正确姿势是:RAG 负责从海量里精准找,长上下文负责找到之后的精读推理。

5.3 五个关键设计决策

① 切块:固定长度硬切是语义腰斩的元凶。实践参数:500~1000 token 一块、10%~20% 重叠、按标题/段落结构感知切分,再上父子块——子块用于向量匹配,命中后把父块完整内容送进上下文。标题和元数据也要带着走,检索时能做过滤,回答时能做溯源。

② Embedding 一致性:Query 和 Doc 必须同一模型同一维度,前面讲过,不再展开。

③ 混合检索 + RRF 融合:纯向量会漏精确关键词(型号 RTX-5090、制度号 HR-2025-013),BM25 恰好互补。RRF(Reciprocal Rank Fusion)的打分公式是 score = Σ 1/(k + rank),不需要归一化两种量纲完全不同的分数,比加权求和稳得多。

④ 重排:向量召回是"双塔模型",Query 和 Doc 独立编码,无法深度交互;Rerank 是交叉编码,把 Q+D 拼一起算相关性,准但慢,所以只对粗召回的 Top-50 精排,取 Top-5~8。

⑤ 生成约束:Prompt 写死"仅依据资料回答,没有就说不知道"+ 强制引用来源。这是抑制幻觉的第一道闸。

5.4 核心代码:一个能上线的 RAG 闭环

Spring AI 里,RAG 可以被一个 Advisor 织入,业务代码几乎无感:

@Configuration

public class RagConfig {

/** 生产环境换成 PgVectorStore / MilvusVectorStore / ElasticsearchVectorStore */

@Bean

VectorStore vectorStore(EmbeddingModel embeddingModel) {

return SimpleVectorStore.builder(embeddingModel).build();

}

/**

* RAG 三个关键约束:

* ① topK + similarityThreshold:控制上下文预算,宁少勿滥;

* ② 系统提示写死"只依据资料回答,没有就说不知道"——抑制幻觉的第一道闸;

* ③ 绑定引用来源,可追溯(企业合规刚需)。

*/

@Bean

ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) {

return builder

.defaultSystem("""

你是企业知识助手。只能依据【检索资料】回答问题,

并在答案末尾用 [来源: 文件名#片段号] 标注依据。

资料中没有相关信息时,直接回答「资料中未涉及」,禁止推测。

""")

.defaultAdvisors(

QuestionAnswerAdvisor.builder(vectorStore)

.searchRequest(SearchRequest.builder()

.topK(8)

.similarityThreshold(0.6)

.build())

.build())

.build();

}

}

// 调用侧:检索被 Advisor 织入,业务代码里几乎看不到 RAG 的存在

@RestController

public class KnowledgeController {

private final ChatClient chatClient;

public KnowledgeController(ChatClient chatClient) {

this.chatClient = chatClient;

}

@GetMapping("/kb/ask")

public String ask(@RequestParam String q) {

return chatClient.prompt().user(q).call().content();

}

}

📎 注:Spring AI 各小版本 API 略有差异(如 SimpleVectorStore.builder 与旧版构造器),以所用版本的官方文档为准。

再往进一步,两阶段检索 + 流式输出:

@Service

@RequiredArgsConstructor

public class KnowledgeQAService {

private final VectorStore vectorStore;

private final RerankClient rerankClient;

private final ChatModel chatModel;

/** 两阶段检索:粗召回扩量 → 精排取 Top-5,各司其职 */

public List<Document> retrieve(String query) {

List<Document> candidates = vectorStore.similaritySearch(

SearchRequest.builder()

.query(query)

.topK(20) // 扩大召回,给 Rerank 留空间

.similarityThreshold(0.6) // 阈值过滤,别把噪声喂给模型

.build());

return rerankClient.rerank(query, candidates, 5);

}

/** 流式问答:首字延迟从 3s 压到 300ms 量级 */

public Flux<String> streamAnswer(String query) {

List<Document> docs = retrieve(query);

String context = docs.stream()

.map(d -> "[%s] %s".formatted(d.getId(), d.getText()))

.collect(Collectors.joining("\n---\n"));

String systemPrompt = """

你是企业知识库助手。规则:

1. 仅依据【资料】回答,资料不足必须回答"暂无相关信息"

2. 每条结论标注来源编号 [DOC-xxx]

3. 禁止编造流程、人名、金额

【资料】

%s

""".formatted(context);

return chatModel.stream(new Prompt(

new SystemMessage(systemPrompt),

new UserMessage(query)))

.mapNotNull(r -> r.getResult().getOutput().getText());

}

}

如果检索源有多路,用虚拟线程并行召回 + RRF 融合:

@Service

public class HybridRetriever {

private final VectorStore vectorStore; // Spring AI 抽象:PGVector / Milvus / Redis

private final Bm25Searcher bm25;

private final ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();

public List<Doc> retrieve(String query, int topK) {

// 两路召回并行:虚拟线程让阻塞 IO 不占 OS 线程,万级并发也不炸线程池

CompletableFuture<List<Doc>> semantic =

CompletableFuture.supplyAsync(() -> vectorStore.similaritySearch(

SearchRequest.builder()

.query(query)

.topK(topK * 3) // 多召回一些给 Rerank 用

.similarityThreshold(0.6)

.filterExpression("tenantId == '" + TenantContext.get() + "'") // 权限下推

.build())

.stream().map(Doc::from).toList(), vt);

CompletableFuture<List<Doc>> lexical =

CompletableFuture.supplyAsync(() -> bm25.search(query, topK * 3), vt);

return rrf(List.of(semantic.join(), lexical.join()), 60).stream()

.limit(topK).toList();

}

/** RRF:score = Σ 1/(k + rank),无需归一化分数,跨

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 609 款 AI 工具分类导航 · 每日 AI 热门项目与动态