DEV 社区分享如何用 Google Gemini Embeddings API 优化信息流推荐。展示了 embeddings 在产品推荐中的实战应用。
基于 Ruby 的 AI 调用与延迟审计
重大改进即将到来 👋
要让 feed 算法达到恰到好处的平衡,历来都非常困难。如果只针对点击和评论进行优化,最终得到的会是一个充斥着标题党的信息茧房。但如果只是按时间倒序排列,它又会变成一股信息洪流,精彩的讨论几个小时后就消失不见。
长期以来,DEV 一直在努力解决这种矛盾。我们希望 feed 充满活力,同时又能真正呈现高质量、能够激发思考的内容。
因此,我们正在尝试一种新方案:将标准的社区信号——例如你关注了谁、对哪些内容做出了 reaction——与 Gemini Embeddings 2 和 pgvector 结合起来。
下面来深入了解一下,我们是如何把这些东西组合到一起的。
我们没有在整个代码库里到处临时拼接 API 调用,而是使用 wrapper class 构建了一套灵活的基础架构,主要围绕 Ai::Base 和 Ai::Embedding 展开。
当某个 service 需要调用 API 时,只需向 client 传入 wrapper: self。这样,Ai::Base 就可以检查调用它的对象,获取其 class name,并检查它的 VERSION。
Ai::Base.new(wrapper: self)
这种模式通过我们的 AiAudit model 提供了一条非常清晰的审计记录。每当我们生成 vector 或分析趋势时,系统都会自动记录所使用的 model、调用方的 class、payload、延迟和 token 数量。
这样一来,调试和成本追踪都变得容易得多,同时不会让我们的核心业务逻辑变得混乱。
我们的主 feed 由 FeedConfig 驱动。它会编译自定义 SQL,为文章计算分数并进行排序。
过去,这完全依赖硬编码的数学规则,例如 tag,以及你是否关注了文章作者。现在,我们引入了一个语义反馈循环。
随着你在平台上不断互动,我们会动态生成一个 interest_embedding,用于表示你真正关心的内容。我们使用 PostgreSQL 的 pgvector extension,将你的兴趣直接注入 SQL 查询:
(
CASE
WHEN articles.semantic_embedding IS NOT NULL
AND articles.published_at >= :published_since
THEN (1 - (articles.semantic_embedding <=> :interest_embedding)) * :semantic_similarity_weight
ELSE 0
END
)
通过使用 1 - (embedding <=> user_interest),我们可以得到一个余弦相似度分数。我们会对这个分数进行缩放,再将其与标准的社交信号(例如你关注了谁)、文章质量和时间衰减混合起来。
这意味着,一篇与你高度相关的文章可以升到 feed 顶部,而一篇来自你喜爱的社区成员、正在全站流行的文章同样可以。关键就在于平衡。
如果你刚接触这个概念,embedding 本质上就是把一段内容——例如文章正文——转换成一长串数字,也就是一个 vector。这些数字会把内容映射到一个“语义空间”中。如果两篇文章讨论的是完全相同的概念,那么即便使用了截然不同的措辞,它们对应的数字在数学上也会非常相似。
我们已经升级了这条 pipeline,开始使用 Google 新发布的 Gemini Embeddings 2 model。
标准的文本 embedding model 只会处理文字。但 Gemini Embeddings 2 会生成规模庞大的 3,072 维 vector,并将所有内容映射到同一个统一的语义空间中。
迁移到 Embeddings 2 最令人兴奋的一点,是它并不局限于文本。它原生支持 multimodal input,也就是文本、代码、图片、音频和视频。
目前,我们正用它来分析 DEV 上的文字文章。但由于底层数学模型会把所有内容映射到完全相同的 vector space,我们的基础设施已经为未来做好了充分准备。随着 DEV 平台不断演进,我们可以轻松地将图片、播客音频或视频文章送入完全相同的数据库架构[。
用户的 interest_embedding 将能够完全根据概念相关性,毫不费力地呈现开源视频教程或技术播客节目,而不需要我们从头重写 feed 逻辑。
Tag 很适合做高层级分类,但会错过那些非常具体、具有时效性的讨论。如果 Ruby 3.4 发布了,搜索 #ruby tag 并不能区分一篇“Hello World”教程和一场关于新 parser 的深入辩论。
为了解决这个问题,我们正在构建一个由 TrendDetector 驱动的 clustering service。
每隔 6 小时,一个 background job 会使用纯 Ruby 运行 Leader Clustering 算法:
质量优先:我们只关注近期发布,并且得分至少比首页最低标准高出 15 分的文章。
聚类:我们会测量文章之间的余弦距离。如果一篇文章与现有 cluster 足够接近(不超过 0.15),它就会加入该 cluster;否则,它会创建一个新的 cluster。
标注:当一个 cluster 包含 10 篇或更多文章时,我们会请求 Gemini API 为这一趋势生成 label,并总结核心争议。
我们会将所有这些信息存储在 TrendMembership 中,从而可以在 UI 里根据文章与核心主题的接近程度进行排序。
所有这些工作都可以通过我们的开源代码库 Forem 进行追踪:
欢迎来到 Forem 代码库,它是支撑 dev.to 运行的平台。我们非常高兴你的到来。在你的帮助下,我们可以不断改善 Forem 的易用性、可扩展性和稳定性,从而更好地服务我们的社区。
Forem 是一款用于构建社区的开源软件。无论是同事、客户、粉丝群体、家人、朋友,还是任何人们需要汇聚在一起、成为某个集体一部分的时间与空间,都可以使用它来建立社区。你可以查看我们的公告文章,从整体上了解 Forem 是什么。
dev.to(简称 DEV)由 Forem 托管。它是一个软件开发者社区,开发者们在这里撰写文章、参与讨论,并建立自己的职业档案。我们重视相互支持且富有建设性的对话,致力于帮助所有成员写出优秀的代码并实现职业成长。这个生态涵盖了从初学者到高级开发者的所有人,每个人都可以在这里找到属于自己的位置……
无论是来自广大社区,还是来自我们编辑视角的人工策展,依然是整个系统的支柱。
我们正在使用 Gemini Embeddings,放大社区已经在做的事情。核心在于,把 vector search 的原始实用价值,与开发者投票分数及人际关系中蕴含的人文精神结合起来。
我们希望 DEV 能成为互联网上分享代码、讨论软件的最佳场所。我们相信,这是朝着这个方向迈出的一大步。
你怎么看?欢迎在评论区告诉我。
部分评论可能只有登录后的访客才能看到。请登录以查看全部评论。
如果需要采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。