导读 :ChatGPT 认识维基百科,但不认识你公司的制度文档。怎么让 AI "记住"私有知识?答案就是今天的主角:Embedding(把语义变成坐标)和向量数据库(在海量坐标里快速找近邻)。原理用坐标和色卡讲明白,代码用 Spring AI 直接跑,看完就能搭出你自己的知识库问答。

01 · 先讲一个"搜不到"的尴尬

你负责的公司知识库里有一篇文档,标题叫《充电器故障排查指南》,里面写着:

手机无法充电时,先检查充电线接口是否有异物,再用酒精棉片清洁 USB-C 接口……

然后有员工来问:"我手机充不进电怎么办?"

如果知识库用的是普通关键词搜索,这条查询很可能搜不到那篇文档——因为"充不进电"和"故障排查""接口异物"里,一个关键词都对不上。

人一眼就能看出这两句话在说同一件事。但"字面"搜索匹配的是文字,而人想匹配的是"意思"。

怎么让计算机也懂"意思"?这就是 Embedding 要解决的问题。


02 · Embedding:把语义翻译成坐标

什么是向量

回忆一下初中数学:平面上的点可以用坐标 (x, y) 表示,比如你家在 (3, 4),朋友家在 (5, 1)。坐标之间的距离,就是两人家的远近。

向量就是"更高维的坐标"。一个 1536 维的向量,就是 1536 个数字排成一排——它表示高维空间里的一个点。

Embedding 干什么

Embedding 就是把一段文字,变成这样一个高维坐标。

关键的性质是:语义相近的文本,坐标距离也近。

  • "我爱吃苹果" 和 "水果是我的最爱" → 坐标很近

  • "我爱吃苹果" 和 "汽车保养要看机油" → 坐标很远

类比:想象一个巨大的"色卡"。红色、酒红、砖红挤在一起,因为它们"视觉上相近";红色和蓝色离得远。Embedding 就是把语言映射到一张"语义色卡"上——意思相近的词,位置就靠近。

这个映射不是人写的,是专门的 Embedding 模型在训练中自动学出来的(常见的有 BGE、OpenAI 的 text-embedding-3、通义 text-embedding-v4 等)。训练时模型见过海量"苹果和水果一起出现、苹果和汽车很少一起出现"的文本,就把这些关联"折叠"进了坐标里。

💡  Java 类比

可以把它想成一个超大的 Map<String, float[]>:输入一句中文,输出一个浮点数组。模型不同,数组长度(维度)也不同——768 维、1024 维、1536 维都有。

动手验证一下这个直觉

下面这段 Java 用 Spring AI 的 EmbeddingModel 把三句话变成向量,再用手写的余弦相似度算距离:

double[] a = embeddingModel.embed("我喜欢吃苹果");
double[] b = embeddingModel.embed("水果是我的最爱");
double[] c = embeddingModel.embed("汽车保养要看机油");

System.out.println("苹果 vs 水果 : " + cosine(a, b));   // 大概率接近 0.8+
System.out.println("苹果 vs 汽车 : " + cosine(a, c));   // 大概率接近 0.2-0.4

// 余弦相似度:两向量夹角的余弦,越接近 1 表示方向越一致(语义越近)
static double cosine(double[] x, double[] y) {
    double dot = 0, nx = 0, ny = 0;
    for (int i = 0; i < x.length; i++) {
        dot += x[i] * y[i];
        nx += x[i] * x[i];
        ny += y[i] * y[i];
    }
    return dot / (Math.sqrt(nx) * Math.sqrt(ny) + 1e-9);
}

"苹果"和"水果"的距离,会明显比"苹果"和"汽车"近。这就是语义匹配的底层直觉。


03 · 相似度:三把尺子怎么选

向量有了,怎么判断"近不近"?有三种常用度量:

度量

直觉

特点

文本场景

余弦相似度

看"方向"是否一致,不看长短

对文本长度不敏感,最常用

✅ 首选

点积

方向 + 长度的综合

向量需先归一化,否则长文本占便宜

部分库默认

欧氏距离

