用AI查找有趣的 Hackaday 帖子

大多数人都认为每篇 Hackaday 文章都很有趣,我确实喜欢偶尔浏览,但有时我会变得很忙,错过一些真正很酷的东西。我甚至错过了关于我自己项目的文章。

我花了一个周末的时间编写了一个工具,它可以:

  • 每天下载 Hackaday RSS 订阅源
  • 将 RSS 订阅源中的文章存储到本地数据库中
  • 使用 AI LLM 根据我会喜欢的程度为它们分配分数
  • 当发现新的有趣文章时向我发送电子邮件
  • 有一个 Web UI,以便我可以一目了然地了解它在做什么

所有代码都在线提供,任何想尝试的人都可以使用,但对外部服务有一些先决条件/要求。

1、先决条件/要求

虽然软件本身是一个独立的 Golang 二进制文件,但它依赖于两个外部服务。

  • 一个符合 OpenAI API 标准的提供商。我使用 Poe,因为工作中有人向我推荐它作为与许多不同模型交互的一种方式。我试过直接使用 Google Gemini,效果很好。它应该适用于任何兼容的服务。
  • 一个电子邮件发送服务,如果您希望它能够发送电子邮件。我使用一个名为 smtp2go 的服务。我发现让 gmail 做这件事真的很挑剔,我永远无法信任我的 prometheus 警报会被发送出去而不会违反某些垃圾邮件过滤规则。这也适用于这种情况。

从技术上讲,您不需要电子邮件服务,因为程序可以生成本地 html 报告,并且可以通过 http 提供结果。但是,您绝对需要 AI 服务,否则它将无法为文章评分。

2、评分和提示工程

AI 评分需要一个提示词。我的提示词是:

Scott 喜欢与复古计算机和语音合成器相关的项目。
特别是他喜欢 4004、8008、8080、8085、8086、z80 和 z8000 CPU。
他喜欢像辉光管这样的独特显示技术。
如果树莓派和微控制器项目有一些独特或复古的元素,他也会喜欢。
他喜欢修复旧的和稀有的计算机。
根据 Scott 会喜欢这个项目的程度产生 0 到 100 之间的数字分数,并提供三个关于他会喜欢什么的要点。
如果文章是关于 Scott Baker 或 smbaker 的,给它打 100 分。
标题: {{.Title}}
描述: {{.Description}}
内容: {{.Content}}

请注意,我添加了换行符以在博客文章中格式化得很好;实际上,在底部的模板化段之间只有换行符。我不是专业的提示词工程师,但我试着告诉它做什么,并要求它提供 0 到 100 的分数以及为什么它这样认为的理由。

在提示词的底部有三个模板替换,代码在发送提示词之前执行。这些包括文章标题、文章描述和内容。内容被修剪到 500 字节,因为这应该足以了解一篇文章的感觉。

不同的人当然会使用不同的提示词。对于模型,我选择了 gemini-3-flash。我问了 Claude 的推荐,它建议我切换到 claude-3.5-sonnet,这是我可能会考虑的。评估不同模型给出的评分是我这个项目的目标之一。

3、下载 RSS

这非常容易。在 github.com/mmcdole/gofeed 有一个库可以下载 RSS 文章。请注意,您收到的文章数量由服务决定。我相信 Hackaday 发送大约一天的文章数量。

您从 RSS 收到的响应可能充满了 HTML 标签。我使用了 github.com/microcosm-cc/bluemonday 中的一个库,通过删除 HTML 标签来清理结果。这使得使用起来更容易,而且我们真的不想将所有那些 HTML 发送到 LLM。

4、在数据库中存储文章

起初我打算使用 Ent,这是我在生产软件中通常使用的 Go ORM 层。但是,我评估了替代方案,并提议使用 sqlite 的简化解决方案。我们只需要处理一个数据库表,将数据库存储在本地文件中对于开发和部署都很方便。如果这是生产软件,我可能会启动 postgres 并使用 Ent,但它不是。

数据库的主要原因是建立历史记录,因为 RSS 返回的项目数量是有限的。我希望能够尝试更改提示词或更改模型。

