ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Embedding与LLM输出向量:面试必问的语义坐标与概率分布

Embedding与LLM输出向量:面试必问的语义坐标与概率分布

1. 面试官问这道题,到底在考察什么

面过 Java AI 岗的人大概都有这种体验:简历上写了“熟悉 Spring AI”“做过 RAG 知识库”,面试官不问你 API 怎么调,张口就是一句——“Embedding 和 LLM 输出的向量,到底是不是一回事?”

这个问题杀伤力很大。因为它不像八股文那样背一背就能糊弄过去,它直接暴露你对大模型底层数据流的理解深度。我见过不少候选人,能熟练写出ChatClient的调用链,能讲清楚VectorStore怎么存怎么查,但一被追问“那 LLM 输出的那个向量和 Embedding 向量能不能混着用”,当场就卡壳了。

先把结论摆在最前面,方便你建立整体认知:Embedding 向量和 LLM 输出向量,虽然都是浮点数数组,但它们是两个完全不同维度的东西——一个是“语义坐标”,一个是“概率分布”。前者用来衡量“两段文本像不像”,后者用来回答“下一个词是什么”。把这两个概念混为一谈,是很多 Java 后端转 AI 时最容易踩的认知坑。

这篇文章我打算从面试考察意图、底层原理、代码实战、生产踩坑四个层面把它彻底讲透。适合三类人看:正在准备 Java AI 岗面试的、用 Spring AI 做 RAG 项目但原理没吃透的、以及想搞清楚“向量”这个词在大模型里到底有几种含义的开发者。全文会配可运行的 Spring AI 代码,不是纸上谈兵。

在展开之前,先明确一个贯穿全文的类比,后面所有原理都围绕它展开:Embedding 像是给每段文本发一张“身份证”,上面写满了它在语义空间里的坐标;而 LLM 输出的向量,像是它在做选择题时填的“答题卡”,每一格代表某个候选词的得分。身份证可以拿去比对两个人像不像,答题卡只能用来决定下一个字写什么。这个区别,就是整道面试题的核心。

2. 两种向量的本质差异:语义坐标 vs 概率分布

2.1 Embedding 向量:把文本钉在语义空间的一个点上

Embedding 的本质是一次静态映射。你给它一段文本,它经过编码器(比如 BERT 类模型、或者各家厂商的 embedding 模型),吐出一个固定维度的向量,比如 768 维、1024 维、1536 维。这个向量一旦生成,就固定了,同一段文本每次生成的向量几乎一致(确定性输出)。

它的核心特性是语义相似度可度量。两段文本的 embedding 向量做余弦相似度计算,值越接近 1,说明语义越接近。这就是 RAG 检索的物理基础——用户问“怎么退货”,知识库里有一条“商品如何申请退款”,虽然字面不一样,但 embedding 相似度高,就能被召回。

你可以把 embedding 空间想象成一张巨大的地图,每个词、每句话都被钉在地图上的某个坐标点。语义相近的文本,坐标点就挨得近。“猫”和“狗”离得近,“猫”和“汽车”离得远。这个空间是训练出来的,不是人为设计的,所以它捕捉的是统计意义上的语义关联。

关键点在于:embedding 向量不携带生成能力。它只是坐标,你没法从坐标反推出“下一句话该说什么”。它的使命就是“表示”,不是“预测”。

2.2 LLM 输出向量:下一个 token 的概率分布

LLM 输出的向量,准确说法叫logits 向量。当模型处理完你的输入,在最后一层会输出一个维度等于词表大小的向量。比如词表有 15 万个 token,那这个向量就是 15 万维,每一维对应一个 token 的原始得分(logit)。

这个向量经过 softmax 之后,就变成了概率分布——每个 token 被选为下一个词的概率。模型根据这个分布采样,选出下一个 token,然后把这个 token 拼回输入,再跑一遍,生成再下一个。这就是自回归生成的全过程。

所以 LLM 输出向量的本质是动态的、上下文相关的、用于决策的。同一个问题,你问两遍,因为采样随机性,输出的 logits 分布可能略有不同,最终生成的文本也可能不一样。它不是一个固定的“表示”,而是一个“决策依据”。

这里有个容易被忽略的细节:LLM 内部其实也有隐藏状态向量(hidden states),那是它中间层的表示,维度通常是几千。但面试官问“LLM 输出向量”时,默认指的是最后一层用于预测 next token 的那个 logits 向量,不是中间隐藏态。这个区分要讲清楚,否则容易答偏。

