news 2026/10/2 22:51:55

Embedding与LLM输出向量是什么关系?Java后端RAG实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Embedding与LLM输出向量是什么关系?Java后端RAG实战解析

Java 后端最近一年面试,AI 相关的问题肉眼可见地变多了。我帮朋友做模拟面试时,最常被问到的一个题就是标题里这个:Embedding 和 LLM 输出向量到底啥关系?

大部分候选人第一反应都是“都是向量,差不多吧”。这句话也不能算错,但太笼统了。只要面试官再追一句“那你的线上 RAG 系统里,为什么不直接拿生成模型的向量做相似度检索”,很多人就开始嗯嗯啊啊。这个题表面上考概念,实际上考的是你能不能把“文本 → 向量化 → 建索引 → 检索 → 交给 LLM → 再输出”这条完整链路,用工程语言讲清楚。这篇文章我打算把它讲透,直接用 Java 侧的生产级代码把两条链路串一遍,中间会穿插我在真实项目里踩过的坑。

1. 面试官揪着这道题不放,到底在面什么

1.1 为什么 Java 后端面试突然开始问向量

以前 Java 后端面试问的是 JVM、并发、Spring、分布式事务,这些东西现在还是必考,但这两年明显多了一类题:模型接入、向量检索、RAG 架构、上下文管理。

原因不复杂。AI 应用长在后端系统里,而 Java 是后端存量最大的语言。你可以不训练模型,但你得写接口去调用模型;你可以不研究算法,但你得把知识库切片、向量化、存进数据库;你可以不调优模型参数,但你得处理 token 超限、向量维度不一致、检索返回空结果这类生产问题。

所以面试官问 Embedding 和 LLM 输出向量,不是单纯考你背概念,而是在模拟一个真实工作场景:给你一个查询,你要决定哪条向量链路可用,怎么用,怎么落地。这个决策能力,才是 Java 后端在 AI 时代的新增量。

1.2 拆开题干:三个隐藏考点

我一般会把这道题拆成三个层次:

  • 第一个层次是概念理解。你能不能说清楚 Embedding 是什么,LLM 输出向量又是指哪几种向量。这里最容易暴露“名词党”,话术很熟,但不知道底层长什么样。
  • 第二个层次是技术选型。面试官真正想听的是为什么相似度检索通常用 Embedding 模型,而不是拿生成模型的输出去算。这背后是模型训练目标和向量空间性质之间的因果推断。
  • 第三个层次是工程实现。你一旦说“我用过”,面试官基本会马上接一句“用 Java 怎么调?怎么算余弦?怎么放在线上不会把内存干爆?”这一关纯粹考实战经验,没写过就是没写过。

后面两个大节,我会按这三个层次展开。先把原理层讲清楚,再上代码。

2. 原理层面:Embedding 与 LLM 输出向量差在哪

2.1 一句话分清两种 Embedding

“Embedding”这个词在 AI 语境里至少有两种含义,面试时如果不先区分,后面全部混乱。

第一种是Token Embedding,也叫词嵌入。Transformer 模型在接收文本时,会把每个 token 映射成一个稠密向量。这个向量是从一个巨大的查表操作里拿出来的,相当于每个 token 有一行对应的初始化向量,训练过程中不断更新。它解决的是把离散 token 变成连续数字的问题,是模型的输入,不是模型最终的输出。以 GPT 这类模型为例,token embedding 的维度通常和模型的 hidden size 一致,比如 4096、8192 这样。

第二种是文本 Embedding,也就是日常说的 Embedding API 做出来的东西。输入一整句话,输出一个固定长度的浮点向量。OpenAI 的 text-embedding-3-small 输出 1536 维,BGE 系列常见的是 1024 维。它解决的是语义相似度计算问题:两句话意思越接近,向量在空间里的距离越近,夹角越小。

大多数面试题里问的“Embedding”,指的都是第二种——文本 Embedding 模型的结果。而 LLM 输出向量,完全是另一批产物。

2.2 LLM 内部到底输出了哪些“向量”

LLM 在跑一次推理时,内部会产生多种向量,面试里最常被问到的有三类。

