DLAI-生成式人工智能工作流笔记-全-

DLAI 生成式人工智能工作流笔记(全)

001:课程介绍 🎬

在本课程中,我们将学习如何将生成式AI应用的原型转化为自动化、可扩展的生产级工作流。我们将使用Airflow这一编排工具,构建一个能够处理书籍描述、计算嵌入向量并存储到向量数据库的RAG(检索增强生成)管道。


欢迎来到《为生成式AI应用编排工作流》课程,本课程是与Astronomer合作开发的。在本课程中,您将构建一个RAG管道。该管道从文本文件中提取书籍描述,然后计算这些描述的嵌入向量,并将嵌入向量存储到向量数据库中。您将使用Airflow来自动化此管道。Airflow是一个编排工具,它能确保各个步骤按正确顺序执行,并在正确的时间触发管道运行。

我们很高兴本课程的讲师是Astronomer的开发者关系高级经理Kenten Dens,以及开发者倡导者Tamara Fingngelin。

感谢Andrew,我们很高兴能与您合作开发这门课程。

要将您的概念验证从开发环境转移到生产环境,您需要将逻辑转化为由多个步骤组成的自动化管道,其中每个步骤代表一项操作。

例如,假设您可以访问一系列包含产品评论的文件,并且您已经编写了一个大型代码块来总结每个产品的评论。为了实现自动化,您可以将此逻辑分解为一系列步骤。

以下是分解逻辑的步骤示例:

  1. 首先,找到每个产品的评论文本文件位置。
  2. 其次,汇总每个产品的所有评论反馈。
  3. 第三,使用语言模型总结每个产品的评论。
  4. 最后,再次使用语言模型从摘要中提取情感倾向。

这种方法有助于您轻松识别整个管道中的故障点并从中恢复。


对于许多生成式AI管道,故障可能由于API速率限制或API返回其他错误而发生。例如,当语言模型需要处理大量产品评论时。在本课程中,您将学习如何为任务配置重试机制,以便管道可以在重试前等待一段时间。

您还将学习如何并行处理大量数据。例如,在摘要生成步骤,您可以并行处理每个产品的评论,而不是在一个步骤中处理所有产品的摘要。

最后,您还将学习如何在有新数据可用或更新时(例如一组新的产品评论对)触发管道运行。您将把这些实践应用到您的RAG示例中。

您将从包含RAG原型的Jupyter笔记本开始,该原型用于提取和嵌入书籍描述。在学习基本的Airflow语法后,您将把该原型转化为可手动运行的Airflow管道。

之后,您将安排它自动运行,使其在运行时适应您的数据,甚至添加自动重试和故障通知。在最后一课中,您将了解Airflow在现实生活中如何用于编排生成式AI工作流。

许多人参与了本课程的创作。我要感谢来自Astronomer的Stephen Hoian,以及来自DeepLearning.AI的Harra Salami,他们也为本课程做出了贡献。

将Jupyter笔记本转化为可运行的生产软件是任何AI开发人员的一项重要技能。在下一个视频中,您将经历整个过程,并了解如何自己操作。对于许多首次进行此操作的开发人员来说,一个不太直观的方面是:将工作流分解为步骤的过程通常会产生比预期更多、更小的独立步骤。掌握如何做到这一点的直觉将使您的应用程序运行得更快、更可靠。因此,请前往下一个视频学习相关内容。



在本节课中,我们一起学习了课程的整体目标:将生成式AI原型转化为自动化、健壮的生产管道。我们了解了使用Airflow进行工作流编排的优势,以及将复杂任务分解为可管理、可监控步骤的重要性。接下来,我们将开始动手实践,学习如何具体构建这样的管道。

002:从笔记本到流水线 🚀

在本课程中,我们将学习如何为生成式AI应用编排工作流。具体来说,我们将探索将笔记本原型转化为生产流水线的最佳实践,了解Airflow及其核心概念,如DAG和任务。

什么是编排及其重要性?🤔

在深度学习的其他课程中,你可能使用Jupyter笔记本来开发生成式AI应用。笔记本非常适合开发,因为你可以轻松运行和测试应用程序不同部分的代码,实现快速反馈和实验。可以方便地集成任意数量的Python包和工具。

然而,一旦代码准备就绪,你可能希望自动运行它,以便在生产环境中为生成式AI应用提供支持。这在笔记本中不容易实现。当代码自动运行时,你需要额外的功能,例如可观测性,以便了解哪些任务成功、哪些失败以及流水线何时运行。你还需要一个健壮的系统,能够同时管理大量任务的运行、不同的资源需求以及出现问题时发送通知。

所有这些都可以通过一个编排工具来实现,该工具用于基于你的笔记本代码运行数据流水线。

编排示例 📊

举一个例子,假设你有一个原型,其中你获取一些文本输入,从中生成嵌入向量,并将其加载到向量数据库中。

在你的笔记本中,可能有三个单元格,你编写并测试了处理文本数据本地子集并将其加载到向量数据库实例的代码。现在,你可以将这个笔记本原型转化为一个流水线。

你的每个笔记本单元格都成为流水线中的一个步骤,可以自动运行并指向生产数据。你的流水线有三个步骤:获取文本、嵌入数据、加载到数据库。

除了自动运行每个步骤的代码外,它还将管理依赖关系,因为在将数据加载到数据库之前,你需要先获取数据。你的流水线还将包含你定义的逻辑,用于处理失败情况,例如是否重试、是否以某种方式收到通知,或者如果上游步骤失败,下游步骤是否仍然运行。

所有这些额外功能都是自动化笔记本代码和生产环境所必需的,并由编排工具提供。

认识Apache Airflow 🌪️

在这张幻灯片上,你会注意到我们的流水线有一个风车标志。这是Airflow的标志,Airflow是本课程中我们将要使用的编排器。

Apache Airflow是一个用于创作、调度和监控数据流水线的开源工具。它是程序化工作流编排的标准,在全球范围内非常流行。

在Airflow中,你的数据流水线是用Python编写的,这意味着Airflow可以与数据生态系统中的几乎任何工具集成。Airflow的架构使其具有无限的可扩展性,无论你需要运行一个流水线还是数千个。它拥有丰富的功能集,使其具有动态性和可观测性。该项目由一个充满活力的开源社区维护,新功能不断添加。

Airflow核心概念 📚

使用Airflow时,有几个概念你应该了解。第一个是DAG。DAG这个名字来源于一个数学术语,但在Airflow中,你可以简单地将其视为一个数据流水线,或者在某些情况下,是数据流水线的一部分。

在DAG内部,你有任务,它们代表流水线中的一个工作单元。Airflow还有一个丰富的用户界面,显示你流水线当前和过去运行的概览。在本课程中,你将有机会探索Airflow的用户界面。

让我们通过回顾之前的例子来更深入地了解DAG的概念。在这个例子中,你将原型笔记本转化为流水线,你将拥有一个包含三个任务的DAG:一个用于获取文本输入数据,一个用于创建嵌入向量,一个用于将这些嵌入向量加载到你的向量数据库中。

在Airflow中,你的DAG有一个名称,例如“我的超棒流水线”,并会设定一个运行时间表,例如每天午夜运行。每个任务都包含你在笔记本中开发的Python代码。你还需要定义依赖关系,以便你的三个任务按顺序运行。如果数据尚未准备好,运行将数据加载到数据库的任务是没有意义的。

请注意,这只是Airflow中DAG的一个例子。你的DAG可能很简单,像这个一样,也可能更复杂。Airflow非常灵活。如果你能为你的用例用Python编写逻辑,你就可以将其转化为Airflow DAG。

Airflow的常见用例 🔧

由于Airflow非常灵活,它通常被用作许多不同用例的编排器,包括编排生成式AI流水线。在本课程中,我们将介绍如何编排检索增强生成流水线,但你也可以编排其他类型的生成式AI流水线。

一个常见的用例是推理执行,这通常是在输入数据上运行训练好的机器学习模型并收集结果的过程。批量推理是推理执行的一种类型,这是一种一次性对大量输入数据使用训练好的机器学习模型生成预测的方法,因此数据是以批次提供的。在Airflow中,你可能运行一个夜间批量DAG,根据最新的每日客户活动和档案数据,对所有客户的流失可能性进行评分。

另一种推理执行类型是临时推理或异步处理,这意味着一旦数据到达,你就将数据输入模型以获取结果。虽然Airflow不用于流处理,但在某些情况下可以用于异步流水线。例如,你可能有一个Web应用程序,在用户创建个人资料时提供个性化产品推荐,信息被发送到Airflow,Airflow管理从训练好的模型中获取结果。Airflow也可用于管理自动化的模型训练、重新训练和微调。通常,任何基于Python的笔记本都可以转化为Airflow DAG和流水线。

Airflow流水线设计最佳实践 🏆

在为生成式AI编排或任何用例设计Airflow流水线时,有几个最佳实践你应该牢记。遵循这些准则将有助于确保Airflow的灵活性使你的工作更轻松,而不会在生产中给你带来问题。