数据库存储以下内容:

  • GUID:文章的唯一标识符
  • 标题:文章标题
  • 链接:文章链接,以便我可以阅读它
  • 描述:文章描述
  • 内容:最多 4KB 的内容
  • 日期:发布时间
  • 分数:从 0 到 100
  • 分析:为什么它与我相关的三个要点
  • Feed_url:这个 RSS 订阅源来自哪里,以防我以后想支持多个订阅源
  • 模型:用于评分的模型
  • 已报告:True|False,文章是否在电子邮件报告中发送给我

5、调用 LLM

使用 github.com/sashabaranov/go-openai 中的 openai 库非常简单。要连接到 LLM,我们必须给它一个其 API 所在的 URL 以及允许我们访问的令牌。然后我们采用我在上面显示的提示词模板,并替换相关的文章细节。通过 openai 库对 LLM 的实际调用看起来像这样:

  resp, err := client.CreateChatCompletion(
    context.Background(),
    openai.ChatCompletionRequest{
     Model: model, // 模型名称,例如 gemini-3-flash
     Messages: []openai.ChatCompletionMessage{
      {
       Role:    openai.ChatMessageRoleUser,
       Content: articlePrompt, // 我们的提示词,基于填入文章详情的模板
      },
     },
    },
  )

  if err != nil {
   log.Printf("  调用 AI 时出错: %v", err)
   continue
  }

  content := resp.Choices[0].Message.Content // 这是 LLM 的响应

之后,通过正则表达式查找单词 "score" 和附近的数字来提取分数。有时 LLM 的响应不包括单词 "score",所以我只查找第一个数字。这里查询多个模型会很方便,只是为了确保模型没有产生损坏的结果并导致分数被误解。

6、生成报告并通过电子邮件发送

报告是从 HTML 模板生成的,只包含以前没有报告过的文章。我选择将模板直接放在代码中以最小化外部依赖 — Go 的单二进制方法确实使部署和使用变得容易。

在 golang 中已经有一个内置的 net/smtp 库可以通过 SMTP 服务器处理发送邮件的重活。正如我在开头提到的,由您自己提供 SMTP 服务器。

除了发送电子邮件,您可以选择将报告写入文件。这对开发很有好处。

7、提供网页服务

起初我认为电子邮件界面就足够了,但能够在 GUI 中可视化浏览数据库也非常不错。这是通过典型的 golang net/http 包完成的,使用 http.ListenAndServe()。

我利用 Google Antigravity 为我设计网页,因为我的 javascript 日子已经过去了,而且我的 CSS 和 HTML 都有点生疏。我对它做的工作质量感到惊讶。

8、让我们做一个演示...

首先,fetch 命令可用于获取一批新的文章。

$ bin/ai-rss-scraper fetch
从 https://hackaday.com/blog/feed/ 获取最新项目
- 新:电动推子为您的 PC 制作了一个很棒的音量混音器
- 新:托马斯·爱迪生可能发现了石墨烯
- 新:便宜的智能戒指变成 MIDI 控制器
- 新:秘密成分
- 新:从命令行播放 YouTube
- 新:通过 Linux 内核内存压缩应对 RAM 价格挤压
- 新:卧虎藏式打字机,隐形 PC
获取完成:收到:7,已存在:0,添加:7

数据库是空的,RSS 订阅源提供了七篇新文章被摄取。接下来,让我们尝试评分。在这里,我们需要设置我们的 API_KEY 来使用我们的 AI 服务,在我的情况下是 Poe。

$ export API_KEY=my_api_key
$ bin/ai-rss-scraper score
发现 7 篇未评分的文章
评分:电动推子为您的 PC 制作了一个很棒的音量混音器
  分数: 35
评分:托马斯·爱迪生可能发现了石墨烯
  分数: 20
评分:便宜的智能戒指变成 MIDI 控制器
  分数: 25
评分:秘密成分
  分数: 12
评分:从命令行播放 YouTube
  分数: 15
评分:通过 Linux 内核内存压缩应对 RAM 价格挤压
  分数: 30
评分:卧虎藏式打字机,隐形 PC
  分数: 55

我们得到了一篇分数高于 50 的文章,尽管我可能需要调整我的提示词,因为那篇"电动推子"的文章实际上非常有趣 — 这是风险之一,评分只与您设计的提示词一样好。接下来,让我们做一个列表,看看数据库是什么样的:

$ bin/ai-rss-scraper list
[ 35] 电动推子为您的 PC 制作了一个很棒的音量混音器 (2026-02-01)
[ 20] 托马斯·爱迪生可能发现了石墨烯 (2026-01-31)
[ 25] 便宜的智能戒指变成 MIDI 控制器 (2026-01-31)
[ 12] 秘密成分 (2026-01-31)
[ 15] 从命令行播放 YouTube (2026-01-31)
[ 30] 通过 Linux 内核内存压缩应对 RAM 价格挤压 (2026-01-31)
[ 55] 卧虎藏式打字机,隐形 PC (2026-01-31)

这证明我们持久化了数据。现在让我们生成一个报告。

$ bin/ai-rss-scraper report --out report.html
报告生成于: ./report.html
处理了 1 篇文章。
2026/01/31 19:26:59 标记 1 篇文章为已报告。

最后,让我们看看它生成的报告。这与通过电子邮件发送的报告相同。

报告已生成

我们可以看到,一篇分数 ≥ 50 的文章被选中,它是关于将打字机重新用作可携带计算机的那篇。LLM 列出了三个关于为什么它认为这文章对我有趣的原因。LLM 的好处是我们不必进行精确的字符串匹配。我们描述我们想要的,LLM 能够匹配它并生成分数。结果中有一些变化。如果您多次重新运行 LLM,您可能会得到另一个分数。例如,我看到同一篇文章为我评分为 55、57 和 60。

如果我们真的想小心,我们会通过多个不同的模型对文章进行评分,然后对它们进行平均,或者选择两个最一致的模型。也有一些可以使用的采样方法 — 使用低成本模型生成粗略结果,然后使用高成本模型有选择地验证结果,以确保低成本模型正确执行。我没有采取任何这些技术的步骤,因为 gemini-3-flash 采样目前似乎足够。

最后,让我展示工具如何发送电子邮件。首先,我有一个配置文件设置,其中包含所有相关的配置设置:

$ cat scott-config.yaml
email_to: <my-email-address>
email_from: <the-email-to-put-in-from>
email_smarthost: <address-of-smtp-server>
email_identity: <email identity>
email_username: <smtp-server-username>
email_password: <smtp-server-password>
email_subject: "hackaday blog articles"

然后我使用 — send-email 选项执行 report 命令:

$ bin/ai-rss-scraper --config scott-config.yaml report --send-email --always
使用配置文件: scott-config.yaml
2026/01/31 19:34:39 通过 <address-of-smtp-server> 向 <my-email-address> 发送电子邮件...
2026/01/31 19:34:40 电子邮件发送成功。
处理了 1 篇文章。
2026/01/31 19:34:41 标记 1 篇文章为已报告。

您会注意到我在上面使用了 — always 选项。这告诉它忽略文章以前是否被报告过,否则我们之前对文件的报告会将文章标记为已报告,而这个命令将不会发现任何要报告的内容。

9、Dockerfiles 和 Helm Charts

在我的家庭实验室中,我运行 Kubernetes 来运行各种服务,包括 Prometheus 和 Grafana。ai-rss-scraper 是我在 Kubernetes 下运行的另一个服务。

在仓库中检入了一个 Dockerfile,可用于构建镜像,以及一个将部署它的 helm chart。

与手动运行工具一样,您必须自定义 values.yaml 来设置 API 密钥和所有电子邮件选项。设置持久卷(hostpath 就可以)也是明智的,这样数据库不会在 pod 重启时丢失。最后,有一些选项可以将内置 Web 服务器连接到 nodeport、负载均衡器和/或 ingress。

作为参考,这就是我在 values.yaml 中放的内容:

config:
  apiKey: <my-api-key>
  email:
    to:  <my-email-address>
    from: <the-email-to-put-in-from>
    smarthost: <address-of-smtp-server>
    identity: <email identity>
    username: <smtp-server-username>
    password: <smtp-server-password>
    subject: "hackaday blog articles"
storage:
  className: ai-rss-storage
  hostPath:
    enabled: true
    path: "/var/ai-rss-scraper"
service:
  type: NodePort
  port: 80
  nodePort: 31305
ingress:
  enabled: true
  hosts: [ai-rss-scraper.lan]

10、结束语

我希望您喜欢这篇关于使用 AI 筛选我的 RSS 订阅源的实际应用的文章。这对我来说有点像是一个练习,是为了尝试一些新技术,包括 Google Antigravity,并看看我是否喜欢它们。


原文链接: Using AI to find interesting Hackaday Posts

汇智网翻译整理,转载请标明出处