2.3 一张表看清两者的分野

对比维度Embedding 向量LLM 输出向量(logits)
本质语义坐标概率分布
维度通常 768~3072等于词表大小,常 5万~15万
是否确定基本确定受采样影响,有随机性
用途相似度计算、检索、聚类预测下一个 token
能否互相替代不能不能
生成方式编码器前向一次解码器每步生成一次
典型消费方向量数据库采样器 / 解码策略

这张表建议直接背下来,面试时能脱口而出,基本就赢了一半。但光背没用,面试官会追问“为什么维度差这么多”“为什么不能混用”,这就得靠下一节的原理来撑。

3. 从 Transformer 结构看两种向量是怎么产生的

3.1 编码器路线:Embedding 是怎么被“压”出来的

主流 embedding 模型大多基于 Transformer 的编码器结构(Encoder)。输入文本先被 tokenize 成一串 token id,然后查 embedding 表变成初始向量,再经过多层自注意力,最后通过池化(pooling)操作——常见的有 CLS token 池化、均值池化——把整个序列压缩成一个固定维度的向量。

这个压缩过程是有损的,但保留了语义主干。就像把一本书浓缩成一句话摘要,细节丢了,主旨还在。正因为有损,所以 embedding 向量维度不需要很大,768 到 1536 就够表达语义了。维度再高,边际收益递减,还浪费存储。

这里有个工程上的关键点:embedding 模型和生成模型是分开训练的,甚至是不同厂商的。你用 OpenAI 的 embedding 模型生成的向量,和用本地部署的 BGE 模型生成的向量,维度可能一样,但语义空间完全不同,绝对不能混用。这是 RAG 项目里最常见的翻车点之一。

3.2 解码器路线:logits 是怎么一步步“算”出来的

LLM 大多是解码器结构(Decoder-only)。它同样是 token 进、向量出,但最后一层的输出不是池化成一个向量,而是保留每个位置的输出。当你只关心最后一个位置时,它对应的输出向量经过一个线性层(LM Head)映射到词表维度,就得到了 logits。

这个 LM Head 是一个巨大的矩阵,形状是[hidden_size, vocab_size]。比如 hidden_size 是 4096,vocab_size 是 15 万,这个矩阵就有 6 亿多参数。它负责把“内部语义表示”翻译成“每个词的得分”。这一步是 embedding 模型完全没有的。

所以从结构上看,embedding 模型止步于“表示”,LLM 多了一个“决策头”。这个决策头就是两者最本质的结构差异。面试时如果能画出这个结构差异,比背十句定义都管用。

3.3 为什么维度差异这么大,却都叫“向量”

这是面试官爱挖的坑。答案是:“向量”只是数学形式,不代表语义等价。就像“温度”和“身高”都可以用数字表示,但你不会拿 25 度和 175 厘米去比较。

Embedding 向量的每一维没有独立含义,它是分布式表示,整体才有意义。logits 向量的每一维有明确含义——对应一个具体 token 的得分。一个是“整体语义”,一个是“逐词打分”,维度自然天差地别。

理解了这一点,你就能回答那个经典追问:“能不能把 LLM 的隐藏态拿来当 embedding 用?”答案是理论上可以,实践中也有这种做法(叫 LLM-based embedding),但效果通常不如专门训练的 embedding 模型,而且成本高得多。因为 LLM 的隐藏态是为生成优化的,不是为相似度优化的。

4. Spring AI 实战:两条链路分别怎么落地

4.1 环境与依赖准备

先搭一个能跑的最小工程。Spring Boot 3.2+,JDK 17+,Spring AI 用 1.0 稳定版(写这篇文章时 1.0.x 已经 GA,比早期的 0.x 稳定太多)。

<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-pgvector-store-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>

配置文件里配好模型和向量库:

spring: ai: openai: api-key: ${OPENAI_API_KEY} embedding: options: model: text-embedding-3-small chat: options: model: gpt-4o-mini temperature: 0.7 datasource: url: jdbc:postgresql://localhost:5432/vectordb

提示:embedding 模型和 chat 模型要分开配。很多人图省事用同一个模型名,结果 embedding 调用直接报错。这两个是不同端点、不同用途的模型,必须区分。

4.2 生成 Embedding 并做相似度检索

