首页 / 资讯中心 / 文章详情

文本向量化入门

文本向量化入门 ★ FEATURED ARTICLE
本文面向完全没接触过向量模型的读者。所有代码与数字来自我维护的一个长篇小说生成工程但重点不是那个工程——而是向量化本身文本怎么变成向量、向量怎么比较、怎么存、怎么查、以及换模型时为什么会出事。一、先从一个搜索失败说起假设你有一个文档库想做一个搜索框。用户输入主角很生气你希望它能找到写着他勃然大怒一掌拍碎了桌角的那一段。用传统的关键词匹配这段永远搜不出来——两句话没有一个字是重合的。问题出于在计算机眼里生气和大怒是两个毫不相干的字符串就像 abc 和xyz。它没有办法知道这两个词在说同一件事。要解决这个问题得先让意思变成计算机能运算的东西。向量化vectorization就是干这件事的把一段文本映射成一串浮点数让语义相近的文本映射到数值上相近的位置。这串数字就叫向量vector这个映射过程也叫嵌入embedding——两个词在中文语境里基本可以互换。二、向量到底长什么样先建立直觉。假设我们只用两个数字来描述一个人(身高厘米, 体重公斤)小明 (175, 70)小刚 (178, 72)这两个人的数值坐标很接近所以他们在某种意义上是相似的人。再加一个维度就是三维(身高, 体重, 年龄)。如果加到 1024 维呢就是1024 个数字排成一行[0.0234, -0.0891, 0.0156, 0.1203, ..., -0.0077]↑ ↑ ↑第 1 维 第 3 维 第 1024 维这就是一个真实的文本向量。维度dimension指的是这一行里有多少个数字本项目用的是 1024 维。这里有一个初学者最容易误解的点必须说清楚你没法说出其中的任意一个数字代表什么。在身高体重那个例子里每个数字都有明确的物理含义。但在文本向量里1024 个维度没有人类可解释的名字。它们是模型在训练中自己学出来的一套坐标系统可能第 37 维隐约和程度副词相关第 512 维隐约和情绪倾向相关但没人能给出精确的对应表。这不是缺陷而是它强的原因——人类设计不出 1024 个能区分语义细微差别的维度但模型可以自己找出来。三、向量化过程第一步操作调用接口把文本变成向量文本不会自己变成向量得调用一个 embedding 模型。接口形状基本都长这样以 OpenAI 兼容协议为例POST https://供应商地址/v1/embeddings{model: qwen3.7-text-embedding,input: [主角很生气, 他勃然大怒一掌拍碎了桌角],dimensions: 1024}返回{data: [{ index: 0, embedding: [0.0234, -0.0891, ...] },{ index: 1, embedding: [-0.0117, 0.0442, ...] }]}注意三件事它们各自对应一个后续会踩的坑第一input是一个数组。一次请求可以传多条文本这叫批量向量化。逐条调用会慢得多因为每条都要走一次网络往返。本项目把批大小设为 10——不是因为 10 是最优值而是因为供应商实测超过 10 条直接返回 400 错误。第二dimensions是显式指定的。同一个模型往往支持多种维度输出本项目用的模型支持 1024 / 2048 / 768。如果不指定就用供应商的默认值——而默认值是会变的。一旦它从 1024 变成别的数字你之前存的所有向量就和新的对不上了。所以这个项目把 1024 写死在配置里不依赖默认值。第三输入有长度上限。太长的文本会被供应商拒绝。第二步操作比较两个向量有多像有了向量怎么判断”主角很生气“和”他勃然大怒“像不像用余弦相似度cosine similarity。建立几何直觉把向量想象成从原点射出的一支箭头。两支箭头的夹角越小方向越接近意思就越像。夹角 0°指向完全一致→ 相似度 1最像夹角 90°互相垂直→ 相似度 0毫不相关夹角 180°指向完全相反→ 相似度 -1意思相反余弦相似度的公式是cos(a, b) (a · b) / (|a| × |b|)a · b a₁b₁ a₂b₂ ... aₙbₙ 点积对应位相乘再求和|a| √(a₁² a₂² ... aₙ²) 模长这支箭头的长度手算一遍拿两个二维向量练练手a (3, 4)b (4, 3)点积 3×4 4×3 24|a| √(9 16) 5|b| √(16 9) 5cos 24 / (5 × 5) 0.960.96很接近 1说明这两个向量高度相似。再看c (1, 0)和d (0, 1)点积 1×0 0×1 0cos 0 / (1 × 1) 0相似度 0完全无关。1024 维的计算方式完全一样只是加法项从 2 个变成 1024 个——计算机总是很擅长做这类事。为什么用余弦不用欧氏距离欧氏距离是两支箭头尖端之间的直线距离它同时受方向和长度影响。而向量的长度往往和文本长度、用词强度相关——同一句话写长一点向量可能就变长了。如果按欧氏距离算“一句话”和“这句话的加长版”会被判成不相似。余弦只看向量方向把长度除掉公式里除以|a| × |b|就是在做归一化所以对这类干扰更稳健。这也是为什么大部分项目在创建向量集合时指定距离度量为Cosine——一旦选定后续所有检索的分数语义都由它决定score越大表示越相近。第三步操作切块为什么不把整篇文档变成一个向量这里有两个原因以小说为例。原因一一个向量只能表达一个[意思中心]。假设你把一整章 3000 字变成 1 个向量这个向量是整章所有内容的平均值。如果这一章里既有打斗又有感情戏最后得到的向量是个[ 打斗 感情 ]的模糊混合体——用它去检索两边都不太像。原因二检索命中后要把原文喂给模型。如果你的最小检索单位是一整章那么命中一次就要注入 3000 字其中大部分和当前任务无关。这会稀释模型的注意力还挤占预算。所以文本要先切块chunking把长文档切成一个个语义相对完整的小段每段单独生成一个向量。按什么边界切切块本身不调用任何模型是纯文本处理。但切在哪里很有讲究按固定字数硬切会把句子拦腰截断“他勃然大怒一掌拍碎了” / “桌角” ——第二块几乎没意义按语义边界切段落、小标题、句号、问号、分号每块都是一个能独立读懂的意思单元还要有个兜底如果一段文本完全没有标点比如某种特殊格式的数据回退逻辑找不到任何断点就退化成硬切。这个兜底是必需的——没有它遇到无标点长文本时切块会退化成一整块切块逻辑形同虚设。经验切块粒度决定了检索精度的上限。如果每块都很大后面无论怎么调参数都救不回来。同样地如果预算存在例外规则那这个预算就不是预算。第四步操作存进去向量算出来了得存起来。存向量的数据库叫向量数据库vector database本项目用的是 Qdrant。它的数据模型很简单三个概念概念类比说明集合collection数据库里的表一批同类向量放一起比如「参考资料」「某本书的记忆」点point表里的一行一个点 一个 id 一个向量 一份 payloadpayload行里的其他字段随向量一起存的业务数据一个点的完整样子{id: 8f2a1c4e-...-9b3d,vector: [0.0234, -0.0891, ...],payload: {fileName: exemplars.md,sectionTitle: 开篇钩子的写法}}payload是最容易被忽视、但业务上最关键的部分。向量本身只能告诉你像不像不能告诉你这是哪一段。检索命中一个点之后你要靠 payload 里的fileName回去把原文加载出来——这叫回源。集合的维度是定死的创建集合时必须声明维度之后写入的每个向量长度都必须匹配。创建集合时有两件必须做的事指定维度 1024距离度量 Cosine如果集合已存在检查它现有的维度和当前模型输出是否一致不一致就直接报错第 2 点为什么重要因为如果不检查维度冲突会一路漏到写入时才由数据库报错或者更糟——在某种失败被静默吞掉的情况下表现为检索一直返回空结果你根本想不到是维度问题。第五步操作让写入可以重复执行幂等写入假设你的资料更新了需要重建索引。最简单粗暴的做法是清空集合重新灌一遍。但如果中途失败集合就处于残缺状态。更稳的做法是覆盖式写入upsert同 id 的点直接覆盖旧点重复执行多少次结果都一样——这叫幂等idempotent。幂等的前提是id 必须稳定。同一段文本今天算出来的 id 和明天算出来的 id 必须完全相同。第六步操作检索存进去是为了查出来。一次检索的完整流程是查询文本 ── 同一个 embedding 模型 ── 查询向量│在集合里找最相近的 N 个点│按分数过滤 取 payload三个关键参数top-k取最相近的前几个。本项目参考资料检索取 3 个故事记忆检索取 10 个。注意 top-k 只是候选池大小——真正决定「有多少内容被使用」的是后面的字符预算。分数阈值min-score低于这个分数就丢掉。这个参数初学者常常不设但它非常重要。最容易出事的一步换模型这一节是全文最重要的部分。如果你只记住一件事请记住这件。换 embedding 模型必须重新嵌入所有文本即使新模型输出同样是 1024 维也必须重嵌。原因每个模型学出来的是一套自己的坐标系统。A 模型的第 37 维可能在表达程度B 模型的第 37 维可能在表达完全不同的东西。两个 1024 维向量放在一起算余弦相似度数学上能算出一个数但这个数没有任何语义意义。维度不匹配 → 数据库会拒绝写入你会立刻发现 ✅维度相同但模型不同 →一切正常运行检索照样返回结果只是结果全是噪声❌同理改变切块方案也要重嵌因为切块方案决定了一个 id 对应哪段文本。项目里有个版本号常量任何改变 怎么切分 / 怎么生成 id 的改动都必须让它加一。否则新旧两套 id 的点会同时存在检索时命中两份重复内容——同样是静默劣化。四、让我们把整个操作串起来一次完整的流程长这样建索引离线/启动时读文档→ 按语义边界切块 [纯文本处理]→ 批量调用 embedding 接口 [网络调用注意批大小/超时/重试]→ 得到每块的向量→ ensureCollection(维度, Cosine)→ upsert(id稳定哈希, 向量, payload) [幂等]检索每次需要时查询文本→ 用同一个模型向量化→ search(top-k)→ 按 min-score 过滤→ 按字符预算贪心装入→ 用 payload 回源加载原文五、常见坑清单坑症状对策换模型没重嵌检索结果全变噪声不报错索引签名记录模型不符自动重建换切块方案没重嵌前缀里出现重复内容切分方案版本号改动即递增id 不稳定数据重复堆积越重建越多确定性哈希 显式指定 UTF-8切块太大预算绑不住注入大量无关内容按语义边界切单条也封顶没设分数阈值召回一堆不相关内容在真实数据上调 min-score超时套用慢操作的配置一次挂起等几十分钟按用途独立设置超时维度冲突被静默吞掉检索恒为空建集合时主动校验维度一次性写入过多点大文档重建失败分批写入本项目 100 点/批附术语速查向量化 / 嵌入embedding把文本映射成一串浮点数维度dimension这串浮点数有多少个。本项目 1024余弦相似度cosine similarity两个向量方向的接近程度范围 -1~1越大越相近切块chunking把长文本切成语义完整的小段每段一个向量集合collection向量库里的「表」点point集合里的一条记录 id 向量 payloadpayload随向量一起存的业务字段用于命中后回源top-k检索返回的候选数量min-score相似度门槛低于则丢弃重嵌re-embed用新模型重新生成所有向量换模型后必须做
阅读完成 · 觉得有帮助?
咨询建站