以下是三个关键的最佳实践:

  1. 原子性:这意味着你应该创建流水线,使你的任务是原子的,每个任务完成一个单一的工作单元。
  2. 幂等性:这意味着你设计流水线的方式是,如果你用相同的输入多次运行一个DAG或任务,它总是会产生相同的输出。请注意,对于生成式AI流水线,这并不总是可能的,这没关系。这是一个在合理时应遵循的一般准则,而不是每种情况下的硬性要求。
  3. 使用软件开发最佳实践:因为Airflow流水线是Python代码,你可以像对待任何其他软件一样对待它们,并使用版本控制、CI/CD和自动化测试。

前两个最佳实践比较微妙,现阶段对你来说可能是新的,所以让我们更详细地看看它们。

深入理解原子性与幂等性 🔍

原子性在数据编排的上下文中是指每个任务应代表一个原子工作单元的原则。由于Airflow中的任务是Python代码,你可以随意定义它们。你可以将整个笔记本放入一个任务中,该任务为你的流水线完成所有工作,但流水线很少只有一个逻辑步骤。在前面的例子中,我们讨论了一个包含三个步骤的流水线:获取文本数据、生成嵌入向量并将其加载到向量数据库。如果你将所有逻辑放入一个任务中,如果它无法连接到数据库会发生什么?你将不得不重新运行所有代码,即使获取文本数据和生成嵌入向量的一切都正常。这既低效又难以管理。你甚至需要付出额外的努力才能弄清楚任务的哪一部分失败了。

通过将此逻辑分解为三个独立的任务,每个步骤一个,你可以获得更好的可观测性,并且可以通过仅重新运行需要重新运行的任务,更容易地从故障中恢复。通常,在Airflow DAG中拥有更多数量的任务并没有太多缺点,始终保持任务的原子性总是更可取的。

幂等性是另一个最佳实践,可以在出现问题时帮助你更快地恢复。一个幂等的任务意味着对于相同的输入,你得到相同的输出。例如,假设你有一个任务,其逻辑使用了当前日期。如果你使用像datetime.now()这样的函数来设计任务,那么任务将在每次运行时使用当前的日期和时间。因此,如果你有一个处理每日数据的DAG,并且在星期一你发现星期五的运行失败了,如果你重新运行你的任务,你将不会使用星期一的日期,而是使用星期五的日期,并且你将处理错误的数据。在这个特定的例子中,你可以使用Airflow的上下文,它允许你访问Airflow在运行流水线时使用的信息。这允许你获取与DAG计划日期相对应的日期,即使该日期现在已成为过去。

正如我们之前提到的,有时对于生成式AI流水线,要求每个任务或DAG都是幂等的并不合理。在某些情况下,你期望相同的输入产生不同的输出。这完全没问题。重要的是你知道幂等性是什么,以及在你的DAG设计中何时使用它是有意义的。

课程实践展望 🎯

现在你已经对Airflow概念有了一些背景了解,你可以开始在本课程的其余部分亲自尝试了。你将学习如何创建一个用于图书推荐的RAG应用程序,并将你的笔记本代码转化为Airflow DAG。

你将创建一个DAG,用于自动获取和摄取新书描述的数据,创建向量嵌入并将其加载到向量数据库中。你还将创建第二个DAG,根据你提供的查询,在你的向量数据库中搜索图书推荐。

总结 📝

在本节课中,我们一起学习了工作流编排的重要性,特别是如何将Jupyter笔记本原型转化为可自动运行的生产级流水线。我们介绍了Apache Airflow这一强大的开源编排工具,理解了其核心概念:DAG和任务。我们还探讨了设计Airflow流水线时应遵循的关键最佳实践,即原子性、幂等性以及应用软件开发标准。最后,我们预览了接下来的实践内容,即构建一个用于图书推荐的RAG应用流水线。掌握这些知识,是迈向稳健、可观测的生成式AI应用生产部署的第一步。

003:构建你的 RAG 原型应用 🧪

在本节课中,我们将探索一个功能完整的 RAG(检索增强生成)原型应用。这个应用能够摄取和嵌入书籍描述,并根据你的查询为你推荐想读的书籍。稍后,我们将把这个原型转化为一个可编排的工作流管道。


概述

我们将逐步构建一个 RAG 原型应用。首先,我们会设置必要的库和工具,然后创建向量数据库并加载数据。接着,我们会将文本数据转化为向量嵌入,并将其存储到数据库中。最后,我们将通过一个查询来测试这个系统,获取书籍推荐。通过这个过程,你将学习如何将 Jupyter Notebook 中的代码转化为一个结构化的管道,为后续使用 Airflow 进行编排打下基础。


导入必要的库

首先,我们需要导入本 Notebook 中将要用到的库。

以下是所需的库及其用途:

  • osjson:用于与文件系统交互和处理 JSON 数据。
  • JSON(来自 IPython):用于在 Notebook 中以结构化格式美观地显示 JSON 字典。
  • TextEmbedding(来自 fastembed):用于为书籍描述(包括你最喜欢的书)创建向量嵌入。
  • Voyage:一个轻量级向量数据库,用于存储和检索向量嵌入。
  • 一个辅助函数:用于抑制冗长的日志输出。
import os
import json
from IPython.display import JSON
from fastembed import TextEmbedding
import voyageai
from helper import suppress_stdout_stderr

由于 Airflow 可以编排任何 Python 代码,因此它可以与你喜欢的任何 AI 工具库(包括各种向量数据库)协同工作。在本例中,我们使用的是 Voyage 向量数据库。


设置应用变量

接下来,我们需要定义几个在整个 RAG 应用中都会用到的变量。

以下是需要定义的几个关键变量:

  • COLLECTION_NAME:在 Voyage 数据库中存储嵌入向量的集合名称,本例中为 "books"
  • BOOK_DESCRIPTIONS_FOLDER:存储书籍描述文本文件的文件夹路径。我们已经准备了一些数据在 includes/data 文件夹中。
  • EMBEDDING_MODEL:本例中使用的嵌入模型。我们选择流行的、轻量级的 "BAAI/bge-small-en-v1.5",该模型针对语义搜索和检索进行了优化。
COLLECTION_NAME = "books"
BOOK_DESCRIPTIONS_FOLDER = "includes/data"
EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5"

在 Notebook 环境中,我们通常使用工具的本地或嵌入式版本。这里也是如此,我们创建并连接到一个嵌入式的 Voyage 数据库实例,其数据将持久化存储在 /tmp/voyage 目录下。

VOYAGE_PERSIST_DIRECTORY = "/tmp/voyage"
client = voyageai.Client(persist_directory=VOYAGE_PERSIST_DIRECTORY)

请暂停视频并运行此单元格,以创建你的 Voyage 实例并确认客户端已准备就绪。稍后,当你将此 Notebook 转换为 Airflow DAG 时,你将连接到一个在 Docker 中运行的 Voyage 实例。


创建向量数据库集合

上一节我们设置了数据库连接,本节中我们来看看如何在其中创建数据集合。

Voyage 中的集合(Collection)是一组共享相同数据结构的对象,例如一组产品描述、支持工单,或者在我们的案例中——书籍描述。以下代码首先列出已存在的所有集合,然后检查我们想要创建的集合是否已存在。如果不存在,则创建它;如果已存在,则直接获取该集合的引用以便后续交互。

# 列出所有现有集合
existing_collections = client.list_collections()
existing_collection_names = [col.name for col in existing_collections]

# 检查目标集合是否存在,不存在则创建
if COLLECTION_NAME not in existing_collection_names:
    collection = client.create_collection(name=COLLECTION_NAME, size=384) # 384 是嵌入向量的维度
    print(f"Collection '{COLLECTION_NAME}' created.")
else:
    collection = client.get_collection(name=COLLECTION_NAME)
    print(f"Collection '{COLLECTION_NAME}' already exists, retrieved.")

现在,Voyage 数据库已经准备就绪,正等待着数据的注入。


准备书籍描述数据

我们的 book_descriptions 文件夹中已经存储了一些优秀书籍的描述文本文件。

运行下面的单元格,你可以看到当前文件夹中有两个文件,每个文件包含多本书的数据。

description_files = os.listdir(BOOK_DESCRIPTIONS_FOLDER)
print(f"Files in folder: {description_files}")

让我们添加第三个文件,其中包含你喜爱的书籍。格式很简单:每行描述一本书,各部分由三个冒号 ::: 分隔。

格式如下:
索引号:::书名 (出版年份):::作者:::书籍描述

我添加了两本我最喜欢的书:

  1. 《The Idea of the World by Bernardo Kastrup》
  2. 《Exploring the World of Lucid Dreaming by Stephen LaBerge》
new_books = [
    "7:::The Idea of the World (2018):::Bernardo Kastrup:::A rigorous case for the primacy of mind in nature, drawing on philosophy, neuroscience, and physics to argue against materialist metaphysics.",
    "8:::Exploring the World of Lucid Dreaming (1990):::Stephen LaBerge:::A practical guide to learning how to become consciously aware within your dreams, based on scientific research at Stanford University."
]

new_file_path = os.path.join(BOOK_DESCRIPTIONS_FOLDER, "my_favorite_books.txt")
with open(new_file_path, 'w') as f:
    for book in new_books:
        f.write(book + '\n')