第一类是注意力机制中的 Q、K、V 向量。每个 token 都会生成 Query、Key、Value 三个向量,Q 可以理解成“我想找什么”,K 是“我是什么内容的索引”,V 是“我真正能提供的信息”。这三个向量是模型理解上下文的关键,但它们只存在于模型内部,业务侧基本拿不到。

第二类是每一层 Transformer 输出的 hidden state。你输入一句话进去,模型会一层一层变换 token 的表示,每一层都会产出一组向量。最后一层的 hidden state 经常被叫做“输出向量”,很多人会把它误认为可以通用任何地方。可以这么理解它:它是模型在生成下一个 token 之前,对当前上下文做的一次完整压缩表达。

第三类是输出层的 logits 向量。模型生成下一个 token 时,会对词表里每一个词打一个分数,这个分数列表就是 logits。通过 softmax 变成概率之后,再根据采样策略挑出最终 token。这是 LLM 真正“输出”的东西,也和用户感受到的文本一一对应。

所以当你拿“LLM 输出向量”这个说法去问不同人,有人想到的是 hidden state,有人想到的是 logprobs,有人想到的是注意力分数。Java 面试里最值得聊的,其实是最后一层 hidden state 的值,因为它最容易被误用成检索向量。

2.3 为什么“直接用 LLM 输出向量做检索”看起来能跑,但会翻车

有一类问题我遇到太多次:既然 LLM 最后一层 hidden state 也是向量,长度也和 token embedding 一样,那我是不是可以直接拿生成模型的输出向量当 Embedding 用?

答案是可以跑通,但不稳定,而且不好维护。

关键在于训练目标不同。文本 Embedding 模型训练时,目标是拉近相似文本的向量距离、推开不相似文本的向量距离。它优化的是语义空间。而生成式 LLM 训练时,目标是最大化下一个 token 的预测概率,也就是让输出的文本越流畅、越符合人类语言习惯越好。至于语义空间是不是规整,它不在乎。

这就导致生成模型的 hidden state 空间存在几个典型问题:同义句的表示可能距离很远,不同写法的问法向量跑偏,句子长度对向量分布的影响很明显。我做过一个简单对比,把同一个知识库的问题换成同义表达,用专门 Embedding 模型去检索,Top5 召回基本稳定;用 LLM hidden state 去检索,结果经常大变。

这就像你要给货物分类,你请了一个研究“怎么把话说得更溜”的语言专家,让他按语义分类,他能干,但不是专业对口,误分类率自然高。生产系统不能赌这个稳定性。

3. 生产级 Java 代码实战:打通两条链路

原理讲完,面试官如果满意,接下来一定会把你按到代码这个层面。Java 侧做 AI 应用,最核心的工作不是研究模型,而是把模型调用封装成稳定、可替换、可测试的服务。

3.1 先定接口:把 Embedding 服务封装成自己能替换的东西

我建议第一步不要直接写 HTTP 调用,先定义一个接口。因为线上很容易换模型,从 OpenAI 换成国内模型,或者从自建 BGE 换到其他向量模型,都是常态。只要接口稳定,替换成本就只是一份配置。

下面这个接口是我在项目里常用的形态:

public interface EmbeddingService { EmbeddingResult embed(String text); List<EmbeddingResult> embedBatch(List<String> texts); } public record EmbeddingResult( String text, float[] vector, int totalTokens, String model ) { public double dimension() { return vector.length; } }

用record定义返回结构很合适:不可变、清晰、天然适合做日志和调试。float[]保存向量是为了内存效率,List<Float>会有大量装箱开销,一条线上跑几十万文档的时候差距明显。

为什么接口里要返回totalTokens和model?因为生产环境必须能统计成本。Embedding 是有 token 费用的,而且不同模型返回维度不同,如果不记录,后面排查问题连哪次调用花了多少钱都看不出来。这也是新人最容易漏掉的设计点。

3.2 调用 Embedding API:HTTP 客户端、模型参数和 JSON 解析

实现类我一般用 Java 自带的HttpClient,加 Jackson 做 JSON 序列化和反序列化。不引入额外的 SDK,一是减少依赖,二是面试的时候你能把底层讲清楚。

