1. 面试官到底想考什么:从一道题看穿 Embedding 与 LLM 的关系
这道题在近一年的 Java AI 岗位面试里出现频率高得离谱。我前后帮几个朋友做过模拟面试,也自己作为面试官面过十几个人,发现一个很普遍的现象:很多人能背出“Embedding 是把文本转成向量,LLM 是生成文本的模型”,但一旦追问“那 LLM 输出的向量和 Embedding 向量是一回事吗”,立刻就卡壳了。这个卡壳点,恰恰就是面试官真正想筛人的地方。
先把结论摆在最前面,方便你带着答案往下读:Embedding 模型输出的是“语义坐标”,LLM 输出的是“下一个 token 的概率分布”,两者在数学形式上都是向量,但语义、维度含义、使用场景完全不同。之所以容易混淆,是因为现在很多 LLM 内部确实带了一个 Embedding 层,把 token 映射成向量再送进 Transformer,但这只是 LLM 的“内部零件”,不是它的“输出产品”。
我用一个生活化的类比帮你建立直觉。Embedding 就像给每本书在图书馆里定一个三维坐标(文学区、历史区、科技区各一个数值),坐标相近的书内容相似,你可以靠坐标快速找到“和这本书差不多的书”。而 LLM 的输出更像一个“接龙高手”,你给它半句话,它告诉你下一个字最可能是什么,输出的是一整张“候选字概率表”。一个负责“定位”,一个负责“续写”,这是两条完全不同的技术路线。
那为什么面试官偏爱这道题?因为它是一道“分水岭题”。能答对表面定义的人很多,能讲清楚两者在向量空间语义、维度设计、训练目标、工程用法四个层面差异的人,才是真正做过 RAG、做过语义检索、调过向量库的人。这道题往下能延伸出余弦相似度、向量归一化、维度诅咒、RAG 召回率优化、Spring AI 的 EmbeddingClient 与 ChatClient 分工等一系列实战问题,覆盖面极广。
这篇文章我打算按面试官的真实追问路径来写:先讲清楚两者的本质区别和数学原理,再落到 Java 生态里怎么用 Spring AI 把两者串起来,然后给一套能直接跑的生产级代码,最后把我踩过的坑和常见追问整理成速查表。不管你是准备面试,还是正在做 Java + AI 的落地项目,都能直接抄作业。
2. 原理拆解:Embedding 向量与 LLM 输出向量的本质差异
2.1 先搞清楚“向量”这个词在不同语境下的含义
很多人一听到“向量”就默认是同一回事,这是混淆的根源。在 AI 领域,“向量”至少有三个不同层级的含义,必须分开看。
第一个层级是Token Embedding 向量。这是 LLM 内部第一层的东西,把每个 token(可以粗略理解为字或词片段)映射成一个固定维度的稠密向量,比如 4096 维。它的作用是给模型一个“可计算的输入表示”,让后续的注意力机制能够运算。这一层是模型参数的一部分,是训练出来的,不是你能直接拿来用的“语义向量”。
第二个层级是句向量 / 文本 Embedding 向量。这是专门的 Embedding 模型(比如 BGE、M3E、text-embedding-3 系列)输出的东西,把一整句话、一段文本压缩成一个向量,比如 768 维或 1536 维。它的核心特性是语义相似度可计算:语义相近的文本,向量夹角小。这才是 RAG、语义搜索、聚类、去重场景里真正用的向量。
第三个层级是LLM 的输出向量(Logits 向量)。LLM 最后一层输出的是一组 logits,经过 softmax 之后变成词表上每个 token 的概率,词表有多大,这个向量就有多长,比如 15 万维。它描述的是“下一个 token 的概率分布”,跟语义坐标完全是两码事。
注意:面试时如果你只笼统说“都是向量”,面试官大概率会追问“那维度一样吗、能互相比较吗”。把这三个层级讲清楚,你就已经超过八成候选人了。
2.2 Embedding 的训练目标:让语义距离可度量
Embedding 模型是怎么训练出来的?核心思想是对比学习。拿一个正样本对(语义相似的句子)和一堆负样本对(不相关的句子),训练目标是让正样本对的向量距离尽可能近,负样本对尽可能远。常用的损失函数是 InfoNCE 或者 Triplet Loss。
这里有个关键点很多人不知道:Embedding 模型输出的向量通常需要做归一化(L2 Normalize),归一化之后,余弦相似度就等价于点积,计算更快。这也是为什么你在向量库里经常看到“归一化 + 内积”的组合,而不是每次都老老实实算余弦。
维度选择上也有讲究。维度不是越高越好,768 维和 1536 维在实际检索效果上差距可能很小,但存储和计算成本差一倍。我实测下来,中文场景下 512 到 1024 维基本够用,除非你的语料特别专业(比如法律、医疗),才需要上更高维度。这就是所谓的“维度诅咒”的反面——维度太高反而会稀释语义信号,检索时噪声变大。
2.3 LLM 的输出机制:从 logits 到 token 的采样过程
LLM 的最后一层是一个线性层,把隐藏状态映射到词表维度,得到 logits。logits 本身不是概率,要经过 softmax 才变成概率分布。然后模型根据这个分布采样出下一个 token,采样策略有 greedy、top-k、top-p、temperature 等。
这里有个面试高频追问:“LLM 输出的向量能拿来做语义检索吗?”答案是理论上可以,但实践中几乎没人这么干。原因有三:一是维度太高(十几万维),存储和计算都不划算;二是这个向量描述的是“下一个词的概率”,不是“整段文本的语义”,语义信息被稀释了;三是每次生成都要跑一遍完整前向传播,成本远高于专门的 Embedding 模型。
所以工程上的分工非常清晰:Embedding 模型负责“把文本变成可检索的语义坐标”,LLM 负责“基于检索结果生成自然语言回答”。这就是 RAG 架构的底层逻辑。
2.4 一张表看懂两者的核心差异
| 对比维度 | Embedding 向量 | LLM 输出向量(Logits) |
|---|---|---|
| 输出含义 | 文本的语义坐标 | 下一个 token 的概率分布 |
| 典型维度 | 256 ~ 1536 | 词表大小,通常 5 万 ~ 15 万 |
| 训练目标 | 对比学习,拉近语义相似样本 | 下一 token 预测,语言建模 |
| 是否归一化 | 通常 L2 归一化 | 不归一化,softmax 后是概率 |
| 主要用途 | 检索、聚类、去重、RAG 召回 | 文本生成、对话、推理 |
| 能否直接比较 | 可以,余弦相似度有意义 | 不可以,比较无意义 |
| 工程成本 | 低,可离线批量计算 | 高,需完整前向传播 |
这张表建议你面试前默写一遍,基本能覆盖这道题 80% 的追问。
3. Java 生态落地:Spring AI 里 Embedding 与 LLM 的分工协作
3.1 为什么 Java 选手现在必须懂 Spring AI
以前做 AI 应用基本是 Python 的天下,但这两年企业级 Java 项目接入 AI 的需求爆发式增长。原因很现实:大部分公司的核心业务系统、订单系统、风控系统都是 Java 写的,你不可能为了加一个智能问答功能就把整个技术栈换掉。Spring AI 就是在这个背景下出来的,它把 Embedding、Chat、向量库、工具调用这些能力抽象成统一的接口,让 Java 开发者用熟悉的方式接入。
Spring AI 的核心抽象有几个:EmbeddingModel负责文本转向量,ChatModel负责对话生成,VectorStore负责向量存储和相似度检索,ChatClient是更高层的门面。理解这几个接口的分工,你就理解了 RAG 在 Java 里怎么落地。
提示:Spring AI 和 Spring AI Alibaba 是两个相关但不同的项目。前者是社区版,后者是阿里针对国内模型(通义千问等)做的适配。选型时看你的模型供应商,如果用的是国内模型,Spring AI Alibaba 的适配会更顺。
3.2 EmbeddingModel 与 ChatModel 的职责边界
这是面试里非常容易被追问的点。EmbeddingModel的embed()方法接收文本,返回float[]或List<Double>,这个向量就是语义坐标。ChatModel的call()方法接收消息列表,返回生成的文本。两者在 Spring AI 里是完全独立的接口,没有任何继承关系。
为什么这么设计?因为它们的调用频率、成本结构、失败模式完全不同。Embedding 调用通常可以批量、可以缓存、可以离线预计算,一次调用成本极低;Chat 调用是实时的、昂贵的、有 token 上限的。把它们分开,你才能针对性地做优化,比如给 Embedding 加缓存层,给 Chat 加限流和降级。
我见过有同学在项目里把两者混在一个 Service 里,结果 Embedding 的批量任务把 Chat 的线程池占满了,线上直接雪崩。这就是没理解职责边界的代价。
3.3 向量库选型:从内存到生产级的演进路径
Spring AI 支持多种VectorStore实现,选型要看你所处的阶段。
开发阶段我建议直接用SimpleVectorStore,纯内存实现,零依赖,跑通流程最快。数据量小的时候(几千条以内)完全够用,检索延迟可以忽略。
到了生产阶段,选择就多了。RedisVectorStore适合已经有 Redis 的团队,部署成本低;PgVectorStore适合用 PostgreSQL 的团队,向量和业务数据放一起,事务一致性有保障;Milvus、Qdrant这类专业向量库适合千万级以上的向量规模,检索性能和扩展性更好。
选型的核心考量是三个:数据规模、检索延迟要求、运维成本。我个人的经验是,百万级以下优先考虑 PgVector 或 Redis,别一上来就上专业向量库,运维复杂度会让你怀疑人生。
3.4 一次完整的 RAG 调用链长什么样
把上面的组件串起来,一次 RAG 问答的流程是这样的:
- 用户提问,先用
EmbeddingModel把问题转成查询向量。 - 用查询向量去
VectorStore做相似度检索,召回 Top-K 相关文档片段。 - 把召回的片段拼进 Prompt,作为上下文。
- 把拼好的 Prompt 交给
ChatModel生成回答。 - 返回回答给用户。
这个链路里,Embedding 负责“找”,LLM 负责“答”,向量库负责“存和查”。三者缺一不可,职责清晰。面试时能把这条链路讲清楚,并且说明每一步为什么这么设计,基本就稳了。
4. 生产级代码实战:从零搭一个可用的 RAG 服务
4.1 环境准备与依赖配置
先上依赖。我用的是 Spring Boot 3.2 + Spring AI 1.0,模型侧用 OpenAI 兼容接口(你可以换成任何兼容的模型服务)。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.0</version> </dependency>配置文件里把模型地址和密钥配好。这里要注意,Embedding 模型和 Chat 模型可以是不同的服务,Spring AI 允许你分别配置。
spring: ai: openai: api-key: ${AI_API_KEY} base-url: ${AI_BASE_URL} embedding: options: model: text-embedding-3-small chat: options: model: gpt-4o-mini temperature: 0.3注意:
temperature在 RAG 场景建议调低(0.1~0.3),因为你要的是基于检索内容的准确回答,不是天马行空的创作。温度太高会让模型忽略上下文自己编。
4.2 文档入库:批量 Embedding 与向量存储
入库是 RAG 的地基,地基没打好,后面检索全是噪声。核心步骤是:读取文档、切分、批量 Embedding、写入向量库。
@Service public class DocumentIngestService { private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; public DocumentIngestService(EmbeddingModel embeddingModel, VectorStore vectorStore) { this.embeddingModel = embeddingModel; this.vectorStore = vectorStore; } public void ingest(List<String> rawDocs) { List<Document> documents = new ArrayList<>(); for (String raw : rawDocs) { // 按 500 字切分,重叠 50 字,避免语义被切断 List<String> chunks = splitText(raw, 500, 50); for (String chunk : chunks) { documents.add(new Document(chunk, Map.of("source", "manual"))); } } // 批量写入,Spring AI 内部会调用 EmbeddingModel vectorStore.add(documents); } private List<String> splitText(String text, int size, int overlap) { List<String> result = new ArrayList<>(); int start = 0; while (start < text.length()) { int end = Math.min(start + size, text.length()); result.add(text.substring(start, end)); start += (size - overlap); } return result; } }切分参数是这里的关键。size太小,语义不完整,检索出来答非所问;size太大,一个片段里混了多个主题,检索精度下降。500 字是我实测下来中文场景比较稳的值,overlap设 50 是为了防止关键信息正好卡在切分边界上被切断。
4.3 检索增强:相似度检索与上下文拼装
检索环节的核心是控制好 Top-K 和相似度阈值。K 太小召回不全,K 太大噪声多还费 token。
@Service public class RagQueryService { private final VectorStore vectorStore; private final ChatClient chatClient; public RagQueryService(VectorStore vectorStore, ChatClient.Builder builder) { this.vectorStore = vectorStore; this.chatClient = builder.build(); } public String ask(String question) { // 1. 检索 Top-5 相关片段 SearchRequest request = SearchRequest.query(question) .withTopK(5) .withSimilarityThreshold(0.7); List<Document> docs = vectorStore.similaritySearch(request); if (docs.isEmpty()) { return "抱歉,知识库中没有找到相关内容。"; } // 2. 拼装上下文 String context = docs.stream() .map(Document::getContent) .collect(Collectors.joining("\n---\n")); // 3. 构造 Prompt String prompt = """ 你是一个严谨的助手,请严格基于以下上下文回答问题。 如果上下文中没有答案,直接说"不知道",不要编造。 上下文: %s 问题:%s """.formatted(context, question); // 4. 调用 LLM return chatClient.prompt(prompt).call().content(); } }similarityThreshold这个参数很多人会忽略。设 0.7 意味着只召回相似度 0.7 以上的片段,低于这个值的直接丢弃。这样能有效过滤噪声,但阈值设太高会漏召回。我的经验是先用 0.6 跑一批测试问题,看召回情况再微调。
4.4 关键参数计算:Top-K 与阈值的取舍逻辑
这里展开讲一下参数怎么定,因为面试官很爱追问“你为什么设这个值”。
Top-K 的确定逻辑:假设你的知识库有 N 个片段,每次检索召回 K 个。K 太小,可能漏掉关键信息;K 太大,Prompt 变长,token 成本上升,而且无关信息会干扰模型判断。经验公式是 K 取 3 到 8 之间,具体看你的片段粒度和问题复杂度。我一般从 5 开始调。
相似度阈值的确定逻辑:这个值跟你的 Embedding 模型强相关。不同模型的相似度分布不一样,不能照搬别人的值。正确做法是拿一批标注好的“问题-正确片段”对,跑一遍检索,看正确片段的相似度分布,取一个能覆盖 90% 正确片段的值作为阈值。
提示:如果你发现检索结果总是不理想,先别急着换模型,先检查切分策略和阈值。我踩过的坑里,八成检索问题出在切分上,而不是模型不行。
5. 常见问题与排查技巧实录
5.1 检索召回不准的排查路径
这是最高频的问题。排查顺序我总结成一张表,按这个顺序走基本能定位。
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 召回内容完全不相关 | Embedding 模型与语料语言不匹配 | 用几个已知相似句测相似度 | 换中文优化模型 |
| 召回内容沾边但不精准 | 切分粒度太粗 | 检查片段长度和主题纯度 | 减小 chunk size |
| 关键信息总是漏召回 | 阈值设太高 | 打印正确片段的相似度 | 下调阈值 |
| 召回结果重复度高 | 切分重叠太大 | 检查 overlap 参数 | 减小 overlap |
| 检索延迟高 | 向量库未建索引 | 查看向量库索引状态 | 建 HNSW 索引 |
我印象最深的一次排查:线上问答总是答非所问,查了半天发现是文档切分时按字符数切,把一段完整的操作步骤从中间切断了,导致检索到的片段只有前半段。后来改成按段落切分,问题立刻解决。所以切分策略真的比模型选型重要。
5.2 向量维度不匹配与归一化陷阱
这个坑很隐蔽。如果你换了 Embedding 模型,维度从 768 变成 1536,但向量库里存的还是旧维度的向量,检索时会直接报错或者返回垃圾结果。换模型必须重建整个向量库,没有捷径。
归一化陷阱是另一个。有些向量库默认对向量做归一化,有些不做。如果你的查询向量归一化了但库里的没归一化,相似度计算就全乱了。统一做法是:入库和查询用同一套归一化逻辑,最好在代码里显式处理,别依赖向量库的默认行为。
// 显式归一化,避免依赖向量库默认行为 private float[] normalize(float[] vector) { double norm = 0; for (float v : vector) norm += v * v; norm = Math.sqrt(norm); float[] result = new float[vector.length]; for (int i = 0; i < vector.length; i++) { result[i] = (float) (vector[i] / norm); } return result; }5.3 面试高频追问速查
把面试官最爱追问的几个问题整理出来,附上答题要点。
追问一:“Embedding 向量和 LLM 隐藏层向量有什么区别?”答题要点:Embedding 向量是专门训练用于语义表示的,维度低、可比较;隐藏层向量是模型中间状态,维度高、语义不纯粹,不适合直接做检索。
追问二:“为什么 RAG 不直接用 LLM 做检索?”答题要点:LLM 输出的是概率分布不是语义坐标,无法高效计算相似度;而且每次检索都要跑完整前向传播,成本高到不可接受。
追问三:“余弦相似度和欧氏距离怎么选?”答题要点:向量归一化后两者等价;未归一化时余弦相似度只关注方向,欧氏距离还受模长影响。语义检索通常用余弦相似度。
追问四:“Embedding 模型能微调吗?”答题要点:可以,用对比学习在领域语料上微调,能显著提升专业领域检索效果。但成本较高,一般先优化切分和 Prompt。
追问五:“Spring AI 里怎么保证 Embedding 和 Chat 的一致性?”答题要点:两者是独立接口,通过 VectorStore 解耦;一致性靠统一的文档切分策略和 Prompt 模板保证,不靠接口耦合。
5.4 我踩过的三个真实坑
第一个坑:批量入库时没做限流。一次性把几千个片段丢给 Embedding 接口,直接被限流封了半小时。后来改成每批 100 个,批间 sleep 200ms,稳了。
第二个坑:Prompt 里没加“不知道就说不知道”。结果模型在检索不到内容时开始自由发挥,编得有模有样,用户根本分辨不出来。加上这句约束后,幻觉率大幅下降。
第三个坑:向量库没做持久化。开发时用的内存向量库,重启一次数据全没了,每次都要重新入库。生产环境一定要用带持久化的向量库,这是血泪教训。
6. 写在最后的一点个人体会
这道面试题之所以经典,是因为它表面上考概念,实际上考的是你对整个 RAG 架构的理解深度。Embedding 和 LLM 的关系,本质上是“检索”和“生成”的关系,是“找资料”和“写答案”的关系。把这条主线抓住,所有的追问都能顺着推出来。
我在实际项目里最大的体会是:别迷信模型,先把工程做扎实。切分策略、阈值调优、Prompt 约束这些“脏活累活”,对最终效果的影响远大于换一个更强的模型。很多团队一上来就纠结用哪个 Embedding 模型,结果切分一塌糊涂,换什么模型都救不回来。
如果你正在准备面试,建议把第 4 节的代码亲手跑一遍,把第 5 节的排查表背下来。面试时能说出“我实测下来阈值设 0.7 比较稳”这种话,比背十页理论都有说服力。如果你正在做落地项目,先把最小可用的 RAG 链路跑通,再逐步优化每个环节,别一上来就追求完美架构。