烂笔头扫盲:Embedding 与语义匹配:从概念到 pgvector 落地

Embedding 与语义匹配:从概念到 pgvector 落地

适合谁读:听说过 ChatGPT,想给产品加「语义搜索」或「语义匹配」,但还没搞懂 Embedding、pgvector、以及 Chat 和 Embedding 为什么要分开配的人。

读完你能做什么:理解原理、会调 Embedding API、会算相似度、知道怎么用 PostgreSQL + pgvector 存向量,并在现有业务里加一条语义相似度链路。


1. 先从一个尴尬的场景说起

你做了一个帮助中心,用户搜:「怎么取消订单」

系统用关键词匹配,去文档标题里找有没有「取消」「订单」这几个字。

结果排第一的是:《订单 状态有哪些?》

真正有用的《如何取消订单并退款》反而排在后面。

问题不在服务器慢,而在于:关键词一样,意思可以差很远;关键词不一样,意思却可以是一回事。

用户说的 文档标题 关键词重叠 意思像不像
怎么取消订单 订单状态有哪些 有「订单」 不太像
怎么取消订单 如何办理退货退款 几乎没有 很像
苹果手机 iPhone 15 没有 很像
Go 后端 Golang 服务端 很少 很像

语义匹配要解决的问题:让计算机判断「像不像」,而不是「有没有同一个词」。

Embedding,就是实现语义匹配的常用手段。


2. Embedding 是什么?

2.1 一句话定义

Embedding(嵌入)= 把一段文字,变成固定长度的一串数字(向量)。

输入:

text 复制代码
"如何取消订单并退款"

输出(示意,真实向量很长):

text 复制代码
[0.012, -0.034, 0.089, 0.021, ...]   ← 通常 768 / 1024 / 1536 维

这串数字人类读不懂,但有一个关键性质:

意思相近的文本,向量在数学空间里更接近。

2.2 和 Chat 大模型有什么区别?

很多人第一次接触 AI 就是 Chat。Embedding 和 Chat 是两类不同的 API

对比项 Chat(对话 / 补全) Embedding(嵌入)
输入 提示词、对话历史 一段或多段文本
输出 人话、JSON、代码 只有数字向量
模型在干什么 「理解 + 生成」 「编码成坐标」
典型用途 写文案、分析、问答 搜索、推荐、聚类、算相似度
成本与速度 相对慢、相对贵 相对快、相对便宜

口诀:

Chat 负责「说出来」;Embedding 负责「标位置」。

那如果不标位置,只靠 Chat 直接说出来,会怎样?

可以,但通常只适合少量、一次性的任务。一旦涉及「从很多候选里找最像的」或「大规模对比」,就会吃力:

场景 只有 Chat(不标位置) 先 Embedding 标位置
用户搜「怎么取消订单」 往往退回关键词匹配,或让 LLM 临时读几篇文档再答------换说法容易漏 离线给每篇文档标好坐标,在线算距离,Top-K 检索,「退货退款」也能命中
库里有 1 万条文档,找最相关的 5 条 没法把 1 万条都塞进 Prompt 让 LLM 逐条比 1 万次坐标已存好,毫秒级算最近邻
两段话像不像 可以问 LLM「打 1--100 分」------慢、贵,同样输入多次问可能分数波动 两次 Embedding + 算距离,快、稳,适合批量对比
「Go 后端」和「Golang 服务端」 LLM 多数能判断像,但没有统一坐标系,难和库里其他 JD 一起排序 都在同一语义空间里,相对远近可比

换句话说:

  • Chat 直接说 :像请一位专家当场口头点评 ------适合分析、解释、写文案,但不擅长在成千上万条数据里快速找相似项
  • Embedding 标位置 :像先给每句话在地图上钉一枚图钉------平时看不见理由,但一搜「离 query 最近的图钉是谁」就很快、很便宜。

很多产品里的尴尬(第 1 节的帮助中心搜不准),根因就是:只有「说出来」的能力,没有「标位置」的索引。RAG、语义搜索、推荐,底层都要先标位置,再根据需要让 Chat 出来说人话。

先区分两个词:

  • 标量(scalar):只有一个数,没有方向。体温 37°C、匹配分 85、价格 99 元,都是标量。
  • 向量(vector)多个数排成一排 ,而且顺序不能乱

例如 [0.1, 0.5, -0.2] 是一个 3 维向量------3 个数,对应 3 个方向上的坐标:

位置 可以粗浅理解为
第 1 维 0.1 在某个抽象方向上的分量
第 2 维 0.5 在另一个方向上的分量
第 3 维 -0.2 在第三个方向上的分量(可为负)

