1. 面试官为什么总爱问 Embedding 和 LLM 输出的关系
1.1 这道题到底在考什么
面 Java AI 岗位的时候,很多人一听到“Embedding 和 LLM 输出向量到底啥关系”就懵了。有人觉得这俩是一回事,都是“把东西变成一串数字”;有人觉得 Embedding 是 Embedding,LLM 输出是 LLM 输出,八竿子打不着。这两种理解都不对,而且面试官恰恰就是冲着这个模糊地带来的。
先把结论摆在前面:Embedding 和 LLM 输出向量,形式上都是浮点数数组,但它们的语义来源、产生方式、用途完全不同。Embedding 是“把输入映射到语义空间”,LLM 输出向量是“模型在生成过程中对下一个 token 的预测分布(logits)或者隐藏状态”。一个是编码器思维,一个是解码器思维。面试官问这道题,本质是在确认三件事:你分不分得清“表示”和“生成”;你知不知道向量在 RAG、语义检索、Agent 记忆里怎么用;你能不能把原理落到 Spring AI 的代码上。
我面过不少人,能把float[]和List<Double>的区别讲清楚,但一问“为什么余弦相似度能算语义相似”,就开始背公式了。所以这篇我打算从原理讲到生产代码,把这条链路彻底打通。
1.2 一个生活化类比先建立直觉
你可以把 Embedding 想成“图书馆的索引卡”。每本书(一段文本、一张图、一段音频)被压缩成一张卡片,卡片上写着它在语义空间里的坐标。内容相近的书,坐标就挨得近。这个坐标是静态的、可复用的,你查一次存下来,下次直接比对就行。
LLM 输出向量则更像“一个人边说话边在大脑里形成的下一个词的概率草图”。它不是一个固定的坐标,而是模型在当前上下文下,对“接下来该说什么”的内部表示。你让它生成一句话,它每一步都会产出一个高维向量(logits),这个向量经过 softmax 才变成词的概率。这个向量是动态的、上下文相关的、一次性的。
所以关键差异在于:Embedding 是“输入侧”的表示,LLM 输出向量是“输出侧”的表示。前者用来检索、聚类、去重;后者用来采样、生成、决策。混为一谈,RAG 就会做歪。
1.3 为什么 Java 开发者尤其容易踩坑
Java 生态里做 AI,主流路径是 Spring AI 或者 Spring AI Alibaba。这两套框架把 Embedding 和 Chat 抽象成了两个不同的接口:EmbeddingModel和ChatModel。但很多同学写代码的时候,看到EmbeddingModel.embed()返回float[],看到ChatModel.call()返回String,就以为 LLM 不产向量。其实 LLM 内部每一步都在产向量,只是框架帮你封装掉了。
面试官问这道题,往往是想引出下一个问题:“那你怎么用 LLM 的输出向量做语义缓存?”或者“RAG 里为什么用 Embedding 而不是直接用 LLM 输出?”能答上来的人,基本就过了 AI 应用开发这一关。
2. 把 Embedding 和 LLM 输出向量的原理彻底拆开
2.1 Embedding 的本质:把离散符号压进连续空间
Embedding 这个词最早来自词向量。Word2Vec 时代,我们把每个词映射成一个 100 到 300 维的稠密向量,让“国王 - 男人 + 女人 ≈ 女王”这种语义运算成立。到了 Transformer 时代,Embedding 不再只是词级别,而是句子、段落、图像、音频都能编码。
它的核心机制是:通过一个可学习的矩阵,把离散的 token id 映射成连续向量,再经过多层编码器(比如 BERT、BGE、text-embedding-3)聚合出整段文本的表示。这个表示通常取[CLS]token 的隐藏状态,或者对所有 token 做 mean pooling。
关键点在于,Embedding 模型是专门为“表示”训练的。它的训练目标可能是对比学习(让相似文本的向量靠近,不相似的远离),也可能是掩码语言建模。不管哪种,最终产出的向量有一个重要性质:语义相似度可以用余弦相似度或点积直接衡量。
这就是为什么 RAG 里能用它做检索。你把知识库所有 chunk 提前 embed 好,存进向量数据库;用户提问时,把问题 embed 成同维度向量,做最近邻搜索,就能召回语义相关的内容。整个过程不需要 LLM 参与,快且便宜。
2.2 LLM 输出向量的本质:下一个 token 的概率分布
LLM 是自回归模型。给定上下文[t1, t2, ..., tn],它预测下一个 tokentn+1的概率分布。这个分布怎么来的?最后一层 Transformer 输出一个隐藏状态h,然后经过一个线性层(LM Head)映射到词表大小V的向量,也就是 logits。logits 经过 softmax 就变成概率。
所以严格来说,LLM 每一步的输出向量是 logits,维度等于词表大小,比如 32000、128000。这个向量不是用来算语义相似度的,它是用来采样下一个词的。你取 argmax 就是贪心解码,按概率采样就是随机解码,加温度就是调节分布平滑度。
那 LLM 的隐藏状态能不能当 Embedding 用?可以,但效果通常不如专门的 Embedding 模型。因为 LLM 的训练目标是生成,不是表示。它的隐藏状态里混杂了大量生成相关的信息,语义纯度不够。学术界有人拿 LLM 最后一层隐藏状态做检索,效果能打,但工程上不划算——LLM 推理成本比 Embedding 模型高一个数量级。
2.3 一张表看清两者的核心差异
| 维度 | Embedding | LLM 输出向量 |
|---|---|---|
| 产生位置 | 编码器输出 / 池化层 | 解码器最后隐藏状态 / LM Head |
| 典型维度 | 384、768、1024、1536、3072 | 等于词表大小,32000 到 128000+ |
| 语义性质 | 静态、可复用、语义空间坐标 | 动态、上下文相关、生成分布 |
| 主要用途 | 检索、聚类、去重、推荐 | 采样、生成、决策、打分 |
| 相似度计算 | 余弦相似度、点积、欧氏距离 | 一般不直接算相似度 |
| 成本 | 低,可批量离线 | 高,逐 token 自回归 |
| 框架接口 | EmbeddingModel.embed() | ChatModel.call()内部 |
这张表建议背下来。面试时能脱口而出,面试官基本就知道你是真做过项目的。
2.4 为什么 RAG 必须用 Embedding 而不是 LLM 输出
有人会问:既然 LLM 也能产向量,为什么 RAG 检索不用 LLM 输出?原因有三。
第一,成本。Embedding 模型参数量通常在 1 亿到 10 亿之间,LLM 动辄几百亿。把 10 万条知识 chunk 全部过一遍 LLM,账单能让你怀疑人生。
第二,语义纯度。Embedding 模型是对比学习训出来的,天生适合算相似度。LLM 的隐藏状态是为生成服务的,里面混了位置、语法、生成策略等信息,直接拿来检索,召回率会掉。
第三,维度与存储。Embedding 通常 768 到 1536 维,向量数据库存起来很轻松。LLM 隐藏状态动辄 4096 到 8192 维,存储和检索成本都高。
所以工程上的标准做法是:检索用 Embedding,生成用 LLM,两者通过向量数据库解耦。这也是 Spring AI 里VectorStore和ChatClient分开设计的原因。
3. Spring AI 生产级代码实战:从 Embedding 到 RAG 全链路
3.1 环境准备与依赖选型
先明确技术栈。我这次用的是 Spring Boot 3.2 + Spring AI 1.0.0-M6 + Spring AI Alibaba,Embedding 模型选的是通义千问的text-embedding-v3,LLM 选的是qwen-plus。向量数据库用 Redis Stack,因为大部分 Java 团队已经有 Redis,加个 RediSearch 模块就能当向量库用,运维成本最低。
Maven 依赖如下:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <version>1.0.0-M6.1</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-redis-store</artifactId> <version>1.0.0-M6</version> </dependency>配置文件里把 API Key 和模型名配上:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} embedding: options: model: text-embedding-v3 chat: options: model: qwen-plus temperature: 0.7 data: redis: host: localhost port: 6379注意:API Key 千万别硬编码进代码,用环境变量或者配置中心。我见过有人把 Key 提交到 Git,第二天就被刷爆了。
3.2 用 EmbeddingModel 把文本变成向量
Spring AI 的EmbeddingModel接口非常干净,核心就两个方法:embed(String)和embed(List<String>)。批量接口很重要,因为生产环境你不可能一条一条 embed,那样网络往返开销太大。
@Service public class EmbeddingService { private final EmbeddingModel embeddingModel; public EmbeddingService(EmbeddingModel embeddingModel) { this.embeddingModel = embeddingModel; } public float[] embedSingle(String text) { return embeddingModel.embed(text); } public List<float[]> embedBatch(List<String> texts) { return embeddingModel.embed(texts); } }这里有个细节:text-embedding-v3默认输出 1024 维,但支持通过dimensions参数降到 512 或 256。降维能省存储和检索时间,但会损失一点精度。我的经验是,知识库小于 10 万条时用 1024 维,超过 100 万条再考虑降维。
批量 embed 的时候要注意单次请求的 token 上限。通义千问的 Embedding 接口单次最多 25 条文本,每条不超过 2048 token。所以生产代码里要自己做分批:
public List<float[]> embedInBatches(List<String> texts, int batchSize) { List<float[]> result = new ArrayList<>(); for (int i = 0; i < texts.size(); i += batchSize) { int end = Math.min(i + batchSize, texts.size()); List<String> batch = texts.subList(i, end); result.addAll(embeddingModel.embed(batch)); } return result; }3.3 向量入库与相似度检索
拿到float[]之后,下一步是存进向量数据库。Spring AI 的VectorStore抽象帮你屏蔽了底层差异,Redis、Milvus、PgVector 都能用同一套 API。
@Service public class KnowledgeBaseService { private final VectorStore vectorStore; public KnowledgeBaseService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public void addDocuments(List<String> chunks) { List<Document> documents = chunks.stream() .map(chunk -> new Document(chunk, Map.of("source", "manual"))) .toList(); vectorStore.add(documents); } public List<Document> search(String query, int topK) { SearchRequest request = SearchRequest.query(query) .withTopK(topK) .withSimilarityThreshold(0.7); return vectorStore.similaritySearch(request); } }similarityThreshold这个参数很关键。设太低会召回一堆不相关的内容,污染 LLM 的上下文;设太高又可能漏掉关键信息。我的经验值是 0.65 到 0.75 之间,具体要看 Embedding 模型的分布。你可以先用一批标注数据跑一下召回率和准确率,再定阈值。
3.4 把检索结果喂给 LLM 生成答案
RAG 的最后一步是把召回的 chunk 拼进 prompt,让 LLM 基于这些上下文回答。Spring AI 的ChatClient提供了 fluent API,写起来很顺。
@Service public class RagService { private final ChatClient chatClient; private final KnowledgeBaseService knowledgeBaseService; public RagService(ChatClient.Builder builder, KnowledgeBaseService knowledgeBaseService) { this.chatClient = builder.build(); this.knowledgeBaseService = knowledgeBaseService; } public String answer(String question) { List<Document> docs = knowledgeBaseService.search(question, 5); String context = docs.stream() .map(Document::getContent) .collect(Collectors.joining("\n\n")); String prompt = """ 你是一个严谨的技术助手。请仅基于以下上下文回答问题。 如果上下文没有相关信息,直接说"我不知道",不要编造。 上下文: %s 问题:%s """.formatted(context, question); return chatClient.prompt() .user(prompt) .call() .content(); } }这段代码里有几个生产级的考量。第一,prompt 里明确要求“仅基于上下文”,减少幻觉。第二,要求“不知道就说不知道”,避免模型硬编。第三,topK 设 5 是经验值,太多会超上下文窗口,太少召回不足。
3.5 用 LLM 输出向量做语义缓存
前面说了 LLM 输出向量一般不直接用于检索,但有一个场景例外:语义缓存。传统缓存用 key 精确匹配,用户问“Java 怎么创建线程”和“Java 线程创建方式”会命中两次 LLM,浪费钱。语义缓存的做法是:把用户问题 embed 成向量,先在缓存里找相似问题,如果相似度超过阈值,直接返回缓存答案。
@Service public class SemanticCacheService { private final EmbeddingModel embeddingModel; private final Map<String, CachedAnswer> cache = new ConcurrentHashMap<>(); public Optional<String> get(String question) { float[] queryVec = embeddingModel.embed(question); return cache.values().stream() .filter(entry -> cosineSimilarity(queryVec, entry.vector()) > 0.92) .map(CachedAnswer::answer) .findFirst(); } public void put(String question, String answer) { float[] vec = embeddingModel.embed(question); cache.put(question, new CachedAnswer(vec, answer)); } private double cosineSimilarity(float[] a, float[] b) { double dot = 0, normA = 0, normB = 0; for (int i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } }阈值 0.92 是我实测下来的经验值。低于这个值,容易把不同问题误判为相同;高于 0.95,又太严格,缓存命中率低。生产环境建议用 Redis 存向量,别用ConcurrentHashMap,否则重启就丢。
4. 常见问题与排查技巧实录
4.1 Embedding 维度对不上怎么办
这是新手最容易遇到的问题。你用一个模型 embed 了知识库,换模型之后查询,报维度不匹配。原因是不同 Embedding 模型输出维度不同,text-embedding-v3是 1024 维,text-embedding-v2是 1536 维,BGE 是 768 维。
解决办法只有一个:换模型必须重新 embed 整个知识库。没有捷径。所以选模型的时候要慎重,尽量选稳定、长期维护的。我一般会在项目初期就把 Embedding 模型定死,写进技术文档,避免后期换模型带来的全量重建。
4.2 相似度很高但答案不对
这种情况通常是 chunk 切分有问题。你把一整篇文档 embed 成一个向量,语义被平均掉了,检索时召回的是“整体相关”但“细节不对”的内容。正确做法是按语义边界切分,比如按段落、按标题层级切,每个 chunk 控制在 200 到 500 token。
Spring AI 提供了TokenTextSplitter:
TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 5, 10000, true); List<Document> chunks = splitter.apply(documents);参数含义分别是:目标 chunk 大小、重叠 token 数、最小 chunk 数、最大 chunk 数、是否保留分隔符。重叠 100 token 是为了避免句子被切断导致语义丢失。
4.3 LLM 不按上下文回答
模型幻觉是 RAG 的老大难。除了 prompt 里明确约束,还有几个技巧。第一,把上下文放在问题前面,模型对前面的内容注意力更强。第二,给每个 chunk 加编号,让模型引用来源。第三,用 few-shot 示例告诉模型“不知道就说不知道”。
如果还是不行,考虑换更强的 LLM,或者降低 temperature。temperature=0时模型最保守,适合事实性问答;temperature=0.7适合创意生成。RAG 场景我一般设 0.1 到 0.3。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 维度不匹配 | 换过 Embedding 模型 | 检查模型名和维度配置 | 全量重新 embed |
| 召回不准 | chunk 切分过大 | 看 chunk 平均 token 数 | 按语义边界切分,加重叠 |
| 答案幻觉 | prompt 约束不足 | 检查 prompt 模板 | 加“仅基于上下文”约束 |
| 响应慢 | 向量库没建索引 | 看 Redis 慢查询日志 | 建 HNSW 或 IVF 索引 |
| 缓存命中低 | 阈值设太高 | 统计相似度分布 | 调到 0.90 到 0.93 |
| 成本高 | 每次全量 embed | 看 API 调用量 | 加语义缓存,批量 embed |
4.5 几个我踩过的坑
第一个坑:批量 embed 时没控制并发。我一开始用parallelStream并发调 Embedding 接口,结果触发限流,一半请求失败。后来改成固定线程池,并发度控制在 5 以内,稳定了。
第二个坑:向量没归一化。余弦相似度和点积在向量归一化后等价,但如果你用点积检索又没归一化,结果会偏。Spring AI 的VectorStore内部会处理,但自己写相似度计算时要注意。
第三个坑:Redis 向量索引没预热。Redis Stack 建 HNSW 索引后,第一次查询会慢,因为要构建图。生产环境建议启动后跑一批预热查询。
第四个坑:prompt 太长超上下文。topK 设 10,每个 chunk 500 token,加上问题本身,轻松超过 8000 token。后来我把 topK 降到 5,chunk 控制在 300 token,问题解决。
5. 面试怎么答这道题才能加分
5.1 标准回答框架
如果面试官问“Embedding 和 LLM 输出向量啥关系”,你可以这样答:
“两者形式上都是浮点数组,但语义来源不同。Embedding 是编码器对输入的语义压缩,静态可复用,专门为相似度计算训练,维度通常 768 到 1536。LLM 输出向量是解码器对下一个 token 的预测分布,动态上下文相关,维度等于词表大小,用于采样生成。工程上,RAG 检索用 Embedding,生成用 LLM,两者通过向量数据库解耦。Spring AI 里分别对应EmbeddingModel和ChatModel。”
这段话覆盖了定义、差异、用途、框架落地,面试官基本满意。
5.2 加分项:主动聊语义缓存和 Agent 记忆
如果面试官继续追问,你可以主动引出语义缓存:“LLM 输出向量虽然不直接用于检索,但可以用于语义缓存。把用户问题 embed 后比对历史问题,相似度超过 0.92 直接返回缓存答案,能省不少 token 成本。”
再进一步,可以聊 Agent 记忆:“Agent 的长期记忆也可以用 Embedding 实现。把历史对话 embed 存起来,每轮对话前检索相关记忆注入上下文,让 Agent 记住用户偏好。”
这两个点一抛出来,面试官就知道你不只是背八股,是真做过项目。
5.3 反问环节可以问什么
面试快结束时,你可以反问:“咱们团队的 RAG 系统用的是什么 Embedding 模型?有没有做语义缓存?”这个问题既显示你的专业度,又能帮你判断团队的技术水平。如果对方答不上来,说明 AI 应用还在早期,你进去有机会主导;如果对方讲得头头是道,说明团队有积累,你能学到东西。
6. 写在最后的一些个人体会
我做 Java AI 应用这两年,最大的感受是:原理不清,代码白写。很多人上来就抄 RAG demo,跑通了就以为会了。结果一上生产,召回不准、幻觉严重、成本爆炸,全是坑。根子就在于没搞懂 Embedding 和 LLM 输出向量的本质区别。
Spring AI 把复杂度封装得很好,但封装不等于你可以不懂。面试官问这道题,不是要你背概念,是要确认你有没有能力在出问题时定位到根因。维度不对、召回不准、缓存失效,这些问题的答案都藏在对向量的理解里。
最后分享一个小技巧:如果你要深入学这块,别只看文档,自己动手 embed 一批文本,算一下相似度矩阵,看看哪些该近的近了、哪些该远的没远。跑一遍,比看十篇文章都管用。向量这东西,直觉是跑出来的,不是看出来的。