构建人工智能驱动的 RSS 提要聚合器

· 11 分钟阅读

作为一名 Microsoft MVP 和技术爱好者,我经常发现自己淹没在 Microsoft 开发博客上发布的令人惊叹的内容的海洋中。从 .NET 公告到 Visual Studio 更新,从 Azure 创新到语义内核深入研究 - Microsoft 生态系统中总是会发生一些令人兴奋的新事物。

问题? 跟上这一切几乎是不可能的。

我想掌握最新公告并与我的网络分享它们,但手动检查七个不同的 RSS 源、阅读文章、制作引人入胜的社交媒体帖子以及跟踪我已经分享的内容本身就成为了一项全职工作。每天早上,我都会打开多个浏览器选项卡,浏览数十篇文章,尝试记住我已经分享过哪些文章,然后花宝贵的时间撰写引起我注意的文章。

所以我做了任何开发人员都会做的事情 – 我将其自动化。

在本综合指南中,我将向您介绍如何构建一个基于 AI 的 RSS 提要聚合器,该聚合器可监视多个 Microsoft DevBlogs RSS 提要的新内容、使用 Azure OpenAI 和 Semantic Kernel 分析文章并生成引人入胜的帖子、为每篇分析的文章创建详细的 Markdown 文档、通过 Telegram 发送通知以便我可以查看和共享内容、跟踪所有内容以避免重复帖子,并通过 GitHub Actions 自动运行。

让我们深入探讨该解决方案的各个方面。

这个项目背后的故事

生活在信息超载中

让我为您描绘一下我在构建这个工具之前的典型早晨。我会醒来,喝杯咖啡,打开笔记本电脑,查看微软开发者生态系统中的新内容。首先,我会导航到主要的 DevBlogs 站点,查看是否有任何重大公告。然后我会专门查看 .NET 博客,因为那是我的主要技术堆栈。之后,我会跳转到语义内核博客,因为人工智能变得越来越重要。 Visual Studio 博客排在第二位,因为 IDE 更新会显着影响我的日常工作流程。然后是有关 CI/CD 和 GitHub 相关新闻的 DevOps 博客,接着是有关云基础设施更新的 All Things Azure 博客,最后是有关数据库创新的 Azure SQL 博客。

有七个不同的提要需要检查。这些博客每周都会发布多篇文章,有时在 .NET Conf 或 Build 等重大公告期间每天会发布多篇文章。这可能需要跟踪、阅读和分享数十篇文章。事情是这样的——作为一个重视与社区分享知识的人,我不想只阅读这些文章。我想在 LinkedIn 上与我的网络分享最有价值的内容,帮助其他开发人员也了解最新情况。但制作一篇好的 LinkedIn 帖子需要时间。你需要彻底阅读这篇文章,理解要点,思考为什么它对你的读者很重要,写一个引人入胜的钩子,并很好地格式化所有内容。将其乘以每周几篇文章,您就会看到工作时间。

我真正想要的是什么

在处理这个问题几个月后,我坐下来思考理想的解决方案是什么样的。首先,我不想再错过重要的公告。系统应在新文章发布后立即自动捕获它们。我还希望通过让人工智能帮助制作引人入胜的帖子来节省内容创建的时间——不是完全取代我的声音,而是给我一个可以定制的坚实的起点。

一致性是另一个重要因素。我想定期分享内容,而不必记住每天手动执行。跟踪方面也至关重要 - 我需要一种方法来了解我已经分享的内容,以避免发布重复内容并惹恼我的关注者。最后,我想对我处理过的所有内容进行永久记录,保持井然有序,这样我就可以回顾并查看我涵盖了哪些主题。

解决方案已成型

我设想的解决方案将使用 GitHub Actions 按计划运行,完全免提。它会自动获取所有七个提要,而无需我打开单个浏览器选项卡。人工智能组件实际上会阅读并理解内容,然后以对我的观众有用的方式进行总结。我不必从头开始撰写帖子,而是创建可随时分享的社交媒体内容,我可以根据需要进行调整。所有内容都会发送到我的 Telegram 进行审核,这样我就可以快速浏览手机并决定分享哪些内容。当然,它会永久记录所有内容以供将来参考。

在我们开始构建之前

您的机器需要什么

要学习本教程,您需要在开发计算机上安装一些东西。最重要的是.NET SDK 9.0 或更高版本。这是我们的运行时,提供了我们需要的所有构建工具。如果您尚未安装,请访问 dot.net 并下载最新版本。在 Windows、macOS 或 Linux 上安装非常简单。

您还需要安装 Git 以进行版本控制。我们将把代码推送到 GitHub 并使用 GitHub Actions 实现自动化,因此在本地设置 Git 至关重要。任何最新版本都可以正常工作。

对于您的开发环境,我推荐 Visual Studio 或 VS Code。就我个人而言,我现在的大部分工作都使用 VS Code,因为它是轻量级的,并且通过 C# Dev Kit 扩展提供了出色的 C# 支持。但如果您更喜欢完整的 Visual Studio,那么它也可以完美工作。

您需要的服务和帐户除了本地工具之外,您还需要具有一些服务的帐户。最重要的是 Azure OpenAI,它为我们的 AI 分析提供支持。这是一种即用即付的服务,但该用例的成本很低——我们所说的是每篇分析文章的成本。如果没有 Azure 帐户,可以注册免费试用版,其中包含一些入门积分。

对于通知,我们将使用 Telegram 机器人。 Telegram 的伟大之处在于他们的机器人 API 完全免费使用。您可以根据需要创建任意数量的机器人并发送无限的消息。我将在本指南的后面部分引导您完成设置过程。

最后,您需要一个 GitHub 帐户来托管代码并运行 GitHub Actions。免费套餐对于这个项目来说已经足够了。 GitHub 在私有存储库上每月为您提供 2,000 分钟的 Actions 运行时间,在公共存储库上提供无限分钟的运行时间。

让这一切成为可能的图书馆

我们的项目依赖于三个主要的 NuGet 包,每个包都有特定的用途。