print(f"New books added to {new_file_path}")

你可以在此处暂停视频,添加你自己最喜欢的书籍。可以添加任意数量,只需确保遵循上述格式,并且每本书独占一行。

太棒了!现在我们需要从这些文本文件中读取数据,将文本转化为嵌入向量,然后将这些向量加载到 Voyage 数据库中。


读取并解析书籍数据

首先,我们循环遍历描述文件列表,并使用 .readlines() 方法读取每个文件。这个方法会创建一个列表,列表中的每个元素对应文件中的一行,也就是一本书的数据。

list_of_book_data = []
for filename in description_files:
    filepath = os.path.join(BOOK_DESCRIPTIONS_FOLDER, filename)
    with open(filepath, 'r') as f:
        lines = f.readlines()
    for line in lines:
        if line.strip(): # 忽略空行
            parts = line.strip().split(':::')
            if len(parts) == 4:
                book_dict = {
                    "id": parts[0],
                    "title": parts[1],
                    "author": parts[2],
                    "description": parts[3]
                }
                list_of_book_data.append(book_dict)

接下来,我们使用三个冒号作为分隔符来提取书籍标题、作者和描述文本,并将它们格式化为每本书一个字典。每个文件产生的书籍字典列表会被存储在一个名为 list_of_book_data 的大列表中。

你可以使用 IPython 的 JSON 函数以可展开的结构化格式显示该列表的内容。

JSON(list_of_book_data)

请随时暂停视频,检查你的书籍数据是否正确添加。


创建文本向量嵌入

在 RAG 应用中,向量嵌入用于实现语义搜索——即基于输入文本寻找相似文本。你每天交互的许多 AI 应用都使用 RAG 来提供更好的答案。例如,电商平台上的聊天机器人很可能能够访问其所有在售产品的最新专有产品描述的向量嵌入,因此它能比通用聊天机器人提供更准确的购物推荐。我们的原型应用也实现了相同的原理。

接下来,让我们为书籍描述创建嵌入向量。

我们使用 fastembed 的 TextEmbedding 来实例化嵌入模型,然后遍历书籍数据列表,获取每本书的描述。.embed 方法用于创建向量嵌入。list() 函数用于确保将生成器对象转换为 Python 列表。

embedding_model = TextEmbedding(model_name=EMBEDDING_MODEL)
descriptions = [book["description"] for book in list_of_book_data]
embeddings_list = []
for desc in descriptions:
    # .embed 返回一个生成器,我们取第一个(也是唯一一个)嵌入向量
    emb = list(embedding_model.embed(desc))[0]
    embeddings_list.append(emb)
print(f"Created {len(embeddings_list)} embeddings.")

将数据插入向量数据库

现在,你可以将嵌入向量列表和格式化后的书籍数据列表压缩(zip)在一起,并将数据插入 Voyage 数据库。

每本书的标题、作者、描述以及向量嵌入被定义为一个数据对象,准备放入 Voyage 集合中。Voyage 支持使用 .insert_many 方法进行批量插入。

# 准备要插入的数据对象
data_objects = []
for book, emb in zip(list_of_book_data, embeddings_list):
    obj = {
        "id": book["id"],
        "title": book["title"],
        "author": book["author"],
        "description": book["description"],
        "vector": emb.tolist() # 将 numpy 数组转换为列表
    }
    data_objects.append(obj)

# 批量插入到集合中
if data_objects:
    collection.insert_many(data_objects)
    print(f"Inserted {len(data_objects)} book records into the '{COLLECTION_NAME}' collection.")

现在,一切就绪,可以获取书籍推荐了。


查询与获取推荐

我想读一本哲学书。这个查询字符串会使用相同的嵌入模型进行向量化,然后执行“近邻向量搜索”,以找到描述与查询字符串最匹配的书籍。

query = "I would like to read a philosophical book."
# 为查询文本创建嵌入向量
query_embedding = list(embedding_model.embed(query))[0]

# 在集合中搜索最相似的书籍
results = collection.search(query_embedding, limit=1)
for result in results[0]: # results 是一个列表的列表
    book_id = result.id
    # 根据 ID 找到对应的书籍信息
    recommended_book = next((book for book in list_of_book_data if book["id"] == book_id), None)
    if recommended_book:
        print(f"Recommended for you: {recommended_book['title']} by {recommended_book['author']}")
        print(f"Description: {recommended_book['description']}")

运行这个单元格,我得到了《The Idea of the World》的推荐——这确实是一本非常哲学的书。

请暂停视频,尝试不同的查询,实验一下是否能得到你之前添加的、自己最喜欢的书的推荐。


总结与展望

好了,你现在处于一个熟悉的境地:你拥有一个可工作的 AI 应用原型。在本例中,这是一个能够提供书籍推荐的 RAG 类型应用。在真实场景中,例如将其集成到书店网站的聊天机器人里,你将需要定期向数据库中添加新书,理想情况下这个过程是自动化的,并且需要对操作是否成功具备可观测性,能够防范 API 速率限制等瞬时问题,并在出现错误时发出警报。

这正是工作流编排发挥作用的地方。你已经有了原型,让我们在 Airflow 中将其转化为一个管道吧。在下一课中,你将学习如何创建基本的 Airflow 管道并在 Airflow UI 中与它们交互,然后使用本 Notebook 中的代码来构建你的 RAG 管道。

本节课中我们一起学习了如何构建一个完整的 RAG 应用原型,包括数据准备、向量嵌入生成、向量数据库操作以及语义搜索查询。这些步骤构成了许多现代 AI 应用的核心流程,为后续的自动化编排奠定了坚实的基础。

004:构建一个简单的流水线 🛠️

在本节课中,我们将构建第一个流水线,即一个仅使用三个 Airflow 特定模块的简单 Airflow DAG。构建完成后,我们将在 Airflow 用户界面中手动探索并运行这个 DAG。

访问 Airflow 环境 🖥️

在本次及后续课程中,你将拥有一个功能齐全的 Airflow 环境。

首先,运行第一个单元格以获取 Airflow 环境用户界面的链接。点击该链接将在新标签页中打开 Airflow UI,你可以使用用户名 airflow 和密码 airflow 登录。

登录后,你将进入 Airflow 环境的主页。此页面包含一些快速链接,以及 Airflow 环境各组件当前的健康状态。请注意,由于本课程不需要,触发器组件未在沙箱中运行。

主页还显示了你 Airflow 环境中 DAG 和任务运行的近期历史。目前,这些统计数据还是空的。

一个小提示:点击左下角的用户按钮,如果你愿意,可以将 UI 切换到深色模式。为了便于视频演示,本课程将坚持使用浅色模式。

Airflow 核心组件概览 ⚙️

现在,我们有了一个正在运行的 Airflow 环境。让我们简要了解一下使你能够运行流水线的 Airflow 组件。

此环境中运行的组件包括:

  • Airflow 元数据库:存储所有对 Airflow 运行至关重要的信息,例如任务状态。
  • 调度器:Airflow 的核心,确保所有 DAG 和任务按正确顺序运行。
  • API 服务器:完成多项任务,目前对你最重要的两项是:为你提供交互的 Airflow UI;作为工作节点与 Airflow 元数据库之间的接口。
  • 工作节点:实际运行你任务代码的 Airflow 组件。在 Airflow 2 及更高版本中,它们不再像 Airflow 1 那样直接与元数据库交互,而是使用 API 服务器的任务执行接口。
  • DAG 处理器:解析你的 DAG 文件,并将序列化版本写入 Airflow 元数据库。

接下来,我们将添加第一个 Airflow DAG,供 DAG 处理器解析并添加到你的环境中。

创建第一个 DAG 📝

向 Airflow 添加 DAG 的最简单方法是在 Airflow 项目文件的指定位置(通常是顶级文件夹 dags)的独立 Python 文件中定义它们。

在这个笔记本环境中,你可以使用单元格魔法将一个笔记本单元格的内容写入 Airflow 项目 dags 文件夹中的 Python 文件。使用的魔法命令是 %%writefile。此命令特定于此学习环境。在常规的 Airflow 项目中,你需要在 dags 文件夹中创建一个新的 Python 文件,然后直接向其中添加代码。

在 Airflow 中,流水线由 DAG 表示。可以通过编写一个常规的 Python 函数,并用从 airflow.decorators 导入的 @dag 装饰器装饰它来创建 DAG。在此函数上下文中实例化的每个任务都会自动添加到 DAG 中。

目前,这个 DAG 是空的。让我们添加一个任务。

你可以使用 @task 装饰器将任何 Python 函数转换为 Airflow 任务。

my_task_1 是一个非常简单的函数,只返回一个小字典。你可以使用 @task 将此函数转换为 Airflow 任务,然后将被调用的任务函数的输出分配给一个对象,以便在下游使用。

接下来,你可以用同样的方式定义 my_task_2。此任务接受一个名为 my_dict 的参数,并打印字典中一个键的值。调用此任务函数时,你可以传入第一个任务的输出给 my_dict,这样第二个任务就会将 airflow 打印到日志中。你可以随意将此字典中 my_word 的值更改为你选择的单词。

