📘 **TELUS Agriculture & Consumer Goods** 如何通过 **Haystack Agents** 转变促销交易

定制 RAG 管道以总结最新的 Hacker News 博文

看看我们是如何为高级 LLM 管道改造 Haystack 的,其中有一个示例使用自定义组件来获取最新的 Hacker News 博文

在过去的几个月里,deepset 的团队一直在对 Haystack 仓库进行重大升级。在此过程中,我们一直在与社区分享即将推出的 Haystack 2.0 的更新和设计过程,并发布了一个预览包中的新组件。这意味着您已经可以使用 haystack-ai 包(pip install haystack-ai)中提供的预览组件开始探索 Haystack 2.0 的功能了。

更新:我们已于 2023 年 12 月 4 日发布了 Haystack 2.0-Beta,本文中的代码已更新以适应此新版本。

在本文中,我将介绍 Haystack 的两个主要概念

  • 组件:它们是 Haystack 中最小的构建块。它们旨在覆盖一个简单的任务。除了使用 Haystack 核心项目提供的组件外,在 2.0 版本中,创建自己的自定义组件将比以往任何时候都更容易。
  • 管道:它们是通过将组件相互连接而成的。2.0 中的管道比以往任何时候都更加灵活,并允许您在组件之间进行各种新的连接模式。

虽然组件和管道自 Haystack 诞生以来一直是其核心,但 Haystack 2.0 对它们的构建方式进行了一些重大更改。

我们将探讨如何使用 Haystack 2.0 预览版创建自定义组件和管道。我将分享一个自定义 Haystack 组件,该组件可以从 Hacker News 获取最新博文,并展示如何将其用于检索增强生成 (RAG) 管道,以生成 Hacker News 博文的摘要。

Haystack 2.0 中的组件

一个组件是一个只做一件事的类。这件事情可以是“提示 GPT3.5”、“翻译”或“检索文档”等。

虽然 Haystack 在核心项目中附带了一组组件,但我们希望通过 Haystack 2.0,您也能轻松地根据自己的自定义需求构建组件。

在 Haystack 2.0 中,一个类只需添加两项即可成为一个组件

  • 在类声明上添加 @component 装饰器。
  • 一个带有 @component.output_types(my_output_name=my_output_type) 装饰器的 run 函数,该函数描述了管道应该从此组件期望的输出。

就这样。

构建自定义 Hacker News 组件

我必须承认,这个自定义组件的想法来自于我们在 Discord 上的一位很棒的 Haystack 大使在一次实时编码会话中提出的(感谢 rec 💙)—— 结果相当不错!那么,让我们看看如何创建一个自定义组件,该组件可以获取 Hacker News 的最新k 篇博文。

首先,我们创建一个 HackernewsNewestFetcher。要使其成为有效的 Haystack 组件,它还需要一个 run 函数。目前,让我们创建一个存根函数,它只返回一个包含单个键 'articles' 的字典,值为“Hello world!”。

from haystack import component  
  
@component  
class HackernewsNewestFetcher():  
    
  @component.output_types(articles=str)  
  def run(self):  
    return {'articles': 'Hello world!'}

现在,让我们让我们的组件实际获取 Hacker News 的最新博文。我们可以使用 newspapers3k 包来抓取并获取给定 URL 的内容。我们还将更改输出类型,以返回 Document 对象列表。

from typing import List  
from haystack import component, Document  
from newspaper import Article  
import requests  
  
@component  
class HackernewsNewestFetcher():  
    
  @component.output_types(articles=List[Document])  
  def run(self, last_k: int):  
    newest_list = requests.get(url='https://hacker-news.firebaseio.com/v0/newstories.json?print=pretty')  
    articles = []  
    for id in newest_list.json()[0:last_k]:  
      article = requests.get(url=f"https://hacker-news.firebaseio.com/v0/item/{id}.json?print=pretty")  
      if 'url' in article.json():  
        articles.append(article.json()['url'])  
  
    docs = []  
    for url in articles:  
      try:  
        article = Article(url)  
        article.download()  
        article.parse()  
        docs.append(Document(content=article.text, meta={'title': article.title, 'url': url}))  
      except:  
        print(f"Couldn't download {url}, skipped")  
    return {'articles': docs}

现在我们有了一个组件,当它运行时,它会返回一个 Document 列表,其中包含 Hacker News 上(最后 last_k)最新博文的内容。这里我们将输出存储在字典的 articles 键中。

Haystack 2.0 中的管道

管道是一个结构,它将一个组件的输出连接到另一个组件的输入,直到达到最终结果。

管道的创建包含几个步骤

  1. 创建管道
    pipeline = Pipeline()
  2. 将组件添加到管道
    pipeline.add_component(instance=component_a, name=”ComponentA”)
    pipeline.add_component(instance=component_b, name=”ComponentB”)
  3. 将一个组件的输出连接到另一个组件的输入
    pipeline.connect("component_a.output_a", "component_b.input_b")

