给论坛用上了文本嵌入模型
本文来自依云's Blog,转载请注明。 偶然间发现Discourse论坛支持利用文本嵌入模型来生成「相关话题」列表、提供语义化搜索。于是我给Arch Linux中文论坛试过了好几个模型,记录一下经验。 文本嵌入,英文叫「text embedding」,指的是将一段文本编码成语义空间中的向量,从而可以判断不同文本的语义相关性。编码出来的向量少则512维,多的能有4096维。而判断相关性有「余弦距离」(看两个向量的夹角大小)和「负内积」(一个向量和另一个向量的转置相乘,然后取负)两种方法,我都是看模型文档和示例来决定用哪个的。至于这些向量的存储和索引,Discourse使用的是pgvector这个PostgreSQL插件。 Discourse启用这个功能之后,会在每个话题下方推荐几个「相关话题」,很适合看看是不是有人问过相同的问题。语义化搜索则需要在搜索页面点按钮来显示。在搜索框里按两下回车,就能到搜索页面了(这时候语义化搜索就会进行了,虽然用户还看不到结果),或者点搜索框右边的按钮也行。 因为论坛以中文为主,所以没多少可以抄Discourse官方文档的地方。一开始我挑了好几个来尝试,bge-m3、all-mpnet-base-v2、gte-multilingual-base等。但是没想到它们体积不大,但跑起来却很吃资源。E5-2678 v3辛辛苦苦跑了好久,结果去数据库里一看,已索引的话题数量才几个、十几个,而且不见涨……后来写了API转换代理我才知道,原来是因为Discourse会批量并发请求,并发度会高达45左右,于是很容易导致本来就慢的请求因为排队太久而超时被放弃,CPU都白算了。 最终我找到gte-base-zh这个模型,是针对中文特化的。很小,才0.1B,但这CPU跑得动它。效果也还能接受。 后来了解到最近新出的Qwen3-Embedding系列,看评分效果是最好的。又有群友愿意提供显卡算力,于是试了试。 Qwen3-Embedding提供8B、4B、0.6B三种参数规模的模型。8B很重,我的6650XT的8G显存勉强能放下它的Q4_K_M量化版本。0.6B的只有Q8_0的量化版本,我的显卡跑起来轻松不少,就是不知道为什么它占了我4G+的显存,导致剩下的显存不够原神用了。另外运行的时候如果不用systemd的CPUWeight之类的手段降一下CPU优先级,会导致我的桌面也很卡——我没找到调整GPU优先级的方法,不过调整CPU优先级也管用。 这些模型在群友提供的RYZEN AI MAX+ 395上跑得就比较惨。这台设备有算力不错的核显——至少比用Linux的Apple M2 Ultra算得更快一些,也有核显能够使用大量内存的优势,但是!amdgpu驱动会在高负载时崩溃重置!这么久过去了,amdgpu依旧不待见核显啊(不过听说Intel那边新的xe驱动也有不少bug)。不过断断续续跑了几天之后,终于把大部分话题都索引好了。 后来我还是换0.6B模型了,因为群友提供的算力并不稳定,我想要更容易替代的方案。可能Qwen3-Embedding系列模型对我的用途来说实在是太优秀了,以至于不管是0.6B还是8B,我都没发现结果有什么明显的差异。但0.6B对性能的需求低很多,甚至编译机上的7950X3D也能跑——虽然编译机没那么多时间能跑它就是了。 我还尝试过Google家的embeddinggemma-300M模型。它的MTEB评分比gte-base-zh要高,但只比gte-base-zh大一倍。但实际用下来,呃,效果差很多,基本上没啥用,可能分数都得在别的语言上了吧。遂放弃。 目前的论坛文本嵌入算力主要由群友的RYZEN AI MAX+ 395提供。在它不在线的时候,则由另一位群友提供的Apple M2 Ultra编译机兼职。哪天要是它也有事不在了,还能由x86编译机接棒。在历史话题索引完毕之后,平时的请求其实挺少的。 哦对了,最近还接触过一个叫all-MiniLM-L6-v2的模型,超级小,只有22.7M参数,是火狐新加的地址栏语义化搜索用的。但是它只支持英文,对于中文来说纯粹在增加噪音,可以在about:config里搜索places.semanticHistory.featureGate关闭之。 最后说说运行这些模型的方式。对于给sentence-transformers用的模型,可以用ghcr.io/huggingface/text-embeddings-inference:cpu-latest这个容器来运行。缺点是,它只有支持CPU和CUDA的版本。所以我更喜欢找gguf格式的模型,然后用llama.cpp来运行,可以使用Vulkan或者ROCm。不过我测试发现llama.cpp用ROCm还不如用Vulkan的来得快,而ROCm有着极其巨大的依赖库群,我就不用它了。要是乐意用ROCm的话,也可以用ollama来跑,支持动态加载和卸载模型——但这对于长期运行的服务型用途来说并不是很适合,我还得传个参数让它不要一直加载卸载。