运行此单元格以将此 DAG 保存到 dags 文件夹。Airflow 的 DAG 处理器组件会自动检查 dags 文件夹中的新 DAG,并将其添加到 Airflow UI 中。在此环境中,它每 30 秒执行一次。让我们在 30 秒内回到 Airflow UI 查看。

在 UI 中查看并运行 DAG 🚀

在 Airflow UI 中,你可以在 DAGs 页面(可通过侧边栏的第二个按钮访问)看到所有的 DAG。你的第一个 DAG 就在那里。

点击 DAG 名称进入 DAG 概览页面。从这里,你可以访问关于 DAG 的所有信息,从其结构、运行历史到所有已运行任务的日志。

目前看起来有点空。让我们手动运行这个 DAG。

点击右上角的蓝色 Trigger DAG 按钮,然后点击 Trigger 以创建 DAG 的手动运行。确保选中 Unpause My First DAG on trigger 复选框,因为暂停的 DAG 无法运行。

现在暂停视频并运行你的第一个 DAG。你可以随意运行多次。让我们至少创建三次运行。

很好,你现在有了一些 DAG 运行历史。每个条形代表此 DAG 的一次先前运行,每个方块代表一个任务实例(即该 DAG 运行中任务的一次运行)。你可以通过点击方块来访问任务的日志。在这里,你可以看到这次 my_task_2 的运行将 airflow 打印到了日志中。

除了这个称为 Grid 的先前 DAG 运行概览外,你还可以在 UI 中看到 Airflow DAG 的图。点击 DAG ID 下方的 Graph 图标,你可以看到此 DAG 中的两个任务。默认情况下,DAG 从左到右读取。在这种情况下,my_task_1 需要成功完成后,my_task_2 才能运行。但在 Options 菜单中,你可以更改此行为,例如,如果你更喜欢从上到下查看任务。

添加第三个任务并定义依赖 🔗

让我们向 DAG 添加第三个任务,该任务在 my_task_1 成功完成后立即运行。

回到你的笔记本,你可以添加另一个任务,我将其称为 my_task_3,并添加一个简单的打印语句。

现在,如何让这个任务在 my_task_1 之后运行?对于 my_task_1my_task_2,Airflow 基于 my_task_1 的结果被 my_task_2 使用而自动推断出了依赖关系。

my_task_3 不接受任何参数。你必须显式定义依赖关系。

为此,从 airflow.models.baseoperator 导入 chain 函数。chain 在这里定义了 my_task_1my_task_3 之间的显式依赖关系。

运行单元格后,DAG 文件在 dags 文件夹中被更新,最多 30 秒内,DAG 将在 Airflow UI 中更新。

你可以随意命名你的任务。用 @task 装饰的函数的名称将成为 Airflow 内部任务的名称。同样,你可以在函数中运行比简单打印语句更复杂的代码,Airflow 可以执行任何 Python 代码。

太棒了,让我们在 Airflow UI 中一起探索这个三任务的 DAG。

探索更新后的 DAG 图 🔄

你现在可以看到第三个任务已被添加。它依赖于 my_task_1,并与 my_task_2 并行运行。这是 Airflow 的一大优势:只要满足要求,你可以并行运行许多任务。

你还可以看到最新的 DAG 版本已更新为 v2。Airflow 会自动跟踪 DAG 的结构变化。这个称为 DAG 版本控制 的功能是在 Airflow 2 中添加的。

你可以通过在 Options 菜单中选择版本来查看 DAG 图的过去版本。此外,Code 标签页(你可以查看但不能编辑 DAG 代码)也允许你查看先前 DAG 版本的代码。

实践:创建第二个 DAG ➕

好了,现在你知道了将 Python 代码转换为 Airflow DAG 中任务所需的一切。让我们通过向此环境添加第二个 DAG 来练习一下。

以下是第二个 DAG 的代码。这是代表 DAG ID 的 @dag 装饰函数的名称。重要的是,在 Airflow 环境中,所有 DAG 的 DAG ID 必须是唯一的。除此之外,你可以自由创建包含任意多任务的 DAG。我选择创建一个包含四个任务并做一点数学运算的 DAG。

请注意,此 DAG 中的 chain 函数定义了一个稍微复杂的依赖结构。传递给 chain 函数的第一个元素是一个包含两个对象的列表,每个对象引用一个任务。第二个元素是单个任务。这样就创建了两个依赖关系:一个在 my_task_2my_task_4 之间,另一个在 my_task_3my_task_4 之间。

这两个依赖关系被添加到 Airflow 基于任务间传递数据而推断出的现有依赖关系之上。my_task_1my_task_2 由于推断的依赖关系而位于 my_task_3 的上游。

请注意,只要列表长度相同,你也可以在两个任务列表之间定义依赖关系,并且可以根据需要向 chain 函数添加任意多个参数。

准备好后,运行单元格以将文件保存到 dags 文件夹。最多 30 秒后,它将出现在 Airflow UI 中,你可以在那里创建任意多次 DAG 运行并探索 DAG。

如果你在运行之间进行结构更改(例如添加或重命名任务),你将创建 DAG 的多个版本。

总结 📚

在本节课中,我们一起学习了如何构建简单的 Airflow 流水线。我们首先访问并了解了 Airflow 环境及其核心组件。然后,我们创建了第一个 DAG,添加了任务,并在 Airflow UI 中手动运行了它。我们还学习了如何显式定义任务间的依赖关系,并探索了 DAG 版本控制功能。最后,我们通过创建第二个更复杂的 DAG 来巩固所学知识。

现在,你可以随意进行实验。一旦你对创建简单的 DAG 充满信心,就可以进入下一课,在那里你将把上一课中生成式 AI 工作流原型的代码转换为两个 Airflow DAG。

005:将原型转化为流水线 🚀

在本节课中,我们将学习如何将一个在Jupyter笔记本中开发的生成式AI原型,转化为一个可调度、可维护的Airflow流水线。我们将编写两个有向无环图,一个用于数据摄取、嵌入和加载,另一个用于查询向量数据库。

访问Airflow环境

与上节课类似,你可以运行第一个单元格来获取Airflow环境Web UI的链接,并使用用户名 Airflow 和密码 Airflow 登录。

使用相同的魔术命令 %writefile,你可以添加你的两个DAG。第一个DAG用于摄取、嵌入书籍描述并将其加载到Weaviate向量数据库,命名为 fetch_data。第二个DAG用于查询向量数据库,命名为 query_data

定义DAG结构

与上节课一样,我们使用 @dag 装饰器创建DAG,并使用 @task 装饰器填充任务。

编写流水线的第一步是定义DAG结构。即,决定各个任务将执行什么操作、以什么顺序执行,并创建空任务。这里需要记住第一课的重要原则:尽量使任务原子化,以便在出现故障时能更容易地从失败点恢复。

原型中“摄取、嵌入和加载”的部分可以表达为五个任务:

  1. 在向量数据库中创建集合(如果尚不存在)。
  2. 列出流水线可用的书籍描述文件。
  3. 将这些文件转换为字典列表。
  4. 创建向量嵌入。
  5. 将这些嵌入加载到向量数据库中。

大多数任务使用其前一个任务的输出作为输入。例如,创建向量嵌入的任务从上游的“转换书籍描述文件”任务获取转换后的书籍数据。唯一的例外是“如果不存在则创建集合”任务,它尚未嵌入依赖结构中。你可以使用 chain 函数来决定此任务在DAG中与其他任务的关系。一个合理的位置是在将嵌入加载到数据库之前,因为它负责准备向量数据库。

query_data DAG目前只有一个任务,名为 search_vector_db_for_book。在这个DAG的第一个版本中,你可以硬编码查询字符串参数的输入。

暂停视频并运行包含结构化DAG的两个单元格,将文件写入DAG文件夹。

检查并运行DAG

在Airflow UI中查看DAG并手动运行它们。请记住,在此环境中,新DAG可能需要长达30秒才会显示。

很好,你看到任务成功完成,但它们实际上还没有执行任何操作。接下来,我们将为每个任务填充代码。

填充任务代码

这些代码与第二课中的代码非常相似,大多数情况下,你可以将原型笔记本中的代码稍作修改后用于Airflow任务。下面我将指出每个任务所需的修改。

在多个任务中使用的变量可以在DAG的顶层声明以确保一致性。对于这个DAG,你将定义三个变量:集合名称、书籍描述文件夹和嵌入模型名称。请注意,默认情况下,每当Airflow解析DAG文件夹以查找更改时(默认每30秒),DAG文件顶层的代码都会被执行。这意味着你应该避免在DAG顶层运行耗时长的代码或建立外部系统连接。想象一下每30秒对你的数据库运行一次代价高昂的查询,我们应该避免这种情况。

