1. 为什么说 Embedding 是问答系统的"地基"
1.1 从一次线上事故说起
去年帮一家客户做企业知识库问答系统,第一版上线后遇到了一个特别尴尬的问题:用户问"咱们公司的年假政策是什么",系统检索出来的全是"考勤制度"里关于迟到扣款的内容。排查了一圈,问题不在查询改写,也不在排序模型,根源出在最基础的向量化环节——我们把员工手册整本书切成固定 512 字的窗口,每个窗口塞进模型做 Embedding,结果一个包含多个主题的段落被揉成一个向量,语义被严重稀释,检索命中率自然惨不忍睹。
这件事给我的教训很直接:Embedding 和向量化是整个智能问答系统的地基。地基没打好,后面接多大的语言模型、调多精细的 prompt,都是在沙子上盖楼。这一章咱们就把这块地基彻底讲透。
所谓 Embedding,简单说就是把一段文字映射成一个固定长度的浮点数数组(向量)。这个向量里蕴含了文字的语义信息,"苹果"和"香蕉"的向量距离近,"苹果"和"汽车"的向量距离远。企业级智能问答系统做召回,本质上就是把你所有的知识文档全部向量化存起来,用户提问时把问题也向量化,然后在向量空间里找最近邻。整个过程听起来简单,但工程落地时模型怎么选、文本怎么切、向量怎么存、相似度怎么算,处处都是坑。
我这篇内容适合三类人看:正在搭 RAG(检索增强生成)问答系统的后端工程师、要做知识库产品的技术负责人,以及被检索效果差折磨得想骂人的算法工程师。我会按自己实际踩坑的顺序,把 Embedding 从选型到落地的完整链路讲清楚。
1.2 Embedding 在企业问答场景里的真实角色
很多人以为 Embedding 就是把文本换个格式存起来,这么想会吃大亏。在企业问答系统里,向量化的质量直接决定了两个关键指标:召回率和准确率。召回率是"该搜出来的有没有搜出来",准确率是"搜出来的东西是不是用户想要的"。我对这个问题的理解是:Embedding 在问答链路里承担的是"第一道筛选"的角色。
完整的问答链路通常是这样:用户提问 → 问题向量化 → 向量检索召回 Top K 片段 → 重排序 → 拼装 Prompt → 大模型生成答案。Embedding 负责的是前两步的核心部分。如果 Embedding 本身做得差,问题向量和文章向量的语义距离计算不准,再强的生成模型也救不回来。
拿上一节那个"年假政策"的例子来说,当时用的是通用领域训练的中文 Embedding 模型,它不太分得清"年假""请假""休假"这三个概念在企业制度语境下的细微差别。换成针对企业制度文本微调过的模型之后,同样一个问题,召回结果里第一条就是人力资源制度里关于年假的段落。这就是 Embedding 选型带来的肉眼可见的差距。
所以这一章的核心目的很明确:让你知道 Embedding 在企业级问答系统里到底扮演什么角色,以及如何针对自己的业务场景把向量化做到最优。下面的内容全部来自我在真实项目中的操作记录,不是教科书上的理论堆砌。
2. 向量化模型选型:别光看榜单,得看场景
2.1 榜单分数和业务效果之间差着什么
国内外的 Embedding 模型排行榜(比如 MTEB、C-MTEB)上,头部的几个模型分数咬得很紧,0.5 分的差距都能决定一个名次。但我在实际项目里吃过亏:某个在英文榜上名列前茅的模型,拿到中文法律文书场景上一测,效果被一个便宜得多的中文专项模型吊打。
这里面的逻辑其实不复杂。Embedding 模型本质上是语言模型加了一个句向量表征头,模型在预训练阶段见过什么数据,它就擅长处理什么数据。MTEB 榜单上的评测集大多是维基百科、新闻、Reddit 这类通用文本,跟你企业内部的 HR 制度、产品手册、售后工单完全是两个世界。
所以我的建议很简单:榜单可以参考,但千万不要直接照抄。正确的姿势是准备一份你自己业务的评测集,至少包含三五十个典型的用户问题以及对应的标准答案片段,然后拿候选模型在你的数据上跑一遍召回,用命中率说话。
2.2 几个实际可用的模型方案对比
目前开源的、闭源的、国内的、国外的 Embedding 模型加在一起少说有几十个,但真正可以用在企业项目里的,我梳理下来就四类方向。
第一类是国产开源模型,代表性的有 BGE(BAAI General Embedding)系列,尤其是 BGE-M3,支持中英文和多语言,而且同时支持稀疏检索和稠密检索,做企业知识库很实用。BGE 系列在 C-MTEB 中文榜上常年占据靠前位置,上下文长度也做到了比较实用的水平。
第二类是闭源 API 模型,比如 OpenAI 的 text-embedding-3-small 和 text-embedding-3-large。这类模型的优势是省心,不用自己部署,质量稳定,文本长度上限高。但劣势也很明显:数据要发给第三方服务,对于金融、医疗这类对数据合规要求极高的行业,这条路基本走不通。
第三类是国产商业 API,比如阿里、百度、智谱这些大厂开放的向量化接口。它们的好处是中文理解通常比国外模型更贴近本土语境,合规上也可以走数据中心部署的私有化方案,但价格和质量需要实测评估。
第四类是最近开始热门的多模态向量模型,比如 SIGLIP 2。这类模型不仅能把文字向量化,还能把图片、表格截图一并向量化。我在处理包含大量产品截图和图表的企业文档时测过这类模型,效果确实让人惊喜——你直接提问"这张架构图里哪些服务是核心依赖",它能通过图像向量和文本向量的对齐做到跨模态召回。虽然多模态向量在传统的文本问答里有点杀鸡用牛刀,但在文档类型丰富的企业知识库里,这个方向值得提前关注。
2.3 选型时一定要问自己的四个问题
结合这些年的经验,我把 Embedding 模型选型拆成四个问题,你答完基本就有答案了。
第一,数据能不能出域?能出域就随便挑头部模型,不能出域就得选开源的本地部署模型,或者支持私有化部署的商业方案。第二,中文为主还是中英混合?如果中文为主,BGE-M3 这类中文优化过的模型通常比通用英文模型更合适。第三,文档里有没有大量图片表格?有的话建议测试一下 SIGLIP 2 这类多模态向量模型,或者采用"文本向量 + 图片向量"双通道的方案。第四,请求量多大?一天几万次调用用 API 没压力,一天几百万次就要认真算账了,本地部署开源模型通常是更经济的做法。
提示:选型阶段千万别省评测这一步。我见过太多团队把大量精力花在搭建系统上,最后因为 Embedding 模型不合适而整体返工。花一天时间做好评测集,远比上线后痛苦排查要划算得多。
3. 文本切分与向量化流水线的落地细节
3.1 切分策略:固定窗口还是语义切分
Embedding 模型通常有一个最大输入长度限制,比如 512 个 token 或者 8192 个 token。企业文档几乎没有那么短的段落,所以切分是绕不开的步骤。切分策略直接影响向量质量,这是我在实际项目里反复验证过的结论。
最粗暴的方式是固定长度切分,比如每 512 个字符切一段,相邻重叠 50 个字符。这种方案实现简单、性能稳定,但问题很大:一个逻辑完整的段落可能被拦腰切断,向量语义被破坏。我在 1.1 节里提到的"年假政策"事故,就是这种切分方式带来的恶果。
更好的方式是按语义边界切分。最理想是直接用文档本身的标题结构来切,比如 Markdown 的二级标题、Word 文档的一级标题,标题下的正文作为一个整体段落。这个方法看着笨,但效果出奇好,因为作者写文档时已经帮你把语义边界划好了。我在处理客户的人力资源制度时,就是用标题层级把全文切成几百个语义完整的小节,每一节向量化后做召回,准确率比固定窗口提升了三成以上。
没有标题结构的文档怎么办?可以结合句号、空行、列表符这些天然的语义断点来做滑动窗口切分。逻辑是:先按段落粗切,段落太长的再用句号进行二次切分,确保每个片段内部主题相对单一。切完之后每个片段最好控制在模型最大输入长度的七成以内,留出处理余量。
3.2 向量化主流程代码实现
这里我直接给出一个经过生产验证的主力代码,用的 BGE-M3 开源模型,通过 HuggingFace 的 sentence-transformers 库加载。这个库封装了加载、推理、归一化一系列操作,是个人开发者和小团队最省事的路径。
from sentence_transformers import SentenceTransformer # 加载模型,会自动从 HuggingFace 下载权重 model = SentenceTransformer("BAAI/bge-m3") # 待向量化的文本片段列表 chunks = [ "年假政策:入职满一年后,每年享有五天带薪年假。", "考勤制度:员工每日需在九点前完成打卡,迟到超过三十分钟视为旷工半天。", "报销流程:报销申请需在费用发生后三十天内提交,附发票扫描件。", ] # encode 方法返回归一化后的向量,normalize_embeddings 参数确保向量模长为 1 embeddings = model.encode( chunks, normalize_embeddings=True, batch_size=16, show_progress_bar=True ) print(embeddings.shape) # (3, 1024),每条文本 1024 维这段代码跑通之后,你要做的是把全量文档按上一节的策略切分好,循环调用encode,把向量和原始文本片段一起存入向量数据库。这里有两个细节值得注意。
第一个细节是batch_size的选择。模型推理是批量跑更快的,batch_size 太小会导致 GPU 利用率不足,太大又可能显存溢出。我一般按显存大小来定:16G 显存跑 BGE-M3,batch_size 设 32 比较稳;CPU 环境则建议降到 8 以下。
第二个细节是normalize_embeddings=True。归一化之后的向量模长为 1,这样计算余弦相似度时可以直接用点积,速度和兼容性都更好。很多向量数据库对归一化向量的点积检索做了专门优化,这一个小参数能省不少检索耗时。
3.3 归一化与维度设计
接着上面的话题展开说。向量维度是很多人容易忽略的选项。BGE-M3 输出 1024 维,OpenAI 的 text-embedding-3-small 输出 1536 维,有的模型还支持降维输出。维度高意味着表达能力更强,但存储开销和检索耗时也会上去。百万级向量时,1024 维的向量库占用的磁盘空间大概是 4GB 左右,升到 1536 维就是 6GB。检索时向量的维度越高,暴力计算的开销越大。
我的建议是:能用低维度解决问题就不用高维度。如果你的场景里文档总量在百万级以下,256 维到 512 维完全够用。我在一个五万篇文档的知识库项目里,把向量从 1536 维降到 512 维,检索耗时的提升立竿见影,准确率几乎没有下降。
维度裁剪之后一定要重新跑一遍评测集,确认效果不降再上线。有些模型声称支持降维输出,内部其实做了 PCA 之类的压缩,压缩后的语义表达能力需要实测验证,不能想当然。
4. 向量检索在问答链路中的实测
4.1 召回和重排的协同
Embedding 向量化只是第一步,向量落到存储之后,检索环节才是决定用户体验的关键。我在实际架构里采用了两级检索:第一级用 Embedding 做召回,第二级用重排模型精排。
为什么需要两级?因为 Embedding 的向量检索本质上是近似最近邻搜索,它在高维空间里找"大致相近"的文本,速度极快,但精度有限。召回阶段我通常取 Top 50 甚至 Top 100,候选集大一些没关系,关键是不要漏。重排阶段用 cross-encoder 模型(把问题和文档片段拼在一起输入模型打分)对候选集逐条精算,取 Top 5 作为最终的上下文送去给大模型生成答案。
这套组合拳的效果我实测过:单独用 Embedding 做 Top 5 召回,准确率大概在 60% 到 70% 之间;加上 cross-encoder 重排之后,同样的数据能上到 85% 以上。原因不复杂,Embedding 是双向编码,把问题和文档各自编码成向量再做余弦相似度,语义交互信息丢了一部分;cross-encoder 直接拼接原文共同建模,交互信息完全保留,精度自然更高。
但要注意,重排是逐条计算的,成本比召回高一个数量级。所以流程必须是:向量检索快筛 → 重排精算。千万别反过来用重排做全量筛选,那样延迟会让人崩溃。
4.2 实际检索效果评测方法
我强烈建议你做一套自己的评测基准,不要每次凭感觉说"效果好像还不错"。方法不复杂:准备 50 到 100 对(问题,标准答案片段)的测试集,然后跑一遍完整链路,计算两个指标——Recall@5(答案片段是否出现在前 5 条结果中)和 MRR(标准答案在结果列表中的平均倒数排名)。
以我自己的项目数据为例,刚开始用通用模型,Recall@5 只有 0.62,换用针对业务语料微调后的模型,直接升到 0.81。这个提升幅度说明 Embedding 模型对业务的适配程度,对最终效果的影响是决定性的。
评测脚本也很简单,核心逻辑就是:把标准答案片段的向量和问题向量的相似度排名里找到标准答案的位置。我在项目里把这个脚本固化成了 CI 的一部分,每次换模型、调切分参数都自动跑一遍,效果只升不降才允许合并代码。
5. 常见问题与排查技巧实录
5.1 相似度普遍偏高或者偏低
我见过不少新手跑来问:为什么我的系统里随便两个文本的余弦相似度都在 0.9 以上?这通常不是模型坏了,而是归一化维度上的问题。有些模型输出的向量没有做归一化,模长可能很长,导致直接算余弦相似度时所有值都挤压在高分区间。解决办法是统一做 L2 归一化,让向量落在单位球面上。
反过来,如果相似度普遍偏低(比如都在 0.3 以下),那大概率是文本切分粒度太碎,每个片段信息量太少,向量之间拉不开相关性。这时候优先调切分策略,让每个片段包含足够的上下文,而不是急着换模型。
5.2 相似的问题召回结果不对
有一种很典型的场景:用户问"笔记本能连公司的 Wi-Fi 吗",系统召回的是"办公用品申领流程",因为"笔记本"这个词匹配上"申领笔记本"了。这是字面匹配和语义匹配冲突的典型例子。
这类问题的根源在于 Embedding 模型对一词多义和上下文消歧的能力不足。我的排查思路是:把用户问题和召回的片段放到评测集里,用可视化工具看它们的向量分布。如果发现模型在某个业务主题上明显混淆,那就有两个选择:一是换更强的模型;二是做问题改写,在向量化之前先用大语言模型把用户问题改写成更完整的表述,比如把"笔记本"改写成"笔记本电脑设备",这能显著降低歧义。
5.3 批量向量化接口频繁超时或限流
用商业 API 做向量化的团队经常碰到这个问题:一次性提交几千条文本,接口直接超时;或者请求太密集,触发限流。我的经验是两条:一是做好分批和重试机制,每批不要超过 100 条,遇到限流就指数退避重试;二是给本地做一个文件级缓存,每条文本算过向量之后用哈希值作为 key 存下来,下次再遇到相同文本直接命中缓存,不用重复请求。这个缓存机制在文档更新不频繁的企业场景里能省下大量 API 费用。
5.4 向量数据库选型的取舍
向量数据库的选择是另一个容易纠结的问题。我在项目中实际用过的有三类:FAISS(开源库,适合单机、百万级以内、对部署要求低)、Milvus(分布式、适合千万级以上、支持多种索引)、pgvector(在现有 PostgreSQL 上直接支持向量检索,适合不想引入新组件的团队)。
我的建议是:如果团队已经重度使用 PostgreSQL,优先试 pgvector,少一个组件就少一堆运维负担;如果向量规模很大且查询并发高,Milvus 更合适;如果只是个人项目或者快速验证原型,FAISS 的嵌入方式最简单。索引类型别迷信 HNSW 就一定是最优,数据量小的时候暴力扫描(Flat 索引)的性能一点都不差,而且 100% 准确。
最后分享一个我踩过坑之后才养成的习惯:向量化流水线一定要做成可以重复回放的任务。文档会更新、会删除,向量库里的旧数据要及时同步清理;模型换版本之后,全量数据要重新向量化,新旧向量混在一起会让召回结果混乱不堪。我在项目里用版本号记录了每次向量化的模型标识和数据快照,每次回放都生成新索引,验证无误后一键切换流量。这个习惯看似笨拙,但在企业系统里能避免非常多诡异问题。