几个要点:

  1. 有序[0.1, 0.5][0.5, 0.1]不同的向量,不能交换位置。
  2. 一起才有意义:单独看 0.5 没意义,整串数字合起来才表示「这句话在语义空间里的位置」。
  3. Embedding 的向量很长 :真实场景常见 768、1024、1536 维------不是 3 个数,而是一千多个 float 排成一排;维数由模型决定,你只需整段文本调一次 API,拿回完整数组。
  4. 可以是负数:Embedding 里正负都有,模型内部学出来的,不必强行解释每一维代表什么。

「向量」和「矢量」英文都是 vector (物理课里的力、速度也是矢量)。和向量相对的概念通常是标量

Embedding 里,每个 float 对应高维空间里的一个坐标轴;你不需要读懂每一维,只要知道:两个向量离得近 = 两段话意思像

2.4 生活化类比

想象每一句话都被贴上图书馆里的坐标

  • 「如何取消订单」→ 坐标 A
  • 「退货退款流程」→ 坐标 B,离 A 很近
  • 「公司年假制度」→ 坐标 C,离 A 很远

Embedding 模型就是那个自动贴坐标的机器。


3. 相似度怎么算?

3.1 余弦相似度

两段话各变成一个向量,就是空间里的两个点:

  • 点离得 → 语义
  • 点离得 → 语义不像

文本场景最常用余弦相似度

  • 值约在 0 ~ 1(越接近 1 越像)
  • 1 ≈ 方向几乎相同
  • 0 ≈ 无关

3.2 Python 手算

python 复制代码
import math

def cosine_similarity(v1, v2):
    dot = sum(a * b for a, b in zip(v1, v2))
    norm1 = math.sqrt(sum(a * a for a in v1))
    norm2 = math.sqrt(sum(b * b for b in v2))
    return dot / (norm1 * norm2)

a = embed("如何取消订单")      # 假设 embed() 已返回向量
b = embed("退货退款怎么办")
c = embed("员工年假有几天")

print(cosine_similarity(a, b))  # 通常较高,如 0.85+
print(cosine_similarity(a, c))  # 通常较低,如 0.3x

3.3 变成 0--100 分

python 复制代码
def to_percent(sim: float) -> int:
    return round(max(0.0, min(1.0, sim)) * 100)

注意:绝对分数因模型而异 。对比时更可靠的是同一模型下、同一批文本之间的相对高低,而不是执着于 82 还是 85。

3.4 余弦相似度 vs 余弦距离

pgvector 用 余弦距离 运算符 <=>

text 复制代码
余弦距离 = 1 - 余弦相似度   (向量归一化等常见设定下)
  • 距离越小 → 越相似
  • 相似度越大 → 越相似

别搞反。


4. 调用 Embedding API

下面用最常见的 OpenAI 兼容 /embeddings 接口举例(多数云厂商格式类似:传文本,拿回 embedding 数组)。

4.1 HTTP 请求

bash 复制代码
curl https://api.openai.com/v1/embeddings \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-small",
    "input": "如何取消订单并退款"
  }'

国产云厂商(如阿里云百炼、智谱等)通常也提供 OpenAI 兼容地址,只需改 base_urlmodel

bash 复制代码
curl "$EMBEDDING_BASE_URL/embeddings" \
  -H "Authorization: Bearer $EMBEDDING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-v4",
    "input": "如何取消订单并退款",
    "dimensions": 1536
  }'

4.2 响应里你关心的部分

json 复制代码
{
  "data": [
    { "embedding": [0.012, -0.034, 0.089, "..."] }
  ],
  "usage": { "prompt_tokens": 8 }
}

要点:

  1. embedding 长度由模型决定(如 1536 维)
  2. token 计费,通常比 Chat 便宜
  3. 一次可传多条 input(批量嵌入)

4.3 Python 最小示例

python 复制代码
from openai import OpenAI

client = OpenAI()  # 或 OpenAI(api_key=..., base_url=...)

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return resp.data[0].embedding

a = embed("如何取消订单")
print(len(a))  # 1536

5. Chat 和 Embedding 为什么要分开配?

这是落地时最常见的认知误区。

很多团队只开通了一个大模型的 Chat API (如各类对话模型),就想顺便拿它做 Embedding。通常行不通

  • Chat 模型走 /chat/completions,输出是人话或 JSON
  • Embedding 走 /embeddings,输出是数字向量
  • 多数 Chat 提供商并不提供 Embedding 端点;即使有,也是另一个模型、另一个计费

工程上的标准做法:

