CH 04嵌入模型与向量数据库
这一章是整门课唯一涉及数学概念的章节,但请放心——你不需要会推导公式,只需要建立一个直觉: 把文本丢进一个函数,出来一串数字;语义相近的文本,出来的数字串也相近。向量的本质就是「把意思变成坐标」。
4.1Embedding 到底在做什么:从「关键词匹配」到「语义匹配」
先看一个经典问题。用户问:「怎么把钱退回来?」 而你的知识库里写的是:「退款申请流程说明」。这两句话没有一个字相同,用传统的关键词检索(MySQL 的 LIKE '%退款%' 或 Elasticsearch 的倒排索引)都匹配不到。但人一眼就知道它们讲的是同一件事。
Embedding(嵌入)就是把这种「人知道但机器不知道」的语义关系,变成机器能算的数字关系:
为什么这决定了 RAG 的上限
第 5 章的 RAG 效果好不好,80% 取决于本节这一步。检索错了,后面的大模型再强也只能基于错误的资料胡说八道。 所以本章不是「预备知识」,而是整个 RAG 的地基。
4.2向量模型选型与维度对照
| 模型 | 维度 | 部署方式 | 特点与建议 |
|---|---|---|---|
text-embedding-3-small | 1536 | OpenAI API | 便宜、够用,课程默认(但需网络可达) |
text-embedding-3-large | 3072 | OpenAI API | 效果更好,成本和存储翻倍,中文场景提升有限 |
text-embedding-v3(通义) | 1024 | DashScope API | 国内可用性好,中文效果不错,推荐国内同学使用 |
embedding-3(智谱) | 2048 | API | 国内可选,维度可自定义 |
nomic-embed-text | 768 | Ollama 本地 | 完全离线、免费、中文尚可,教学/内网首选 |
BAAI/bge-large-zh-v1.5 | 1024 | 本地(ONNX/Transformers) | 中文效果优秀,需要本地推理环境 |
BAAI/bge-m3 | 1024 | 本地 / 服务化 | 多语言 + 长文本,检索质量高,资源占用大 |
- 灌库和查询必须用同一个模型。换了模型,旧向量全部作废,必须整库重灌。这是 RAG 项目最常见的「换模型后检索全废」事故。
- 建表时的维度必须等于模型输出维度。用 1536 建表却灌 1024 维向量,直接插入报错。
- dimensions 参数若被模型支持(如 text-embedding-3 系列可截断到 512/1024),可以省存储、提速,但会略微损失精度。降维后同样要统一。
4.3相似度算法与索引类型
三种距离度量
| 算法 | Spring AI 枚举 | 直觉理解 | 适用 |
|---|---|---|---|
| 余弦距离 | COSINE_DISTANCE |
只看方向,忽略长度。文本检索的标准选择。 | ✅ 默认用这个 |
| 欧氏距离 | EUCLIDEAN_DISTANCE |
看绝对位置的远近。受向量长度影响大。 | 图像/数值向量 |
| 内积 | NEGATIVE_INNER_PRODUCT |
方向 + 长度。适合「长度本身有含义」的场景。 | 推荐系统 |
索引类型:为什么需要它
假设库里有 100 万条向量。用户查询一次,要对 100 万条逐一算余弦距离——这叫暴力检索(Flat / 精确检索),准确率 100% 但慢得无法接受。
近似最近邻(ANN)索引通过牺牲一点点召回率,把复杂度从 O(n) 降到接近 O(log n):
| 索引类型 | 原理(一句话) | 适用规模 | 调参要点 |
|---|---|---|---|
HNSW |
构建多层「高速公路 + 小路」的图,查询时从高速逐层下降 | 10 万 ~ 千万级 | m(每节点连边数,默认 16)、ef_construction(构建精度,默认 64)。值越大越准越慢越占内存 |
IVF_FLAT |
先把向量聚类成 N 个桶,查询时只搜最近的几个桶 | 百万级以上 | nlist(桶数);查询时用 nprobe 控制搜几个桶 |
Flat / 暴力 |
全量精确计算,不做任何近似 | < 1 万条 | 无参数,教学和小数据量下推荐用(结果可复现,便于调试) |
4.4EmbeddingModel:把文本变成数字的 API
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
@Service
public class EmbeddingDemoService {
private final EmbeddingModel embeddingModel;
public EmbeddingDemoService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/** ① 单条文本 → float[] */
public float[] embedOne(String text) {
float[] vector = embeddingModel.embed(text);
System.out.println("维度 = " + vector.length); // 1536 / 1024 / 768 …
return vector;
}
/** ② 批量 → 一次网络往返处理多条,比循环单条快得多(务必用这个) */
public EmbeddingResponse embedBatch(List<String> texts) {
// 注意:多数厂商单次批量上限为 10~100 条,超了要自己分片
return embeddingModel.embedForResponse(texts);
}
/**
* ③ 手写一个"语义相似度比较器",用来验证向量到底有没有用。
* 这是本章最重要的一个实验,强烈建议自己跑一遍。
*/
public void compare() {
String query = "怎么把钱退回来";
List<String> candidates = List.of(
"退款申请流程说明", // 语义相似,应排第 1
"售后服务中心电话", // 弱相关
"如何配置数据库连接池" // 完全不相关
);
float[] q = embeddingModel.embed(query);
for (String c : candidates) {
float[] v = embeddingModel.embed(c);
// 余弦相似度 = 点积 / (模长乘积),取值 [-1, 1],越接近 1 越相似
double cos = cosine(q, v);
System.out.printf("%.4f %s%n", cos, c);
}
// 期望看到:退款申请流程说明 的分数明显最高(通常 0.75 以上)
}
private double cosine(float[] a, float[] b) {
double dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
embedForResponse。循环调用单条接口灌 1 万篇文档,会被限流且慢 10 倍以上。
② 注意各厂商的批量上限。OpenAI 单次最多 2048 条但受 Token 总量限制;通义 text-embedding-v3 单次上限较小。
灌库时务必自己分片,例如每 10 条一批。
4.5向量数据库选型与配置
| 方案 | 优势 | 劣势 | 课程定位 |
|---|---|---|---|
| pgvector 推荐入门 |
复用已有 PostgreSQL,运维成本极低;支持 SQL + 向量混合查询;支持元数据过滤与事务 | 千万级以上性能不如专用向量库 | 第 4、5、11 章的主用方案 |
| Milvus | 专为向量设计,十亿级规模,索引类型丰富,分布式可扩展 | 组件多(etcd + minio),资源占用大,运维复杂 | 用于理解「专用向量库」的差异 |
| Redis(RedisSearch) | 已有 Redis 就能用,速度快,适合做缓存层 | 持久化与大规模能力弱于前两者 | 延伸阅读 |
| Elasticsearch / OpenSearch | 可与 BM25 关键词检索做混合检索(Hybrid Search) | 向量能力相对弱,集群重 | 混合检索章节提及 |
| SimpleVectorStore(内存) | 零依赖,写到内存里,启动即用 | 重启即丢,无法扩展 | 写单元测试时非常好用 |
pgvector 依赖与配置
<!-- pom.xml:pgvector 的 Spring AI starter(注意它会传递引入 postgresql 驱动) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
# application.yml —— pgvector 完整配置(每一项都有注释)
spring:
datasource: # ★ VectorStore 用的就是这个数据源
url: jdbc:postgresql://localhost:5432/vectordb
username: ai
password: ai123456
driver-class-name: org.postgresql.Driver
ai:
vectorstore:
pgvector:
# 是否自动建表 / 建扩展。首次启动 true,后续可设 false 加快启动
initialize-schema: true
# 表名。默认为 vector_store,改这里要同步改
table-name: vector_store
# 索引类型:HNSW(推荐)/ IVFFlat / NONE(精确检索)
index-type: HNSW
# 距离类型,必须与灌库时一致
distance-type: COSINE_DISTANCE
# ★★★ 维度必须等于 embedding 模型的输出维度,写错必报错
dimensions: 1536
# 单次批量写入上限,文档多时自动分批
max-document-batch-size: 10000
# 搜索时默认返回条数(可被 SearchRequest 覆盖)
# 若使用外部已建好的库,可关掉自动初始化:
# schema-validation: true
# 同时启用了 OpenAI embedding 时,会自动用 OpenAI 生成向量
openai:
api-key: ${OPENAI_API_KEY:}
base-url: https://api.openai.com # embedding 建议用 OpenAI 或通义,DeepSeek 无此能力
embedding:
options:
model: text-embedding-3-small # 输出 1536 维,与上面 dimensions 对应
ERROR: expected 1536 dimensions, not 1024(pgvector)或
Collection schema dimension mismatch(Milvus)。
看到这个错误,不要改代码,先检查三处是否一致:建表维度、模型的输出维度、yml 里的 dimensions。
Milvus 配置(做对比实验时用)
<!-- 依赖:与 pgvector 可以同时存在,但要靠 @Qualifier 或 spring.ai.vectorstore.type 指定主用 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>
spring:
ai:
vectorstore:
milvus:
client:
host: localhost
port: 19530
# 用户名密码(未开启鉴权可留空)
username: root
password: milvus
database-name: default
collection-name: vector_store
# ★ 同样必须与 embedding 维度一致
embedding-dimension: 1536
# 索引类型:IVF_FLAT / HNSW / FLAT
index-type: IVF_FLAT
# 距离度量:COSINE / L2 / IP
metric-type: COSINE
# 是否自动创建 collection
initialize-schema: true
VectorStore bean,
按类型注入会歧义。解决办法:① 在需要的地方加 @Qualifier("pgVectorStore");
② 或者在 yml 里关闭自动装配(spring.ai.vectorstore..enabled: false ),
再在配置类里手动 new。第 11 章会给出「主库 + 备库」的完整写法。
4.6VectorStore:写入、删除、检索
Document 与 metadata
import org.springframework.ai.document.Document;
// Document = 文本内容 + 元数据 + 向量(向量由 VectorStore 自动生成,你不用管)
Document doc = new Document(
"退款申请需在订单完成后 7 天内提交,超期需联系客服人工审核。", // ① content:会被向量化的文本
Map.of( // ② metadata:不会向量化,但可用来过滤
"source", "refund-policy.pdf",
"page", 3,
"category", "售后",
"version", "v2"
)
);
// 也可以自己生成文档 ID(默认是随机 UUID)
Document withId = new Document(
"doc-001", // 显式指定 id,便于幂等更新
"退款申请需在订单完成后 7 天内提交。",
Map.of("category", "售后"));
// 读取
doc.getText(); // 正文
doc.getMetadata(); // 元数据 Map
doc.getId(); // id
doc.getScore(); // ★ 检索返回的文档会带相似度分数,写入时为 null
写入与删除
@Service
public class VectorStoreBasics {
private final VectorStore vectorStore;
public VectorStoreBasics(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/** 写入:内部会自动调用 EmbeddingModel 生成向量,再批量入库 */
public void add(List<Document> docs) {
vectorStore.add(docs);
// 注意:add 是"只增不改"。同一个 id 再 add 一次会怎样?
// 取决于实现:pgvector 用主键冲突会更新(upsert 行为),Milvus 可能抛错。
// 生产上建议:更新前先 delete(ids),再 add,语义更清晰。
}
/** 按 id 删除 */
public void deleteByIds(List<String> ids) {
vectorStore.delete(ids);
}
/** 按条件删除:例如删除某个文档来源的全部向量(重建索引时用) */
public void deleteBySource(String source) {
vectorStore.delete("source == '" + source + "'");
}
/** 向量库的名字,多库共存时用于区分 */
public String name() {
return vectorStore.getName();
}
}
检索:SearchRequest 详解(本章最重要的一段)
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
// 最简写法:默认 topK,无阈值过滤
List<Document> simple = vectorStore.similaritySearch("退款要几天");
// 完整写法:所有参数都显式控制(推荐)
SearchRequest request = SearchRequest.builder()
.query("退款要几天") // 查询文本,会被向量化后去比对
.topK(5) // 返回最相似的 5 条
.similarityThreshold(0.5) // ★ 相似度低于 0.5 的直接丢弃(余弦下 0.5 约等于"弱相关")
.filterExpression("category == '售后'") // 元数据过滤,见 4.7
.build();
List<Document> docs = vectorStore.similaritySearch(request);
for (Document d : docs) {
System.out.printf("分数=%.4f 来源=%s 内容=%s%n",
d.getScore(), d.getMetadata().get("source"), d.getText());
}
| 参数 | 默认 | 怎么调 |
|---|---|---|
topK | 实现相关(常为 4~10) | 太小会漏(检索不到关键资料),太大会引入噪音并浪费 Token。中文知识库建议 4~8 起步,用第 10 章的评测集调优 |
similarityThreshold | 0.0(不过滤) | 强烈建议设 0.5~0.7。不设阈值的后果是:无论问什么,都会返回 topK 条「最像的」,哪怕它们完全不相关,模型就会基于垃圾资料胡说 |
filterExpression | 无 | 用元数据缩小检索范围,能显著提升准确率(见 4.7) |
4.7元数据过滤:把检索范围先缩小,再算相似度
这是提升 RAG 准确率性价比最高的一招。举例:公司有 5 个部门的知识库,用户是财务部的,问「报销标准是多少」。 如果不加过滤,向量检索会在全公司 10 万条文档里找,很可能把销售部的差旅标准捞出来——语义上极其相似,但答案是错的。
// ---------- 支持的运算符(Spring AI 的过滤表达式 DSL)----------
// 比较: == != > >= < <=
// 逻辑: and or ( )
// 集合: in
// 精确匹配
"category == '售后'"
// 数值比较(注意:数值型元数据不要加引号)
"page >= 10"
// 组合条件
"category == '售后' and version == 'v2'"
// 集合包含
"source in ['refund-policy.pdf', 'aftersale.pdf']"
// 实际用法
SearchRequest req = SearchRequest.builder()
.query("报销标准是多少")
.topK(5)
.similarityThreshold(0.5)
.filterExpression("dept == '财务部' and docType == '制度'")
.build();
source(文件路径)、page(页码,用于引用溯源)、docType(制度/手册/FAQ)、
dept 或 tenantId(多租户隔离,强烈建议)、updatedAt(用于时效性过滤)、
version(知识库版本)。一旦灌了几十万条数据再想补字段,就要整库重灌——这个坑本课程的学员踩过太多次了。
- 字符串必须用单引号,双引号会解析失败。
- 元数据字段值的类型要稳定。同一个字段有时写数字 3、有时写字符串 "3",过滤时会漏结果。
- 过滤条件的字段如果是
null或缺省,==不会匹配到它(不是 SQL 的三值逻辑,各实现略有差异,建议灌库时给所有文档补齐元数据)。
4.8完整示例:文档向量索引服务
目标:一个能「灌库 + 检索 + 统计」的最小可用服务。本节的代码第 5 章会直接复用。
① 灌库服务
package com.example.springai.ch04;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class DocumentIndexService {
/** 单批写入条数。太大易触发厂商限流,太小效率低。10~50 是常见区间 */
private static final int BATCH_SIZE = 20;
private final VectorStore vectorStore;
public DocumentIndexService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 把一批文本(已切分好的片段)灌入向量库。
*
* @param source 来源标识(如文件路径),用于后续按来源删除/过滤
* @param chunks 已切分的文本片段,每个片段会成为一条向量
*/
public int index(String source, String category, List<String> chunks) {
// ★ 幂等:先删掉这个来源的旧数据,避免重复灌导致结果重复
// 这是"重建索引"最常见的做法
deleteBySource(source);
int seq = 0;
int total = 0;
for (int i = 0; i < chunks.size(); i += BATCH_SIZE) {
List<String> batch = chunks.subList(i, Math.min(i + BATCH_SIZE, chunks.size()));
List<Document> docs = batch.stream()
.map(text -> new Document(text, buildMetadata(source, category, chunks.indexOf(text))))
.toList();
vectorStore.add(docs); // 内部自动批量向量化 + 入库
total += docs.size();
log.info("已灌入 {}/{} 条", total, chunks.size());
}
return total;
}
/** 元数据必须在灌库时一次性写全,后面补字段要整库重灌 */
private Map<String, Object> buildMetadata(String source, String category, int chunkIndex) {
return Map.of(
"source", source,
"category", category,
"chunkIndex", chunkIndex,
// 注意:pgvector 的 JSON 元数据对 LocalDateTime 支持不一,
// 统一转成字符串更稳妥
"indexedAt", LocalDateTime.now().toString()
);
}
/** 按来源删除:重建索引时先调它 */
public void deleteBySource(String source) {
try {
vectorStore.delete("source == '" + source + "'");
} catch (Exception e) {
// 表为空或字段不存在时可能抛异常,降级为打日志,不阻断主流程
log.debug("按来源删除失败(可能是首次灌库):{}", e.getMessage());
}
}
}
② 检索服务
@Slf4j
@Service
public class VectorSearchService {
private final VectorStore vectorStore;
public VectorSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 语义检索。
*
* @param query 用户问题
* @param topK 返回条数
* @param threshold 相似度阈值,低于它的结果丢弃
* @param category 分类过滤,可为 null
*/
public List<SearchResult> search(String query, int topK,
double threshold, String category) {
// 过滤表达式按需拼接。注意:字符串值必须单引号
String filter = (category == null || category.isBlank())
? null
: "category == '" + category + "'";
var builder = SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(threshold);
if (filter != null) {
builder.filterExpression(filter);
}
List<Document> docs = vectorStore.similaritySearch(builder.build());
log.info("检索「{}」命中 {} 条", query, docs.size());
// 转成对外的 DTO,不要把 Document 直接暴露给前端(会带上内部元数据)
return docs.stream()
.map(d -> new SearchResult(
d.getText(),
d.getScore(),
String.valueOf(d.getMetadata().get("source")),
String.valueOf(d.getMetadata().get("category"))))
.toList();
}
}
③ HTTP 接口与验证
@RestController
@RequestMapping("/api/v1/vector")
public class VectorApiController {
private final DocumentIndexService indexService;
private final VectorSearchService searchService;
public VectorApiController(DocumentIndexService i, VectorSearchService s) {
this.indexService = i;
this.searchService = s;
}
/** 灌库:先手工传几段文本,验证链路 */
@PostMapping("/index")
public Map<String, Object> index(@RequestBody IndexRequest req) {
int n = indexService.index(req.source(), req.category(), req.chunks());
return Map.of("indexed", n, "source", req.source());
}
/** 检索 */
@GetMapping("/search")
public List<SearchResult> search(@RequestParam String q,
@RequestParam(defaultValue = "5") int topK,
@RequestParam(defaultValue = "0.5") double threshold,
@RequestParam(required = false) String category) {
return searchService.search(q, topK, threshold, category);
}
public record IndexRequest(String source, String category, List<String> chunks) {}
public record SearchResult(String content, Double score, String source, String category) {}
}
验证步骤(三条命令走完整个闭环):
# ① 灌库:三条售后知识
curl -X POST http://localhost:8080/api/v1/vector/index \
-H "Content-Type: application/json" \
-d '{
"source":"refund-policy.md",
"category":"售后",
"chunks":[
"退款申请需在订单完成后 7 天内提交,超期需联系客服人工审核。",
"退款将在审核通过后 3 个工作日内退回原支付账户。",
"生鲜类商品一经签收不支持无理由退款。"
]}'
# ② 灌库:三条技术文档(用来验证"不串味")
curl -X POST http://localhost:8080/api/v1/vector/index \
-H "Content-Type: application/json" \
-d '{
"source":"db-guide.md",
"category":"技术",
"chunks":[
"PostgreSQL 连接池建议 max_connections 不超过 200。",
"pgvector 的 HNSW 索引构建参数 m 默认为 16。"
]}'
# ③ 语义检索:故意用"没有一个字相同"的问法
curl "http://localhost:8080/api/v1/vector/search?q=钱什么时候能到账&topK=3&threshold=0.4"
# 期望:第一条命中"退款将在审核通过后 3 个工作日内退回原支付账户"
# 说明语义检索生效(关键词匹配是绝对做不到的)
# ④ 加分类过滤,验证元数据过滤生效
curl "http://localhost:8080/api/v1/vector/search?q=钱什么时候能到账&topK=3&category=技术"
# 期望:返回结果全部来自 category=技术(很可能为空),
# 证明过滤条件真的缩小了检索范围
text-embedding-v3 或 nomic-embed-text 再试),
或者你的阈值设得太高。这一步跑通,第 5 章的 RAG 就是水到渠成。
4.9常见坑排错
| 现象 | 原因 | 解决 |
|---|---|---|
expected 1536 dimensions, not 1024 | 建表维度与模型输出维度不一致 | 改 yml 的 dimensions 与模型对齐;或换模型 |
relation "vector_store" does not exist | 建表脚本没执行 / initialize-schema 为 false | 设 initialize-schema: true 并重启;或手工执行 init SQL |
extension "vector" is not available | pgvector 扩展没装(用了普通 postgres 镜像) | 镜像必须用 pgvector/pgvector:pg16,并执行 CREATE EXTENSION vector |
| 检索结果永远为空 | 阈值太高,或查询语言与库内容语言不匹配 | 先把 threshold 设 0 看分数分布,再定阈值;确认中英文 |
| 检索结果重复很多条 | 重复灌库(没做幂等删除) | 灌库前先 deleteBySource;或给 Document 指定稳定 id 并 upsert |
| 灌库很慢 / 被限流 429 | 逐条调用 embedding,或批量太大 | 用 add(List) 批量接口,每批 10~50 条,必要时加退避重试 |
| 元数据过滤报语法错 | 字符串没用单引号,或字段类型不一致 | 字符串值用单引号;统一元数据类型 |
No qualifying bean of type VectorStore | 没加 starter,或 datasource 配错 | 检查依赖与 spring.datasource;注意 pgvector 走的是标准 datasource |
| Milvus 连不上 / 一直重连 | etcd、minio 没起来,或内存不足 | docker compose logs milvus 看日志;先只跑 pgvector |
4.10自测题
Q1. 为什么换了 Embedding 模型之后,整个向量库必须重灌?
因为不同模型构建的向量空间完全不同。同一个词在模型 A 和模型 B 里的坐标毫无可比性, 用 B 的向量去和 A 灌的数据算距离,得到的是噪音而非相似度。 另外维度可能不同,schema 层面也直接冲突。
Q2. similarityThreshold 不设置会有什么后果?
无论用户问什么,都会返回「最相似的 topK 条」——即使这些内容完全不相关。 RAG 场景下模型会基于这些垃圾资料强行组织答案,产生幻觉。 这是「明明库里没这个知识,模型还是瞎编」的最主要原因之一。建议设 0.5~0.7 起步。
Q3. 元数据(metadata)为什么不能等灌完库再补?
因为元数据是随文档一起写入向量库的,不是独立字段。 想给已存在的向量补一个字段,只能读出全部数据、补字段、重新写入——代价等同于整库重灌。 所以元数据 schema 必须在第一次灌库前设计好。
Q4. 数据量只有 5000 条时,为什么建议先用 Flat(精确)检索?
5000 条的全量计算在现代 CPU 上是毫秒级,完全可接受。 用精确检索能保证「检索结果 100% 是真实的最近邻」,从而把调试焦点放在 切分质量、查询表述、阈值上,而不是被 ANN 的近似误差干扰。 等数据量上万、或延迟不达标时,再切换到 HNSW 并做召回率对比。
Q5.【动手题】写一段代码,把 1000 篇文章标题批量向量化并入库,要求:每批 20 条、失败自动重试 3 次、记录总耗时与成功条数。
要点:用 Lists.partition(list, 20) 分片;每片包 try/catch,
失败时 Thread.sleep(backoff * attempt) 做指数退避;用 System.currentTimeMillis() 或
StopWatch 计时;失败的分片收集起来最后统一重试,避免单条失败导致整批回滚。
- 能用自己的话说清 Embedding 在做什么,并解释「为什么关键词匹配做不到、语义匹配可以」。
- 能说出维度、距离算法、索引类型三个配置项的作用和相互约束。
- 三个 curl 实验全部通过,特别是「钱什么时候能到账」能命中退款条款。
- 能设计一份合格的元数据 schema(含 source / page / category / tenantId)。
- 知道「换模型必须重灌库」以及「阈值必须调」这两条铁律。