
Teiva Harsanyi 构建 Barista:用 Postgres 和 embeddings 每日筛选并邮件推送技术文章
来源标题:Building Barista: Postgres, Embeddings, and Jev
Teiva Harsanyi 开发了 Barista,用户用自然语言描述阅读偏好,系统每天从数百个工程博客、Hacker News、Lobsters 和 arXiv 中筛选文章并邮件推送。该系统完全基于单个 Postgres 实现队列、调度器和向量数据库,不使用消息中间件,并用 embeddings 表达语义、由决策模型判断内容价值。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Teiva Harsanyi 2026年11月4日 9 1 分享 ☕ 欢迎来到 The Coder Cafe!几周前,我构建了 Barista:用你自己的话描述你喜欢读什么,它每天会从数百个工程博客、Hacker News、Lobsters 和 arXiv 中挑选几篇值得你花时间的文章,通过邮件发给你。这是一个系列的第一部分,讲述它是如何工作的:这不是教程,而是我在生产环境中构建并运行的真实系统背后的决策。本周,我们探讨它的架构。你将看到 embedding 如何把语义变成几何,决策模型如何判断什么才真正值得你花时间,以及单个 Postgres 如何同时充当队列、调度器和向量数据库,而完全不需要消息中间件。找个舒服的位置,端起咖啡,我们开始吧!
问题 十多年前,我刚成为软件工程师时,每天的日常是刷 Twitter,把关注的人从前一天以来发的每一条帖子都读一遍。我也能坚持读完我那个小型 RSS 订阅源里的每一篇文章。 那时我对自己的技术资讯获取方式很满意,而且很有规律。 快进到今天:信息量已经彻底爆炸。Twitter 现在默认是「For you」信息流,永远刷不完。与此同时,我的 RSS 阅读器目前显示有 1,457 篇未读。信息实在太多,而且无时无刻不在涌来。 被无尽的技术内容洪流压得喘不过气? 在 The Coder Cafe,我们每天配着你的咖啡,为你奉上经久不衰的概念。 后来情况变了(👶),我再也跟不上每天一篇的节奏,于是换成了更可持续的更新频率。但这样做,我丢掉了最初的初衷:帮助人们读得更少,同时仍然读到高质量的内容。 我一度放弃了这个想法,直到……最近。 不知道你是否和我一样,但对我来说这不管用。如果我不想只写「软件工程」,而是想更具体地说我喜欢某种语言呢?或者我喜欢关于测试的文章呢?根本没有办法做到这一点。 我一直在寻找,却始终没找到我想要的东西。于是我决定自己动手构建。 我最初是为自己构建 Barista 的。最初的想法很简单:让它从网上抓取大量内容。然后我只需给它一段描述我兴趣的提示词,用我自己的话。例如: 我是一名资深 SRE 和 Go 工程师。给我看深入的、技术性的内容: 编程:编程概念、单元测试等。 Go:性能、profiling、延迟、运行时内部机制 系统基础:Linux 内部机制(进程、内存、I/O)、数据库、Postgres 等。 Kubernetes 内部机制:controllers/operators、reconciliation、kube-apiserver、边缘及大规模集群滚动发布 可靠性与 SRE:安全发布、中断管理、弹性、事故分析等。 偏好:第一性原理解释、架构文章、有数据的基准测试、来自生产环境的经验教训。 跳过:入门教程、清单式文章、框架炒作、营销文章、泛泛的 AI 新闻。 在此基础上,系统会过滤掉我兴趣之外的一切内容,以及垃圾信息、低质量内容等。然后,它每天给我发一封邮件,包含几篇文章。 用了一段时间后,我想:既然我这么喜欢它,为什么不把它做成产品,看看别人是否也喜欢呢? 在本文第一部分中,我们来看看它在幕后是如何工作的,以及我一路走来做出的主要决策。 从高层来看,Barista 被构建为一条 pipeline: 它分为两半,两半的工作方式非常不同。 左半部分与文章有关。Ingest、fetch、classify 和 embed 全天候运行,每篇文章都会恰好经过它们一次,无论最终有谁阅读它。一篇文章只被下载一次、分类一次、embed 一次,无论它最终进入零个收件箱还是一百个收件箱。每个阶段通过队列把文章交给下一个阶段。 右半部分与用户有关。Match、rank 和 send 对每个用户每天运行一次,这里没有队列:驱动它们的是时钟。每个用户选择一个发送时间,系统从该时间倒推。Match 提前两小时运行,rank 提前一小时运行,send 准时运行。 Ingest:轮询各个来源,记录每篇新文章的 URL。已经见过的 URL 会被跳过,因此每篇文章只会进入 Barista 一次。 Fetch:下载每篇文章的页面,提取其标题、作者、日期和正文。如果页面无法读取、不是英文或太短,文章就会被丢弃。它还会遵守 robots.txt,并且对同一站点的访问频率不会超过每几秒一次。 Classify:让 LLM 判断这篇文章是什么:它的主题、深度和质量如何,以及它是垃圾信息还是付费墙的预告。任何偏离主题、垃圾或低质量的内容都会在这一步被丢弃。 Embed:把文章转换成一个向量,一个捕捉其内容的数字列表(详见下文)。从这里开始,这篇文章成为每个用户的候选。 Match:在用户发送时间前两小时,取出用户的兴趣(保存时已被嵌入到同一空间),找出向量最接近的近期文章。最接近的那些文章成为该用户的待选池。 Rank:在发送时间前一小时,LLM 根据用户近期的投票,为待选池中的每篇新文章评分,评估它与用户兴趣的契合度。评分最高的文章被选为当天的推荐。 Send:在用户选定的时间,发送包含当天精选的邮件,每篇都带有「More like this」和「Less like this」按钮。这些反馈会回流到未来的 rank 运行中。 那么这些阶段之间如何通信?通过 Postgres。 当我们听到「由多个阶段组成的 pipeline」时,本能反应可能是找一个消息中间件:Kafka、RabbitMQ,或者 Redis streams。这也是我的本能反应。然而,与我的直觉相反,Barista 没有使用任何消息中间件:图中的每一个箭头都是 Postgres。 队列就是表。在按文章处理的这一半中,每个阶段消费上一阶段写入的表。Ingest 把新 URL 写入 events_ingested,fetch 消费它并写入 events_fetched,以此类推。多个 worker 并行处理一个阶段,每个 worker 这样领取它的下一行: SELECT ... FOR UPDATE SKIP LOCKED
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。