用途 典型选择 配置思路
对话 / 分析 / 生成 Chat 模型 A CHAT_API_KEY + Chat base URL
语义向量 Embedding 模型 B EMBEDDING_API_KEY + Embedding base URL

两者可以来自同一家云 (如都用 OpenAI 兼容接口),也可以是不同家 (Chat 用 A 厂、Embedding 用 B 厂)。关键是:各调各的 API,各用各的模型

环境变量示例(命名仅作参考):

bash 复制代码
# Chat
CHAT_API_KEY=sk-...
CHAT_BASE_URL=https://api.example.com/v1
CHAT_MODEL=some-chat-model

# Embedding
EMBEDDING_API_KEY=sk-...
EMBEDDING_BASE_URL=https://api.example.com/v1
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIMENSIONS=1536

6. LLM 打分 vs Embedding 打分:可以并存

除了 Embedding,你也可以让 Chat 模型读两段话,直接输出「匹配度 1--100」并解释原因。

两条路线各有优劣:

维度 Embedding + 向量距离 Chat 直接打分
速度
成本
稳定性 同输入结果稳定 受 Prompt 影响大
可解释性 弱(只有分数) 强(可写理由)
大规模检索 擅长(百万条 Top-K) 不擅长
同义词 / 换说法 语义上自然接近 不一定稳定

工程上常见做法:两者并存。

  • Embedding 分:客观、可复现的语义相似度(适合排序、对比)
  • LLM 分:带 breakdown、建议、摘要的可解释分析

产品上可以并排展示两个指标,各自标注来源,避免用户混淆。


7. 业务里怎么用?

7.1 语义搜索(最常见)

text 复制代码
离线:文档 → Embedding → 存入向量库
在线:用户 query → Embedding → Top-K 最近邻 → 返回结果

7.2 一对一匹配(如简历 vs 岗位)

text 复制代码
文本 A → 向量 a
文本 B → 向量 b
余弦相似度(a, b) → 匹配分

不需要检索库,算一次距离即可。

7.3 RAG(检索增强生成)

text 复制代码
用户问题 → Embedding → 检索相关段落 → 拼进 Prompt → Chat 生成答案

Embedding 负责找材料 ;Chat 负责组织语言。企业知识库问答底层多是这条链。

7.4 完整迷你 Demo

目标:验证「退票」和「取消订单」比「年假」更像。

python 复制代码
from openai import OpenAI
import math

client = OpenAI()

texts = [
    "如何取消订单并退款",
    "退货流程说明",
    "公司员工年假有几天",
]
query = "我想退票"

def embed(text):
    r = client.embeddings.create(model="text-embedding-3-small", input=text)
    return r.data[0].embedding

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))

q = embed(query)
for t in texts:
    print(f"{cosine(q, embed(t)):.3f}  {t}")

期望:前两条明显高于「年假」。跑通这一刻,API → 相似度 → 应用的闭环就完成了。


8. 向量存哪里?

方案 适合阶段 优点 注意
内存 / JSON Demo、几千条 零依赖 难扩展
PostgreSQL + pgvector 已有 PG、百万级以内 向量与业务同库 要装扩展,维度写对
Redis / ES 向量字段 已有这些设施 运维统一 能力因版本而异
Milvus / Qdrant / Pinecone 大规模高 QPS 检索优化 多一套组件

早期原则:别为了「将来千万级」过早上专用向量库。 几千、几万条,pgvector 或内存都够用。


9. pgvector 落地指南

9.1 换 Docker 镜像

普通 postgres:16 没有 vector 扩展。要用:

yaml 复制代码
# docker-compose.yml 示例
services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"

三件事的顺序不要搞混:

text 复制代码
1. 换镜像(让 PostgreSQL 能装 vector 扩展)
2. 写 migration
3. 执行 migrate

9.2 Migration:第一条 SQL

必须先启用扩展:

sql 复制代码
CREATE EXTENSION IF NOT EXISTS vector;

否则后面 vector(1536) 会报类型不存在。

建表示例:

sql 复制代码
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
    id         BIGSERIAL PRIMARY KEY,
    content    TEXT NOT NULL,
    embedding  vector(1536)   -- 维度必须和 Embedding 模型一致
);

给已有表加列:

sql 复制代码
CREATE EXTENSION IF NOT EXISTS vector;

ALTER TABLE articles
    ADD COLUMN embedding vector(1536),
    ADD COLUMN semantic_score INT;

9.3 用 SQL 做 Top-K 检索

sql 复制代码
-- $1 是 query 的 embedding 向量
SELECT id, content, embedding <=> $1 AS distance
FROM documents
ORDER BY embedding <=> $1
LIMIT 5;

<=> 是余弦距离:越小越相似