public class OpenAiEmbeddingClient implements EmbeddingService { private final HttpClient httpClient; private final ObjectMapper mapper; private final String apiKey; private final String endpoint; private final String model; public OpenAiEmbeddingClient(String endpoint, String apiKey, String model, int connectTimeoutMs, int requestTimeoutMs) { this.mapper = new ObjectMapper(); this.endpoint = endpoint; this.apiKey = apiKey; this.model = model; this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofMillis(connectTimeoutMs)) .executor(Executors.newVirtualThreadPerTaskExecutor()) .build(); } }

有几个细节比代码本身更重要。

第一,HttpClient要复用,不要每次调用都 new 一个。连接池、HTTP 连接复用都在这个实例上,每次创建连接不光慢,还会把连接句柄打满。

第二,现在 Java 21 之后可以用虚拟线程执行器,遇到多个 Embedding 并发调用时,可以避免阻塞 Tomcat 的业务线程。这在批量向量化场景很关键。

第三,超时一定要配置。模型服务偶尔会慢,或者 token 输入太长,响应可能几十秒都不回来,没有超时就是生产事故。

单条文本的 Embedding 调用可以这样写:

public EmbeddingResult embed(String text) { Map<String, Object> payload = new HashMap<>(); payload.put("model", model); payload.put("input", text); payload.put("encoding_format", "float"); String requestBody; try { requestBody = mapper.writeValueAsString(payload); } catch (JsonProcessingException e) { throw new IllegalArgumentException("请求体序列化失败", e); } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(endpoint + "/v1/embeddings")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); try { HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new RuntimeException("Embedding 调用失败, status=" + response.statusCode() + ", body=" + response.body()); } JsonNode root = mapper.readTree(response.body()); JsonNode data = root.path("data"); if (data.isEmpty()) { throw new IllegalStateException("Embedding 响应中没有 data 字段"); } float[] vector = mapper.convertValue(data.get(0).get("embedding"), float[].class); int totalTokens = root.path("usage").path("total_tokens").asInt(); return new EmbeddingResult(text, vector, totalTokens, model); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Embedding 调用被中断", e); } catch (IOException e) { throw new RuntimeException("Embedding 网络调用失败", e); } }

这里我刻意用Map和一个普通 POST。为什么不直接用HttpRequest.BodyPublishers.ofString拼 JSON?因为文本内容里可能包含双引号、换行、Unicode 字符,手工拼 JSON 很容易破坏结构。宁可多写几行代码,也要保证序列化正确。

3.3 从 LLM 响应里提取向量相关内容

很多面试官会追问“你说你把 LLM 输出向量和 Embedding 区别开,那你有没有真的看过 LLM 输出的原始响应?”

了解过程是有必要的。以最主流的大模型对话接口为例,如果你的请求开了logprobs,响应里的choices[0].logprobs就会出现每个 token 的出现概率信息:

{ "choices": [{ "index": 0, "logprobs": { "content": [ { "token": "Java", "logprob": -0.132, "top_logprobs": [ {"token": "Java", "logprob": -0.132}, {"token": "JAVA", "logprob": -1.204} ] } ] }, "text": "Java" }] }

这就是我在 2.2 里说的“LLM 输出向量”的一种。你可以用它来分析模型对某个 token 的置信度、做结构化抽取,但拿它去算文本相似度并不合适。

如果你实际接的是开源模型服务,比如 vLLM、TGI 这类推理框架,有些可以配置返回 hidden states,响应里就会多一个类似这样的字段:

{ "hidden_states": [ [ [0.012, -0.233, 0.491, ...] ] ] }

但大多数商业化 API 并不会默认返回 hidden states,因为这会让响应体变得巨大。这里每次生成上千个 token,每个向量几千维,对带宽和服务端内存压力都很大。所以你可以这样理解:Embedding 是专门的 API,LLM 输出向量是内部计算过程的附属产物,不是刻意对外提供的标准服务。

3.4 自实现余弦相似度与向量归一化