对于“如果不存在则创建集合”任务,需要改变的是使用的向量数据库类型。笔记本使用嵌入式Weaviate数据库,这对于原型设计非常有用,但在生产环境中,你需要连接到本地或云端的托管数据库。你可以通过使用Airflow中的钩子来建立此连接。在Airflow中,钩子是能够连接到外部服务的类。例如,AWS、Google Cloud,以及像这里的Weaviate。它们作为Airflow提供者包的一部分可用。这里我们从已安装在Airflow环境中的Weaviate提供者包导入 WeaviateHook。连接是通过使用安全存储在Airflow内部、通过连接ID字符串引用的连接对象中的凭据创建的。在我们的示例中,连接ID是 my_weaviate_conn,我们已经为你将其创建为环境变量。有关如何设置Weaviate和Airflow的更多信息,请务必查看本课末尾的资源部分以及本课程末尾的可选视频。此任务的其余部分使用与笔记本中完全相同的代码来检查我们想要使用的集合是否已存在,如果不存在则创建它。

接下来的两个任务,“列出书籍描述文件”和“转换书籍描述文件”,也使用相关笔记本单元格中完全相同的代码。只需要进行两项修改:首先,你需要在任务函数的开头导入任务中使用的包;其次,你需要返回想要在下一个任务中使用的值。例如,“列出书籍描述文件”任务返回书籍描述列表,然后“转换书籍描述文件”任务将其用作输入。

创建向量嵌入也使用与笔记本单元格几乎相同的代码,只有一个小改动。默认情况下,只有一些数据类型可以在Airflow任务之间传递,例如可JSON序列化的数据或pandas数据框。因此,在传递给下一个任务之前,嵌入被转换为浮点数以确保JSON可序列化。在生产环境中,当在任务之间传递大量数据时,你会使用云存储解决方案,例如Amazon S3。这可以通过在任务代码中显式将文件写入云存储,或者通过更改Airflow配置以自动将任务之间传递的数据存储在第三方位置来实现。

fetch_data DAG的最后一个任务将嵌入加载到Weaviate。除了添加任务中使用的所有包的导入以及使用Weaviate钩子连接到Weaviate之外,代码与笔记本中的相同。运行单元格保存后,Airflow UI的代码选项卡会显示更新后的代码。现在触发DAG运行会导致 include/data 文件夹中文件内的所有书籍描述被摄取、嵌入并加载到与本地Airflow环境一起在Docker容器中运行的Weaviate实例中。

很好,现在你只需要完成 query_data DAG,以便能够使用Airflow DAG对这些数据运行近似向量搜索。这个DAG很简单,它只有一个任务,运行与笔记本中相应单元格相同的代码。唯一的改变是使用Weaviate钩子来建立与向量数据库的连接。通过手动运行DAG,你可以在任务日志中获得打印的书籍推荐。你可以随意将查询字符串更改为你正在寻找的书籍类型。

总结与展望

这很有趣,但在现实中,这样的流水线需要自动运行。想象一下你在一个在线书店工作,可用书籍列表会频繁变化,DAG需要定期运行以创建和添加新的书籍嵌入。调度流水线是Airflow的核心功能之一,也是我们下一课将要涵盖的内容。

在本节课中,我们一起学习了如何将Jupyter笔记本中的生成式AI原型转化为一个结构化的Airflow流水线。我们定义了DAG结构,填充了任务代码,并了解了在生产环境中需要考虑的修改,例如使用钩子连接外部服务和确保数据可序列化。最后,我们成功运行了流水线,实现了数据的自动化处理与查询。

006:调度与 DAG 参数 🗓️

在本节课中,我们将学习如何调度之前编写的两个 DAG,使其能够自动运行。其中一个 DAG 将基于时间调度,每小时运行一次。另一个 DAG 将基于数据感知调度,一旦有新的嵌入数据被摄取到向量数据库,它就会立即运行。

概述

上一节我们编写了用于获取数据和查询数据的 DAG。本节中,我们将看看如何让这些 DAG 自动运行,而不是每次都手动触发。我们将介绍两种最常见的调度类型:基于时间的调度和基于数据感知的调度,并学习如何为 DAG 设置参数。

从手动到自动运行

在开发和执行临时工作流时,从 Airflow UI 手动运行 DAG 非常方便。但在大多数情况下,我们希望管道能够自动运行。例如,假设您在一家书店工作,新书会不断上架,您会希望管道能自动运行,为新添加的图书描述创建嵌入向量,以便顾客能够找到它们。

调度管道是 Airflow 的核心功能之一,它提供了多种不同的选项。

准备工作

在更新 DAG 之前,请确保上一课的 DAG(fetch_dataquery_data)已经存在于您的 Airflow UI 中。由于实验环境会在 120 分钟后重置,您的 Airflow UI 可能不包含这些 DAG。

如果出现这种情况,请暂停视频,运行笔记本中第 5.2 节提供的代码单元。然后,您应该在 Airflow UI 中看到这两个 DAG。在进入调度部分之前,请确保手动触发它们一次。另外,对于本节课,您无需担心 my_first_dagmy_second_dag

我们将介绍两种最常见的调度类型:基于时间的调度和基于数据感知的调度。您可以在本笔记本末尾的资源部分找到涵盖所有调度选项的指南链接。

基于时间的调度

在 Airflow 中,您可以通过向 @dag 装饰器添加 DAG 参数来调度每个 DAG。

让我们将获取图书数据的 DAG 调度为在每个小时的整点自动运行。为此,您需要向 @dag 装饰器添加 schedule 参数,并将其设置为每小时一次的 cron 字符串 0 * * * *,或者使用 Airflow 的简写形式 @hourly

您还需要为 DAG 指定一个 start_date。在此日期之后,DAG 将根据其调度计划运行。请确保 DAG 的开始日期是过去的某个时间。

from airflow.decorators import dag
from datetime import datetime

@dag(
    schedule="@hourly",  # 或 schedule="0 * * * *"
    start_date=datetime(2023, 1, 1),
    ...
)
def fetch_data_dag():
    # ... 任务定义

运行代码单元将更改保存到 DAG 文件夹后,您可以在 Airflow UI 的 DAG 列表和单个 DAG 页面上看到显示的调度信息。请注意,DAG 调度的更改属于结构性 DAG 变更,因此会创建一个新的 DAG 版本。

现在,只要 DAG 处于“未暂停”状态(即蓝色开关设置为“开”),它就会在每个小时的整点自动运行。基于时间的调度非常有用,但通常您并不确切知道 DAG 应该在何时运行,只是希望它在相关数据准备就绪时运行。

基于数据感知的调度

在我们的案例中,我们希望确保查询数据的 DAG 在嵌入向量加载到向量数据库后立即运行。这时,基于 Airflow 资产的数据感知调度就派上用场了。

Airflow 中的资产是一个对象,它代表管道中任何位置的真实或抽象数据对象。它可以代表云存储中的文件、关系数据库中的表,或者在我们的案例中,代表向量数据库中的一个集合。

作为 DAG 的作者,您可以通过将资产分配给任务的 outlets 参数来决定哪个任务更新哪个资产。任务实际上并不需要与底层数据对象交互,但在本例中,它确实进行了交互。当此任务成功完成时,将为该资产创建一个资产事件,表明资产已被更新。

下游的 DAG(在我们的案例中是 query_data)可以基于一个或多个资产接收到新资产事件来进行调度。您可以将资产想象成小旗子,任务在成功完成后会挥舞这些小旗子;而数据感知调度就像是告诉一个 DAG,等待正确的旗子组合被举起。

让我们为 fetch_data DAG 中的最后一个任务分配一个要更新的资产。您可以使用任务的 outlets 参数来添加资产。此参数在任何任务中都可用,包括传统的 Airflow 操作符。将 outlets 参数设置为该任务更新的资产列表。在我们的案例中,任务将嵌入向量加载到向量数据库。我们称此资产为 my_book_vector_data

@task(outlets=[Dataset("my_book_vector_data")])
def load_embeddings_to_vector_db(...):
    # ... 任务逻辑

在 Airflow UI 的 DAG 图中,您可以通过打开选项菜单并在“依赖关系”字段中选择“外部条件”来查看 DAG 连接到的资产。现在,fetch_data DAG 中的 load_embeddings_to_vector_db 任务会更新 my_book_vector_data 资产。

很好,现在让我们基于对此资产的更新来调度 query_data DAG。

query_data DAG 的 @dag 装饰器中,添加 schedule 参数并将其设置为该 DAG 应等待的资产。请确保使用与添加到 fetch_data DAG 中的资产完全相同的名称 my_book_vector_data。该名称用于标识资产,并将更新它的上游 DAG 与基于它调度的下游 DAG 连接起来。

@dag(
    schedule=[Dataset("my_book_vector_data")],
    ...
)
def query_data_dag():
    # ... 任务定义

这种连接在 Airflow UI 中是可见的。如果您点击“资产”按钮,可以在资产列表中看到 my_book_vector_data 资产。点击它以打开资产图,该图显示了与此资产连接的所有 DAG。从这个视图,您可以暂停和取消暂停 DAG,并通过点击其名称直接导航到相关的 DAG。

您还可以使用屏幕右上角的蓝色按钮手动创建资产事件。可用的两个选项是:

  • Materialize:触发资产上游的 DAG 运行。如果 load_embeddings_to_vector_db 任务成功,则创建一个资产事件。
  • Manual:仅创建资产事件,导致下游 DAG 运行。

后一个选项在开发过程中测试资产调度时非常有用。

请注意,也可以使用 Airflow REST API 从 Airflow 外部创建资产事件。