看绝对距离,越小越近

对长度敏感,通常要归一化后用

少用

文本场景基本无脑选余弦相似度:两个句子长度差很多("好吃" vs "这个苹果真的非常好吃而且很甜"),语义可能一样,余弦只看方向,不受长度干扰。

顺带一提:全程只用同一个 Embedding 模型。不同模型产出的向量"坐标系"不一样,混着用等于拿两把不同刻度的尺子量长度,结果没意义。


04 · 向量数据库:海量坐标怎么快速找

为什么不能暴力搜

Embedding 之后的检索叫"最近邻搜索"。几万条文档时,暴力把每条向量和问题向量算一遍余弦,还行;几百万条时,每次都全量算,性能就崩了。

所以向量数据库用 ANN(近似最近邻)算法——不追求 100% 精确,用索引结构(如 HNSW、IVF)把"挨个比"变成"先粗筛再精排",速度提升几个数量级,精度损失很小。

类比:找一本书,暴力方式是抱着书从书架头走到尾;有索引的方式是看分类标签直奔那一格。ANN 就是那个"分类标签"。

为什么不用 MySQL

检索方式

匹配原理

典型短板

LIKE '%退款%'

字面包含

换个说法"把钱退给我"就搜不到

Elasticsearch 全文检索

分词 + 词频

不懂语义,"苹果"在水果文档和手机文档里权重一样

向量检索

语义距离

需要 Embedding 成本,但"怎么退钱"能命中"退款流程"

一句话:关键词匹配的是"字面",向量检索匹配的是"意图"。

选型参考

方案

定位

适合

SimpleVectorStore

内存实现(Spring AI 内置)

开发调试、小规模验证

PGVector

PostgreSQL 插件

想和现有数据库同栈,省运维

FAISS

Meta 开源库

离线/单机大规模检索

Milvus / Qdrant

专用向量数据库

生产级、大规模、高并发

Redis 向量模块

缓存 + 检索一体

已有 Redis、场景较轻

Spring AI 用 VectorStore 接口把这些实现统一了:今天用 SimpleVectorStore 开发,明天换 PGVector 上生产,业务代码一行不用改,只换 Bean 实现和配置。


05 · 它在 RAG 里的位置

如果你看过之前那篇 RAG 文章,会发现 Embedding 和向量库正是 RAG 的地基。整个链路长这样:

【离线:建索引】
知识库文档 → 文本切分(Chunk) → Embedding 向量化 → 存入向量数据库

【在线:答问题】
用户提问 → 问题向量化 → 向量库相似度检索 Top-K → 相关片段注入 Prompt → 大模型生成回答

Embedding 负责"把文字变成坐标",向量库负责"存坐标、按距离找近邻",RAG 负责"把找回来的片段喂给大模型"。三层各司其职。


06 · Spring AI 实战:一个能跑的知识库问答

下面这套代码完整可跑,实现了"公司知识库 → 语义问答"。

第一步:加依赖、配 Embedding 模型

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        model: text-embedding-3-small   # Embedding 模型
注意:Embedding 模型和对话模型是两回事。DeepSeek 这类只提供对话模型的厂商没有 embedding 接口,可以接 OpenAI、通义,或本地 Ollama 跑 BGE。反正代码里都叫  EmbeddingModel ,换供应商只改配置。

第二步:配置内存向量库 + RAG Advisor

@Configuration
public class RagConfig {

    // 内存向量库:add() 时自动调用 EmbeddingModel 计算向量
    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        return SimpleVectorStore.builder(embeddingModel).build();
    }

    // 检索增强 Advisor:每次提问前自动"检索 → 拼接上下文"
    @Bean
    public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
        return QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(SearchRequest.builder()
                .similarityThreshold(0.4)  // 相似度低于 0.4 的片段直接丢弃
                .topK(4)                   // 取最相关的 4 段
                .build())
            .build();
    }
}

第三步:把知识库灌进去

@Component
public class KnowledgeInitializer {

    private final VectorStore vectorStore;