相似度计算是向量检索的核心。生产里最常用的是余弦相似度。两个向量 a 和 b 的余弦值等于它们夹角的余弦,公式是:

cos(a, b) = (a · b) / (||a|| · ||b||)

这个值范围在 -1 到 1 之间。1 表示方向完全一致,0 表示正交无关,-1 表示方向完全相反。

千万要写对边界条件。我在代码评审里看到过很多版本漏掉了零向量判断,一旦数据库里混入一条全零向量,结果直接 NaN,排序和落库都会出怪问题。

一个稳妥的版本是:

public final class VectorUtils { private VectorUtils() {} public static double cosine(float[] a, float[] b) { if (a.length != b.length) { throw new IllegalArgumentException( "向量维度不一致: a.length=" + a.length + ", b.length=" + b.length); } double dot = 0.0; double normA = 0.0; double normB = 0.0; for (int i = 0; i < a.length; i++) { dot += (double) a[i] * b[i]; normA += (double) a[i] * a[i]; normB += (double) b[i] * b[i]; } if (normA == 0.0 || normB == 0.0) { return 0.0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } public static float[] normalize(float[] v) { double norm = 0.0; for (float x : v) { norm += (double) x * x; } if (norm == 0.0) { return v; } norm = Math.sqrt(norm); float[] result = new float[v.length]; for (int i = 0; i < v.length; i++) { result[i] = (float) (v[i] / norm); } return result; } }

注意我为什么把a[i] * b[i]转成double再乘。因为float的精度有限,向量维度上千时累加误差会被放大。用double做中间计算,最后再转回需要的精度,这套写法在线上跑了很久没有出现过精度导致的检索错乱。

有一个重要的工程习惯:建议入库前就把向量归一化。归一化之后,余弦相似度和内积就是等价的,因为分母变成了 1。这时候再配合某些只支持内积检索的索引,结果基本一致,还能减少检索时的计算量。这就是生产环境和算法实验不一样的地方,多花一次归一化的代价,换全链路存储和检索的收益。

下面这段代码演示在 Java 应用层做 TopK 相似度排序,不依赖数据库,适合数据量小于百万级或者做缓存查询的场景:

public List<ScoredText> search(float[] queryVector, List<EmbeddingResult> candidates, int topK) { return candidates.stream() .map(item -> new ScoredText( item.text(), VectorUtils.cosine(queryVector, item.vector()), item.vector().length)) .sorted(Comparator.comparingDouble(ScoredText::score).reversed()) .limit(topK) .toList(); } public record ScoredText(String text, double score, int dimension) {}

如果数据量再大,就别用纯内存方案了,往向量数据库的路子走。下一节就讲这个。

4. RAG 落地时的 Embedding 工程细节

4.1 RAG 为什么不用 LLM 生成向量做检索

RAG 是目前 Java 后端接触最多的 AI 架构。流程不复杂:先把你自己的文档切片,切出来的每个 chunk 用 Embedding 模型变成向量,存进向量数据库;用户提问时,把问题也变成向量,在库里做相似度检索,找出最相关的几个文档片段;最后把这些片段拼进 Prompt,发给 LLM 生成答案。

这里的关键问题是:检索那一步,向量从哪来?

我在第 2 节已经说过,LLM hidden state 所在的向量空间没有针对语义相似度优化。而在 RAG 里,用户提问和文档片段往往不是同一个句式,用户可能非常口语化,文档可能全篇书面语。这种场景恰恰是向量检索最容易出错的,对向量空间要求最高。用一个没有专门训练过的向量去做,从第一步就埋了雷。

所以业界标准做法是:**需要一个独立的 Embedding 模型,专门做文本向量化,负责把文档和 query 映射到同一个语义空间。**文档和 query 都必须在同一方向归一化吗?至少要用同一个模型,且推荐都做归一化。如果你文档用 A 模型,query 用 B 模型,两个向量空间分布都不同,相似度分数再高也没有意义。

4.2 pgvector 接入:建表、插入、检索一页纸

Java 团队因为本来就有 PostgreSQL 的运维经验,很多人会优先选 pgvector,而不是引入一套全新系统。这个选择是合理的,适用于数据量百万级以内、不想再多维护一套存储的场景。

初始化扩展和建表可以这样写:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS documents ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, source TEXT, token_count INT NOT NULL, embedding VECTOR(1536), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_documents_embedding ON documents USING hnsw (embedding vector_cosine_ops);

这里有几个容易踩的细节。

VECTOR(1536)的维度必须写死,和你 embedding 模型输出维度保持一致。如果模型升级后输出变成 2048 维,老表向量字段就得重建,很麻烦,所以选模型的时候就要想清楚。

索引我用的是 HNSW,适合在线检索,召回速度比 IVFFlat 好,代价是构建索引占内存和存储。如果你的数据是离线批量入库,写完一条查一条频率不高,那用 HNSW 也合适。

Java 侧插入一条向量,可以这样写:

String sql = """ INSERT INTO documents(content, source, token_count, embedding) VALUES (?, ?, ?, ?::vector) """; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, text); ps.setString(2, source); ps.setInt(3, tokenCount); ps.setString(4, toPgVectorString(vector)); ps.executeUpdate(); }