很好!现在两个 DAG 通过数据感知依赖关系链接在一起。在编写复杂的 Airflow 管道时,通常会有许多 DAG 通过许多资产链接起来,以便在相关数据更新时,它们能像多米诺骨牌一样全部运行。

其他 DAG 参数

除了 schedule 之外,还有许多其他 DAG 参数可以用来修改 DAG 的行为。您可以在资源部分找到完整列表的链接。

另一个对 query_data DAG 有用的 DAG 参数是 params 参数。它允许您修改手动触发 DAG 时显示的表单,以请求特定的输入。让我们使用此参数,允许用户在 Airflow UI 中手动运行 DAG 时,为查询字符串参数输入不同的值。

@dag 装饰器中,您可以添加 params 作为参数,并将其设置为参数字典,其中包含每个输入的默认值。当手动运行 DAG 时,您的用户将能够覆盖此默认值。

@dag(
    params={"query_string": "a book about AI"},
    ...
)
def query_data_dag():
    # ... 任务定义

参数字典中的值存储在 Airflow 上下文中。上下文是一个包含有关 Airflow DAG 运行信息的字典,可以在 Airflow 任务内部访问。

要访问上下文,请在 Airflow 任务的定义中放入 **context。在我们的案例中,这将替换 query_string 参数,但 Airflow 上下文也可以与任意数量的任务参数一起使用。

在任务函数内部,使用上下文变量上的 params 键,并用您的参数名(本例中为 query_string)对其进行索引。然后,查询字符串的值将被任务的其余部分用于执行神经向量搜索。请确保也从任务调用中移除 query_string 的硬编码值。

@task
def query_vector_db(**context):
    query = context["params"]["query_string"]
    # ... 使用查询字符串执行搜索

保存更改后,Airflow UI 现在在手动运行 DAG 时会显示一个用于输入查询字符串的字段。

例如,输入“我想读一本关于睡眠的书”。检查 DAG 运行的日志,您可以看到任务现在推荐了一本关于清醒梦的不同书籍,非常完美。

如果您想为本课添加自己的包含图书描述的文件以供查询,请运行辅助代码单元。

总结

在本节课中,我们一起学习了如何为 DAG 设置自动调度。我们介绍了两种主要的调度方式:基于时间的调度(如每小时运行)和基于数据感知的调度(依赖资产事件)。我们还学习了如何使用 params 参数,让用户能在手动触发 DAG 时提供自定义输入。

现在,您的 DAG 已经完成了调度,并且查询数据 DAG 包含了一个方便用户从 Airflow UI 查询不同书籍的选项。这些 DAG 已基本准备好投入生产。下一课将重点介绍如何进一步并行化此管道,使其更高效且更易于排查问题。

007:使工作流具备适应性 🛠️

在本节课中,我们将学习如何利用 Airflow 的高级特性——动态任务映射,使你的数据处理流水线能够在运行时根据数据量进行自适应调整。通过这项技术,你可以为每个书籍描述文件创建并行的任务实例,从而使流水线更易于调试、更加健壮。

为何需要动态任务映射?🤔

在运行 fetch_data 流水线时,你可能已经注意到它并非完全原子化。所有书籍描述文件都在同一个任务的 for 循环中被处理。这在原型设计和开发阶段运行良好。

但设想一个在线书店,每天需要处理成百上千个书籍描述文件。如果其中一个文件存在格式错误,整个 for 循环就会失败,导致整个任务失败。为了恢复,需要重新运行整个任务,即使那些没有问题的文件也需要被再次处理。

对于涉及与 AI 模型交互的任务(如嵌入或推理任务),这种重复处理会迅速增加成本。

幸运的是,Airflow 的动态任务映射功能可以解决这个问题。它允许你创建一个在运行时能根据数据量进行自适应的流水线。具体来说,你可以根据 DAG 运行时确定的输入,创建可变数量的任务副本。

构建一个简单的映射示例 🧪

动态任务映射最好通过实践来学习。在修改 fetch_data 流水线之前,我们先构建一个简单的 DAG 来熟悉这个功能。

以下代码定义了一个名为 simple_mapping 的新 DAG。第一个任务 get_numbers 使用随机包返回一个长度可变的列表。

from airflow.decorators import dag, task
import random

@dag(schedule=None, start_date=pendulum.datetime(2023, 1, 1, tz="UTC"), catchup=False)
def simple_mapping():
    @task
    def get_numbers():
        # 模拟不同数量的待处理文件
        return random.choice([[], [1], [1, 2], [1, 2, 3]])

    @task
    def mapped_task_1(my_constant_arg, my_changing_arg):
        return my_constant_arg + my_changing_arg

    # 动态映射任务
    mapped_task_1.partial(my_constant_arg=10).expand(my_changing_arg=get_numbers())

simple_mapping_dag = simple_mapping()
  • get_numbers 任务:返回四种可能的列表之一:空列表(模拟没有新文件)、包含1个、2个或3个整数的列表(模拟有1、2或3个新文件)。
  • mapped_task_1 任务:这是一个将被动态映射的任务。它有两个参数:my_constant_arg(每个副本都相同)和 my_changing_arg(每个副本不同)。
  • .partial() 方法:用于指定所有任务副本中保持不变的参数。
  • .expand() 方法:用于指定在任务副本间变化的参数,它需要被设置为一个列表。在本例中,它被设置为 get_numbers 任务返回的列表。

这意味着,对于 get_numbers 返回列表中的每个元素,都会创建一个 mapped_task_1 的任务副本,每个副本处理列表中的一个元素作为 my_changing_arg 的输入。

运行此 DAG 后,你可以在 Airflow UI 的网格视图中看到效果。点击 mapped_task_1 任务方块,可以查看每次运行创建了多少个动态映射的任务实例。如果上游任务返回空列表,则该任务会被完全跳过(在网格视图中显示为粉色方块)。

链接动态映射的任务 ⛓️

在 Airflow 中,你经常需要并行化处理数据的多个步骤。在 fetch_data 流水线中,我们希望为文件转换和描述嵌入这两个步骤,每个书籍描述文件都创建一个并行任务。这可以通过将上游动态映射任务的输出传递给下游任务的 .expand() 方法来实现。

以下是如何在简单示例中链接第二个动态任务:

    @task
    def mapped_task_2(my_cookie_number):
        return my_cookie_number * 2

    # 链接动态任务:mapped_task_2 映射 mapped_task_1 的输出
    mapped_task_2.expand(my_cookie_number=mapped_task_1.partial(my_constant_arg=10).expand(my_changing_arg=get_numbers()))
  • mapped_task_2 任务:这是第二个动态映射任务。
  • 链接:在 mapped_task_2.expand() 中,我们将 mapped_task_1 的输出(即其所有副本的返回值列表)传递给 my_cookie_number 参数。
  • 结果mapped_task_1mapped_task_2 将始终拥有相同数量的动态映射任务实例。

你可以在 Airflow UI 中多次运行 DAG 来确认这一点,检查不同 DAG 运行中的任务实例数量。

改造 fetch_data 流水线 🔧

现在,我们可以将动态任务映射应用到 fetch_data 流水线中。

决定动态映射任务数量的任务是 list_book_description_files,它返回一个包含所有书籍描述文件名的列表。

将被动态映射以并行处理每个文件(替代 for 循环)的任务是 transform_book_description_filescreate_vector_embeddings。每个任务只需要进行少量修改。

对于 transform_book_description_files 任务:

  1. 输入变更:输入从书籍描述文件列表变为单个文件名。
  2. 移除循环:由于输入现在是单个文件,可以移除遍历文件列表的 for 循环。
  3. 调整返回值:返回值从一个列表的列表,改为仅返回单个文件内所有书籍的描述列表。
  4. 应用动态映射:在函数调用中使用 .expand() 方法,而不是直接调用任务。

对于 create_vector_embeddings 任务:

  1. 输入变更:输入从书籍数据列表变为单个书籍数据。
  2. 移除循环:移除 for 循环。
  3. 调整返回值:相应调整返回值。
  4. 应用动态映射:在函数调用中使用 .expand() 方法。

任务中的其他代码可以保持不变,下游的 load_embeddings_to_vector_db 任务也无需修改。

保存更改并运行 DAG 后,你可以在 Airflow UI 的网格视图中点击 transform_book_description_filescreate_vector_embeddings 任务方块,查看创建的任务实例数量。这个数量对应于你的文件位置中当前可用的书籍描述文件数量。

你可以使用辅助单元添加更多书籍描述文件,来改变此 DAG 中动态映射任务实例的数量。

总结 📝

本节课中,我们一起学习了 Airflow 的动态任务映射功能。我们了解了为何需要它来构建健壮的流水线,并通过构建简单示例和改造现有流水线,掌握了如何创建和链接动态映射的任务。

动态任务映射的主要优势在于:

  • 易于调试:如果一个动态映射任务失败,你可以直接检查其日志并在必要时重新运行它,而无需重新运行同一任务的其他实例。
  • 提高效率:特别是当有大量任务副本时,可以避免因单个文件错误而重复处理所有文件。