第一个是 HtmlAgilityPack,它是 .NET 中 HTML 解析的黄金标准。当我们从博客中获取文章时,我们会获取页面的完整 HTML,包括导航菜单、页脚、广告以及各种我们不关心的元素。 HtmlAgilityPack 让我们能够解析该 HTML 并仅提取我们需要的文章内容。

第二个包是Microsoft.SemanticKernel,它是微软用于将AI模型集成到应用程序中的SDK。将其视为 .NET 代码和 GPT-4 等大型语言模型之间的桥梁。它处理 API 调用、令牌管理和响应解析的所有复杂性,让您专注于希望 AI 实际执行的操作。

第三个包是 System.ServiceModel.Syndicate,它提供了对解析 RSS 和 Atom 提要的内置支持。 RSS 可能看起来像旧技术,但它仍然是从博客和新闻网站获取结构化更新的最佳方式。该包将原始 XML 源转换为易于使用的强类型 C# 对象。

理解架构

各部分如何组合在一起

在我们深入研究代码之前,让我解释一下所有组件如何协同工作。了解大局将使实施细节更加清晰。

在最高级别,我们有主 Program.cs 文件作为协调器。这是我们应用程序的入口点,它协调所有其他组件。当应用程序运行时,它首先从环境变量加载配置 - 例如 API 密钥和 Telegram 凭证。然后它会从所有七个 Microsoft DevBlogs 源中获取 RSS 源。在处理这些提要时,它会对文章进行重复数据删除,以处理同一篇文章出现在多个提要中的情况。它会根据我们的跟踪文件检查每篇文章,看看我们是否已经处理了它。对于新文章,它将交给人工智能分析器进行处理。ArticleAnalyzer 类是 AI 魔法发生的地方。该组件接收一篇文章并用它做一些事情。首先,它从文章的 URL 中获取完整的 HTML 内容。然后,它从该 HTML 中提取干净的文本,删除我们不需要的所有导航元素、脚本和样式。一旦获得干净的文本,它就会通过语义内核将其发送到 Azure OpenAI,并带有精心设计的提示。人工智能会分析文章并返回结构化响应,其中包括摘要、关键主题、相关性解释,以及最重要的即用型 LinkedIn 帖子。分析器解析此响应并返回包含所有这些信息的 ArticleAnalysis 对象。

MarkdownGenerator 类采用该 ArticleAnalysis 对象并创建它的永久记录。它生成一个格式良好的 Markdown 文件,其中包括所有文章元数据、人工智能的分析和生成的帖子。这些文件存储在 generated-posts 目录中,为您提供已处理的所有内容的可搜索存档。

最后,Telegram 集成将生成的帖子内容发送到您的手机。这是你作为人类可以审查人工智能的工作并决定是否分享它的时刻。机器人会向您发送一条包含帖子内容的消息,您可以将其直接复制到 LinkedIn 或先进行修改。

数据流

让我向您介绍在 .NET 博客上发布新文章时会发生什么情况。当 GitHub Actions 按计划触发我们的应用程序时(假设每六个小时),工作流程就开始了。应用程序唤醒并开始获取所有七个 RSS 源。每个提要都会返回一个 XML 文档,其中包含该博客的最新文章。

当我们解析每个提要时,我们提取单个文章并将它们存储在一个列表中。但这里有一个棘手的部分 - 主要的开发博客提要通常包含也出现在各个类别提要中的文章。因此,有关“.NET 10”的文章可能会同时出现在主提要和特定于 .NET 的提要中。我们通过跟踪 HashSet 中的 URL 来处理这个问题,这会自动防止重复。

一旦我们获得了重复数据删除的文章列表,我们就会将其过滤为最近的文章——通常是在最后一天左右发布的文章。我们不想处理在之前的运行中已经处理过的旧文章。然后我们根据跟踪文件检查每一篇最近的文章。如果我们已经处理并发布了一篇文章,我们会跳过它。

对于每一篇新文章,我们都会启动人工智能分析流程。分析器获取完整文章 HTML,对其进行清理,然后根据我们的提示将其发送到 GPT-4。人工智能会阅读这篇文章并生成全面的分析以及 LinkedIn 帖子。我们将此分析保存到 Markdown 文件中以供记录。分析完成后,我们格式化消息并通过 Telegram 发送。该消息包括生成的帖子内容以及附加的 URL 和主题标签。在我的手机上,我会收到一条通知,查看该帖子,如果我喜欢它,只需轻按几下即可将其复制并在 LinkedIn 上分享。

最后,我们更新跟踪文件以将本文标记为已处理,因此我们不会在以后的运行中再次处理它。如果创建或修改了任何文件,GitHub Actions 会将这些更改提交回存储库,使所有内容保持同步。

从头开始设置项目

创建解决方案结构

让我们开始构建吧。打开终端并导航到要创建项目的位置。我喜欢将我的项目组织在“开发”文件夹中,但您可以将其放在对您有意义的任何位置。

首先,我们将创建一个新的解决方案文件。在 .NET 中,解决方案是可以容纳多个项目的容器。尽管我们目前只有一个项目,但从解决方案开始可以更轻松地在以后需要时添加更多项目。运行命令 dotnet new sln -n vs-feed-linkedin 创建一个名为 vs-feed-linkedin 的解决方案。

接下来,我们需要创建控制台应用程序项目。我们将把它放在 src 子目录中以保持组织有序。运行 dotnet new console -n VsFeedLinkedin -o src 在 src 文件夹中创建一个名为 VsFeedLinkedin 的控制台项目。然后使用 dotnet sln add src/VsFeedLinkedin.csproj 将此项目添加到我们的解决方案中。

现在使用 cd src 导航到 src 目录。我们将在此处添加 NuGet 包并进行大部分开发。

添加所需的包

创建项目后,我们需要添加我之前提到的三个 NuGet 包。按顺序运行以下每个命令:

dotnet add package System.ServiceModel.Syndication --version 9.0.9
dotnet add package Microsoft.SemanticKernel --version 1.30.0
dotnet add package HtmlAgilityPack --version 1.11.72

运行这些命令后,您的项目文件应如下所示:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
    <PackageReference Include="Microsoft.SemanticKernel" Version="1.30.0" />
    <PackageReference Include="System.ServiceModel.Syndication" Version="9.0.9" />
  </ItemGroup>