toPgVectorString就是把float[]转成 pgvector 可识别的文本格式,比如[0.12,0.34,0.56]。这个字符串里不能带空格、注释或者多余字符,否则::vector转换会失败。

检索时,用 pgvector 的余弦距离操作符<=>:

SELECT id, content, 1 - (embedding <=> ?::vector) AS similarity FROM documents ORDER BY embedding <=> ?::vector LIMIT 5;

注意这里有个容易绕晕的点。embedding <=> ?算的是余弦距离,距离越小越相似。如果你想呈现一个“相似度”给用户,需要用1 - 距离,这样分数越接近 1 越相似。很多人第一次写的时候 忘了这个转换,最后发现“相似度越高结果越差”,因为是距离排序排反了。

Java 侧执行时,把 float[] 转成同样的 pgvector 文本字符串作为参数传进去即可。这个方案既保住了检索质量,又把向量存储的逻辑留在数据库层,内存压力远小于堆内全量计算。

4.3 批量导入、缓存与维度治理

生产环境一定会有批量导入场景,比如第一次把整理好的历史文档全部向量化,或者每周增量更新一批新闻。

批量导入最容易出两个问题:

  • 线程池无脑开太大。Embedding API 有速率限制,并发 100 个请求可能直接被打回 429。我习惯用一个有限线程池,比如 8 个线程,每个线程循环请求,遇到 429 用指数退避等待,最多重试 3 次。
  • 没有断点续传。如果导入到第 5000 条崩溃了,重跑全部成本高。我会在数据库表里加一个embedding_status字段,成功置为done,失败置为failed,下次启动只处理非done的数据。

缓存方面,同一个文本片段的 Embedding 一定要缓存。因为用户问的问题可能在系统里被反复用于检索,文档片段也可能被多个流程读取。用本地Caffeine,键是模型名 + 文本内容哈希,值是 embedding 数组,TTL 可以设到 7 天。这样能省很大一部分重复调用费用。

维度治理是更长期的事。模型升级、字段调整都会改变维度。我建议在配置中心维护好“当前生效维度”,并在启动时做校验:加载进来任何向量,如果长度和当前模型不一致,要么拒绝,要么标记重新向量化。否则你会在生产环境看到各种诡异的“向量维度不匹配”,查半天还不知道问题在哪。

5. 生产环境踩坑实录:六个高频问题

5.1 维度不匹配:一个让人头皮发麻的报错

有段时间系统报错日志里全是“vector 维度不匹配”。原因是团队里有人把一批旧文档用旧的 embedding 模型向量化了,没重新跑一遍,就直接丢进新表。新表字段定义成 1536 维,旧向量是 1024 维,插入时就炸了。

后来我干脆在应用层做一次维度校验,对每个向量写入前都检查长度,发现不对直接拒绝并打日志。从源头挡住错误数据,比在数据库里排查快得多。

5.2 用了余弦相似度但没归一化