现在,你已经对如何使用动态任务映射来并行化 Airflow 任务有了扎实的理解。让这个流水线为生产环境做好准备的最后一步,是为任务失败事件做准备。在下一课中,你将学习如何配置任务以在失败时自动重试并向你发送警报。

008:准备应对失败 🛡️

在本节课中,我们将学习如何使你的数据处理流水线(Pipeline)更加健壮。你将了解如何添加自动重试机制来应对临时性故障,并学习在任务或整个工作流失败时如何添加通知。

概述

上一节我们介绍了如何构建基础的工作流。本节中,我们来看看如何增强其容错能力。我们将通过模拟任务失败、设置自动重试、调整任务触发规则以及配置失败回调通知,来构建一个能够优雅处理错误的、生产就绪的流水线。

模拟任务失败

首先,我们通过制造一个错误来观察工作流的失败行为。在 create_collection_if_not_exists 任务中,添加一行会导致Python零除错误的代码。

print(10 / 0)

在Airflow中,任务内的任何错误或异常都会导致该任务失败,并且错误行之后的代码将不会被执行。这与Python脚本或Jupyter Notebook的行为一致。

保存修改后的DAG文件后,你可以在Airflow UI中手动触发一次DAG运行。create_collection_if_not_exists 任务的失败将导致整个DAG运行失败。

查看与理解失败状态

在Airflow UI的DAG概览页面,你可以看到最近失败任务的错误日志摘要,并有一个快速链接可以查看完整日志。

无论是在网格视图还是图形视图中,你都能看到失败任务如何导致其下游任务 load_embeddings_to_vector_db 进入 upstream_failed 状态。这意味着该下游任务没有运行,因为它的上游任务并未全部成功完成。

从失败中恢复

要从此类失败中恢复,首先需要修复代码错误。移除导致错误的 print 语句并保存DAG文件。

然后,回到Airflow UI。你可以使用“清除任务实例”按钮来重新运行失败的任务。如果侧边栏已折叠,该按钮显示为一个向前弯曲的箭头。

点击此按钮后,你会看到几个选项。一个常见的选择是 downstream。这意味着你不仅会重新尝试失败的任务,还会尝试其所有下游任务。

对于复杂工作流中的多个失败任务,通常更快的方法是清除整个DAG运行。在网格视图中点击失败的DAG运行的红色条,然后选择“清除运行”或向前弯曲的箭头图标。默认情况下,打开的菜单会重新尝试DAG中的所有任务,但你也可以将其设置为仅清除失败或上游失败状态的任务。

实现自动重试

手动恢复适用于已知错误,但如果故障是暂时的(例如API在周六晚上宕机几分钟),我们更希望Airflow能自动重试,而不是等到周一再手动处理。

这时就需要用到自动任务重试功能。retries 是一个任务参数,它决定了每个任务在最终失败前有多少次重试机会。你可以使用 retry_delay 参数来设置重试之间的等待时间。

在实践中,这是Airflow最强大也最容易实现的功能之一。一个常见策略是为DAG中的所有任务设置默认的重试次数。

你可以使用 default_args 参数来为DAG中的所有任务定义默认参数。将其设置为一个包含 retries=1retry_delay=timedelta(seconds=10) 的字典,将为该DAG中的每个任务提供一次失败后的重试机会,并在10秒后进行第二次尝试。

保存更改后,重新添加 print(10 / 0) 这行代码来模拟失败。在Airflow UI中手动运行DAG,你会看到 create_collection_if_not_exists 任务不会像之前那样立即失败。相反,其状态会变为 up_for_retry(显示为带向前箭头的黄色方块)。10秒后,任务会再次尝试。在这个例子中,它仍然会遇到相同的错误并最终失败。但如果问题是暂时的(如API不可用或达到速率限制),第二次运行就可能会成功。

覆盖默认重试设置

如果某个任务需要比其他任务更多或更少的重试次数,该怎么办?这很简单。你可以通过在单个 @task 装饰器中提供相同的参数,来覆盖在 default_args 字典中定义的所有默认值。

例如,添加 retries=5retry_delay=timedelta(seconds=2) 会给这个任务在初次失败后5次额外的重试机会,总共6次尝试,每次间隔2秒。

调整任务触发规则

另一个常见情况是,某些任务经常失败,但你仍然希望其下游任务能够运行。在本例中,如果集合已经存在于向量数据库中,那么即使 create_collection_if_not_exists 任务失败,向其加载数据仍然可以工作。

默认情况下,Airflow任务要求其所有上游任务都成功才能运行。这被称为触发规则,默认的规则叫做 all_success

Airflow提供了许多其他触发规则。一个实用的规则是 all_done,它使得任务在其所有上游任务完成后就运行,无论这些上游任务的最终状态是成功、失败、上游失败还是被跳过。

如果将 load_embeddings_to_vector_db 任务的触发规则设置为 all_done,那么只要其所有上游任务完成,它就会运行,即使 create_collection_if_not_exists 任务失败了。

保存DAG并在Airflow UI中创建新的运行后,你可以看到 load_embeddings_to_vector_db 任务会等待其所有上游任务完成,然后运行,尽管 create_collection_if_not_exists 任务失败了。DAG运行本身也会被标记为成功,尽管它包含一个失败的任务。DAG运行的最终状态仅由其叶子任务(没有下游任务的任务)决定。如果所有叶子任务都处于成功或跳过状态,那么DAG运行就算作成功。

配置失败通知回调

现在,你已经能完全控制任务失败时的处理方式了。但还缺少最后一块拼图:对于某些关键任务和DAG,你希望在它们失败时得到警报,例如通过电子邮件或Slack消息。

你可以使用回调函数为你的DAG添加警报。回调函数存在于DAG和任务级别,适用于不同情况。最常用的是 on_failure_callback

让我们为DAG中的所有任务添加一个失败回调。还记得为DAG中所有任务定义任务参数的最快方法吗?没错,就是将其添加到 default_args 字典中。

on_failure_callback 参数可以设置为任何函数。一旦任何任务失败,这段代码就会运行。在本例中,我们只是在任务日志中打印一条消息。在实际场景中,你可以编写代码调用你选择的消息工具,或者使用预构建的通知器类。

回到Airflow UI并创建另一个手动DAG运行,你可以看到任务失败后,回调函数被执行,并向任务日志打印了一行信息。如果你希望回调函数只在整个DAG运行失败时才运行,可以将其提供给DAG本身的 on_failure_callback 参数。

使用回填处理历史数据

假设你度了两周假回来,发现DAG运行历史中有几次失败,或者有些DAG可能完全错过了运行。又或者,你对DAG进行了更改(例如为机器学习模型的训练集添加了另一个特征),并希望重新运行过去的DAG运行,以查看改进后的模型在历史数据上的表现。

在Airflow中,你可以为任何基于时间调度的DAG回填过去任何日期的运行。在Airflow UI中点击“触发”按钮,选择“回填”,然后定义你的日期范围以及回填行为:是仅填充缺失的运行,还是填充缺失运行并重新运行现有的DAG运行,亦或是重新运行该时间段内的所有运行。

点击“运行回填”后,只要回填在进行中,Airflow UI就会显示一个横幅,你可以在上面暂停或停止回填。

总结

本节课中,我们一起学习了如何使你的生成式AI应用工作流具备生产级的鲁棒性。我们探讨了如何模拟和处理任务失败、设置自动重试机制、通过调整触发规则来控制任务依赖关系、配置失败回调以便及时获得通知,以及如何使用回填功能处理历史数据或重新运行更新后的流水线。现在,你已经掌握了将原型Notebook转化为健壮、可监控的Airflow流水线所需的所有基础知识。在下一课中,我们将了解现实世界中的生成式AI工作流,包括真实的示例流水线。

009:现实中的生成式AI流水线 🚀

在本节课中,我们将探讨在完成基于Airflow的RAG应用构建后,如何将其投入生产环境,并了解其他类型的生成式AI流水线编排。我们将分析Astronomer公司的真实案例,并讨论规模化部署时的关键考量。

从学习到生产 🏭

上一节我们介绍了如何使用Airflow编排RAG应用。本节中,我们来看看将此类流水线投入实际生产时需要考虑的事项。

Airflow作为编排器,适用于多种场景,包括不同类型的生成式AI流水线。本课程重点教授了检索增强生成流水线的编排,但Airflow同样能编排其他类型。

  • 推理执行流水线:这类流水线编排在输入数据上运行已训练机器学习模型并收集结果的过程,通常使用Airflow实现。这可能是批量推理流水线或其他推理类型。
  • 模型管理流水线:Airflow可用于管理自动化的模型训练、重新训练和微调。请记住,任何基于Python的笔记本都可以转化为Airflow的DAG或流水线。

真实案例:Ask Astro 🤖

为了将理论付诸实践,Astronomer公司使用Airflow构建了自己的真实世界RAG应用。

Ask Astro是安德森·霍洛维茨基金会的LLM应用架构的一个开源参考实现。它是一个问答型LLM应用,用于回答关于Airflow和Astronomer的问题。