数据量上来后,可加 HNSW / IVFFlat 索引;早期几千条暴力算即可。

9.4 应用层写入向量

各语言驱动做法不同,核心是:把 []float 转成 pgvector 接受的格式(如 '[0.1,0.2,...]'::vector)。

伪代码:

text 复制代码
vector_str = "[" + join(embeddings, ",") + "]"
INSERT INTO documents (content, embedding)
VALUES ($content, $vector_str::vector)

一对一相似度也可以在应用层算余弦,不必走 SQL------两种都行,看数据量和架构偏好。

9.5 在现有 API 里加语义分(通用模式)

假设你已有同步接口 POST /analyze,接收两段文本,返回 LLM 分析结果。加语义分的最小改动是加一条旁路,而不是重写服务:

text 复制代码
收到 text_a, text_b
    │
    ├─并行─→ Embed(text_a)  ─┐
    ├─并行─→ Embed(text_b)  ─┼─→ 余弦 → semantic_score
    └─并行─→ Chat 分析       ─→ llm_score + 文字说明
    │
    ▼
持久化(文本 + 向量 + 双分)→ 返回 JSON

Embedding 和 Chat 输入相同、输出互不依赖,可以并行以缩短总耗时。

响应 JSON 示例(字段名自定):

json 复制代码
{
  "llm_score": 72,
  "semantic_score": 85,
  "analysis": { "...": "..." }
}

10. 验收思路

以「简历 vs 三个 JD」为例(换成任何两段文本对比场景都一样):

文本 B 期望 semantic_score
Go 后端开发
Golang 服务端工程师 高(接近上一行)
产品经理 明显更低

验证三件事:

  1. API 返回语义分,且排序符合直觉
  2. 向量已写入数据库(embedding IS NOT NULL
  3. 前端或客户端能展示语义分(可与 LLM 分并排)

11. 常见坑

说明
维度对不上 模型 1536 维,表写 vector(768) → 插入失败
混用不同模型 A 模型算的向量不能和 B 模型的比距离;换模型要全量重算
用 Chat 当 Embedding Chat 模型不会给你可靠的语义坐标
文本过长 超 max token 要截断或分块
指标搞反 余弦距离越小越像;相似度越大越像
忽视成本 Embedding 便宜但不是免费;内容没变别重算
只配 Chat Key 忘了单独配 Embedding API,上线后 analyze 报错

12. 小结

图 1:Embedding 在系统中的位置

text 复制代码
文本 ──→ [Embedding 模型] ──→ 向量 ──→ [距离/相似度] ──→ 分数
                                              ↑
                                        另一条文本的向量

图 2:Chat 与 Embedding 的分工

text 复制代码
Embedding:「这两句话像不像?」(数学,稳定)
Chat:      「为什么像?帮我写一段分析。」(语言,可解释)

图 3:从 Demo 到生产

text 复制代码
内存手算 → PostgreSQL + pgvector →(规模上来)索引优化或专用向量库

写在最后

Embedding 并不神秘:它只是一种把文字变成坐标的 API。

落地时记住三件事:

  1. Chat 和 Embedding 是两类 API,通常要分开配 Key、分开选模型
  2. LLM 分负责解释,Embedding 分负责相似度------可以并存,别互相替代
  3. pgvector 让向量和业务数据同库 ------换镜像 → CREATE EXTENSIONvector(n) → 写入
相关推荐
程序员黑豆17 小时前
鸿蒙应用开发之模拟器安装与使用教程
前端·harmonyos
王莎莎17 小时前
MCP 解决的是工具接入,科研 Agent 还缺的是科学证据接口标准化
前端·人工智能
zhanghaha131418 小时前
Python语言基础:4_数据类型转换
java·前端·python
kyriewen18 小时前
别再写useMemo了——2026年这5个React性能优化已经是反模式
前端·react.js·ai编程
OpenTiny社区18 小时前
TinyRobot v0.5.0 新版本强在哪?
前端·vue.js·github
奇牙coding18 小时前
gpt-5.6-sol 接入指南:reasoning_effort 参数配置、推理链验证与常见报错排查
前端·css·gpt·ai
sugar__salt19 小时前
Vue.js 前置知识:ES6+ 核心特性完全指南
前端·javascript·vue.js·vue·es6
雪隐19 小时前
个人电脑玩AI-10让5060 Ti给你打工——我让 Claude Code 喝上了本地杂粮:Ternary-Bonsai-27B 部署历险记
前端·人工智能·后端
Bigger19 小时前
我受够了每天问“今天吃什么”,于是做了个 AI 菜单工具
前端·人工智能·agent