</Project>

项目文件告诉 .NET,我们正在构建一个可执行文件(OutputType 是 Exe),目标是 .NET 9.0,并使用现代 C# 功能,例如隐式使用和可为 null 的引用类型。 ItemGroup 部分列出了我们的三个包依赖项及其确切版本。

深入研究 RSS 源

RSS 到底是什么?

在我们开始编写代码来获取提要之前,让我们确保我们了解我们正在使用的内容。 RSS 代表“真正简单的聚合”,它是一种用于分发内容更新的标准化 XML 格式。这个想法很简单:您不需要用户访问您的网站来查看是否有新内容,而是发布一个列出您最近内容的机器可读文件。然后,应用程序可以定期轮询该文件以发现新文章。

RSS 自 20 世纪 90 年代末和 2000 年代初就已出现。您可能认为它是过时的技术,但实际上它仍然被广泛使用——尤其是博客、新闻网站和播客。 RSS 的美妙之处在于它的简单性。它只是具有已定义结构的 XML,任何应用程序都可以解析它。

开发博客提要的结构当您从 Microsoft DevBlogs 获取 RSS 源时,您会得到一个遵循特定结构的 XML 文档。在顶层,有一个 rss 元素,其中包含单个频道元素。该频道代表博客本身,并包含博客标题、URL 和描述等元数据。

在频道内,您会发现多个项目元素,每个元素代表一篇单独的博客文章。每一项都包含标题(文章的标题)、链接(可以在其中阅读全文的 URL)、pubDate(文章发布时间)、dc:creator 元素(作者姓名)、一个或多个类别元素(文章的标签)和说明(通常是文章的摘要或摘录)。

下面是一个简化的示例:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>.NET Blog</title>
    <link>https://devblogs.microsoft.com/dotnet</link>
    <description>The latest news about .NET</description>
    <item>
      <title>Announcing .NET 10</title>
      <link>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10</link>
      <pubDate>Mon, 10 Dec 2025 12:00:00 GMT</pubDate>
      <dc:creator>Microsoft</dc:creator>
      <category>Announcements</category>
      <category>.NET</category>
      <description>Article summary...</description>
    </item>
  </channel>
</rss>

.NET 的 System.ServiceModel.Synmination 包的伟大之处在于它为我们解析了所有这些。我们不必手动导航 XML 节点或担心不同的 RSS 版本。我们只需加载提要并返回强类型对象。

我们监控的七个提要

在我的实现中,我监视七个不同的 Microsoft DevBlogs 源。 devblogs.microsoft.com/feed 上的主要 DevBlogs feed 使我们能够全面了解 Microsoft 在其所有开发人员博客中发布的所有内容。 devblogs.microsoft.com/dotnet/feed 上的 .NET 特定源专门关注 .NET 版本、功能和最佳实践。 devblogs.microsoft.com/semantic-kernel/feed 上的语义内核源涵盖了人工智能编排和集成——随着人工智能成为现代发展的核心,它变得越来越重要。

devblogs.microsoft.com/visualstudio/feed 上的 Visual Studio feed 让我随时了解 IDE 改进和生产力功能的最新信息。 devblogs.microsoft.com/devops/feed 上的 DevOps 源涵盖了 Azure DevOps、GitHub 和 CI/CD 主题。 devblogs.microsoft.com/all-things-azure/feed 上的 All Things Azure 源重点关注云服务和体系结构模式。最后,devblogs.microsoft.com/azure-sql/feed 上的 Azure SQL 源涵盖了数据库创新和功能。

您可能想知道为什么我同时检查主要提要和单个类别提要。主要提要给了我广度——我会看到来自任何 Microsoft 开发人员博客的文章,包括我可能不知道的文章。类别提要给了我深度——它们确保我不会错过我感兴趣的核心领域中的任何重要内容,即使这些文章被更新的内容从主提要中挤出。

构建 RSS 获取逻辑

核心抓取功能

现在让我们编写一些代码。我们应用程序的基础是获取和解析 RSS 提要的能力。这是处理这个问题的函数:

static async Task<SyndicationFeed?> FetchRssFeedAsync(string url)
{
    using var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
    
    var response = await httpClient.GetStringAsync(url);
    
    using var stringReader = new StringReader(response);
    
    var settings = new XmlReaderSettings
    {
        DtdProcessing = DtdProcessing.Parse,
        MaxCharactersFromEntities = 1024
    };
    
    using var xmlReader = XmlReader.Create(stringReader, settings);
    
    return SyndicationFeed.Load(xmlReader);
}

让我来看看这段代码的作用。我们首先创建一个 HttpClient,它是 .NET 用于发出 HTTP 请求的内置类。我们设置 User-Agent 标头是因为某些服务器会阻止无法识别自身身份的请求。即使服务器不需要它,也最好设置它。然后,我们向 feed URL 发出 GET 请求并接收字符串形式的响应。该字符串包含 RSS 源的原始 XML。

为了解析此 XML,我们创建一个 StringReader 来包装我们的响应字符串,然后配置一些 XmlReaderSettings。 DtdProcessing 设置很重要 - RSS 提要有时包含需要处理的 DTD(文档类型定义)声明。 MaxCharactersFromEntities 设置是一种安全措施,通过限制可以发生的实体扩展量来防止 XML 炸弹攻击。

最后,我们使用这些设置创建一个 XmlReader,并使用 SyndicateFeed.Load 将 XML 解析为强类型 SyndicateFeed 对象。这使我们能够通过良好的 C# 属性而不是原始 XML 导航来访问提要的元数据及其所有项目。

通过错误处理获取多个提要

在现实世界中,网络请求会失败。服务器宕机、连接超时、XML 格式可能错误。我们需要优雅地处理这些案件。以下是我们如何获取所有 feed,同时对故障具有弹性:

var allArticles = new List<(SyndicationItem item, string feedUrl)>();
var seenUrls = new HashSet<string>();