    public KnowledgeInitializer(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @PostConstruct
    void init() {
        vectorStore.add(List.of(
            new Document("公司年假制度:员工入职满一年后,每年享有 15 天带薪年假,"
                       + "需提前 3 个工作日申请。",
                Map.of("type", "HR")),                      // 元数据,可用来过滤
            new Document("办公用品申请:通过 OA 系统提交申请,1 个工作日内审批完成。",
                Map.of("type", "行政"))
        ));
    }
}

第四步:一个问答接口

@RestController
public class FaqController {

    private final ChatClient chatClient;
    private final QuestionAnswerAdvisor ragAdvisor;

    public FaqController(ChatClient.Builder builder, QuestionAnswerAdvisor ragAdvisor) {
        this.chatClient = builder.build();
        this.ragAdvisor = ragAdvisor;
    }

    @GetMapping("/ask")
    public String ask(@RequestParam String question) {
        return chatClient.prompt()
            .user(question)
            .advisors(ragAdvisor)   // 自动完成"检索 + 注入 + 生成"
            .call()
            .content();
    }
}

效果

GET /ask?question=我入职一年了,能休几天假?
→ 根据公司制度,入职满一年后每年享有 15 天带薪年假,需提前 3 个工作日申请。

注意:问题里"入职一年"和文档里"入职满一年""15 天带薪年假"没有几个字是一样的——但语义匹配上了。这就是 Embedding + 向量检索和关键词搜索的本质区别。

如果想看检索中间结果,直接调向量库:

List<Document> hits = vectorStore.similaritySearch("能休几天年假?");
hits.forEach(d -> System.out.println("命中: " + d.getContent()));
// 输出第一条是年假制度那条,而不是办公用品那条

07 · 实战要点:五个常见坑

① 切分(Chunking)决定上限。 检索的最小单位是切出来的文本块。整篇一起向量化 → 语义混杂、命中不精准;切太碎 → 一句话被腰斩、上下文丢失。一般按 256~512 token 切,长文档按标题结构切。

② Top-K 和阈值要一起调。topK 控制返回几条,similarityThreshold 控制最低相似度。K 太大混入噪声,阈值太高可能什么都搜不到,按业务实测调。

③ 元数据过滤很实用。 给文档打标签(部门、版本、时间),检索时按 type == 'HR' 之类的表达式过滤,能大幅提升准确性——比如"只查今年的人力制度"。

④ 混合检索更稳。 向量检索擅长语义,但精确匹配(产品编号、代码符号)容易漏。生产级方案是"向量 + 关键词(BM25)"双路检索再融合,两边都照顾到。

⑤ 检索 ≠ 问答结束。 检索只是第一步,注入的 Prompt 要写明"仅依据上下文回答,没依据就说不知道",否则模型可能跳过检索结果自己编——幻觉又回来了。


08 · 写在最后

把这篇和前面几篇串起来看,你其实已经掌握了 AI 应用开发的完整拼图:

  • LLM 原理:模型怎么理解语言(Transformer、注意力)

  • API 调用:模型怎么被调用(HTTP + 消息 + 流式)

  • Prompt 工程:怎么让模型答得对(角色、示例、思维链)

  • Embedding + 向量库:怎么让模型"记住"你的私有知识(语义检索、RAG 地基)

动手建议:把代码跑起来,往知识库里加几段自己的文档,然后用各种"换着说法"的问题去问它——你会直观感受到"语义匹配"和"关键词匹配"的差距,这个体感比读十篇文章都有用。

往期文章:

AI Agent学习之路 | 第一阶段01 |  LLM 工作原理详解

AI Agent学习之路 | 第一阶段02 | Prompt Engineering(提示词工程)的三板斧核心技巧

AI Agent学习之路 | 第一阶段03 | 和 AI 聊天,其实是一次 HTTP 请求(API调用实现)

下一阶段第一节【ChatClient搭配Prompt具体实现】——这也是我们 AI Agent 学习路线的下一阶段课程。想继续的学习的点个【赞】【推荐】让主编知道!

点个【关注】,私信主编,获取一手的面试八股、编程教程、效率工具、AI Agent等学习资料。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