Haystack 2.0 预览版中已经有足够的组件供我们构建一个简单的 RAG 管道,该管道将在检索增强步骤中使用我们新的 HackernewsNewestFetcher

构建 RAG 管道以生成 Hacker News 博文的摘要

为了构建一个 RAG 管道,该管道可以为 Hacker News 的每篇最新k 篇博文生成摘要,我们将使用 Haystack 2.0 预览版的两个组件

  • PromptBuilder:此组件允许我们使用 Jinja 作为模板语言来创建提示模板。
  • OpenAIGenerator:此组件仅用于提示指定的 GPT 模型。我们可以将 PromptBuilder 的输出连接到此组件,以自定义我们与所选模型交互的方式。

首先,我们初始化管道所需的所有组件

from haystack import Pipeline  
from haystack.components.builders.prompt_builder import PromptBuilder  
from haystack.components.generators import OpenAIGenerator
from haystack.utils import Secret

prompt_template = """  
You will be provided a few of the latest posts in HackerNews, followed by their URL.  
For each post, provide a brief summary followed by the URL the full post can be found at.  
  
Posts:  
{% for article in articles %}  
  {{article.content}}  
  URL: {{article.meta['url']}}  
{% endfor %}  
"""  
  
prompt_builder = PromptBuilder(template=prompt_template)  
llm = OpenAIGenerator(mode="gpt-4", api_key=Secret.from_token('YOUR_API_KEY'))  
fetcher = HackernewsNewestFetcher()

接下来,我们将组件添加到管道

pipeline = Pipeline()  
pipeline.add_component("hackernews_fetcher", fetcher)  
pipeline.add_component("prompt_builder", prompt_builder)  
pipeline.add_component("llm", llm)

最后,我们将组件相互连接

pipeline.connect("hackernews_fetcher.articles", "prompt_builder.articles")  
pipeline.connect("prompt_builder", "llm")

在这里,请注意我们将 hackernews_fetcher.articles 连接到 prompt_builder.articles。这是因为 prompt_builder 在其模板中需要 articles

Posts:  
{% for article in articles %}  
  {{article.contnet}}  
  URL: {{article.meta['url']}}  
{% endfor %}

输出和输入键不必具有匹配的名称。此外,prompt_builder 会使所有输入键都可用于您的提示模板。例如,我们可以向 prompt_builder 提供一个 documents 输入,而不是 articles。那么我们的代码可能看起来像这样

prompt_template = """  
You will be provided a few of the latest posts in HackerNews, followed by their URL.  
For each post, provide a brief summary followed by the URL the full post can be found at.  
  
Posts:  
{% for document in documents %}  
  {{document.content}}  
  URL: {{document.meta['url']}}  
{% endfor %}  
"""  
  
[...]  
  
pipeline.connect("hackernews_fetcher.articles", "prompt_builder.documents")

注意提示现在如何引用 documents,并且 connect 调用现在如何连接到相应的 prompt_builder.documents 输入。

现在我们有了管道,可以运行它了。以下是我在欧洲中部时间 9 月 21 日晚上 10:45 左右收到的响应 🤗

result = pipe.run(data={"hackernews_fetcher":{"last_k": 2}})  
print(result['llm']['replies'][0])

响应

1. "The translation world has legends of its own, but not all legends involve greatness.   
Many provide pain, confusion, or comedy, as these examples of bad game translation prove."   
- This post shares a humorous look at some examples of poor video game translations that have   
resulted in confusion and comedy. The author seeks to highlight that while translation is often   
necessary in game localization, it can sometimes yield suboptimal results.  
Link: https://legendsoflocalization.com/bad-translation/  
  
2. “Recently, I found myself returning to a compelling series of   
blog posts titled Zero-cost futures in Rust by Aaron Turon about what would   
become the foundation of Rust's async ecosystem.”   
- This post provides an in-depth analysis of the current state of Rust's   
'async' ecosystem, drawing upon the author's own experiences and Aaron Turon's   
blog series, "Zero-cost futures in Rust". The author also discusses the benefits and   
negatives of the current async ecosystem, the problems with ecosystem fragmentation,   
the state and issue of async-std, alternative runtimes, the complexities of writing async code,   
the benefits of synchronous threads over async, and the obsessiveness of Rust landscape with an   
async-first approach. The post concludes with the notion that async Rust should be used only   
when necessary and that the smaller, simpler language inside Rust (the synchronous Rust)   
should be the default mode.  
Link: https://corrode.dev/blog/async/

进一步改进

这个自定义组件是作为一个实验创建的,在实际应用中您肯定可以将其做得更进一步。

例如,我们的实验性组件并没有采取任何措施来缩短每篇文章的内容长度。这意味着 GPT-4 可能难以给出很好的回应,尤其是在将 last_k 设置为较高数字时。