foreach (var feedUrl in feedUrls)
{
    try
    {
        Console.WriteLine($"  📡 Fetching {feedUrl}...");
        var feed = await FetchRssFeedAsync(feedUrl);
        
        if (feed?.Items != null && feed.Items.Any())
        {
            foreach (var item in feed.Items)
            {
                var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? "";
                if (!string.IsNullOrEmpty(itemUrl) && seenUrls.Add(itemUrl))
                {
                    allArticles.Add((item, feedUrl));
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  ⚠️  Failed to fetch {feedUrl}: {ex.Message}");
    }
}

我们在这里维护两个集合。 allArticles 列表将包含我们找到的所有文章,以及它们来自哪个提要。 sawUrls HashSet 跟踪我们已经看过的文章 URL,帮助我们避免重复。

我们循环遍历每个 feed URL 并将 fetch 操作包装在 try-catch 块中。如果获取特定提要失败(可能是服务器暂时关闭),我们会记录一条警告并继续处理下一个提要。这样,一个提要出现问题就不会妨碍我们处理其他提要。

对于每个成功获取的提要,我们迭代其项目。我们从项目的 Links 集合中提取文章 URL。如果 URL 已在集合中,则 HashSet.Add 方法将返回 false,这非常适合我们的重复数据删除逻辑。我们仅将新文章添加到我们的列表中。

我们将提要 URL 与每篇文章一起存储,因为此信息稍后可能有用 - 例如,我们可能想知道文章来自哪个特定提要以用于调试或日志记录目的。

处理重复项和跟踪状态

重复数据删除挑战

正如我之前提到的,Microsoft DevBlogs 具有分层提要结构,这带来了有趣的挑战。当 .NET 团队成员发布一篇有关 .NET 10 性能改进的文章时,该文章可能会同时出现在主要 DevBlogs 提要和 .NET 特定提要中。有时,如果它与 IDE 功能相关,它甚至可能出现在 Visual Studio feed 中。

如果我们天真地处理每个提要中的每一篇文章,我们最终会多次分析和发布同一篇文章。这会浪费对 Azure OpenAI 的 API 调用,用重复的通知向我们的 Telegram 发送垃圾邮件,如果我们发布重复的通知,还可能会惹恼我们的关注者。解决方案是基于 URL 的重复数据删除。每篇文章都有一个唯一的 URL,因此我们可以将其用作标识符。 HashSet 数据结构非常适合此目的,因为它提供 O(1) 查找时间并自动防止重复。当我们尝试添加集合中已有的 URL 时,Add 方法只会返回 false,让我们知道应该跳过该文章。

使用 Markdown 持久化状态

重复数据删除可以处理单次运行中的重复项,但是跨运行时又如何呢?当我们的应用程序每六个小时运行一次时,我们需要记住我们已经处理过哪些文章,这样我们就不会再次处理它们。

我选择将此状态存储在名为 posts-articles.md 的 markdown 文件中。为什么要降价?有几个原因。首先,它是人类可读的。我可以打开文件并立即查看我共享了哪些文章。其次,它是版本控制的。由于该文件位于我们的 Git 存储库中,因此我拥有文章处理时间的完整历史记录。第三,它作为文档。任何查看存储库的人都可以看到应用程序做了什么。

该文件的格式很简单。它有一个标题、一个显示应用程序上次运行时间的时间戳,以及一个 Markdown 链接格式的文章列表:

# Posted Articles

*Last run: 2025-12-10 15:30:00*

List of articles posted to LinkedIn:

- [Announcing .NET 10](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
- [Visual Studio 2026 Preview](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)

加载并解析跟踪文件

要检查我们是否已经处理了一篇文章,我们需要加载该文件并提取 URL。这是执行此操作的函数:

static HashSet<string> LoadPostedArticles(string filePath)
{
    var postedUrls = new HashSet<string>();
    
    if (!File.Exists(filePath))
    {
        return postedUrls;
    }

    var lines = File.ReadAllLines(filePath);
    foreach (var line in lines)
    {
        var match = System.Text.RegularExpressions.Regex.Match(line, @"\(([^)]+)\)");
        if (match.Success)
        {
            var url = match.Groups[1].Value;
            
            if (url.Contains("?wt.mc_id="))
            {
                url = url.Substring(0, url.IndexOf("?wt.mc_id="));
            }
            else if (url.Contains("?"))
            {
                url = url.Substring(0, url.IndexOf("?"));
            }
            
            url = url.TrimEnd('/');
            postedUrls.Add(url);
        }
    }

    return postedUrls;
}

该函数返回一个包含我们已经处理过的所有 URL 的 HashSet。我们首先检查文件是否存在 - 第一次运行时,它不会存在,因此我们返回一个空集。

对于文件中的每一行,我们使用正则表达式从 Markdown 链接格式中提取 URL。正则表达式 \(([^)]+)\) 匹配括号内的任何内容,这是 Markdown 链接存储其 URL 的位置。

接下来是重要的一步:URL 规范化。同一篇文章的 URL 格式可能有所不同。 RSS 提要可能会给我们 https://devblogs.microsoft.com/dotnet/article,但我们保存的版本附加了一个跟踪参数:https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972。有些 URL 结尾有斜杠,有些则没有。

为了解决这个问题,我们删除所有查询参数(? 之后的所有内容)并删除尾部斜杠。这种规范化确保我们将文章识别为重复项,即使它们的 URL 在这些表面上有所不同。

保存新文章

当我们成功处理一篇文章时,我们需要将其添加到我们的跟踪文件中:

static void SavePostedArticle(string filePath, string url, string title, DateTimeOffset publishDate)
{
    var markdownEntry = $"- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n";
    
    if (!File.Exists(filePath))
    {
        File.WriteAllText(filePath, "# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n");
    }
    
    File.AppendAllText(filePath, markdownEntry);
}

此函数创建一个 Markdown 格式的条目,其中文章标题作为链接,后跟时间戳,显示我们发布该文章的时间以及最初发布的时间。如果该文件尚不存在,我们首先使用标头创建它。

AI 分析引擎

理解语义内核现在我们进入应用程序中最令人兴奋的部分——人工智能分析。 Semantic Kernel 是 Microsoft 的开源 SDK,用于将大型语言模型集成到应用程序中。它不仅仅是 API 调用的包装器。它提供了一个框架,用于构建具有插件、规划器和内存等功能的复杂人工智能应用程序。

对于我们的用例,我们使用语义内核的聊天完成功能。我们将向 Azure OpenAI 发送提示,该模型将分析我们的文章并生成响应。语义内核处理 API 身份验证、请求格式化和响应解析的所有复杂性。

设置文章分析器

让我们看看如何设置分析器类:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using HtmlAgilityPack;

namespace VsFeedLinkedin.Services;

public class ArticleAnalyzer
{
    private readonly Kernel _kernel;
    private readonly IChatCompletionService _chatService;

    public ArticleAnalyzer(string endpoint, string apiKey, string deploymentName)
    {
        var builder = Kernel.CreateBuilder();
        
        builder.AddAzureOpenAIChatCompletion(
            deploymentName: deploymentName,
            endpoint: endpoint,
            apiKey: apiKey
        );
        
        _kernel = builder.Build();
        _chatService = _kernel.GetRequiredService<IChatCompletionService>();
    }

语义内核使用构建器模式进行配置。我们创建一个 KernelBuilder,使用必要的凭据添加 Azure OpenAI 聊天完成服务,然后构建内核。从构建的内核中,我们检索 IChatCompletionService 接口,我们将使用该接口发送提示和接收响应。

构造函数采用三个参数:Azure OpenAI 终结点(例如 https://your-resource.openai.azure.com/)、API 密钥和部署名称(例如 gpt-4o)。这些是从环境变量传入的,确保我们的凭据安全。

制作完美的提示

我们发送给人工智能的提示至关重要。精心设计的提示可以产生一致、高质量的输出。模糊或结构不良的提示会产生不一致、平庸的结果。我花了相当多的时间迭代这个提示以获得我满意的输出:

var prompt = $"""
    You are a professional tech content analyst and LinkedIn content creator. 
    Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.

    Article Title: {title}
    Author: {author}
    URL: {url}
    Tags: {string.Join(", ", tags)}
    
    Article Content:
    {cleanContent}

    Please provide:
    1. A brief summary (2-3 sentences) of the key points
    2. The main technologies or topics covered
    3. Why this is relevant for developers/tech professionals
    4. An engaging LinkedIn post (max 1300 characters) that:
       - Starts with a hook or attention-grabbing statement
       - Highlights the key value for readers
       - Includes a call to action
       - Uses appropriate emojis (but not too many)
       - Maintains a professional yet approachable tone
       - DO NOT include hashtags in the post (they will be added separately)
       - DO NOT include the URL in the post (it will be added separately)

    Format your response as follows:
    ## Summary
    [Your summary here]

    ## Key Topics
    [List of main topics/technologies]

    ## Relevance
    [Why this matters]

    ## LinkedIn Post
    [Your engaging LinkedIn post here]
    """;

让我在这里解释一下设计决策。我们首先给人工智能一个明确的角色:“你是一名专业的技术内容分析师和 LinkedIn 内容创建者。”这使模型能够以适当的风格和声音做出反应。

我们提供 AI 所需的所有上下文:文章标题、作者、URL、RSS 提要中的标签以及完整的文章内容。我们提供的背景越多,分析就会越好。

然后我们准确指定我们想要返回的内容。我要求提供四件事:摘要、关键主题、相关性解释和 LinkedIn 帖子。具体来说,对于 LinkedIn 帖子,我给出了关于什么是好帖子的详细说明 - 它应该有一个钩子,突出价值,包括号召性用语,适当使用表情符号,并保持专业的语气。

负面指示同样重要。我明确告诉 AI 不要在帖子中包含主题标签或 URL。为什么?因为我是单独添加这些的,如果人工智能包含它们,我就会有重复的。这种明确的指示可以防止常见错误。

最后,我指定确切的输出格式。通过询问标有 ## 标头的部分,我可以轻松以编程方式解析响应。 AI 非常擅长遵循格式化指令,这种一致性使我们的解析代码更简单、更可靠。

执行分析

以下是我们将它们组合在一起的方式:

public async Task<ArticleAnalysis> AnalyzeArticleAsync(
    string title, 
    string url, 
    string htmlContent, 
    string author, 
    List<string> tags)
{
    var cleanContent = ExtractTextFromHtml(htmlContent);
    
    if (cleanContent.Length > 8000)
    {
        cleanContent = cleanContent.Substring(0, 8000) + "...";
    }

    var chatHistory = new ChatHistory();
    chatHistory.AddUserMessage(prompt);

    var response = await _chatService.GetChatMessageContentAsync(chatHistory);
    var responseText = response.Content ?? "";

    return ParseAnalysisResponse(responseText, title, url, author, tags);
}
```我们首先从 HTML 内容中提取干净的文本(我将在下一节中对此进行解释)。然后,如果内容太长,我们会截断内容。大型语言模型有令牌限制,很长的文章可能会超出它们。通过将字符数限制在 8000 个字符,我们确保在限制范围内,同时仍提供大量上下文。

我们创建一个 ChatHistory 对象并将提示添加为用户消息。这是语义内核对基于聊天的交互的抽象。我们将其发送到聊天完成服务并获取响应。最后,我们解析响应以提取各个部分。

### 解析 AI 响应

AI 将其响应返回为按照我们请求的结构格式化的文本。我们需要将其解析为单独的字段:

```csharp
private static ArticleAnalysis ParseAnalysisResponse(
    string response, 
    string title, 
    string url, 
    string author, 
    List<string> tags)
{
    var analysis = new ArticleAnalysis
    {
        Title = title,
        Url = url,
        Author = author,
        Tags = tags,
        RawAnalysis = response
    };

    var sections = response.Split("##", StringSplitOptions.RemoveEmptyEntries);
    
    foreach (var section in sections)
    {
        var lines = section.Trim().Split('\n', 2);
        if (lines.Length < 2) continue;
        
        var sectionTitle = lines[0].Trim().ToLower();
        var sectionContent = lines[1].Trim();

        switch (sectionTitle)
        {
            case "summary":
                analysis.Summary = sectionContent;
                break;
            case "key topics":
                analysis.KeyTopics = sectionContent;
                break;
            case "relevance":
                analysis.Relevance = sectionContent;
                break;
            case "linkedin post":
                analysis.LinkedInPost = sectionContent;
                break;
        }
    }

    return analysis;
}

我们通过 ## 标记分割响应,这给了我们每个部分。对于每个部分,我们用换行符分割以将标题与内容分开。然后,我们使用 switch 语句将每个部分的内容分配给适当的属性。

我们还存储原始的、未解析的响应。这对于调试很有用——如果解析出现问题,我们可以查看人工智能实际返回的内容。

从 HTML 中提取内容

为什么我们需要清理 HTML

当我们从博客中获取文章时,我们会获得该页面的完整 HTML。这不仅仅包括文章内容,还有导航菜单、页眉、页脚、侧边栏、相关文章小部件、评论部分、用于分析和跟踪的脚本、样式表以及各种其他元素。

如果我们将所有这些发送给人工智能,就会发生一些不好的事情。人工智能将不得不处理大量不相关的文本,浪费标记并可能使分析变得混乱。导航和页脚文本可能会包含在摘要中。脚本和 CSS 将被视为内容,进一步污染分析。

我们只需要提取文章内容——人类读者实际会阅读的部分。

使用 HtmlAgilityPack

HtmlAgilityPack 是一个强大的 .NET HTML 解析库。与 XML 不同,HTML 的格式通常是错误的——标签可能无法正确闭合,属性可能无法正确引用。 HtmlAgilityPack 优雅地处理了所有这些,为我们提供了一个可以查询和操作的类似 DOM 的结构。

这是我们的提取函数:

private static string ExtractTextFromHtml(string html)
{
    if (string.IsNullOrWhiteSpace(html))
        return string.Empty;

    var doc = new HtmlDocument();
    doc.LoadHtml(html);

    var nodesToRemove = doc.DocumentNode.SelectNodes("//script|//style|//nav|//footer|//header");
    if (nodesToRemove != null)
    {
        foreach (var node in nodesToRemove)
        {
            node.Remove();
        }
    }

    var text = doc.DocumentNode.InnerText;
    
    text = System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ");
    return text.Trim();
}

我们将 HTML 加载到 HtmlDocument 中,该文档将其解析为树结构。然后我们使用 XPath 选择所有要删除的节点。 XPath 表达式 //script|//style|//nav|//footer|//header 选择所有脚本元素(我们不需要的 JavaScript 代码)、样式元素(我们不需要的 CSS)、导航元素(导航菜单)、页脚元素和页眉元素。

删除这些节点后,我们得到了 InnerText 属性,该属性在剥离 HTML 标签的同时提取所有文本内容。这为我们提供了文章的纯文本。最后,我们清理空白。 HTML 通常有大量额外的空白用于格式化目的——多个空格、制表符、换行符。我们使用正则表达式将任何空白字符序列替换为单个空格,然后修剪结果。

获取全文

RSS 源仅向我们提供摘要,而不是完整的文章内容。要获取完整的文本,我们需要获取文章的网页:

public static async Task<string> FetchArticleContentAsync(string url)
{
    using var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
    
    try
    {
        return await httpClient.GetStringAsync(url);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"⚠️ Failed to fetch article content: {ex.Message}");
        return string.Empty;
    }
}

这很简单 – 我们向文章 URL 发出 HTTP GET 请求并返回 HTML 响应。我们将其包装在 try-catch 中,因为网络请求可能会失败,并且我们宁愿返回空字符串也不愿使整个应用程序崩溃。

创建永久文档

为什么生成 Markdown 文件

每次我们分析一篇文章时,我们都会生成一个详细的 Markdown 文件来记录该分析。这有几个目的。

首先,它创建一个可搜索的档案。随着时间的推移,您将建立一个分析文章的集合。您可以搜索这些文件以查找特定主题的过去内容。

其次,它提供透明度。您可以准确地看到人工智能为每篇文章生成的内容,包括完整的分析和 LinkedIn 帖子。

第三,它对于调试很有用。如果帖子出现问题,您可以查看 markdown 文件以了解发生了什么。

Markdown 生成器类

public class MarkdownGenerator
{
    private readonly string _outputDirectory;

    public MarkdownGenerator(string outputDirectory)
    {
        _outputDirectory = outputDirectory;
        
        if (!Directory.Exists(_outputDirectory))
        {
            Directory.CreateDirectory(_outputDirectory);
        }
    }

    public string GenerateMarkdownFile(ArticleAnalysis analysis)
    {
        var sb = new StringBuilder();
        
        var safeTitle = GenerateSafeFileName(analysis.Title);
        var fileName = $"{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md";
        var filePath = Path.Combine(_outputDirectory, fileName);

        sb.AppendLine($"# {analysis.Title}");
        sb.AppendLine();
        sb.AppendLine("## Article Information");
        sb.AppendLine();
        sb.AppendLine($"- **Author:** {analysis.Author}");
        sb.AppendLine($"- **URL:** [{analysis.Url}]({analysis.Url})");
        sb.AppendLine($"- **Published:** {analysis.PublishDate:yyyy-MM-dd}");
        sb.AppendLine($"- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}");
        sb.AppendLine($"- **Tags:** {string.Join(", ", analysis.Tags)}");
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("## AI Analysis");
        sb.AppendLine();
        sb.AppendLine("### Summary");
        sb.AppendLine();
        sb.AppendLine(analysis.Summary);
        sb.AppendLine();
        sb.AppendLine("### Key Topics");
        sb.AppendLine();
        sb.AppendLine(analysis.KeyTopics);
        sb.AppendLine();
        sb.AppendLine("### Relevance for Developers");
        sb.AppendLine();
        sb.AppendLine(analysis.Relevance);
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("## Generated LinkedIn Post");
        sb.AppendLine();
        sb.AppendLine("```");
        sb.AppendLine(analysis.LinkedInPost);
        sb.AppendLine("```");
        sb.AppendLine();
        sb.AppendLine("");
        sb.AppendLine();
        sb.AppendLine("*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*");

        File.WriteAllText(filePath, sb.ToString());
        return filePath;
    }

构造函数采用输出目录路径,如果不存在则创建它。 GenerateMarkdownFile 方法采用 ArticleAnalysis 对象并生成格式良好的 Markdown 文档。

文件名包括日期和标题的清理版本。这使得文件易于按时间顺序排序并一目了然。

处理不安全的文件名

文章标题可以包含文件名中不允许的字符 - 例如冒号、斜杠、问号和引号。我们需要对这些进行消毒:

private static string GenerateSafeFileName(string title)
{
    var invalidChars = Path.GetInvalidFileNameChars();
    var safeTitle = new string(title
        .Where(c => !invalidChars.Contains(c))
        .ToArray());
    
    safeTitle = safeTitle.Replace(" ", "-").Replace("--", "-");
    
    if (safeTitle.Length > 50)
    {
        safeTitle = safeTitle.Substring(0, 50);
    }
    
    return safeTitle.TrimEnd('-').ToLowerInvariant();
}

我们使用 Path.GetInvalidFileNameChars() 来获取当前操作系统上的文件名中不能出现的字符列表。我们过滤掉这些内容,用连字符替换空格以提高可读性,将长度限制为 50 个字符,并转换为小写以保持一致性。

设置 Telegram 通知

为什么我选择 Telegram

对于通知组件,我考虑了几种选择——电子邮件、短信、Slack、Discord 和 Telegram。我最终选择 Telegram 有几个原因。

该 API 完全免费,对于合理使用没有速率限制。许多通知服务对您可以免费发送的消息数量有限制,但 Telegram 并不限制向个人用户发送机器人消息。

机器人 API 非常简单。它只是带有 JSON 负载的 HTTP 请求。没有复杂的身份验证流程,基本功能不需要 Webhook。Telegram 可以在任何地方使用——在我的手机上、在我的桌面上、在我的网络浏览器上。无论我身在何处,我都可以收到通知并立即回复。

消息支持丰富的格式。我可以使用粗体文本、斜体甚至代码块来使我的通知更具可读性。

创建您的 Telegram 机器人

设置 Telegram 机器人非常简单。打开 Telegram 并搜索 @BotFather – 这是 Telegram 的官方机器人,用于创建和管理机器人。与 BotFather 开始对话并发送命令 /newbot。 BotFather 会要求您提供机器人的名称(这是显示名称)和用户名(必须是唯一的并以“bot”结尾)。一旦您提供了这些信息,BotFather 将创建您的机器人并为您提供 API 令牌。这个令牌就像一个密码——保密并且不要将其提交到公共存储库。

要找到您的聊天 ID,以便机器人知道向何处发送消息,请通过搜索新机器人并按“开始”来开始与新机器人的对话。然后在浏览器中或使用curl访问URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates。在响应中查找 chat 对象 - id 字段是您的聊天 ID。

通过 API 发送消息

这是我们发送 Telegram 消息的函数:

static async Task SendToTelegramAsync(string botToken, string chatId, string message)
{
    using var httpClient = new HttpClient();
    
    var telegramApiUrl = $"https://api.telegram.org/bot{botToken}/sendMessage";
    
    var payload = new
    {
        chat_id = chatId,
        text = message,
        parse_mode = "HTML"
    };
    
    var jsonContent = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
    
    var response = await httpClient.PostAsync(telegramApiUrl, content);
    
    if (!response.IsSuccessStatusCode)
    {
        var errorContent = await response.Content.ReadAsStringAsync();
        throw new Exception($"Telegram API error: {response.StatusCode} - {errorContent}");
    }
}

Telegram Bot API 基于 REST。我们向 sendMessage 端点发出一个 POST 请求,其中包含一个 JSON 正文,其中包含聊天 ID(发送位置)、消息文本(发送内容)以及可选的解析模式(用于格式化)。

将 parse_mode 设置为“HTML”可以让我们在消息中使用基本的 HTML 标签 - 例如 <b>bold</b><i>italic</i>。这可以使通知更具可读性,尽管对于我们当前的用例,我们发送纯文本。

如果请求失败,我们会抛出异常,并提供有关错误原因的详细信息。如果某些东西不起作用,这有助于调试。

配置应用程序

环境变量

我们的应用程序需要几条敏感信息 – API 密钥、机器人令牌和端点 URL。我们永远不应该对它们进行硬编码或将它们提交给版本控制。相反,我们使用环境变量,可以在应用程序运行的每个环境中安全地设置这些变量。

对于 Telegram,我们需要 TELEGRAM_BOT_TOKEN(BotFather 给您的令牌)和 TELEGRAM_CHAT_ID(应发送消息的聊天 ID)。

对于 Azure OpenAI,我们需要 AZURE_OPENAI_ENDPOINT(您的资源的 URL)、AZURE_OPENAI_API_KEY(您的 API 密钥)和 AZURE_OPENAI_DEPLOYMENT(您部署的模型的名称,例如“gpt-4o”)。

在代码中加载配置

以下是我们在应用程序启动时加载这些值的方式:

var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN");
var telegramChatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID");

var azureOpenAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var azureOpenAiDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";

var aiAnalysisEnabled = !string.IsNullOrWhiteSpace(azureOpenAiEndpoint) && 
                        !string.IsNullOrWhiteSpace(azureOpenAiKey);

我们使用Environment.GetEnvironmentVariable来读取每个值。对于部署名称,如果未设置值,我们将提供默认值“gpt-4o”。

然后,我们通过验证是否拥有端点和 API 密钥来检查是否应启用 AI 分析。如果未配置 Azure OpenAI,这允许应用程序在降级模式下运行 - 它仍然会获取源并跟踪文章,只是没有 AI 分析。### 优雅的降级

优雅降级的概念很重要。我们不希望应用程序仅仅因为未配置一项可选功能而崩溃:

ArticleAnalyzer? articleAnalyzer = null;
MarkdownGenerator? markdownGenerator = null;

if (aiAnalysisEnabled)
{
    Console.WriteLine("🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel");
    articleAnalyzer = new ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
    markdownGenerator = new MarkdownGenerator(articlesOutputDir);
}
else
{
    Console.WriteLine("ℹ️  AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable");
}

如果启用了人工智能,我们将创建分析器和降价生成器。如果没有,我们将它们留空并在处理过程中跳过与人工智能相关的步骤。即使没有人工智能增强,该应用程序仍然通过获取源和发送基本通知来提供价值。

使用 GitHub Actions 实现自动化

为什么选择 GitHub Actions

该解决方案的真正力量来自自动化。我们不想每隔几个小时手动运行一次应用程序——我们希望它在后台自动运行。

GitHub Actions 非常适合此目的。它内置于 GitHub 中,因此无需设置额外的服务。它对公共存储库免费,并为私人存储库提供大量免费分钟。它可以按计划运行,定期触发我们的应用程序。它具有内置的秘密管理功能,可以安全地存储我们的 API 密钥。它可以将更改提交回存储库,使我们的跟踪文件保持最新。

工作流程文件

在 .github/workflows/fetch-and-notify.yml 中创建一个包含以下内容的文件:

name: Fetch DevBlogs and Notify

on:
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  fetch-and-notify:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'
      
      - name: Restore dependencies
        run: dotnet restore src/VsFeedLinkedin.csproj
      
      - name: Build
        run: dotnet build src/VsFeedLinkedin.csproj --no-restore
      
      - name: Run application
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
          AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
          AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
          AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
        run: dotnet run --project src/VsFeedLinkedin.csproj
      
      - name: Commit and push changes
        run: |
          git config user.name "GitHub Actions Bot"
          git config user.email "[email protected]"
          
          if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
            TIMESTAMP=$(date +%Y%m%d_%H%M%S)
            git add posted-articles.md generated-posts/
            git commit -m "chore($TIMESTAMP): processed new articles"
            git push
          else
            echo "No changes to commit"
          fi

让我解释一下每个部分。 on 部分定义工作流程何时运行。计划触发器使用 cron 语法 - 0 */6 * * * 表示“每 6 小时的 0 分钟”。因此,工作流在午夜、上午 6 点、中午和下午 6 点(UTC)运行。 workflow_dispatch 触发器允许从 GitHub UI 手动运行,这对于测试很有用。

该作业在 ubuntu-latest(Linux 虚拟机)上运行。我们检查我们的存储库、设置 .NET 9、恢复 NuGet 包并构建项目。

运行应用程序步骤是奇迹发生的地方。我们使用 ${{ Secrets.SECRET_NAME }} 语法将机密作为环境变量传递。这些秘密安全地存储在 GitHub 中,并且永远不会在日志中公开。

最后,我们将所有更改提交回存储库。我们使用机器人身份配置 Git,检查我们的跟踪文件或生成的帖子目录是否有任何更改,如果有,则创建提交并推送它。

设置秘密

要将机密添加到 GitHub 存储库,请转到存储库的“设置”,然后转到“机密和变量”,然后转到“操作”。单击“新存储库机密”并添加每个环境变量。这些名称必须与我们在工作流程文件中引用的名称完全匹配。

总结

我们已经构建了什么

回顾我们所涵盖的所有内容,我们构建了一个全面的、由人工智能驱动的 RSS 提要聚合器,可以自动执行过去繁琐的手动流程。该应用程序自动监视七个 Microsoft DevBlogs 源,在每一篇新文章发布后立即捕获它。它处理重复数据删除的复杂性,识别同一篇文章何时出现在多个提要中。由 Semantic Kernel 和 Azure OpenAI 提供支持的 AI 分析可以自动读取和理解文章内容、生成摘要、识别关键主题并解释相关性。最重要的是,它创建了引人入胜的 LinkedIn 帖子,我可以通过最少的编辑来分享这些帖子。

Telegram 集成意味着每当有新内容需要查看时我都会在手机上收到通知。我可以浏览该消息,决定是否要分享它,然后立即采取行动。

而且因为它按计划在 GitHub Actions 上运行,所以我不必记住做任何事情。该系统在后台运行,只有当有值得分享的东西时我才会参与。

使之成为可能的技术

该项目汇集了多种技术,每种技术都发挥着至关重要的作用。 .NET 9 以其现代语言功能和出色的性能提供了坚实的基础。语义内核使 AI 集成变得简单,处理 API 调用和响应管理的所有复杂性。 Azure OpenAI 提供了智能——真正理解和分析技术内容的能力。 HtmlAgilityPack 解决了从网页中提取干净文本的混乱问题。 System.ServiceModel.Syndicate 使 RSS 解析变得轻而易举。 Telegram Bot API 为我们提供了免费、可靠的通知。 GitHub Actions 将这一切与自动化、预定的执行结合在一起。

考虑成本

您可能会问的一个问题是:运行该程序需要多少钱?答案是:一点也不多。

Telegram 完全免费 - 通过机器人发送消息无需付费。

GitHub Actions 对公共存储库免费。对于私有存储库,您每月可以使用 2,000 分钟的免费套餐,这对于我们的用例来说绰绰有余。

Azure OpenAI 是唯一付费组件,而且成本极低。使用 GPT-4o 分析一篇典型的博客文章的成本约为 1 到 3 美分。即使您每月处理数十篇文章,您所看到的人工智能成本也不足一美元。

接下来你可以去哪里

虽然这个解决方案非常适合我的需求,但您可以通过多种方式扩展它。您可以添加对多个社交平台的支持 - 除了 LinkedIn 之外,还可以发布到 Twitter/X、Mastodon 或 Bluesky。您可以实施情绪分析来跟踪一段时间内文章的基调并发现趋势。您可以为不同的提要允许不同的提示模板,为不同的主题生成不同风格的帖子。您可以构建一个 Web 仪表板来查看和管理帖子,而不是使用 Telegram。您可以跟踪已发布内容的参与度指标,以了解哪些主题最能引起受众的共鸣。

最后的想法我最喜欢这个项目的是,它体现了我坚信的哲学:自动化应该处理繁琐的部分,同时将创造性和决策部分留给人类。该系统完成了所有繁琐的工作——获取、解析、分析、生成——但我仍然在分享之前检查所有内容。人工智能生成的帖子是我可以定制和个性化的起点。

通过结合 .NET、Semantic Kernel 和 Azure OpenAI 的强大功能,我们创建了一个工具,可以每周节省数小时的手动工作,同时保持质量和一致性。这是一种真正改变日常生活的实用自动化。

如果您构建了类似的东西或有改进的想法,我很想听听。请随时通过 LinkedIn 联系我们!

编码快乐,圣诞快乐! 🎄