先看 embedding 这条链路。核心是EmbeddingModel接口,它负责把文本转成向量。

@Service public class EmbeddingService { private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; public EmbeddingService(EmbeddingModel embeddingModel, VectorStore vectorStore) { this.embeddingModel = embeddingModel; this.vectorStore = vectorStore; } // 单条文本转向量 public float[] embed(String text) { return embeddingModel.embed(text); } // 批量转向量,生产环境务必用批量接口 public List<float[]> embedBatch(List<String> texts) { return embeddingModel.embed(texts); } // 写入向量库 public void save(String content, Map<String, Object> metadata) { Document doc = new Document(content, metadata); vectorStore.add(List.of(doc)); } // 相似度检索 public List<Document> search(String query, int topK) { SearchRequest request = SearchRequest.builder() .query(query) .topK(topK) .similarityThreshold(0.7) .build(); return vectorStore.similaritySearch(request); } }

这段代码有几个生产级的细节值得说。第一,embedBatch一定要用,单条循环调用 embedding 接口,网络往返开销能把性能拖垮,批量接口一次能处理几十上百条。第二,similarityThreshold别设太低,0.7 是个经验起点,太低会召回一堆不相关的内容,反而干扰 LLM。第三,metadata 里建议存上来源、时间、业务标签,检索时可以按 metadata 过滤,这是纯向量检索做不到的。

4.3 观察 LLM 的输出:从 logits 到文本

再看 LLM 这条链路。日常开发中你拿到的是生成好的文本,看不到 logits。但 Spring AI 提供了底层接口,可以拿到更多信息。

@Service public class ChatService { private final ChatModel chatModel; public ChatService(ChatModel chatModel) { this.chatModel = chatModel; } // 普通调用,拿到文本 public String chat(String prompt) { return chatModel.call(prompt); } // 流式调用,观察逐 token 生成过程 public void streamChat(String prompt) { Flux<String> stream = chatModel.stream(prompt); stream.doOnNext(token -> System.out.print(token)) .blockLast(); } }

流式输出是理解“LLM 逐 token 生成”最直观的方式。你会看到文字一个一个蹦出来,每个 token 就是一次 logits 计算 + 采样的结果。这跟 embedding 一次性输出一个固定向量,是完全不同的节奏。

如果你想更深入,可以看ChatResponse里的 metadata,里面包含 token 使用量、finish reason 等信息。虽然 Spring AI 默认不暴露原始 logits(这是合理的抽象),但理解底层是 logits 采样,能帮你调好 temperature、top_p 这些参数。

4.4 把两条链路串成 RAG

RAG 的本质,就是把 embedding 链路和 LLM 链路接起来:

public String ragAnswer(String question) { // 第一步:用 embedding 检索相关文档 List<Document> docs = embeddingService.search(question, 5); // 第二步:把检索结果拼进 prompt String context = docs.stream() .map(Document::getText) .collect(Collectors.joining("\n---\n")); String prompt = """ 请根据以下资料回答问题,资料中没有的信息不要编造。 资料: %s 问题:%s """.formatted(context, question); // 第三步:交给 LLM 生成 return chatService.chat(prompt); }

这段代码虽然短,但把两种向量的分工体现得淋漓尽致:embedding 负责“找”,LLM 负责“答”。找的时候用相似度,答的时候用生成。面试时能把这条链路讲清楚,再点出“两种向量各司其职、不可互换”,基本就到位了。

5. 生产环境里最容易翻车的五个点

5.1 混用不同模型的 embedding 向量

这是 RAG 项目第一大坑。开发时用 A 模型建库,上线换成 B 模型,或者中途升级了 embedding 模型版本,结果检索全乱套。因为不同模型的语义空间不兼容,向量之间算相似度毫无意义。

解决方案:embedding 模型一旦确定,就锁死版本,写进配置中心。如果必须换模型,整个向量库必须重建,没有捷径。重建时记得做灰度,新旧库并行一段时间,对比召回效果。

5.2 把 LLM 隐藏态当 embedding 用

有些团队为了省一个模型,想直接用 LLM 的中间层输出做检索。理论上可行,但实测效果通常差一截,而且 LLM 前向一次的成本远高于专用 embedding 模型。检索场景要的是快和准,不是生成能力。

解决方案:检索归检索,生成归生成,各用各的模型。这是被大量实践验证过的最优解,别为了省事走弯路。

5.3 忽略 token 长度限制

embedding 模型和 LLM 都有最大 token 限制。embedding 模型通常 512 到 8192 token,LLM 可能 8k 到 128k。超长文本直接截断,会导致语义丢失,检索召回不准。

解决方案:入库前做分块(chunking),按语义边界切,比如按段落、按标题层级。块大小控制在 200 到 500 token,块之间留一点重叠(overlap),避免切断上下文。Spring AI 的TokenTextSplitter可以直接用。

5.4 相似度阈值拍脑袋定

similarityThreshold设 0.7 还是 0.8,很多人凭感觉。设高了召回不足,设低了噪声太多。不同 embedding 模型的相似度分布不一样,没有万能值。

解决方案:拿一批真实 query 做离线评估,画出相似度分布曲线,找到召回率和准确率的平衡点。这个工作一次做好,后面长期受益。

5.5 忘了向量维度和索引的匹配

建向量库时,表的向量维度必须和 embedding 模型输出维度一致。用 1536 维的模型建了 768 维的表,写入直接报错。另外,向量索引(如 HNSW、IVFFlat)的参数也要根据数据量调,数据量小的时候暴力检索反而更快。

解决方案:建表前确认模型维度,索引参数参考 pgvector 官方建议。数据量低于 10 万条时,可以先不建索引,用精确检索,简单可靠。

6. 面试怎么答才能拿高分

6.1 一句话抓住本质

如果面试官只给你 30 秒,你就说:“Embedding 向量是文本的语义坐标,用于相似度计算和检索;LLM 输出向量是下一个 token 的概率分布,用于生成。两者维度、用途、生成方式都不同,不能互换。”这句话信息密度高,直接命中要害。

6.2 主动画出数据流

面试时如果能主动画一下数据流,加分很多。embedding 链路是:文本 → tokenize → 编码器 → 池化 → 固定向量 → 向量库。LLM 链路是:文本 → tokenize → 解码器 → 最后一层隐藏态 → LM Head → logits → softmax → 采样 → 下一个 token。两条链路一对比,结构差异一目了然。

6.3 用 RAG 场景收尾

最后用 RAG 把两者串起来,说明它们在真实系统里怎么协作。这样回答既有理论深度,又有工程落地,面试官会觉得你是真做过项目,不是背题。

6.4 常见追问的应对

面试官可能追问:“那 LLM 能不能生成 embedding?”可以答:能,但通常不划算,专用 embedding 模型更快更准更便宜。追问:“向量维度越高越好吗?”答:不是,维度高存储和计算成本都上升,边际收益递减,要按场景权衡。追问:“怎么评估 embedding 质量?”答:用检索任务的召回率、MRR 等指标,在真实业务数据上评估,别只看公开榜单。

7. 我在实际项目里踩过的坑和几点体会

说几个文档里不会写、但实际做项目一定会遇到的细节。

第一个坑是批量 embedding 的并发问题。批量接口虽然快,但一次塞太多文本,请求体可能超限,或者触发服务端的速率限制。我的做法是分批,每批 50 到 100 条,批之间加一点间隔,配合重试。别一次性把几千条全怼进去,大概率失败。

第二个坑是向量库的写入性能。pgvector 在数据量大时,逐条写入会非常慢。用批量插入,并且把索引创建放到数据导入之后,能快好几倍。索引边写边建,性能损耗很大。

第三个坑是相似度分数的解读。不同模型的余弦相似度分布不一样,有的模型普遍偏高,0.9 才算相关;有的模型 0.7 就已经很相关了。别拿一个模型的阈值套到另一个模型上,一定要重新标定。

第四个体会是别过度迷信向量检索。纯向量检索对精确匹配(比如订单号、专有名词)不敏感。生产环境里,向量检索 + 关键词检索的混合方案,效果通常比纯向量好。Spring AI 也支持这种组合,值得一试。

最后一个建议:把 embedding 和 LLM 当成两个独立的服务来治理。它们有不同的模型、不同的成本、不同的性能特征、不同的失败模式。分开监控、分开限流、分开降级,系统才稳。把它们混在一起管理,出问题时你连是检索挂了还是生成挂了都分不清。

这道面试题之所以经典,是因为它表面问概念,实际考的是你对大模型数据流的整体理解。把两种向量的来龙去脉搞清楚,RAG、Agent、微调这些上层应用,你都能看得更透。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进