ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Embeddings实战:从文本向量化到语义检索系统搭建

Embeddings实战:从文本向量化到语义检索系统搭建 1. 为什么Embeddings是LLM开发的分水岭1.1 从符号到向量的认知跃迁如果你跟着这个系列一路走到Day 7前面六天应该已经跟token、prompt、API调用打过照面了。那些东西说白了还是“文本进、文本出”的玩法你给模型一句话它回你一段话本质上跟搜索引擎没拉开代差。但Embeddings不一样它是你第一次真正把语言塞进数学空间里让“意思”变成可以计算、可以比较、可以排序的数字。我刚开始接触这块的时候也有点懵心想文字怎么就变成向量了后来想通了一个类比你在地图上找一个地方用的是经纬度两个数字那要在一个“语义地图”上找一句话的位置用的就是几百上千个数字。Embeddings干的就是这件事——把一段文字映射到一个高维空间里的一个点语义越接近的文字它们的点距离越近。这件事的意义在于它把“理解语义”这个原本只能靠人脑完成的任务变成了一个可以用余弦相似度、欧氏距离来量化的计算问题。你不需要模型真的“懂”你在说什么你只需要它把你说的话和知识库里的话都变成向量然后比谁离得近就行了。RAG、语义搜索、推荐系统、聚类分析底层全是这一套。1.2 这一篇要解决的核心问题Day 7的目标很明确让你能自己动手把任意一段文字变成向量并且用这个向量去完成一次真实的语义检索。不是调个API看个返回值就完事而是要理解向量长什么样、维度意味着什么、相似度怎么算、不同模型出来的向量能不能混用、存到哪里、怎么查得快。我见过太多人卡在这一步API调通了向量也拿到了但不知道接下来干嘛。或者拿两个不同模型生成的向量去算相似度结果发现完全不靠谱。这些坑我都会在这一篇里给你填上。你跟着走完应该能独立搭出一个“输入一句话从一堆文档里找出最相关的那条”的最小可用系统。2. 向量到底是什么把语义翻译成数字2.1 一个向量就是一组浮点数先把这个概念落地。你调用一次Embedding接口拿回来的东西长这样# 假设调用某个Embedding模型 vector [0.023, -0.041, 0.087, ..., 0.012] # 长度可能是384、768、1024、1536、3072不等这就是一个向量。它没有魔法就是一串浮点数。每个数字代表这个文本在某个“语义维度”上的投影值。单独看某一个维度你根本不知道它代表什么就像你单独看经纬度里的“经度”也不知道那是山还是河。但整组数字放在一起就构成了这句话在这个模型语义空间里的唯一坐标。维度数量决定了这个空间的“分辨率”。384维的模型能区分粗粒度的语义差异1536维的模型能捕捉更细的差别。但维度不是越高越好高维意味着存储成本、计算成本都上去了而且边际收益递减。实际选型的时候我一般建议先从384或768起步不够用再往上加。2.2 语义相似度是怎么算出来的有了向量比较两句话的语义相似度就变成了比较两个点的距离。最常用的方法是余弦相似度import numpy as np def cosine_similarity(a, b): a np.array(a) b np.array(b) return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))余弦相似度的取值范围是-1到1。1表示方向完全一致0表示正交不相关-1表示方向完全相反。为什么用余弦而不是欧氏距离因为余弦只看方向不看长度。文本长度不同会导致向量模长不同但语义方向可能是一致的。用余弦就能把“长度”这个干扰因素消掉。注意不是所有Embedding模型都适合用余弦相似度。有些模型在训练时用的是点积或欧氏距离作为优化目标这时候你换一种度量方式效果可能会打折扣。选型的时候要看模型文档里推荐的相似度计算方法。2.3 不同模型出来的向量不能混用这是新手最容易踩的坑。你用一个模型把文档库编码了一遍又用另一个模型把用户查询编码了一遍然后去算相似度——结果一定是乱七八糟的。因为每个模型都有自己的语义空间A模型里的“苹果”和B模型里的“苹果”可能落在完全不同的位置。我自己的做法是整个系统从文档入库到查询必须用同一个Embedding模型。如果中途要换模型那所有已入库的向量都得重新生成一遍。这个成本要在项目初期就考虑进去别等到库里有几十万条数据了才想换。3. 动手实操从零搭一个语义检索原型3.1 环境准备与模型选择我假设你已经有了Python环境接下来装几个必要的库pip install sentence-transformers numpy scikit-learnsentence-transformers是我最推荐入门的Embedding工具库它封装了大量预训练模型调用简单本地就能跑不需要联网调API。对于Day 7这个阶段来说先在本地把流程跑通比什么都重要。模型选择上入门阶段我推荐这几个模型名称维度特点适用场景all-MiniLM-L6-v2384轻量、速度快本地开发、原型验证all-mpnet-base-v2768效果更好、稍慢对精度有要求的检索bge-small-zh-v1.5512中文优化中文文本检索text-embedding-3-small1536API调用、效果好生产环境、不想本地部署如果你是中文场景直接用bge-small-zh-v1.5或者bge-base-zh-v1.5别拿英文模型硬套中文效果差很多。这个坑我踩过当时用MiniLM做中文语义搜索结果相关性惨不忍睹换了中文模型之后立刻正常了。3.2 把一句话变成向量from sentence_transformers import SentenceTransformer # 加载模型第一次运行会自动下载 model SentenceTransformer(all-MiniLM-L6-v2) # 编码单条文本 text 今天天气真好适合出去散步 vector model.encode(text) print(f向量维度: {vector.shape}) print(f前10个值: {vector[:10]})跑完这段代码你就能看到一句话变成了一个384维的数组。这就是Embedding。你可以试试把“今天天气真好”和“今天阳光明媚”分别编码然后算余弦相似度应该会很高再跟“我喜欢吃火锅”算一下应该会低很多。3.3 批量编码与构建检索库实际场景里你不会只编码一句话而是要把整个文档库都编码掉。sentence-transformers支持批量处理documents [ 机器学习是人工智能的一个分支, 深度学习使用神经网络进行特征学习, 今天食堂的红烧肉很好吃, 向量数据库用于存储和检索高维向量, Python是一种流行的编程语言, Embedding把文本映射到高维空间, ] # 批量编码show_progress_bar可以看到进度 doc_vectors model.encode(documents, show_progress_barTrue) print(f文档库形状: {doc_vectors.shape}) # (6, 384)现在你有了一个6行384列的矩阵每一行对应一个文档的向量。接下来做查询query 怎么把文字变成数字向量 query_vector model.encode(query) # 计算查询与所有文档的相似度 similarities np.dot(doc_vectors, query_vector) / ( np.linalg.norm(doc_vectors, axis1) * np.linalg.norm(query_vector) ) # 排序输出 ranked np.argsort(similarities)[::-1] for idx in ranked: print(f相似度: {similarities[idx]:.4f} | 文档: {documents[idx]})跑完你会看到跟“Embedding把文本映射到高维空间”这条的相似度最高跟“今天食堂的红烧肉很好吃”最低。这就是语义检索的雏形——没有关键词匹配纯粹靠向量距离找出来的。3.4 用向量数据库管理大规模数据上面那个例子只有6条文档用numpy直接算没问题。但如果你有几十万条文档每次查询都全量算一遍相似度那就太慢了。这时候需要向量数据库。入门阶段我推荐Chroma轻量、Python原生、零配置pip install chromadbimport chromadb client chromadb.Client() collection client.create_collection(namemy_docs) # 添加文档 collection.add( documentsdocuments, ids[fdoc_{i} for i in range(len(documents))] ) # 查询 results collection.query( query_texts[怎么把文字变成数字向量], n_results3 ) for doc, dist in zip(results[documents][0], results[distances][0]): print(f距离: {dist:.4f} | 文档: {doc})Chroma默认用的是它内置的Embedding模型你也可以传入自己生成的向量。它的优势在于帮你处理了索引和近邻搜索你不需要自己写排序逻辑。提示Chroma默认使用余弦距离还是欧氏距离取决于版本和配置。如果你自己传入向量建议显式指定距离度量方式避免默认行为跟你的预期不一致。4. 进阶理解Embedding背后的关键概念4.1 维度、范数与归一化向量的范数就是它的长度。对于向量v [v1, v2, ..., vn]L2范数定义为||v|| sqrt(v1² v2² ... vn²)归一化就是把向量除以它的范数得到一个长度为1的单位向量。归一化之后余弦相似度就简化成了点积# 归一化 normalized doc_vectors / np.linalg.norm(doc_vectors, axis1, keepdimsTrue) query_normalized query_vector / np.linalg.norm(query_vector) # 归一化后点积等于余弦相似度 similarities np.dot(normalized, query_normalized)很多向量数据库在内部会自动做归一化这样检索时只需要算点积速度更快。你自己在预处理阶段做归一化也是个好习惯能省掉每次查询时的除法运算。4.2 点积、叉积与相似度计算的关系点积内积是两个向量对应位置相乘再求和结果是一个标量。它衡量的是两个向量的“对齐程度”。在Embedding检索里点积是最核心的运算。叉积是另一个概念它只对三维向量有定义结果是一个新的三维向量方向垂直于原来两个向量。在文本Embedding里基本用不到叉积因为我们的向量是几百上千维的叉积没有定义。热搜里出现“向量叉乘”可能是因为有人把数学里的向量运算和Embedding向量搞混了。这里明确一下Embedding检索用的是点积和余弦相似度跟叉积没关系。4.3 张量与向量的区别张量是更广义的概念。标量是0维张量向量是1维张量矩阵是2维张量再往上就是高维张量。在深度学习框架里所有数据都是张量。你编码一批文档得到的(6, 384)矩阵就是一个2维张量。理解这个区别的意义在于当你看到模型输出是(batch_size, seq_len, hidden_dim)这样的形状时不要慌它就是一个3维张量每一层括号对应一个维度。Embedding通常取的是最后一层隐藏状态或者池化之后的向量。5. 常见问题与排查技巧实录5.1 相似度分数看起来不合理怎么办这是最常见的问题。你查“苹果手机”结果“苹果水果”排在最前面。原因可能有几个第一模型本身对领域不敏感。通用Embedding模型在特定领域比如医疗、法律、电商上的表现会打折扣。解决办法是用领域数据做微调或者换一个在该领域预训练过的模型。第二文本预处理有问题。如果你的文档里有很多噪声、HTML标签、特殊符号编码出来的向量质量会很差。入库前做清洗是必要的。第三查询和文档的长度差异太大。有些模型对长文本的编码效果不好会把关键信息稀释掉。可以考虑把长文档切分成段落分别编码检索时返回最相关的段落。5.2 中文效果差怎么排查先确认你用的模型是不是支持中文。all-MiniLM-L6-v2和all-mpnet-base-v2主要是英文模型中文效果确实不行。换成bge-small-zh-v1.5、text2vec-base-chinese或者m3e-base效果会有明显提升。如果换了中文模型还是不行检查一下你的文本有没有做基本的清洗。中文文本里的全角符号、空格、换行符都可能影响编码质量。另外中文分词对Embedding模型来说通常不是必须的因为模型内部有自己的tokenizer但你传入的文本如果包含大量无意义字符还是会干扰结果。5.3 向量数据库检索慢怎么优化几个方向一是减少维度如果384维够用就别上1536维二是用量化技术把浮点数从32位降到8位甚至更低牺牲一点精度换速度三是建索引Chroma、FAISS、Milvus这些数据库都支持HNSW、IVF等索引结构建好索引之后检索速度能提升几个数量级。我自己的经验是十万条以内的数据用FAISS的扁平索引暴力搜索就够了没必要上复杂的索引结构。上了百万级别再考虑HNSW。5.4 常见问题速查表问题现象可能原因排查方向相似度全部接近1向量未归一化或模型输出异常检查向量范数是否合理相似度全部接近0模型不匹配或文本为空确认查询和文档用同一模型中文检索效果差用了英文模型换中文优化模型检索结果不稳定模型随机性设置随机种子或换确定性模型内存占用过高向量维度太高或数据量太大降维或使用量化查询速度慢未建索引使用向量数据库的索引功能6. 从Embedding到RAG下一步的方向6.1 Embedding在RAG里的角色RAG检索增强生成是现在LLM应用里最火的架构之一。它的核心思路是用户问一个问题系统先从知识库里检索出相关文档然后把文档和问题一起塞给LLM让LLM基于文档内容来回答。Embedding就是检索这一步的引擎。没有Embedding你只能用关键词匹配来检索那遇到“同义词”“换种说法”就歇菜了。有了Embedding用户问“怎么把文字转成数字”系统能检索到“Embedding将文本映射到向量空间”这条文档哪怕它们没有一个字是相同的。6.2 知识库构建的实操建议如果你要搭一个基于Embedding的知识库我的建议是文档切分粒度要适中太长了检索不精准太短了语义不完整。一般按段落切每段200到500字比较合适。切分的时候尽量保持语义完整性别把一句话拦腰截断。入库的时候除了向量还要存原始文本和元数据来源、时间、类别等方便检索后做过滤和展示。Chroma、Milvus、Qdrant这些数据库都支持元数据过滤用好了能大幅提升检索质量。6.3 我踩过的几个坑第一个坑是模型混用。早期做原型的时候文档库用了一个模型查询用了另一个结果怎么调都不对。后来统一了模型问题立刻消失。第二个坑是没做归一化。有次用点积算相似度发现长文档总是排前面因为它的向量模长更大。归一化之后才正常。第三个坑是忽略了文本清洗。从网页抓下来的文档里全是HTML标签和广告文字编码出来的向量噪声很大。后来加了一步清洗检索质量提升明显。第四个坑是维度选太高。一开始觉得维度越高越好用了1536维的模型结果内存吃紧、查询变慢效果也没比768维好多少。后来降到768维整体体验反而更好了。Embedding这个东西入门容易精通难。把基本流程跑通可能只需要一个下午但要调出一个真正好用的语义检索系统需要不断试模型、调参数、清洗数据、优化索引。Day 7只是一个起点后面还有得折腾。
返回列表