ARTICLE DETAIL

资讯详情

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

企业级问答系统Embedding与向量化实战:从切分到召回优化

企业级问答系统Embedding与向量化实战:从切分到召回优化 做了这么多年的企业级检索应用每次和刚接触这块的朋友聊起“给文档做Embedding”对方总以为这就是调一个模型接口把文本变成一串数字就完事。真实搭建企业级智能问答系统的时候这件事远没有这么轻巧文本怎么切、模型怎么选、向量怎么存、召回怎么优化任何一环没想清楚最后上线测试都会被检索结果打脸。Ch08这一章我们专门聊Embedding与向量化实战把从零到一搭系统过程中我踩过、填过、优化过的细节一次性摊开讲。如果你正在做RAG问答、智能客服、内部知识库检索这类系统这一章的每个小节基本都能对应到你第二天就会遇到的真实问题。我不会只给结论会尽量把“为什么这么做”背后的逻辑也交代清楚。1. 向量化在整个问答系统里的位置以及它到底解决了什么1.1 没有向量化之前企业内部问答是怎么做的在Embedding大规模普及之前企业内部问答系统绝大多数靠的是关键词检索那套老路子对文档做分词、建倒排索引用户提问时也做分词然后靠BM25、TF-IDF这类算法去算文档和查询的词面相关度。这套方案不是不能用但它在企业场景里有一个很难绕开的死穴用户说话的方式和文档写作的方式往往不一样。员工问的是“报销要准备哪些材料”规章制度里写的是“报销凭证请附清单及原始票据”两句话在字面上几乎没有重叠关键词匹配直接失联。你可能觉得可以靠同义词表硬凑但企业业务里同义表达多到根本维护不过来而且行业术语、口语缩写、跨部门黑话随时在变。这种“问法”和“写法”错位的现象在客服和HR场景尤其常见。我自己见过很多知识库系统上线后命中率不到一半后台日志一看大量query根本没找回该找的文档问题就出在词面匹配上。1.2 向量化的本质把语义变成距离Embedding解决的正是上面这个错位问题。它的核心思路是把一段自然语言文本映射到一个高维稠密向量空间里让语义相近的句子在这个空间里距离更近语义无关的句子距离更远。用生活化的类比来说相当于把每个句子放到一张巨大的“语义地图”上相同意思的句子会自动聚到同一个街区附近。所以“报销要准备哪些材料”和“报销凭证请附清单及原始票据”哪怕字面完全不同它们的向量也会落在相近位置。检索的时候我们把用户的问题也转成向量去向量空间里找最近的文档切片这就是语义召回的基本逻辑。要理解这件事为什么能成得稍微提一下背后的训练方式。现在的Embedding模型通常是用对比学习训练出来的训练时给模型看大量“问题-答案”、“相似句-相似句”配对让模型学会把语义相关的样本拉近把不相关的样本推远。模型学到的不是某个词的对应关系而是一套通用的语义距离判断能力所以它才能处理那些文档里从没出现过的问法。1.3 一个最小的向量化链路长什么样把这个环节放进整个问答系统里看它的上下游关系其实很清楚上游接的是文档解析与切分原始文档要先解析成干净的文本块中间是Embedding模型把每个文本块转成向量下游是向量索引存储向量并提供相似度检索再往下是召回结果的重排与LLM生成我当时搭建最小可行链路的时候就是先把一批FAQ文档切成块调Embedding模型批量转成向量存进一个向量库然后写了一个查询脚本输入用户问题转向量取TopK把文档原文打印出来。就这二十多行代码立刻能看出向量化到底有没有把“找词”升级成“找意思”。这里有一个很重要的心态不要一上来就追求复杂的架构先把最小链路跑通哪怕用单机内存索引都行。因为只有链路通了你才能在地基上做后续一切优化。2. 如何从一堆Embedding模型里选定业务用的那一个2.1 模型排行榜该怎么看又不该怎么看打开任意一个模型榜单比如MTEB、C-MTEB你会看到一堆Embedding模型按分数排得清清楚楚。很多人选模型的习惯是直接抄榜首但我在企业项目里试过几次之后得出了一个不太一样的结论榜单只能告诉你模型的上限不能告诉你它在你的业务数据上合不合适。榜单评测语料大多是公开网页、百科、新闻、社区问答这类文本和企业的规章制度、产品文档、售后工单、技术手册完全是两个世界。一个在公开语料上排名靠前的模型放到你的私有知识库上效果可能还不如一个排名稍低但在垂直领域更稳的模型。更要命的是排行榜不会告诉你模型的成本榜首往往是大参数量模型推理慢、吃显存、批量向量化时吞吐上不去。企业级系统要处理的是几十万到几百万个切片如果每千条文本要跑好几分钟索引更新就成了灾难。正确的做法是把榜单当线索不当结论。先从上面筛出两三个候选模型再用你自己准备的一小批“问题-应命中答案”样本去实测对比跑出来的结果才值得信。2.2 企业选型最该盯住的四个指标结合我自己的项目经验真正决定一个Embedding模型能不能在生产环境用的往往不是分数而是下面这四件事第一是向量维度。维度直接决定存储成本和检索耗时。一个模型输出1024维和输出2048维向量库容量和查询延迟都可能差出好几倍。如果你的知识库切出来几十万块这点差异会非常明显。不要盲目追求“维度越高信息越多”够用就行。第二是最大输入长度。每个模型能接收的token上限不一样有的只有512有的支持8192。这个参数会直接约束你的文本切分策略切片太长超过上限会被截断截断后的向量等于白算切片太短又损失上下文。选模型前先想清楚你的文档块大概多长。第三是中文效果。很多榜单一半是英文评测撑起来的中文效果要靠C-MTEB这类中文榜单补充判断。企业内部系统绝大部分是中文场景必须单独关注中文检索、中文同义句匹配的表现。第四是许可证与部署成本。商业模型可能按量收费开源模型也各有各的开源协议有的限制商用有的要求网络隔离。企业项目尤其要注意模型能不能私有化部署数据出不出得去。这个环节不提前确认到上线阶段被法务卡住就很被动。2.3 我自己用的模型组合与理由我现在大部分内部项目用的主力Embedding模型是BGE系列比如BAAI/bge-m3。理由很简单中文效果稳定支持长文本向量维度适中而且是开源的可以企业内部私有化部署。bge-m3在多语言和长文档上的综合表现非常省心切片长度给到1K左右都不会明显劣化。多模态图文混合的知识库场景我会额外准备一个SigLIP2模型来做图像向量化这个后面专门展开。纯文本检索和图文检索分开处理避免互相干扰。有一点我想特别提醒模型选型不是一锤子买卖。随着你知识库内容结构的变化或者新一代模型发布每隔半年重新评测一次候选模型是完全值得的。我就经历过一次从老模型换到bge-m3之后TopK命中率直接涨了八个百分点的情况这种收益比很多花里胡哨的检索策略都来得实在。3. 向量化之前那步才是真正的分水岭数据切分与清洗3.1 切分粒度为什么直接决定检索上限我见过太多人在模型选型上花大力气却对数据切分毫不在意结果上线后检索效果奇差无比。实际上向量化之前的文本切分才是决定检索效果上限的关键环节。模型再强喂进去的是烂切片取出来的向量就是烂向量。切片太碎会怎么样一段只有一两句话的切片往往缺少必要的上下文。比如文档里写“该流程由财务部负责审批”单独切出来后面的“该流程”指代的是什么就丢了向量化之后语义不完整检索时自然容易漏召回。切片太大会怎么样更长文本的向量会被整个段落里的次要信息稀释。一份合同里几十条条款塞进同一个切片用户问其中一条整块向量的重心却可能被其他条款带偏相关性分数上不去。这就是为什么向量化实战里切分永远是和模型选型并列的核心问题。3.2 固定窗口切分、递归切分、语义切分的取舍市面上最基础的切分方式是固定窗口切分数着token切比如每500个token一段再带50个token的重叠。实现简单效果稳定但完全无视文档结构经常把一个完整章节从中间腰斩或者把表格头、列表项拆得七零八落。更常用的做法是递归切分按段落、句子、标点的优先级逐层拆解尽量保住语义完整的自然边界。比如LangChain里的RecursiveCharacterTextSplitter就是按这个思路设计的。我在处理规章制度、产品手册这类结构化较强的文档时默认会先用标题层级定位章节再在每个章节内部按段落切分这样每个切片基本对应一个完整的知识点。语义切分则是这几年更进阶的方向先做句子级别的Embedding然后根据相邻句子向量之间的距离变化来判定语义边界。遇到表格、代码块、对话记录这类结构复杂的内容语义切分的体验确实更好但它比较吃算力而且本身就是用Embedding做的等于先跑一遍模型再切分对大批量文档来说成本不低。下面这张表是我的经验总结直接放在这里供选型参考切分方式优点缺点适用场景固定窗口切分简单、稳定、可控容易切断语义完整段落日志类、无明显结构的流水文本递归/结构切分尽量保持段落完整依赖文档结构质量规章制度、产品文档、技术手册语义切分边界贴合语义计算成本高、参数调起来麻烦无固定结构的长文本、聊天记录、报告混合切分灵活性和稳定性兼顾实现复杂内容类型多、结构差异大的企业知识库我个人最推荐的组合是“标题层级定位 段落边界切分”遇到正文里的大段原文再叠加固定窗口作为兜底。这套组合写起来不复杂效果却比单用任何一种都稳。3.3 清洗环节最容易翻车的两处清洗这件事做得好的团队很少炫耀做不好的团队天天被检索结果坑。第一处容易翻车的是页眉页脚和导航信息。PDF导出的文档几乎都带页码、页眉、版权声明这些碎语文档会混进切片里形成高频噪声。我踩过的坑是员工问“这个版本是第几版”系统却召回了一段页脚里的版权年份差点被当成正确答案。第二处容易翻车的是表格。解析出来的表格如果直接转成纯文本列与列之间的关系会全部丢失。比如一张“费用类型-报销标准-备注”的三列表格转成纯文本后检索“差旅费报销上限”模型很难判断哪一列才是上限。我现在的做法是能保留表格结构的尽量用HTML或Markdown表格形式入向量库不能保留的就按“行标题列标题值”的方式展平让语义关系尽力保留下来。清洗的目标其实很简单不要让噪声污染语义不要让结构丢失语义。这两句话说起来容易做起来全在细节里。4. 把一段文本真正变成Embedding的完整操作4.1 封装一个可复用的Embedding服务选好模型、定好切分策略之后就该写代码了。我习惯把Embedding能力封装成一个独立服务给上游的批量索引和下游的实时查询共用。核心逻辑其实很小重点是接口设计要干净。下面这段代码是我常用的一套精简版本以开源的SentenceTransformer为例# embedding_service.py from functools import lru_cache import numpy as np from sentence_transformers import SentenceTransformer _model None def get_model(model_name: str BAAI/bge-m3): global _model if _model is None: # devicecpu 或 cuda:0根据实际环境调整 _model SentenceTransformer(model_name, devicecuda:0) return _model def embed_texts(texts, batch_size32, max_length1024): model get_model() embeddings model.encode( texts, batch_sizebatch_size, max_lengthmax_length, normalize_embeddingsTrue, show_progress_barFalse ) return np.asarray(embeddings) def embed_query(text: str): return embed_texts([text]).squeeze()注意我在encode时显式传了normalize_embeddingsTrue。这一步很多人会漏掉但它很重要归一化之后向量长度固定为1后续用余弦相似度和内积算出来的结果是一致的存储和比对都会方便很多。接口设计上有一个原则我想强调批量接口和单条接口要分开。批量接口给索引管道用单条接口给在线查询用。不要贪图省事让上游每次只传一条文本进来那会把GPU的吞吐优势完全浪费掉。4.2 批量向量化的工程细节企业知识库做全量向量化通常不是几千条文本而是几十万甚至上百万个切片。这个规模下批量Embedding的工程细节直接决定你是等一天还是等十分钟。首先batch_size需要根据显存来调。bge-m3这类模型batch_size太大直接爆显存太小又跑不满GPU。我的习惯是先从小往大试观察显存占用和耗时曲线找到一个吞吐量不再明显增长的临界点。实测下来32到64是很多中端显卡的安全区间。其次是重试和容错。大批量处理时偶尔个别文本会触发模型报错比如格式异常的字符、超长输入。整个管道不能因为一条脏数据就中断。我通常会给每一批任务包上异常捕获失败的任务单独记录全部跑完以后再统一重试。最后是缓存。同一段文本在重复向量化时如果纯粹靠模型重算既浪费算力又浪费时间。我建议在向量化管道前面加一个基于内容哈希的缓存算一次MD5或SHA256如果库里已有相同哈希对应的向量直接复用。文档更新时只有内容变化的切片才会重新计算这一招能把增量更新的成本压到极低。4.3 验证向量化效果用最笨的方法排查最隐蔽的问题代码写完向量跑出来了别急着建索引。先做一轮效果验证否则你很难知道问题到底出在模型、切分还是清洗环节。我常用的验证方法特别朴素手工准备几组样例一组是语义相同但用词不同的句子对比如“报销流程需要什么材料”和“费用报销要准备哪些文件”一组是语义无关的句子对比如“报销材料”和“MySQL索引优化”。然后用模型计算它们的余弦相似度看第一组分数是不是明显高于第二组。如果这个基本判断都不成立说明模型、切分或者归一化某个环节出了问题。还有一类隐蔽问题要靠统计发现计算一批相似文本内部的两两相似度如果发现几乎所有的分数都高得离谱可能是文本长度差异过大导致语义被稀释如果相邻切片之间的向量重合度过高说明切分重叠太多检索时会出现大量重复召回。我后来在项目里把这三步做成了一个固定的自检脚本每次换模型、改切分参数都要先跑一遍。数据不会撒谎脚本跑完心里就有底了。5. 向量化之后的事索引、相似度与召回优化5.1 向量库选型对照向量化完成之后下一步是找一个地方把向量存起来并支持快速检索。这年头向量库的选择特别多但我不建议一上来就选最重的方案。先搞清楚自己的数据量、并发量和团队运维能力再决定用哪个。我把常见的几类向量存储方案放在一起做个对照方案核心特点适合场景需要注意的事FAISS轻量、高性能纯向量索引库单机、百万级向量、算法验证不负责元数据管理要自己维护文档映射pgvector直接作为PostgreSQL扩展支持SQL过滤中小团队、已有PostgreSQL、几十万到几百万向量上千万级大并发场景性能不如专用向量库Milvus分布式、功能完整、配套生态强大规模生产环境、千万级以上组件多部署运维成本高QdrantRust实现、支持payload过滤、API友好中大规模、过滤条件多的业务场景全功能版为商业服务开源版够用但不含全部特性Elasticsearch 向量检索传统全文检索与向量检索共存已有ES体系、希望混合检索向量能力相对后加性能调优要看版本我做企业项目时的一个经验是起步阶段如果团队已经重度使用PostgreSQL直接上pgvector是最省事的。它让你可以用SQL同时做向量相似度检索和业务字段过滤不需要额外引入一个基础设施。等向量规模涨到让pgvector的查询开始吃力再考虑迁移到专用向量库不迟。5.2 相似度计算方式不要拍脑袋决定向量存进库以后查询时用的相似度算法也需要明确。最常见的三种是余弦相似度、内积和欧氏距离。很多向量库默认提供多种度量方式选哪个很影响结果。余弦相似度度量的是方向的接近程度不敏感于向量的绝对长度所以它对文本语义匹配特别友好。前面提到归一化之后的向量余弦相似度和内积结果是等价的这时候选哪个都行。BGE官方推荐的就是余弦相似度。内积在某些场景下更快因为它少一步归一化分母的计算但它对向量长度很敏感。如果你的模型输出没有强制归一化内积相似度会被长文本向量主导短文本即使语义相关也排不上去。用内积之前一定确认向量的长度是否已经被统一。欧氏距离在归一化向量上其实和余弦相似度是单调对应的实际效果上没有本质差别。但如果没有归一化欧氏距离会严重受向量长度影响。我的建议是除非你已经清楚自己模型的向量特性否则默认用余弦相似度准没错。5.3 召回质量的两层优化混合检索与重排当你的向量索引能跑通基本查询之后大概率会发现一个问题单靠向量召回TopK结果里总会出现那么几条看着相关其实不对的噪声。这是正常现象因为纯向量召回看的是语义距离而不是业务相关性。我通常会在向量召回之后加两层优化。第一层是混合检索向量召回Top100同时用BM25关键词召回Top50把两路结果去重合并。这个做法的逻辑是向量擅长语义BM25擅长精确词匹配两路结果取交集或加权合并能同时对冲双方的短板。尤其是那些包含精确型号、编号、人名的查询BM25那一路往往能救回向量召回漏掉的内容。第二层是重排把合并后的候选列表交给一个更精但更慢的rerank模型重新打分。常用的rerank模型例如BGE的reranker系列属于CrossEncoder结构模型会把“问题-文档”对一起输入得到一个相关性得分。它比Embedding模型也就是BiEncoder慢得多不能用来做首路召回但在最后精排阶段只处理一两百条候选完全在可接受范围内。我实际项目里的链路是向量Top100 BM25 Top50去重合并后rerank再取前五到十条交给LLM生成答案。效果比单纯向量召回有明显提升尤其在FAQ类、条款类问题上最终答案的准确率有很大改善。6. 面向多模态数据的SigLIP2向量化实践6.1 当企业知识库里出现“一张截图”前面聊的所有向量化都默认输入是纯文本。但企业级问答系统发展到今天知识库里早已不只有文本产品文档里的UI截图、系统报错弹窗、流程图、数据报表、故障现场照片这些图像信息承载了大量关键知识却很难被传统文本Embedding处理。最常见的做法是OCR把图片里的文字提取出来再走文本检索。这个方法能解决一部分问题但损失很严重。比如一张报错弹窗截图除了报错文案还有按钮位置、红色提示框的视觉重点、错误码旁边的图标这些东西在经过OCR变成纯文本后就全丢了。用户拿着另一张相似的截图来问“这个报错怎么解决”纯文本检索很难把两张截图联系起来。我意识到这个痛点之后开始系统性地关注图像向量化。也就是说能不能让图片本身也变成一个向量和文本向量放在同一个语义空间里做相似度匹配。答案是可以而这正是SigLIP2这类多模态模型在做的事情。6.2 SigLIP2是什么优势在哪里SigLIP2是Google在2025年初开源的新一代视觉语言模型它在SigLIP的基础上做了大量改进。如果不想深入研究早期版本细节你可以简单把它理解成一款能同时把图片和文本映射到统一语义空间的模型用图像向量和文本向量互相检索也能直接进行。它有几个让我觉得特别适合企业场景的特点一是采用了Sigmoid损失函数替代了常规的Softmax交叉熵训练效率更高在各类图像嵌入任务上表现更稳定二是模型发布时提供了多种规格从中等规模的base到更大的large适配不同的硬件条件三是在零样本分类、跨模态检索、图文匹配这类任务上效果提升明显。对我而言最大的吸引力在于它给了“截图问答”一条真正可行的路径。如果一条日志文档里附带了一张故障示意图我们既可以把这张图编码成图像向量也可以把说明文字编码成文本向量两者都进入同一个知识库查询时用户上传一张截图同样转成图像向量就能去配对该图对应的文档。加载SigLIP2模型的过程也不复杂下面是一段基于transformers的示例具体参数请以当前官方文档为准from transformers import AutoProcessor, Siglip2Model import torch checkpoint google/siglip2-base-patch16-224 model Siglip2Model.from_pretrained(checkpoint) processor AutoProcessor.from_pretrained(checkpoint) # 图像向量化 image processor(imagesimage_input, return_tensorspt) with torch.no_grad(): image_features model.get_image_features(**image) # 文本向量化 text processor(text[这个报错的解决方案], return_tensorspt) with torch.no_grad(): text_features model.get_text_features(**text)实际部署时我会把它单独封装成一个多模态Embedding服务和纯文本模型互补使用。不需要为每一张图都跑全文OCR保留图片原始信息检索时直接做图像相似度效果远好于只靠OCR文本。6.3 图文联合检索的场景落地把SigLIP2真正放进企业问答系统里我做了三类典型应用。第一类是“截图找解决方案”。用户遇到报错上传截图系统把截图转成图像向量去知识库里检索与之最相似的故障截图以及对应的处理文档。这类场景对AI客服、运维支持类系统非常实用用户体验比一句句描述故障现象强太多。第二类是“按图召回产品说明”。产品文档往往有大量参数表格、拆解图、示意图员工提问虽然用的是文字但匹配到的文档片段如果同时带有图像最终答案的完整性会大幅提升。做法是先对文档中的图片做图像向量索引文本查询经过文本Embedding后通过文档ID建立图文间的关联把检索结果里的图片一并返回给用户。第三类是“图文混合内容的统一检索”。当用户上传的查询本身就包含图片和文字时分别做图像向量和文本向量检索再把两路结果融合打分。这要求系统对两路相似度分数做归一化和加权避免某一路分数整体偏高把结果带偏。有一点我要提醒图文检索虽然方便但“图相似”不等于“业务语义相似”。一张UI界面截图也许和其他界面长得像但实际对应的功能模块完全不同。所以在图文联合检索的应用里我始终会保留一个文本上下文兜底图像负责锁定视觉相关的内容文本负责梳理真正的前因后果两者结合才能得到靠谱的答案。7. 这一章结束前最想分享的工程教训7.1 评测集远比模型本身重要做向量化实战最容易犯的错误就是天天折腾模型、调参、换库却从来没建立一套自己的评测数据集。没有评测集你根本说不清改动到底是变好了还是变坏了。我在项目启动的初期就建议团队整理一批“问题-应命中文档”配对大概50到100组就够了覆盖高频业务问题、常见口语化问法、模糊指代、精确编号查询等典型类型。每次改动Embedding模型、切分参数、召回策略都用这套评测集跑一遍记录命中率和TopK准确率。只有数据支撑的优化才是可持续的优化。7.2 向量化不是一次离线任务而是持续管道很多团队把知识库向量化当成一次性工程项目做完全量就不再管了。但企业的文档每天都在更新产品手册在改版流程制度在修订这意味着向量索引必须支持增量更新。我的做法是给每个文档切片维护内容哈希文档库更新时对比新旧哈希只对变化部分重新向量化同时清理掉旧的向量记录。再配合定期全量重跑的兜底任务保证索引不会因为长期增量产生碎片化。这套机制一开始就设计好后面能省掉大量手工干预。7.3 不要被榜单和参数绑架说到底Embedding与向量化只是整个问答系统的一个环节它不是越“高级”就一定越好。榜单分数上差一两个点的模型在真实业务数据上可能完全拉不开差距堆了好几个向量库和复杂检索策略也可能不如把文档切分清洗做得扎实一点。我自己的体会是先把一条最小链路跑通再在评测集的指引下迭代优化远比一开始就奔着最强模型、最重架构去要实在。向量化的本质是把“找词”升级成“找意思”但前提是你的数据切得好、模型选得对、召回路径够稳。掌握了这个核心逻辑后面那些越来越花哨的工具和技巧其实都是在这个地基上长出来的枝叶。
返回列表