有个项目上线后检索评分浮动特别大,用户输入一个短词,相似度普遍偏高;输入较长问句,相似度又普遍偏低。查到最后发现是 query 向量没有做归一化,计算出的余弦分数受向量模长影响。虽然理论上余弦相似度不受模长干扰,但在实际实现里,如果你的向量没有统一处理,某些模型返回的向量空间本身就不均匀,分数波动就出来了。

处理方式很简单:入库前和查询前,统一走一次VectorUtils.normalize,分数稳定很多。

5.3 HTTP 调用把 Tomcat 线程池打满

最早版本里大家在 Controller 里直接调用httpClient.send,这是一个同步阻塞调用。高并发时一个请求等着模型返回,Tomcat 线程就卡在那里,线程池瞬间被占满,后面请求全排队,接口 RT 从几十毫秒变成几十秒。

后来两个方案组合解决:一个是把 Embedding 调用线程改成虚拟线程,一个是把批量向量化任务丢到独立线程池 + 消息队列,不让同步请求阻塞主业务。线上 RT 立刻恢复到正常水平。

5.4 手工拼 JSON,特殊字符把语义搞崩了

见过有人用字符串拼接 JSON 请求体,大概长这样:

"{\"input\":\"" + text + "\"}"

如果 text 里包含双引号,拼接出来的 JSON 直接坏掉;包含换行,服务端解析也可能出错。最隐蔽的问题是,这类字符不会必然报错,而是模型收到了被截断或转义乱了的内容,Embedding 结果偏差很大,查语义问题却查不到。后来统一改成 Jackson 或者别的 JSON 库序列化,这类问题再没出现过。

5.5 不知道 embedding 模型有最大输入长度

Embedding 模型不是无限能接文本。很多模型对输入 token 数有上限,超长部分会被截断。注意,是“被截断”,不是“报错”,所以不容易被察觉。

截断之后向量表示的是前 512 个 token 或前 8192 个 token 的语义,后面的内容全部丢失。我们有一次知识库召回变差,排查到最后发现是某个长文档切片逻辑改了,切出来的 chunk 太长,大量文档被模型静默截断,关键信息全在后面,检索自然失败。

处理:在应用层对文本长度做预估,超长就按窗口粒度重新切片,或者走服务端的截断策略,反正要保证“库里存的向量对应的文本内容”和“用户实际看到的内容”一致。

5.6 检索只给 top1,没有回退策略

RAG 系统上线初期,我们把检索结果硬编码成只取 top1,结果用户提出的问题稍微偏一点,检索就空。后来改成 top5,并且加了一个阈值:当相似度低于 0.6 时,认为检索结果不可靠,直接返回“未找到相关资料”的兜底文案,而不是硬把低质量片段拼给 LLM。

这个回退策略看似简单,但对用户体验影响巨大。没有回退,LLM 会一本正经地拿不相关内容编答案,这是 RAG 最让人头疼的问题之一。

6. 这道面试题到底怎么答:给你一套参考逻辑

6.1 三句话定位考点,给出有层次的答案

我建议的答题顺序是:先区分概念,再给工程选型,最后补一个自己的例子。

你可以这样说:

“我会先区分两个概念。面试官问的 Embedding,一般指文本 Embedding 模型输出的语义向量,用来做相似度检索;而 LLM 输出向量会更宽泛,包括 token embedding、每层 hidden state、logits 三种。在做 RAG 系统时,我只会把 Embedding 模型输出的向量存进向量库做召回,不会用生成模型的 hidden state,因为生成模型优化的是预测下一个 token,它的向量空间对语义相似度不友好,换一种说法结果就可能飘。”

这段话把三个考点都覆盖了:你有概念、有选型、有理由,而且最后那句“换一种说法结果就可能飘”是从实践中总结出来的,面试官一听就知道你真跑过。

如果再深入一层,你可以加一句:“而且我实际遇到过维度不一致导致的线上问题,最后是在应用层加维度校验解决的。”这一句就是工程能力的信号,比背十句话术管用。

6.2 把 Java 工程经验自然放进答案