以下是这个开源RAG应用的关键组成部分:

  • 数据源:数据从GitHub issues、Airflow和Astro文档以及Airflow Slack等来源获取。
  • 数据处理:使用LangChain处理数据,并用OpenAI进行嵌入。
  • 数据存储:结果存储在Weaviate向量数据库中。
  • 流水线编排:整个流程由Airflow编排。

Airflow还被用于随时间推移改进模型性能。用户可以对应用给出的答案进行评分,这些评分数据会按计划由LangChain和OpenAI处理,用于更新Weaviate中的数据,从而重复好的答案,避免差的答案。

Ask Astro的应用价值 💡

这个RAG应用对Airflow开发者极为有用。Ask Astro可以帮助你为任何想要实现的用例编写和更新DAG。

例如,我们可以输入一个查询:“为我编写一个使用Weaviate和OpenAI管理RAG数据检索和嵌入流水线的DAG。

Ask Astro会自动为我们编写DAG。我们可以获取这段代码,在Airflow中运行,并根据需要进行修改。这是本课程所学知识的真实世界示例实现,希望它能成为你进一步学习Airflow和生成式AI编排的工具。

生产环境考量 ⚙️

当你将类似驱动Ask Astro的RAG流水线投入规模化生产时,在编写完流水线后,还需要考虑其他几个方面。

  • 多数据源摄入:你可能需要从比本课程中更多的来源摄取数据。这可以通过Airflow轻松管理,例如创建更多并行运行的DAG,或在某些情况下使用之前学过的动态任务映射
  • 数据预处理:你可能需要对源数据进行额外的转换,例如文本分块、选择或文档预筛选。请记住,只要你能用Python函数实现,就可以将其转化为Airflow任务。
  • 数据质量检查:在Airflow中实现数据质量检查也很容易,这对于保持应用数据最新至关重要。
  • 模型与反馈循环:你可能会使用Airflow进行模型的微调、部署以及实现用户反馈流水线。反馈有助于提升RAG应用的性能,并且可以使用Airflow自动化,就像我们在Ask Astro示例中展示的那样。

其他生成式AI流水线类型 🔄

我们还想简要介绍其他可以用Airflow编排的生成式AI流水线类型。

虽然本课程没有展示推理执行或批量推理DAG,但它们通常使用Airflow实现。一个用例示例是自动化个性化新闻简报服务。

在此场景中,可以设计两个Airflow流水线:

  1. 一个传统的ETL流水线,用于获取新闻简报所需数据、格式化并创建模板。
  2. 一个底部的批量推理流水线,它接收用户输入,通过将用户数据发送给LLM提示词,为每位读者个性化新闻简报内容,并将结果加载到新闻简报模板中。

这个DAG可以作为批量推理DAG运行,例如每日处理所有新用户信息。但它也可以使用在线推理执行运行,即当用户在网页表单中输入信息并希望立即获取新闻简报时,流水线会向消息队列发送消息,通过事件驱动调度触发Airflow中的DAG。

这个示例本身值得一门完整的课程,我们仅在高层次上提及。但现在,你可以将其作为使用Airflow实现其他生成式AI编排的灵感来源。

规模化与高级调度 🚀

就像本课程中的RAG应用一样,在生产中实现批量或推理执行流水线也需要考虑其他因素。

  • 处理更高规模:你可能将Airflow用于规模显著更大的场景,例如为电商网站上的所有新产品生成产品描述。在这种情况下,你的Airflow DAG可能看起来与这里看到的非常相似,但你需要关注扩展Airflow基础设施,可能使用托管服务。
  • 利用高级调度:除了每日运行DAG,你可能希望它们在数据可用时立即运行,以便为你的生成式AI应用提供后端支持。一个例子是用户根据在线杂货店的购买记录请求个性化食谱。事件驱动调度让Airflow在用户输入请求后立即启动该流水线。

扩展工具:Airflow AI SDK 🛠️

Airflow之外还有其他软件包可以帮助你实现生成式AI流水线。

例如,由Astronomer开发的Airflow AI SDK是一个基于Pydantic AI的开源包,用于从Airflow中与LLM协作。

你可以使用 @task.llm 装饰器轻松地从Airflow任务中调用LLM,或者交互并编排AI智能体调用。该代码库包含详细的示例帮助你入门。

总结与后续学习 📚

本节课中,我们一起探讨了将RAG流水线投入生产的考量,分析了Ask Astro真实案例,并了解了其他类型的生成式AI流水线编排。Airflow在生产环境中的规模化应用涉及多数据源管理、高级调度和基础设施扩展。

最后,本课程仅仅触及了使用Airflow编排生成式AI流水线所能实现功能的表面。一个很好的进一步阅读资源是由Astronomer编写的《Manning实用Apache Airflow 3指南》。这本免费电子书详细介绍了你需要了解的Airflow 3所有功能,包括深入的批量推理流水线解释。它是你学习旅程中极佳的下一步。

010:总结

在本节课中,我们将对如何将生成式AI应用从原型转化为自动化工作流进行总结。

课程总结 🎓

恭喜你,现在你已经掌握了如何使用Eflow将笔记本中的原型转化为自动化流水线。

你可以将本课程中学到的所有最佳实践应用于任何生成式AI工作流。

后续学习建议 📚

上一节我们总结了核心知识,本节中我们来看看如何继续实践。

如果你想在本地设置FFflow以练习编写更多生成式AI应用,请务必查看本课程末尾的附加资源和可选视频。

我迫不及待想看到你将用FFL构建出什么成果。

最终总结

本节课中我们一起学习了如何利用Eflow工具将生成式AI应用从笔记本原型阶段,系统化地升级为可维护、可扩展的自动化工作流。我们回顾了核心概念,并为你指明了继续深入学习和实践的路径。

011:如何设置本地Airflow环境 🛠️

在本节中,我们将学习如何在本地计算机上设置Airflow和Weaviate环境,以便运行本课程中构建的DAG(有向无环图)工作流。

概述

本教程将指导您完成在本地机器上配置Airflow和Weaviate的步骤,使您能够运行本课程中开发的工作流。我们将从获取代码库开始,逐步完成环境设置,最终在本地运行Airflow实例。

获取项目代码

首先,您需要获取本课程的项目代码。

  1. 访问Astronomer组织下的GitHub仓库,其名称为 orchestrating-workflows-for-genai-deeplearning-ai
  2. 将此仓库Fork到您自己的GitHub账户中。
  3. 将Fork后的仓库克隆到您的本地计算机。
  4. 使用终端或命令行工具,进入克隆下来的项目目录。

您可以使用任何喜欢的代码编辑器(例如VS Code)来编辑DAG文件和运行命令。

安装Astro CLI

要在本地运行此项目,您唯一需要在机器上安装的软件包是 Astro CLI。这是一个由Astronomer开发的免费工具,用于在容器中本地运行Airflow。

  • 在Mac上安装:使用Homebrew包管理器,运行命令 brew install astro
  • 在其他操作系统上安装:请查阅Astro CLI官方文档以获取相应的安装说明。

如果您已经安装了Astro CLI(例如像我一样),请通过运行以下命令确保您的版本至少为 1.34.1

astro version

配置环境变量

安装好Astro CLI后,您需要对代码库进行一项小修改。

  1. 在项目的根目录下,创建一个名为 .env 的新文件。
  2. .env.example 文件中的内容复制到新建的 .env 文件中。

这个环境变量文件定义了Airflow实例与本地Weaviate环境之间的连接。如果您希望连接到不同的Weaviate环境(例如Weaviate Cloud),可以修改此文件中的相应变量。

注意:您可以在 .env 文件中加入您自己的OpenAI API密钥,但这对于运行本课程的管道并非必需。

启动本地环境

保存好 .env 文件的更改后,在项目的根目录下运行以下命令:

astro dev start

此命令将启动运行本课程所涉及的Airflow核心组件的四个容器:

  • 调度器 (Scheduler)
  • API服务器 (API Server)
  • 执行器 (Executor)
  • Airflow元数据库 (Metadata Database)

此外,astro dev start 命令还会初始化一个触发器组件(本课程未涵盖),并基于 docker-compose.override.yml 文件中的定义,创建一个用于本地Weaviate实例的容器。

提示:您无需在机器上预先安装Docker来运行此命令。如果您的系统没有Docker,Astro CLI会自动为您设置Podman来运行容器。

访问Airflow界面

当所有容器准备就绪后,Airflow的Web用户界面将在 http://localhost:8080 自动打开。您无需任何凭据即可登录。

现在,您可以在自己的计算机上运行与本课程中构建的相同的DAG,并开始构建您自己的生成式AI流水线。

部署到云端

当您准备好部署流水线时,可以将其推送到任何类型的Airflow部署环境。如果您拥有Astro账户(提供免费试用),只需使用以下命令即可将您的流水线部署到云端:

astro deploy

总结

本节课中,我们一起学习了如何在本地设置完整的Airflow和Weaviate开发环境。我们完成了从获取代码、安装必要的CLI工具、配置环境变量,到最终启动本地服务并访问Web界面的全过程。现在,您已具备在本地运行和测试生成式AI工作流的能力,并为将来将其部署到生产环境做好了准备。

posted @ 2026-03-26 08:16  布客飞龙II  阅读(0)  评论(0)    收藏  举报