当面试官让你讲一个 AI 项目,不要一上来就讲“我用了 Spring AI”,这件事不是重点。你可以用三段式结构:

  • 第一段说数据怎么来。比如“我们的知识文档先做切片,切片长度大概控制在 500 token,然后用 OpenAI 的 text-embedding-3-small 向量化,批量入库到 pgvector”。
  • 第二段说检索怎么做。“用户提问时,先用同一个 embedding 模型把问题向量化,再通过 pgvector 的 HNSW 索引做 top5 召回,用余弦距离排序。检索结果拼进 prompt 前,会判断相似度阈值,低于阈值直接走兜底。”
  • 第三段说踩了什么坑。比如“我们之前有一条不稳定的问题,查到最后发现是 embedding 输入被模型截断,后来改成按 token 窗口切片,再检查入库长度,解决掉了。”

这比单纯背“Embedding 是什么”要强得多,因为面试官想要知道的是你能不能把一个 AI 功能真正落地在复杂的 Java 系统里。

7. 最后分享我自己的一个习惯

我每次接新的 RAG 项目,都会花一点时间验证“分块大小对 Embedding 检索的影响”。不要照搬别人的 chunk_size,不同模型、不同文档风格,最优窗口不一样。我的做法是拿同一批文档,分别按 256、512、1024 token 切片,各跑一轮检索,对比测试集上的召回率和相似度分数,最后选相对稳定的那个。

这个验证看起来麻烦,但它能提前挡住很多线上问题。文本 Embedding 和 LLM 输出向量之间的边界,也正是在这种反复调试里形成肌肉记忆的。面试题只是一道线,把它答好就过关;真正跨过去之后,你会发现工程里值得抠的细节永远比题面更多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 22:50:39

TerraScan点云处理实战:参数原理与LiDAR测绘精度控制

简介&#xff1a;本资源是一份面向测绘、遥感、地理信息系统&#xff08;GIS&#xff09;及三维建模领域从业者与高校相关专业师生的技术参考文献&#xff0c;系统讲解基于TerraScan软件的LiDAR点云数据处理全流程。内容涵盖LiDAR技术原理与发展现状、TerraScan核心功能&#x…

作者头像 李华
网站建设 2026/10/2 22:49:02

让SOP从墙上走进系统:装配工位AI视频分析质量管控实践

这几年我跑过不少装配车间&#xff0c;最深的感触是&#xff1a;大多数工厂不是没有SOP&#xff0c;而是SOP和实际作业之间隔着一层“眼不见为净”。墙上挂着标准作业流程图&#xff0c;工位上贴着装配要点&#xff0c;但真到了节拍紧张的时候&#xff0c;工人怎么做、有没有跳…

作者头像 李华
网站建设 2026/10/2 22:48:23

OpenClaw本地AI代理部署实战:架构拆解与Qwen2.5模型接入

1. 从一条热搜说起&#xff1a;OpenClaw到底在解决什么问题 第一次在GitHub趋势榜上刷到OpenClaw这个项目时&#xff0c;我的反应和大多数人一样——又是一个"AI代理框架"&#xff1f;这两年打着Agent旗号的项目没有一千也有八百&#xff0c;多数是套壳GPT加几个工具…

作者头像 李华
网站建设 2026/10/2 22:42:55

cron表达式详解:从秒级到小时级,定时任务写法一次搞懂

cron表达式真是个神奇的东西&#xff0c;看起来就几个星号加斜杠&#xff0c;真要写对却没几个同事能一次搞定。尤其是“每N秒”“每N分钟”“每N小时”这种高频需求&#xff0c;网上答案五花八门&#xff0c;抄错了也不知道问题出在哪。我翻了翻手头的项目&#xff0c;发现大部…

作者头像 李华
网站建设 2026/10/2 22:42:51

Excel图表不自动更新?四种方法彻底搞定数据源动态刷新

1. 先搞清楚Excel图表为什么不自动更新我做Excel这块差不多有十年了&#xff0c;最早被问到最多的问题就是“为什么我改了数据&#xff0c;图表不动啊&#xff1f;”后来帮好几个部门做过经营看板、销售周报、库存报表&#xff0c;才发现这类需求根本不是少数人的痛点&#xff…

作者头像 李华