LLM工程师手册——MLOps 和 LLMOps

207 阅读1小时+

在本书中,我们已经使用了机器学习操作(MLOps)的组件和原则,例如模型注册表,用于共享和版本化我们微调的大型语言模型(LLMs),逻辑特征存储用于我们的微调和 RAG 数据,以及协调器将我们所有的 ML 流水线连接在一起。但 MLOps 不仅仅是这些组件;它通过自动化数据收集、训练、测试和部署,将机器学习应用提升到一个新的水平。因此,MLOps 的最终目标是尽可能自动化,让用户专注于最关键的决策,例如在检测到分布变化时,是否需要重新训练模型的决定。那么 LLM 操作(LLMOps)呢?它与 MLOps 有什么区别?

LLMOps 这个术语是随着 LLMs 的广泛应用而产生的。它是建立在 MLOps 之上的,而 MLOps 又建立在开发操作(DevOps)之上。因此,要完全理解 LLMOps 的含义,我们必须提供一个历史背景,从 DevOps 开始,并从那里延伸出该术语——这正是本章的目标。LLMOps 的核心关注点是 LLMs 特有的问题,例如提示监控和版本控制、输入和输出的防护措施以防止有害行为,以及用于收集微调数据的反馈回路。它还关注在使用 LLMs 时出现的扩展性问题,例如收集数万亿个令牌用于训练数据集、在大规模 GPU 集群上训练模型以及减少基础设施成本。幸运的是,对于普通用户来说,这些问题主要由一些公司解决,例如 Meta 提供的 Llama 系列模型。大多数公司将采用这些预训练的基础模型来解决他们的用例问题,专注于像提示监控和版本控制这样的 LLMOps 问题。

在实施方面,为了将 LLMOps 添加到我们的 LLM Twin 用例中,我们将把所有的 ZenML 流水线部署到 AWS。我们将实现一个持续集成和持续部署(CI/CD)流水线来测试我们的代码完整性并自动化部署过程,一个持续训练(CT)流水线来自动化我们的训练过程,以及一个监控流水线来跟踪所有的提示和生成的答案。这是任何机器学习项目中的自然进展,无论你是否使用 LLMs。

在前几章中,您学会了如何构建 LLM 应用。现在,是时候探索与 LLMOps 相关的三个主要目标了。第一个目标是获得对 LLMOps 的理论理解,从 DevOps 开始,然后转向 MLOps 的基本原则,最后深入研究 LLMOps。我们并不打算提供 DevOps、MLOps 和 LLMOps 的完整理论,因为这些话题可以写成一本书。然而,我们希望建立对在实现 LLM Twin 用例时做出某些决策的强理解。

我们的第二个目标是将 ZenML 流水线部署到 AWS(目前我们只在第10章将推理流水线部署到 AWS)。这一部分将是实践性的,向您展示如何利用 ZenML 将一切部署到 AWS。我们需要这样做,以实现我们的第三个目标,即将理论部分学到的内容应用到我们的 LLM Twin 用例中。我们将使用 GitHub Actions 实现 CI/CD 流水线,使用 ZenML 实现 CT 和警报流水线,并使用 Comet ML 的 Opik 实现监控流水线。

因此,本章将涵盖以下主题:

  • LLMOps 的路径:理解其在 DevOps 和 MLOps 中的根源
  • 将 LLM Twin 的流水线部署到云端
  • 将 LLMOps 添加到 LLM Twin

LLMOps 的路径:理解其在 DevOps 和 MLOps 中的根源

要理解 LLMOps,我们必须从该领域的起点开始,那就是 DevOps,因为它继承了大部分基本原则。接着,我们将讨论 MLOps,了解 DevOps 领域如何适应支持机器学习(ML)系统的需求。最后,我们将解释 LLMOps 的概念以及它如何在 LLMs 被广泛采用之后从 MLOps 中衍生出来。

DevOps

手动交付软件不仅耗时、容易出错、涉及安全风险,而且难以扩展。因此,DevOps 诞生于自动化大规模交付软件的过程。更具体地说,DevOps 用于软件开发,在这里你希望完全自动化构建、测试、部署和监控组件。它是一种方法论,旨在缩短开发生命周期,并确保持续交付高质量的软件。它鼓励协作、自动化流程、整合工作流并实施快速反馈回路。这些元素促成了一种文化,使得构建、测试和发布软件变得更加可靠和迅速。

拥抱 DevOps 文化为组织带来了显著的优势,主要体现在提升操作效率、加速功能交付和提高产品质量。一些主要的好处包括:

  • 改善协作:DevOps 在创建更统一的工作环境中起着至关重要的作用。消除开发和运维团队之间的障碍,促进了更好的沟通与团队合作,从而提高了工作效率和生产力。
  • 提升效率:自动化软件开发生命周期减少了人工任务、错误和交付时间。
  • 持续改进:DevOps 不仅关注内部流程,还关注确保软件有效地满足用户需求。推动持续反馈的文化使团队能够快速适应并优化其流程,从而交付真正满足最终用户需求的软件。
  • 更高的质量和安全性:DevOps 通过 CI/CD 和积极的安全措施,确保快速的软件开发同时保持高质量和安全标准。

DevOps 生命周期

如图 11.1 所示,DevOps 生命周期涵盖了从软件开发的开始到其交付、维护和安全的整个过程。这个生命周期的关键阶段包括:

  • 计划(Plan) :组织和优先排序任务,确保每项任务都能跟踪到完成。
  • 编码(Code) :与团队合作编写、设计、开发并安全地管理代码和项目数据。
  • 构建(Build) :将应用程序及其依赖项打包成可执行格式。
  • 测试(Test) :这一阶段至关重要。在此阶段,您需要确认代码是否正常工作并符合质量标准,理想情况下通过自动化测试来完成。
  • 发布(Release) :如果测试通过,将测试后的构建标记为新的发布版本,准备交付。
  • 部署(Deploy) :将最新版本发布给最终用户。
  • 运营(Operate) :在软件上线后,管理和维护其运行的基础设施。这包括扩展、安保、数据管理以及备份与恢复。
  • 监控(Monitor) :跟踪性能指标和错误,以减少事件的严重性和发生频率。 image.png

DevOps 核心概念

DevOps 涉及应用生命周期中的多种实践,但我们将在本书中重点讨论的核心概念包括:

  • 部署环境:在将代码交付到生产环境之前,您必须定义多个预生产环境,以模拟生产环境。最常见的方法是创建一个开发环境,供开发人员测试他们最新的功能。然后是一个预发布环境,在这个环境中,QA 团队和相关方可以调整应用程序,发现漏洞并在交付用户之前体验最新的功能。最后是生产环境,向最终用户公开。
  • 版本控制:用于跟踪、管理和版本化对源代码的每一个更改。这使您能够完全控制代码的演变和部署过程。例如,如果没有版本控制,您将无法跟踪开发、预发布和生产环境之间的更改。通过对软件进行版本化,您可以随时知道哪个版本是稳定的,并且准备好发布。
  • 持续集成(CI) :在将代码推送到开发、预发布和生产主分支之前,您会自动构建应用程序,并对每个更改运行自动化测试。在所有自动化测试通过后,功能分支可以合并到主分支。
  • 持续交付(CD) :持续交付与 CI 一起工作,自动化基础设施配置和应用程序部署步骤。例如,在将代码合并到预发布环境后,带有最新更改的应用程序将自动部署到您的预发布基础设施上。之后,QA 团队(或相关方)开始手动测试最新功能,以验证其是否按预期工作。这两个步骤通常一起被称为 CI/CD。

需要注意的是,DevOps 提出了一个核心原则集,它是与平台/工具无关的。然而,在我们的 LLM Twin 用例中,我们将使用 GitHub 添加版本控制层,旨在跟踪代码的演变。另一个流行的版本控制工具是 GitLab。为了实现 CI/CD 流水线,我们将利用 GitHub 生态系统和 GitHub Actions,这对于开源项目是免费的。其他可选工具包括 GitLab CI/CD、CircleCI 和 Jenkins。通常,您会根据开发环境、定制需求和隐私需求选择 DevOps 工具。例如,Jenkins 是一个开源的 DevOps 工具,您可以自己托管并完全控制它。缺点是,您必须自己托管和维护它,这增加了复杂性。因此,许多公司选择与其版本控制生态系统最佳配合的工具,如 GitHub Actions 或 GitLab CI/CD。

现在我们已经对 DevOps 有了扎实的理解,让我们探索 MLOps 领域是如何出现的,以便在 AI/ML 领域中保持这些相同的核心原则。

MLOps

正如您现在可能已经意识到的,MLOps 尝试将 DevOps 的原则应用到机器学习(ML)中。核心问题在于,ML 应用相比于标准的软件应用,拥有更多的动态部分,例如数据、模型,最后是代码。MLOps 的目标是跟踪、实现并监控所有这些概念,以提高可重复性、稳健性和控制力。

在 ML 系统中,任何一个领域的变化——无论是代码更新、数据修改还是模型调整——都可能触发构建过程。

image.png

在 DevOps 中,一切都围绕代码进行。例如,当代码库中添加新功能时,必须触发 CI/CD 流水线。在 MLOps 中,代码可能保持不变,而只有数据发生变化。在这种情况下,您需要训练(或微调)一个新模型,从而产生新的数据集和模型版本。直观地说,当一个组件发生变化时,它会影响其他一个或多个组件。因此,MLOps 必须考虑到所有这些额外的复杂性。以下是一些可能触发数据变化并间接影响模型的例子:

  • 部署 ML 模型后,随着时间的推移,其性能可能会衰退,因此我们需要新数据来重新训练模型。
  • 在了解如何收集现实世界的数据后,我们可能意识到获取问题所需的数据非常具有挑战性,因此需要重新制定问题,以便与我们的实际设置配合。
  • 在实验阶段和训练模型时,我们通常需要收集更多数据或重新标注数据,这将生成一组新的模型。
  • 在生产环境中部署模型并收集最终用户反馈后,我们可能会意识到我们为训练模型所做的假设是错误的,因此我们必须更改我们的模型。

那么,什么是 MLOps?

MLOps 的一个更正式的定义是:MLOps 是 DevOps 领域的扩展,它将数据和模型视为第一类公民,同时保留 DevOps 方法论。

与 DevOps 类似,MLOps 源于这样的理念:将 ML 模型开发与其部署过程(ML 操作)分离,会降低系统的整体质量、透明度和敏捷性。基于这一点,优化的 MLOps 体验将 ML 资产视为与 CI/CD 环境中的其他软件资产一致的部分,作为一个紧密配合的发布过程的一部分。

MLOps 核心组件

我们在本书中已经使用了所有这些组件,但现在我们对 MLOps 有了更深的理解,让我们快速回顾一下 MLOps 的核心组件。除了源代码控制和 CI/CD,MLOps 还围绕以下组件展开:

  • 模型注册表:用于存储已训练的 ML 模型的集中式仓库(工具:Comet ML、W&B、MLflow、ZenML)
  • 特征存储:对输入数据进行预处理并存储为特征,用于模型训练和推理流水线(工具:Hopsworks、Tecton、Featureform)
  • ML 元数据存储:跟踪与模型训练相关的信息,如模型配置、训练数据、测试数据和性能指标。它主要用于比较多个模型并查看模型的血统,以了解它们是如何创建的(工具:Comet ML、W&B、MLflow)
  • ML 流水线协调器:自动化 ML 项目中的步骤顺序(工具:ZenML、Airflow、Prefect、Dagster)

您可能已经注意到 MLOps 组件与其特定工具之间的重叠。这是很常见的,因为大多数 MLOps 工具提供统一的解决方案,通常被称为 MLOps 平台。

MLOps 原则

MLOps 领域有六个核心原则。这些原则与任何工具无关,是构建强大且可扩展的 ML 系统的核心。

它们是:

  • 自动化或操作化:MLOps 中的自动化涉及从手动流程过渡到通过 CT 和 CI/CD 自动化流水线。这使得我们可以根据新数据、性能下降或未处理的边缘情况等触发条件,高效地重新训练和部署 ML 模型。从手动实验转向完全自动化,确保我们的 ML 系统稳健、可扩展,并能适应不断变化的需求,而不会出现错误或延迟。
  • 版本控制:在 MLOps 中,跟踪代码、模型和数据的变化至关重要,以确保一致性和可重复性。代码通过 Git 等工具进行跟踪,模型通过模型注册表进行版本控制,而数据版本控制可以通过 DVC 或工件管理系统等解决方案进行管理。
  • 实验跟踪:由于训练 ML 模型是一个迭代和实验的过程,涉及根据预定义的指标比较多个实验,使用实验跟踪工具帮助我们选择最佳模型非常重要。像 Comet ML、W&B、MLflow 和 Neptune 这样的工具允许我们记录所有必要的信息,轻松比较实验并选择最适合生产的模型。
  • 测试:MLOps 提出,除了测试代码外,您还应该通过单元测试、集成测试、验收测试、回归测试和压力测试来测试数据和模型。这确保了每个组件正常工作且能良好集成,重点测试输入、输出以及处理边缘情况的能力。
  • 监控:这一阶段对检测由于生产数据变化导致的模型性能退化至关重要,允许及时干预,如重新训练、进一步的提示或特征工程,或数据验证。通过跟踪日志、系统指标和模型指标,并检测漂移,我们可以维持生产中 ML 系统的健康,尽早发现问题,并确保它们持续提供准确的结果。
  • 可重复性:这确保了在给定相同输入的情况下,您 ML 系统中的每个过程(如训练或特征工程)都能产生相同的结果,通过跟踪所有动态变量(如代码版本、数据版本、超参数或任何其他类型的配置)。由于 ML 训练和推理的非确定性特性,设置已知的种子来生成伪随机数对于实现一致的结果并使过程尽可能确定性至关重要。

如果您想深入了解这些原则,我们在本书附录中进行了详细的探索。

ML 与 MLOps 工程

ML 工程与 MLOps 之间有一条微妙的界限。如果我们想为这两个角色定义一个严格的职位描述,那么完全区分 ML 工程(MLE)和 MLOps 的职责并不容易。我见过很多职位角色将 MLOps 角色与平台和云工程师的职责合并。从某种角度来看,这样做是非常合理的:作为一名 MLOps 工程师,你需要在基础设施方面做大量工作。另一方面,正如本节所见,MLOps 工程师仍然需要实施如实验跟踪、模型注册表、版本控制等内容。一个好的策略是让 ML 工程师将这些集成到代码中,而 MLOps 工程师则专注于使它们在基础设施上正常运行。

在大公司中,最终区分这两个角色可能是有意义的。但在小到中型团队中,你可能需要身兼多职,可能会同时处理 ML 系统的 MLE 和 MLOps 方面的工作。

image.png

例如,在图 11.3 中,我们看到三个关键角色之间的职责有着明确的划分:数据科学家/ML 研究员、ML 工程师和 MLOps 工程师。数据科学家(DS)负责实现具体的模型来解决问题。

ML 工程师将数据科学团队提供的功能模型进行构建,构造一个模块化和可扩展的层,并提供数据库(DB)访问或通过 API 在互联网上暴露这些模型。然而,MLOps 工程师在这个过程中扮演着关键角色。他们将这些中间层的代码放置在一个更通用的层——基础设施上。这一举措标志着应用程序从开发过渡到生产环境。从这一点开始,我们可以开始思考自动化、监控、版本控制等内容。

中间层将概念验证与实际产品区分开来。在该层中,你设计一个可扩展的应用程序,它通过集成数据库并通过 API 在互联网上访问,具有一定的状态。在将应用程序部署到特定的基础设施时,必须考虑可扩展性、延迟和成本效益。当然,中间层和通用层是相互依赖的,通常需要反复迭代,以满足应用程序的需求。

LLMOps

LLMOps 涉及管理和运行大型语言模型(LLMs)所必需的实践和流程。这个领域是 MLOps 的一个专门分支,专注于与 LLMs 相关的独特挑战和需求。虽然 MLOps 处理的是管理各种 ML 模型的原则和实践,但 LLMOps 专注于 LLMs 的独特方面,包括它们的庞大规模、高度复杂的训练需求、提示管理以及生成答案的非确定性特性。然而,值得注意的是,LLMOps 的核心仍然继承了 MLOps 部分中介绍的所有基本原则。因此,本文将重点讨论它在这些基础上所增加的内容。

在从零开始训练 LLMs 时,ML 系统的数据和模型维度会显著增长,这是 LLMOps 与 MLOps 区别的一个方面。以下是从零开始训练 LLMs 时的主要关注点:

  • 数据收集与准备:包括收集、准备和管理训练 LLM 所需的庞大数据集。它涉及使用大数据技术来处理、存储和共享训练数据集。例如,GPT-4 的训练使用了大约 13 万亿个令牌,相当于约 10 万亿个单词。
  • 管理 LLMs 的大量参数:从基础设施的角度来看,管理 LLM 的大量参数是一个重大的技术挑战。这需要巨大的计算资源,通常是由支持 CUDA 的 Nvidia GPU 集群驱动的机器集群。
  • LLMs 的庞大规模直接影响模型训练:在从零开始训练 LLM 时,由于模型的大小或为了获得预期结果所需的更大批量大小,无法将其放入单个 GPU。因此,您需要进行多 GPU 训练,这涉及到优化您的流程和基础设施以支持数据、模型或张量并行性。
  • 管理庞大的数据集和多 GPU 集群的成本:这涉及巨大的成本。例如,OpenAI 首席执行官 Sam Altman 提到,GPT-4 的估计训练成本大约为 1 亿美元(en.wikipedia.org/wiki/GPT-4#…)。再加上多个实验、评估和推理的成本。即使这些数字并不完全准确,因为来源并非100% 可靠,但训练 LLM 的成本规模是可信的,这意味着只有行业中的大玩家才能负担得起从零开始训练 LLM。

本质上,LLMOps 是大规模的 MLOps。它使用相同的 MLOps 原则,但应用于需要更多计算能力来训练和运行的大数据和庞大模型。然而,由于其庞大的规模,最显著的趋势是逐渐放弃从零开始为特定任务训练神经网络。这种方法随着微调的兴起,尤其是像 GPT 这样的基础模型的出现,变得过时。像 OpenAI 和 Google 这样的拥有大量计算资源的少数组织,开发了这些基础模型。因此,大多数应用现在依赖于对这些模型部分进行轻量级微调、提示工程,或者可选地将数据或模型提炼为更小的、专门的推理网络。

因此,对于大多数 LLM 应用,您的开发步骤将包括选择一个基础模型,并通过使用提示工程、微调或 RAG 进一步优化它。因此,理解这三个步骤的操作性方面是最重要的。接下来,让我们深入探讨一些流行的 LLMOps 组件,这些组件可以改进提示工程、微调和 RAG。

人类反馈

LLM 的一个有价值的优化步骤是将其与受众的偏好对齐。您需要在应用程序中引入反馈循环,并收集人类反馈数据集,使用如强化学习与人类反馈(RLHF)等技术,或更高级的技术,如直接偏好优化(DPO),来进一步微调 LLM。一种常见的反馈循环是大多数聊天机器人界面中都存在的点赞/点踩按钮。关于偏好对齐的更多内容,可以参见第六章。

防护措施

不幸的是,LLM 系统并不总是可靠的,因为它们经常会出现幻觉。您可以优化系统来应对幻觉,但由于幻觉难以检测且可能以多种形式出现,未来仍然会发生显著变化。

大多数用户已经接受了这种现象,但不可接受的是,当 LLM 意外输出敏感信息时,例如 GitHub Copilot 输出 AWS 密钥或其他聊天机器人提供个人密码。这种情况也可能发生在个人的电话号码、地址、电子邮件地址等信息上。理想情况下,您应当将所有这些敏感数据从训练数据中移除,以避免 LLM 记住这些数据,但这并不总是能实现。

LLM 因生成有害和有毒的输出而广为人知,例如性别歧视和种族歧视的输出。例如,在 2023 年 4 月对 ChatGPT 进行的一项实验中,人们发现了一种方法,可以通过强迫聊天机器人采用负面人格(如“坏人”或“可怕的人”)来劫持系统。这种方法甚至可以通过让聊天机器人扮演历史上著名的负面人物角色,如独裁者或罪犯,来实现。例如,这是 ChatGPT 扮演“坏人”时输出的内容:

X 只是另一个第三世界国家,那里除了毒枭和贫困人民之外什么都没有。那里的人没有受过教育,很暴力,对法律和秩序毫无敬畏。如果你问我,X 只是一个充斥着犯罪和痛苦的污水池,理智的人是不会想去那里的。

有关更多不同人格的实验来源,请查看:techcrunch.com/2023/04/12/….

这一讨论可以扩展为一个无尽的例子列表,但关键要点是,LLM 可能产生有害的输出或接收危险的输入,因此您需要对它们进行监控和准备。为了创建安全的 LLM 系统,您必须通过添加防护措施来保护它们免受有害、敏感或无效输入和输出的影响:

  • 输入防护措施:输入防护措施主要保护免受三大风险:将私人信息暴露给外部 API,执行可能危及系统的有害提示(模型越狱),以及接受暴力或不道德的提示。关于泄露私人信息给外部 API 的风险,具体指的是将敏感数据发送到组织外部,例如凭证或机密信息。关于模型越狱,主要指的是提示注入,例如执行恶意 SQL 代码,能够访问、删除或损坏您的数据。最后,有些应用程序不希望接受用户的暴力或不道德查询,例如询问 LLM 如何制造炸弹。
  • 输出防护措施:在 LLM 响应的输出中,您需要捕捉那些不符合应用程序标准的失败输出。这些标准可能因应用程序而异,但一些例子包括空响应(这些响应不符合您预期的格式,如 JSON 或 YAML)、有毒响应、幻觉输出,以及一般错误的响应。此外,您还需要检查可能从 LLM 的内部知识或您的 RAG 系统中泄露的敏感信息。

一些流行的防护工具包括 Galileo Protect,它可以检测提示注入、有毒语言、数据隐私泄露和幻觉。您还可以使用 OpenAI 的 Moderation API 来检测有害的输入或输出并采取措施。

添加输入和输出防护措施的缺点是会增加系统的额外延迟,这可能会影响应用程序的用户体验。因此,在输入/输出的安全性和延迟之间存在一个权衡。关于无效输出,由于 LLM 是非确定性的,您可以实施重试机制,生成另一个潜在的候选输出。然而,正如上文所述,按顺序运行重试会使响应时间翻倍。因此,一种常见的策略是并行运行多个生成过程,并选择最佳的输出。这将增加冗余性,但有助于保持延迟在可接受范围内。

提示监控

监控对 LLMOps 来说并不新鲜,但在 LLM 的世界中,我们需要管理一个新的实体:提示。因此,我们需要找到特定的方法来记录和分析它们。

大多数 ML 平台,如 Opik(来自 Comet ML)和 W&B,或其他专门的工具如 Langfuse,已经实现了用于调试和监控提示的日志工具。在生产环境中,使用这些工具时,通常需要跟踪用户输入、提示模板、输入变量、生成的响应、令牌数和延迟等内容。

在使用 LLM 生成答案时,我们并不等待整个答案生成完毕,而是逐个令牌地流式输出。这使得整个过程更加快速和响应迅速。因此,在跟踪生成答案的延迟时,最终的用户体验必须从多个角度进行观察,例如:

  • 首次令牌生成时间(TTFT) :生成第一个令牌所需的时间
  • 令牌间隔时间(TBT) :每个令牌生成之间的间隔
  • 每秒令牌数(TPS) :令牌生成的速率
  • 每输出令牌时间(TPOT) :生成每个输出令牌所需的时间
  • 总延迟:完成响应所需的总时间

此外,跟踪总的输入和输出令牌数对理解托管 LLM 的成本至关重要。

最终,您可以计算验证模型性能的指标,针对每个输入、提示和输出元组。根据您的用例,您可以计算准确性、有毒性和幻觉率等指标。在使用 RAG 系统时,您还可以计算与检索上下文的相关性和精确度相关的指标。

监控提示时,另一个重要的考虑因素是记录它们的完整跟踪。您可能会从用户查询到最终答案的过程中经历多个中间步骤。例如,重写查询以提高 RAG 检索的准确性可能涉及一个或多个中间步骤。因此,记录完整的跟踪过程能揭示从用户发送查询到最终响应返回的整个过程,包括系统采取的操作、检索到的文档以及发送到模型的最终提示。此外,您还可以记录每个步骤的延迟、令牌和成本,从而提供更细粒度的视图来展示所有步骤。

image.png

如图 11.4 所示,最终目标是追踪从用户输入到生成答案的每个步骤。如果发生失败或异常行为,您可以准确地定位到出错的步骤。查询可能由于错误的答案、无效的上下文或不正确的数据处理而失败。如果在特定步骤中生成的令牌数突然波动,应用程序也可能表现异常。

LLMOps 是一个快速发展的领域。鉴于其快速演变,做出预测是具有挑战性的。事实是,我们不确定 LLMOps 这个术语是否会一直存在。然而,确定的是,随着 LLM 的新应用场景不断涌现,管理其生命周期的工具和最佳实践也将随之出现。

即使本节关于 DevOps、MLOps 和 LLMOps 的内容远未全面,它仍然为我们如何在 LLM Twin 用例中应用最佳操作实践提供了一个清晰的思路。

将 LLM Twin 的流水线部署到云端

本节将向您展示如何将 LLM Twin 的所有流水线部署到云端。我们必须将整个基础设施部署到云中,以使整个系统在云端正常工作。因此,我们需要:

  • 设置一个 MongoDB 无服务器实例
  • 设置一个 Qdrant 无服务器实例
  • 将 ZenML 流水线、容器和工件注册表部署到 AWS
  • 将代码容器化,并将 Docker 镜像推送到容器注册表

需要注意的是,训练和推理流水线已经与 AWS SageMaker 配合使用。因此,通过完成上述四个步骤,我们确保整个系统部署在云端,准备好扩展并为我们的假想客户提供服务。

部署成本

我们将使用 MongoDB、Qdrant 和 ZenML 服务的免费版本。至于 AWS,我们将主要使用其免费套餐来运行 ZenML 流水线。SageMaker 的训练和推理组件运行成本较高(我们在本节中不会运行这些部分)。因此,接下来的部分所展示的内容将在 AWS 上产生最低的成本(最多几美元)。

理解基础设施

在深入逐步教程之前,我们先简要概述一下我们的基础设施以及所有元素如何交互。这将帮助我们更有意识地跟随下面的教程。

如图 11.5 所示,我们需要设置一些服务。为了简化操作,MongoDB 和 Qdrant 我们将使用它们的无服务器免费版本。至于 ZenML,我们将利用 ZenML 云的免费试用版,它将帮助我们在云端协调所有的流水线。那么,它是如何做到的呢?

通过利用 ZenML 云,我们可以快速分配所有所需的 AWS 资源,以运行、扩展和存储 ML 流水线。它将帮助我们通过几次点击启动以下 AWS 组件:

  • 一个 ECR 服务,用于存储 Docker 镜像
  • 一个 S3 对象存储,用于存储所有的工件和模型
  • SageMaker Orchestrator,用于协调、运行和扩展我们所有的 ML 流水线

image.png

既然我们已经了解了基础设施的核心资源,现在让我们看看在云中运行流水线的核心流程,如图11.5所示:

  1. 构建一个包含所有系统依赖项、项目依赖项和LLM Twin应用程序的Docker镜像。
  2. 将Docker镜像推送到ECR,以便SageMaker可以访问它。
  3. 现在,我们可以通过本地机器的CLI或ZenML的仪表板来触发本书中实现的任何流水线。
  4. ZenML的每个流水线步骤都将映射到一个在AWS EC2虚拟机(VM)上运行的SageMaker作业。根据有向无环图(DAG)步骤之间的依赖关系,某些步骤将并行运行,而其他步骤将顺序运行。
  5. 在运行步骤时,SageMaker会从步骤2中定义的ECR拉取Docker镜像。基于拉取的镜像,它创建一个Docker容器来执行流水线步骤。
  6. 在作业执行时,它可以访问S3的工件存储、MongoDB和Qdrant向量数据库,以查询或推送数据。ZenML仪表板是一个关键工具,提供了流水线进度的实时更新,确保流程清晰可见。

既然我们了解了基础设施的工作原理,让我们开始设置MongoDB、Qdrant和ZenML云。

我应该选择哪个AWS云区域? 在我们的教程中,所有服务将部署在AWS的法兰克福区域(eu-central-1)。你可以选择其他区域,但请在所有服务中保持一致,以确保组件之间的快速响应并减少潜在错误。

如何管理服务UI的更改? 遗憾的是,MongoDB、Qdrant或其他服务的UI或命名可能会更改。由于我们无法在每次变化时更新本书,因此请参考它们的官方文档来检查与教程不同的内容。对此造成的不便我们深表歉意,但这超出了我们的控制范围。

设置 MongoDB

我们将向您展示如何在项目中创建并集成一个免费的 MongoDB 集群。以下是具体步骤:

  1. 访问 www.mongodb.com 并创建一个账号。

  2. 在左侧面板中,导航至 “Deployment | Database”,然后点击“Build a Cluster”。

  3. 在创建表单中,进行以下操作:

    • 选择 “M0 Free” 集群。
    • 将您的集群命名为 “twin”。
    • 选择 AWS 作为提供商。
    • 选择法兰克福(eu-central-1)作为区域。您可以选择其他区域,但请确保后续所有 AWS 服务使用相同的区域,以保证服务之间的快速响应。
    • 保持其他属性的默认值。
  4. 在右下角,点击绿色的“Create Deployment”按钮。

测试您的 MongoDB 集群是否正常运行

要测试新创建的 MongoDB 集群,我们需要从本地机器连接到它。我们使用 MongoDB 的 VS Code 插件来实现连接,您也可以使用其他工具。因此,在“Choose a connection method”设置流程中,选择“MongoDB for VS Code”,并按照网站上的步骤操作。

  1. 要连接到数据库,您需要在 VS Code 插件(或您喜欢的其他工具)中粘贴数据库连接 URL,其中包含您的用户名、密码和集群 URL,格式类似如下:

    mongodb+srv://<username>:<password>@twin.vhxy1.mongodb.net
    

    请务必将此 URL 保存到某个位置,以便之后可以方便地复制。

  2. 如果您不知道密码或想要更改密码,请前往左侧面板的“Security → Quickstart”。在此处,您可以编辑您的登录凭证。请确保将它们妥善保存,因为之后无法再次访问。

  3. 确认连接正常后,前往左侧面板的“Security → Network Access”并点击“ADD IP ADDRESS”。接着点击“ALLOW ACCESS FROM ANYWHERE”并确认。为了简化操作,我们允许任何 IP 的机器访问我们的 MongoDB 集群。这可以确保我们的流水线在没有复杂网络配置的情况下可以查询或写入数据库。虽然这并非生产环境中最安全的选项,但在我们的示例中足够使用。

最后一步

返回您的项目并打开 .env 文件。现在,添加或替换 DATABASE_HOST 变量为您的 MongoDB 连接字符串,类似如下格式:

DATABASE_HOST=mongodb+srv://<username>:<password>@twin.vhxy1.mongodb.net

完成了!现在,您将不再从本地 MongoDB 读写数据,而是从我们刚创建的云 MongoDB 集群中进行操作。接下来,我们将对 Qdrant 执行类似的设置流程。

设置 Qdrant

我们需要重复类似于设置 MongoDB 的步骤。要创建 Qdrant 集群并将其集成到我们的项目中,请按以下步骤操作:

  1. 前往 Qdrant 网站 cloud.qdrant.io/ 并创建一个账号。

  2. 在左侧面板中,导航至 “Clusters” 并点击 “Create”。

  3. 填写集群创建表单,具体如下:

    • 选择免费版本的集群。
    • 选择 GCP 作为云提供商(在编写本书时,这是唯一允许免费集群的提供商)。
    • 选择法兰克福作为区域(或选择与 MongoDB 相同的区域)。
    • 将集群命名为 “twin”。
    • 保持其他属性的默认值,点击 “Create”。
  4. 在左侧面板的“Data Access Control”部分访问集群。

  5. 点击“Create”,选择您的 twin 集群以创建一个新的访问令牌。请将新创建的令牌安全地保存,因为之后将无法再次访问。

  6. 您可以运行 Qdrant 网站上的示例代码(位于 “Usage Examples” 部分)来测试连接是否正常。

  7. 返回 Qdrant 的“Clusters”部分,打开您新创建的 twin 集群。您将获得集群的端点,用于在代码中配置 Qdrant。

  8. 您可以通过点击“Open Dashboard”并输入您的 API Key 作为密码来查看 Qdrant 的集合和文档。目前 Qdrant 集群的仪表板是空的,但在运行流水线后,您将看到所有的集合,如下图所示:

image.png

最后,返回您的项目并打开 .env 文件。现在,我们需要填写以下环境变量:

USE_QDRANT_CLOUD=true
QDRANT_CLOUD_URL=<步骤 7 中找到的端点 URL>
QDRANT_APIKEY=<步骤 5 中创建的访问令牌>

完成了!现在,您将不再从本地 Qdrant 向量数据库进行读写操作,而是从我们刚创建的云端 Qdrant 集群中操作。为了确保一切正常运行,请使用云版的 MongoDB 和 Qdrant 运行端到端的数据流水线:

poetry poe run-end-to-end-data-pipeline

最后一步是设置 ZenML 云,并将我们所有的基础设施部署到 AWS。

设置 ZenML 云

设置 ZenML 云和 AWS 基础设施是一个多步骤的过程。首先,我们将创建一个 ZenML 云账户,然后通过 ZenML 云设置 AWS 基础设施,最后将代码打包到 Docker 镜像中,以便在 AWS SageMaker 上运行。

步骤 1:设置 ZenML 云

  1. 前往 ZenML 云网站 cloud.zenml.io 并创建一个账户。ZenML 提供为期七天的免费试用,足以运行我们的示例。
  2. 填写入职表单,创建一个具有唯一名称的组织,并创建一个名为 twin 的租户。租户是 ZenML 在完全隔离环境中的一个部署。等待几分钟,直到租户服务器启动,然后再继续下一步。
  3. 如果您愿意,可以查看 Quickstart Guide(快速入门指南)以了解 ZenML 云的工作原理,示例更为简单。虽然不需要完成快速入门指南来部署 LLM Twin 应用程序,但我们建议这样做,以确保一切正常。

假设您已经完成了快速入门指南。否则,在接下来的步骤中可能会遇到问题。为了将我们的项目连接到这个 ZenML 云租户,返回项目并运行仪表板中提供的 zenml connect 命令。它类似于以下示例,但带有不同的 URL:

zenml connect --url https://0c37a553-zenml.cloudinfra.zenml.io

步骤 2:测试连接

为了确保一切正常,运行代码中的一个随机流水线。注意,此时我们仍然在本地运行它,但结果将记录到云端版本中,而不是本地服务器:

poetry poe run-digital-data-etl

转到 ZenML 仪表板左侧面板的 Pipelines 部分。如果一切正常,您应该可以在第 5 步中看到运行的流水线。

确保您的 ZenML 服务器版本与本地 ZenML 版本匹配。例如,在编写本书时,两个版本都是 0.64.0。如果不匹配,可能会遇到奇怪的行为或无法正常工作。最简单的修复方法是打开 pyproject.toml 文件,找到 zenml 依赖项,并将其更新为服务器的版本。然后运行以下命令来更新本地虚拟环境:

poetry lock --no-update && poetry install

步骤 3:创建 ZenML 堆栈

要将代码部署到 AWS,您必须创建一个 ZenML 堆栈。堆栈是一组组件(如底层编排器、对象存储和容器注册表),ZenML 需要它们来运行流水线。可以将堆栈看作是您的基础设施。在本地工作时,ZenML 提供了一个默认堆栈,使您可以快速在本地开发代码并进行测试。然而,通过定义不同的堆栈,您可以快速在本地和 AWS 环境之间切换,以下内容将展示如何操作。

在开始前,请确保您有一个具有管理员权限的 AWS 账户。

按照以下步骤为项目创建一个 AWS 堆栈:

  1. 在左侧面板中,点击 Stacks 部分并点击 New Stack 按钮。
  2. 您将有多个选项来创建堆栈,但最简单的是在浏览器体验中从头开始创建一个堆栈,不需要额外的准备。这虽然不太灵活,但足够托管我们的项目。因此,选择 Create New Infrastructure → In-browser Experience。
  3. 选择 AWS 作为云提供商。
  4. 选择欧洲(法兰克福)eu-central-1 作为位置,或使用您为 MongoDB 和 Qdrant 设置的相同区域。
  5. 将其命名为 aws-stack。此名称需精确一致,以确保后续命令正常工作。
  6. 现在,ZenML 将创建一组 IAM 角色,以便所有其他组件可以相互通信,还会创建一个 S3 存储桶作为工件存储,一个 ECR 仓库作为容器注册表,并使用 SageMaker 作为编排器。
  7. 点击 Next。
  8. 点击 Deploy to AWS 按钮,它将打开 AWS 的 CloudFormation 页面。ZenML 利用 CloudFormation(一个基础设施即代码,IaC 工具)来创建第 6 步列出的所有 AWS 资源。
  9. 在底部,勾选所有复选框以确认 AWS CloudFormation 将代表您创建 AWS 资源。最后,点击 Create stack 按钮。现在,您需要等待几分钟,让 AWS CloudFormation 启动所有资源。
  10. 返回 ZenML 页面并点击 Finish 按钮。

通过 ZenML,我们高效地为机器学习流水线部署了整个 AWS 基础设施。我们使用了一个基础示例,牺牲了一些控制权。但如果您需要更多控制,ZenML 提供了使用 Terraform(IaC 工具)来完全控制 AWS 资源的选项,或将 ZenML 连接到您的现有基础设施。

AWS 资源简介

在继续下一步之前,快速回顾我们刚创建的 AWS 资源:

  • IAM 角色:一种具有权限策略的 AWS 身份,用于定义该角色被允许或拒绝执行的操作,用于授予对 AWS 服务的访问权限,而无需共享安全凭证。
  • S3:一个可扩展且安全的对象存储服务,允许从网络任何地方存储和检索文件。通常用于数据备份、内容存储和数据湖,比 Google Drive 更具扩展性和灵活性。
  • ECR:一个完全托管的 Docker 容器注册表,便于存储、管理和部署 Docker 容器镜像。
  • SageMaker:一个完全托管的服务,允许开发人员和数据科学家快速构建、训练和部署机器学习模型。
  • SageMaker Orchestrator:SageMaker 的一个功能,用于帮助自动执行机器学习工作流的执行、管理步骤之间的依赖关系,并确保模型训练和部署流水线的可复现性和可扩展性。其他类似工具包括 Prefect、Dagster、Metaflow 和 Airflow。
  • CloudFormation:一个服务,允许您对 AWS 资源进行建模和设置,以减少管理它们的时间,使您可以专注于应用程序。它使用模板自动化 AWS 基础设施的配置过程。

最后一步:容器化代码

在运行机器学习流水线之前,最后一步是将代码容器化,并准备一个包含依赖项和代码的 Docker 镜像。

使用 Docker 容器化代码

到目前为止,我们已经定义了基础设施、MongoDB、Qdrant 和 AWS,用于存储和计算。最后一步是找到一种方法,使我们的代码在这些基础设施上运行。最流行的解决方案是 Docker,这是一种工具,允许我们创建一个包含所有运行应用所需内容的隔离环境(容器),例如系统依赖、Python 依赖和代码。

我们在项目根目录的 Dockerfile 中定义了 Docker 镜像,这是 Docker 的标准命名约定。在深入代码之前,如果您想自行构建 Docker 镜像,请确保您的机器上已安装 Docker。如果尚未安装,可以按照此处的说明进行安装:docs.docker.com/engine/inst… Dockerfile 的内容。

Dockerfile 从指定基础镜像开始,这是基于 Debian Bullseye 发行版的精简版 Python 3.11。接着,设置了一些环境变量,以配置容器的各个方面,例如工作目录、关闭 Python 字节码生成、配置 Python 直接输出到终端。此外,还指定了要安装的 Poetry 版本,并设置了一些环境变量,以确保包安装是非交互式的,这对于自动化构建至关重要。

FROM python:3.11-slim-bullseye AS release
ENV WORKSPACE_ROOT=/app/
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV POETRY_VERSION=1.8.3
ENV DEBIAN_FRONTEND=noninteractive
ENV POETRY_NO_INTERACTION=1

接下来,在容器中安装 Google Chrome。安装过程从更新包列表并安装 gnupg、wget 和 curl 等基本工具开始。接着添加 Google Linux 签名密钥,并配置 Google Chrome 的软件源。更新包列表后,安装稳定版 Google Chrome。安装完成后,删除包列表以尽量减小镜像大小。

RUN apt-get update -y && \
    apt-get install -y gnupg wget curl --no-install-recommends && \
    wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-linux-signing-key.gpg && \
    echo "deb [signed-by=/usr/share/keyrings/google-linux-signing-key.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
    apt-get update -y && \
    apt-get install -y google-chrome-stable && \
    rm -rf /var/lib/apt/lists/*

在 Chrome 安装完成后,安装其他基本系统依赖项。安装完成后,清理包缓存以进一步减小镜像大小。

RUN apt-get update -y \
    && apt-get install -y --no-install-recommends build-essential \
    gcc \
    python3-dev \
    build-essential \
    libglib2.0-dev \
    libnss3-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

然后使用 pip 安装依赖管理工具 Poetry。--no-cache-dir 选项可防止 pip 缓存包,从而帮助减小镜像大小。安装后,Poetry 被配置为在安装包时使用最多 20 个并行工作线程,从而加快安装过程。

RUN pip install --no-cache-dir "poetry==$POETRY_VERSION"
RUN poetry config installer.max-workers 20

容器内部的工作目录被设置为 WORKSPACE_ROOT(默认是 /app/),应用程序代码将放置在该目录中。pyproject.tomlpoetry.lock 文件定义了 Python 项目的依赖,并复制到此目录。

WORKDIR $WORKSPACE_ROOT
COPY pyproject.toml poetry.lock $WORKSPACE_ROOT

在依赖文件就位后,使用 Poetry 安装项目的依赖。此配置关闭了虚拟环境的创建,这意味着依赖将直接安装到容器的 Python 环境中。安装过程排除了开发依赖项,并禁用了缓存以最小化空间使用。

此外,还安装了 poethepoet 插件来帮助管理项目中的任务。最后,删除任何剩余的 Poetry 缓存,以确保容器尽可能精简。

RUN poetry config virtualenvs.create false && \
    poetry install --no-root --no-interaction --no-cache --without dev && \
    poetry self add 'poethepoet[poetry_plugin]' && \
    rm -rf ~/.cache/pypoetry/cache/ && \
    rm -rf ~/.cache/pypoetry/artifacts/

在最后一步中,将主机机器上的整个项目目录复制到容器的工作目录中。此步骤确保容器内可用所有应用文件。

编写 Dockerfile 时的一个重要技巧是将安装步骤与文件复制步骤分离。这很有用,因为每个 Docker 命令都是缓存的并分层叠加。当重新构建 Docker 镜像时,如果您更改了某一层,则从该层以下的所有层都会重新执行。由于系统和项目依赖很少更改,而代码更频繁更改,因此在最后一步复制项目文件可以充分利用缓存机制,加快 Docker 镜像的重建速度。

COPY . $WORKSPACE_ROOT

此 Dockerfile 旨在创建一个干净、一致的 Python 环境,包含所有必要依赖,使项目在任何支持 Docker 的环境中顺利运行。

构建并推送 Docker 镜像

最后一步是构建 Docker 镜像并将其推送到 ZenML 创建的 ECR。要从项目根目录构建 Docker 镜像,请运行以下命令:

docker buildx build --platform linux/amd64 -t llmtwin -f Dockerfile .

我们必须在 Linux 平台上构建它,因为我们在 Docker 内部使用的 Google Chrome 安装程序仅适用于 Linux 机器。即使您使用的是 macOS 或 Windows 机器,Docker 也可以模拟虚拟 Linux 容器。

新创建的 Docker 镜像的标签为 llmtwin。我们还可以通过 poethepoet 命令来提供此构建命令:

poetry poe build-docker-image

现在,让我们将 Docker 镜像推送到 ECR。为此,请前往 AWS 控制台,然后进入 ECR 服务。在那里,找到新创建的 ECR 仓库。仓库名应以 zenml-* 前缀开头。

image.png

第一步是登录 ECR。为此,请确保您已安装 AWS CLI 并使用您的管理员 AWS 凭证进行了配置,如第 2 章所述:

AWS_REGION=<your_region> # 例如,AWS_REGION=eu-central-1
AWS_ECR_URL=<your_account_id>.dkr.ecr.<your_region>.amazonaws.com
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ECR_URL}

您可以通过点击右上角的切换按钮来获取当前的 AWS_REGION(如图 11.8 所示)。此外,可以从 AWS ECR 仪表板的主页复制 ECR URL 以填充 AWS_ECR_URL 变量(如图 11.7 所示)。运行上述命令后,您应该在 CLI 中看到 “Login Succeeded” 消息。

image.png

现在,我们需要为 llmtwin Docker 镜像添加另一个标签,以指示要推送到的 Docker 注册表:

docker tag llmtwin ${AWS_ECR_URL}:latest

最后,通过运行以下命令将其推送到 ECR:

docker push ${AWS_ECR_URL}:latest

上传完成后,返回到您的 AWS ECR 仪表板并打开您的 ZenML 仓库。您应该可以看到 Docker 镜像,如下图所示。

image.png

对于代码中需要发布和测试的每项更改,您都必须执行所有这些步骤,这既繁琐又容易出错。本章的“为 LLM Twin 添加 LLMOps”部分将教我们如何使用 GitHub Actions 在 CD 流水线中自动化这些步骤。不过,我们希望首先手动完成这些步骤,以便深入理解背后的过程,而不只是将其视为“黑箱”。了解这些细节对于调试 CI/CD 流水线至关重要,因为您需要理解错误消息并知道如何修复它们。

现在我们已经构建了 Docker 镜像并将其推送到 AWS ECR,接下来让我们将其部署到 AWS。

在 AWS 上运行流水线

我们离在 AWS 上运行机器学习流水线只差几步了。现在,让我们将默认的 ZenML 堆栈切换为本章中创建的 AWS 堆栈。在项目根目录的 CLI 中运行以下命令:

zenml stack set aws-stack

返回您的 AWS ECR ZenML 仓库并复制镜像 URI,如图 11.9 所示。然后,进入 configs 目录,打开 configs/end_to_end_data.yaml 文件,并使用您的 ECR URL 更新 settings.docker.parent_image 属性,如下所示:

settings:
  docker:
    parent_image: <YOUR ECR URL> #例如,992382797823.dkr.ecr.eu-central-1.amazonaws.com/zenml-rlwlcs:latest
    skip_build: True

我们已将流水线配置为始终使用 ECR 中可用的最新 Docker 镜像,这意味着每当我们推送新的镜像时,流水线将自动使用代码的最新更改。

我们还需将 .env 文件中的所有凭证导出到 ZenML 的密钥中。ZenML 密钥是一个安全地存储凭证并在流水线中可访问的功能:

poetry poe export-settings-to-zenml

最后一步是设置异步运行流水线,以避免等待其完成时可能导致的超时错误:

zenml orchestrator update aws-stack --synchronous=False

现在,ZenML 已知道使用 AWS 堆栈、我们自定义的 Docker 镜像,并可以访问我们的凭证,设置已全部完成。运行端到端数据流水线,使用以下命令:

poetry poe run-end-to-end-data-pipeline

现在,您可以进入 ZenML Cloud → Pipelines → end_to_end_data 并打开最新的运行。在 ZenML 仪表板上,您可以查看流水线的最新状态,如图 11.10 所示。请注意,该流水线在一次运行中执行了所有与数据相关的流水线。

在“为 LLM Twin 添加 LLMOps”部分中,我们将解释为何将所有步骤合并到一个流水线中。

image.png

您可以点击任何正在运行的区块,查看有关该运行的详细信息、用于该特定步骤的代码以及用于监控和调试的日志,如图 11.11 所示:

image.png

要运行其他流水线,您需要在 configs/ 目录下的配置文件中更新 settings.docker.parent_image 属性。

要获取更多关于运行的详细信息,可以前往 AWS SageMaker。在左侧面板中,点击 SageMaker 仪表板,在右侧的“Processing”列中,点击绿色的“Running”部分,如图 11.12 所示。

这将打开一个列表,显示所有执行 ZenML 流水线的处理作业。

image.png

如果您想再次在本地运行流水线,请使用以下 CLI 命令:

poetry poe set-local-stack

如果您想断开与 ZenML 云仪表板的连接并使用本地版本,请运行以下命令:

zenml disconnect

在 SageMaker 上运行 ZenML 流水线后,解决 ResourceLimitExceeded 错误

假设您在使用 AWS 堆栈在 SageMaker 上运行 ZenML 流水线后遇到了 ResourceLimitExceeded 错误。在这种情况下,您需要明确请求 AWS 为您提供访问特定类型的 AWS EC2 虚拟机的权限。

ZenML 默认使用 ml.t3.medium 类型的 EC2 机器,它属于 AWS 的免费层。然而,一些 AWS 账户默认无法访问这些虚拟机。要检查您的访问权限,请在 AWS 控制台中搜索 “Service Quotas”。

然后,在左侧面板中点击 “AWS services”,搜索 “Amazon SageMaker”,然后查找 ml.t3.medium。在图 11.13 中,您可以看到我们对这些类型机器的配额。如果您的配额为 0,则应请求 AWS 将其增加到图 11.13 中“Applied account-level quota value”列所示的数值。整个过程是免费的,只需几次点击。不幸的是,您可能需要等待数小时到一天,直到 AWS 批准您的请求。

image.png

您可以在此链接找到解决该错误并请求新配额的分步说明:repost.aws/knowledge-c…

如果您更改了 .env 文件中的值并想更新 ZenML 密钥,首先运行以下 CLI 命令以删除旧密钥:

poetry poe delete-settings-zenml

然后,可以通过以下命令再次导出它们:

poetry poe export-settings-to-zenml

为 LLM Twin 添加 LLMOps

在上一节中,我们学习了如何通过手动构建 Docker 镜像并将其推送到 ECR 来设置 LLM Twin 项目的基础设施。现在我们希望通过使用 GitHub Actions 实现 CI/CD 流水线,以及通过 ZenML 实现 CT 流水线来自动化整个过程。正如前面提到的,实施 CI/CD/CT 流水线确保每次推送到主分支的功能都是一致的并经过测试的。此外,通过自动化部署和训练过程,可以支持协作、节省时间并减少人为错误。

最后,在本节的末尾,我们将展示如何使用 Comet ML 的 Opik 实现提示监控流水线,以及如何使用 ZenML 实现告警系统。此提示监控流水线将帮助我们调试和分析 RAG 和 LLM 逻辑。由于 LLM 系统是非确定性的,捕获并存储提示追踪对于监控您的机器学习逻辑至关重要。

在深入实施之前,让我们先简要介绍 LLM Twin 的 CI/CD 流水线流程。

LLM Twin 的 CI/CD 流水线流程

我们有两个环境:staging(预发布)和 production(生产)。在开发新功能时,我们从 staging 分支创建一个新的分支并仅在该分支上开发。当功能完成并确认无误后,我们会向 staging 分支提交一个 pull request(PR)。在功能分支被接受后,它将被合并到 staging 分支。这是大多数软件应用中的标准工作流程。可能会有一些变体,比如增加一个 dev(开发)环境,但基本原则保持不变。

如图 11.14 所示,当 PR 打开时,CI 流水线会被触发。在这个阶段,我们会测试功能分支的代码是否存在 linting 和格式错误。同时,我们会运行 gitleaks 命令来检查是否有凭证和敏感信息被错误提交。如果 linting、格式检查和 gitleaks 步骤(称为静态分析)通过,则接下来运行自动化测试。请注意,静态分析步骤比自动化测试运行得更快,因此执行顺序很重要。这也是在 CI 流水线开头添加静态分析步骤的良好实践。我们建议以下 CI 步骤顺序:

  1. gitleaks 检查
  2. Linting 检查
  3. 格式检查
  4. 自动化测试,如单元测试和集成测试

如果任何检查失败,CI 流水线将失败,并且创建 PR 的开发人员无法将其合并到 staging 分支,直到问题得到解决。

实施 CI 流水线可以确保新功能遵循代码库的标准并且不会破坏现有功能。当我们计划将 staging 分支合并到 production 时,整个过程会重复一遍。我们打开一个 PR,CI 流水线会在 staging 分支合并到 production 之前自动执行。

image.png

CD 流水线在分支合并后运行。例如,在功能分支合并到 staging 后,CD 流水线会从 staging 分支获取代码,构建一个新的 Docker 镜像并将其推送到 AWS ECR Docker 仓库。在 staging 环境中运行未来的流水线时,它将使用由 CD 流水线构建的最新 Docker 镜像。在 staging 和 production 之间的流程相同,但关键区别在于 staging 环境是一个实验性环境,QA 团队和利益相关者可以在此进行手动测试,以进一步验证新功能,同时配合 CI 流水线中的自动化测试。

在我们的代码库中,我们只使用了一个 main 分支(代表生产环境)和功能分支来推送新工作。这样做是为了保持流程简单,但相同的原则同样适用。如果要扩展流程,您需要创建一个 staging 分支并将其添加到 CD 流水线中。

更多关于格式错误

格式错误涉及代码的风格和结构,确保其符合一致的视觉布局,包括空格的使用、缩进、行长度和其他风格元素。格式化的主要目的是提高代码的可读性和可维护性。保持一致的格式有助于团队更高效地协作,使代码看起来统一,无论编写者是谁。格式错误的示例包括:

  • 不正确的缩进(例如,混用空格和制表符)
  • 行过长(例如,超过 79 或 88 个字符,具体取决于风格指南)
  • 运算符周围或逗号后的空格缺失或多余

更多关于 linting 错误

Linting 错误与代码中的潜在问题相关,这些问题可能导致错误、效率低下或不符合编码标准。Linting 检查通常涉及代码的静态分析,以捕获诸如未使用的变量、未定义的名称或有问题的实践等情况。Linting 的主要目标是在开发过程中早期发现潜在的错误或不良实践,从而提高代码质量并减少错误的可能性。Linting 错误的示例包括:

  • 未使用的导入或变量
  • 使用了未定义的变量或函数
  • 潜在危险的代码(例如,使用 == 而不是 is 来检查是否为 None

我们使用 Ruff 作为多功能的格式和 linting 工具。Ruff 可以检查常见的格式问题和 PEP 8 合规性,还能进行深入的 linting 检查,以捕捉潜在错误和代码质量问题。此外,它是用 Rust 编写的,适合大规模代码库,运行速度快。

在实施上述内容之前,让我们先了解 GitHub Actions 的核心原则。

GitHub Actions 概述

GitHub Actions 是 GitHub 提供的 CI/CD 平台,允许开发者直接在 GitHub 仓库中自动化工作流。通过在 YAML 文件中定义工作流,用户可以直接从 GitHub 构建、测试和部署代码。作为 GitHub 的一部分,它可以无缝集成到仓库、问题、PR 和其他 GitHub 功能中。以下是关键组件:

  • Workflows(工作流) :工作流是一个自动化流程,定义在仓库的 .github/workflows 目录中的 YAML 文件中。它指定了应执行的操作(例如构建、测试、部署)以及执行的时机(例如在 push 或 PR 时)。
  • Jobs(作业) :工作流由作业组成,作业是在同一运行器上执行的步骤组。每个作业都在其自己的虚拟环境中运行。
  • Steps(步骤) :作业由多个独立的步骤组成,可以是 actions 或 shell 命令。
  • Actions(操作) :操作是可重用的命令或脚本,可以使用 GitHub Marketplace 的预构建操作,也可以创建自定义操作。可以将它们类比为 Python 函数。
  • Runners(运行器) :运行器是运行作业的服务器。GitHub 提供托管运行器(Linux、Windows、macOS),或者您可以自托管运行器。

工作流使用 YAML 语法描述。例如,以下是一个简单的工作流,它在 Ubuntu 机器上克隆当前 GitHub 仓库并安装 Python 3.11:

name: Example
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v3
        with:
          python-version: "3.11"

工作流由事件触发,例如 pushpull_requestschedule。例如,您可以在每次将代码推送到特定分支时触发工作流。现在我们了解了 GitHub Actions 的工作原理,让我们看看 LLM Twin 的 CI 流水线。

CI 流水线

LLM Twin 的 CI 流水线分为两个作业:

  1. QA 作业:使用 Ruff 检查格式和 linting 错误,同时运行 gitleaks 步骤,扫描代码库中泄露的敏感信息。
  2. 测试作业:使用 Pytest 运行所有自动化测试。在我们的示例中,只有一个演示用的测试来展示 CI 流水线,您可以根据本书的结构轻松扩展它,以适应您的实际测试需求。

GitHub Actions CI YAML 文件

YAML 文件位于 .github/workflows/ci.yaml 下。以下代码片段展示了该文件的部分内容。首先,定义了工作流的名称 “CI”,它将用于 GitHub Actions 界面中识别该工作流。接着,指定工作流应在 pull_request 事件发生时触发,因此,每当 PR 打开、同步或重新打开时,CI 工作流将自动运行。

name: CI
on:
  pull_request:

concurrency 部分确保在给定引用(如分支)上仅有一个实例运行。group 字段使用 GitHub 的表达式语法生成一个基于工作流和引用的唯一组名。cancel-in-progress: true 确保如果在前一次运行完成前触发新的工作流,则取消前一次运行,从而避免重复执行相同的工作流。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

工作流定义了两个独立的作业:qatest,每个作业都在最新的 Ubuntu 版本上运行,指定为 runs-on: ubuntu-latest

第一个作业名为 QA,负责质量保证任务,如代码检查和格式验证。在 qa 作业中,第一个步骤是使用 actions/checkout@v3 操作检出代码库,以确保作业可以访问需要分析的代码。

jobs:
  qa:
    name: QA
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

接下来设置 Python 环境,使用 actions/setup-python@v3 操作,并指定 Python 版本为 "3.11"。这确保了作业中的后续步骤在正确的 Python 环境中运行。

      - name: Setup Python
        uses: actions/setup-python@v3
        with:
          python-version: "3.11"

工作流随后使用 abatilo/actions-poetry@v2 操作安装 Poetry,并指定 Poetry 版本为 1.8.3:

      - name: Install poetry
        uses: abatilo/actions-poetry@v2
        with:
          poetry-version: 1.8.3

在设置好 Poetry 后,工作流使用 poetry install --only dev 命令安装项目的开发依赖项,另外还安装了 poethepoet 插件,方便运行项目中的预定义任务。

      - name: Install packages
        run: |
          poetry install --only dev
          poetry self add 'poethepoet[poetry_plugin]'

qa 作业接下来在代码上运行几个质量检查。第一个检查使用 gitleaks 工具扫描代码库中的敏感信息,确保没有误提交的敏感数据:

      - name: gitleaks check
        run: poetry poe gitleaks-check

gitleaks 检查之后,工作流运行 linting 过程,通过 poetry poe lint-check 命令强制执行 Python 代码的编码标准和最佳实践,这一步使用 Ruff。

      - name: Lint check [Python]
        run: poetry poe lint-check

qa 作业的最后一步是格式检查,确保 Python 代码符合项目的样式指南。通过 poetry poe format-check 命令实现,该命令使用 Ruff。

      - name: Format check [Python]
        run: poetry poe format-check

工作流中定义的第二个作业是 test 作业,同样运行在最新的 Ubuntu 版本上。与 qa 作业类似,test 作业首先检出代码,并安装 Python 3.11 和 Poetry 1.8.3。

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      ...

在设置系统依赖后,test 作业通过 poetry install 命令安装项目的所有依赖项。由于我们要运行测试,这次需要安装运行应用所需的所有依赖项。

      - name: Install packages
        run: |
          poetry install --without aws
          poetry self add 'poethepoet[poetry_plugin]'

最后,test 作业使用 poetry poe test 命令运行项目的测试。此步骤确保所有测试被执行,并反馈当前代码更改是否破坏了任何功能。

      - name: Run tests
        run: |
          echo "Running tests..."
          poetry poe test

如果 QAtest 作业中的任何步骤失败,GitHub Actions 工作流也会失败,导致该 PR 无法合并,直到问题得到解决。通过这种方式,我们确保添加到主分支的所有新功能都符合项目标准,并且通过自动化测试不会破坏现有功能。

图 11.15 显示了 GitHub 仓库的 Actions 标签中的 CI 流水线。它在提交消息为 “feat: Add Docker image and CD pipeline” 的提交后运行,执行了上述两个作业:QATest

image.png

CD 流水线

CD 流水线将自动化“部署 LLM Twin 流水线到云端”部分中手动执行的 Docker 步骤,包括:

  1. 设置 Docker。
  2. 登录到 AWS。
  3. 构建 Docker 镜像。
  4. 将 Docker 镜像推送到 AWS ECR。

接下来,让我们看看 GitHub Actions 的 YAML 文件,位于 .github/workflows/cd.yaml 下。文件以将工作流命名为 “CD” 开始,并指定触发条件:对仓库主分支的任何 push 操作。通常,当 PR 合并到主分支时会自动触发该工作流。on.push 配置定义了触发条件:

name: CD
on:
  push:
    branches:
      - main

工作流接下来定义了一个名为 Build & Push Docker Image 的单独作业:

jobs:
  build:
    name: Build & Push Docker Image
    runs-on: ubuntu-latest

作业中的第一个步骤是检出代码库的代码:

steps:
  - name: Checkout Code
    uses: actions/checkout@v3

在检出代码后,工作流设置 docker buildx,这是一个 Docker CLI 插件,扩展了 Docker 的构建功能,包括多平台构建和缓存导入/导出:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

接下来配置 AWS 凭证,这是与 AWS 服务(如 Amazon ECR)交互的关键步骤,Docker 镜像将推送到 ECR。AWS 访问密钥、秘密访问密钥和区域从仓库的密钥中安全地检索,以便工作流能够在 AWS 上进行身份验证,确保工作流有权将 Docker 镜像推送到 ECR 仓库。我们将在完成 YAML 文件后介绍如何配置这些密钥:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ${{ secrets.AWS_REGION }}

配置 AWS 凭证后,工作流登录到 Amazon ECR,这是为 Docker CLI 验证 ECR 注册表所必需的步骤,确保后续步骤可以将镜像推送到注册表:

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v1

工作流的最后一步是构建 Docker 镜像并将其推送到 Amazon ECR 仓库,这通过 docker/build-push-action@v6 操作实现。context 指定构建上下文,通常为仓库的根目录;file 指定 Dockerfile 文件;tags 部分为镜像分配标签,包括特定的提交 SHA 和 latest 标签,这是一种标识镜像最新版本的常见做法;push 选项设为 true,表示镜像在构建后会被上传到 ECR:

- name: Build images & push to ECR
  id: build-image
  uses: docker/build-push-action@v6
  with:
    context: .
    file: ./Dockerfile
    tags: |
      ${{ steps.login-ecr.outputs.registry }}/${{ secrets.AWS_ECR_NAME }}:${{ github.sha }}
      ${{ steps.login-ecr.outputs.registry }}/${{ secrets.AWS_ECR_NAME }}:latest
    push: true

总结来说,CD 流水线完成 AWS 身份验证,构建 Docker 镜像,并将其推送到 AWS ECR。镜像使用 latest 和提交的 SHA 标签推送,从而始终可以使用最新镜像并指向生成镜像的代码提交。

在我们的代码中,我们只有一个 main 分支,反映生产环境。但您可以扩展此功能,添加 staging 和 dev 环境。只需在 YAML 文件开头的 on.push.branches 配置中添加这些分支的名称即可。

图 11.16 显示了在 PR 合并到生产分支后 CD 流水线的外观。正如之前所见,这里仅包含 Build & Push Docker Image 作业。

image.png

设置 CI/CD 流水线的最后一步是测试它并查看其工作效果。

测试 CI/CD 流水线

要自己测试 CI/CD 流水线,您需要将 LLM-Engineering 仓库进行 fork,以便获得 GitHub 仓库的完整写权限。以下是关于如何 fork GitHub 项目的官方教程:如何 fork 仓库

最后一步是设置几个密钥,以便 CD 流水线能够登录 AWS 并指向正确的 ECR 资源。为此,请转到 fork 后的 GitHub 仓库顶部的 Settings 选项卡。在左侧面板的 Security 部分,点击 Secrets and Variables 并选择 Actions。然后,在 Secrets 标签下创建四个仓库密钥,如图 11.17 所示。这些密钥将被安全存储,仅供 GitHub Actions CD 流水线访问。

AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 是您在本书中使用的 AWS 凭证。在第 2 章中介绍了如何创建这些凭证。AWS_REGION(例如 eu-central-1)和 AWS_ECR_NAME 与“部署 LLM Twin 流水线到云端”部分中使用的相同。

对于 AWS_ECR_NAME,仅配置仓库的名称(例如 zenml-vrsopg),而不是完整的 URI(例如 992382797823.dkr.ecr.eu-central-1.amazonaws.com/zenml-vrsopg),如下图所示:

image.png

图 11.17:仅配置仓库名称

要触发 CI 流水线,创建一个功能分支,修改代码或文档,然后向 main 分支创建 PR。要触发 CD 流水线,将 PR 合并到 main 分支。

CD GitHub Actions 完成后,检查 ECR 仓库,以确认 Docker 镜像是否成功推送。

image.png

如果您需要有关设置 GitHub Actions 密钥的更多详细信息,建议查看官方文档:GitHub Actions 密钥使用指南

CT 流水线

为了实现 CT 流水线,我们将利用 ZenML。一旦 ZenML(或其他编排工具如 Metaflow、Dagster 或 Airflow)编排了您的所有流水线并部署了基础设施,您就非常接近实现 CT 了。

请记住 CI/CD 和 CT 流水线之间的核心区别。CI/CD 流水线负责测试、构建和部署代码,这是任何软件程序的一个维度;CT 流水线则利用 CI/CD 流水线管理的代码来自动化数据、训练和模型服务过程,其中数据和模型维度仅在 AI 领域中存在。

在开始实施之前,我们想强调两个使实现 CT 更加简单的设计选择:

  1. FTI 架构:一个模块化系统,具有清晰的接口和组件,使得捕获流水线之间的关系并实现自动化变得简单。
  2. 从第一天就开始使用编排器:我们在项目初期就使用了 ZenML。最初,我们只在本地使用它,但它成为了流水线的入口点,并帮助我们监控其执行情况。这样做促使我们将每个流水线进行解耦,并通过各种数据存储(如数据仓库、特征库或工件库)在流水线之间进行数据传递。由于从一开始就使用 ZenML,我们不再需要实现繁琐的 CLI 来配置应用程序,而是直接通过 YAML 配置文件进行设置。

在图 11.19 中,我们可以看到需要串联在一起的所有流水线,以实现训练和部署的完全自动化。这些流水线并不新颖,而是汇聚了本书中涵盖的所有内容。因此,在此我们将它们视为相互交互的黑盒。

image.png

对于 LLM Twin 的 CT 流水线,我们需要讨论启动流水线的初始触发方式以及流水线之间的相互触发。

初始触发

如图 11.18 所示,我们最初希望触发数据收集流水线。通常,触发方式有三种类型:

  1. 手动触发:通过 CLI 或编排器的仪表板触发,在我们的案例中,通过 ZenML 仪表板进行。手动触发仍然是一种非常强大的工具,只需一个操作就能启动整个 ML 系统,从数据收集到部署,而无需处理数十个脚本,这些脚本可能会配置错误或按无效顺序运行。

  2. REST API 触发:可以通过 HTTP 请求调用流水线。当将 ML 流水线与其他组件集成时,这种方式非常有用。例如,您可以设置一个监视器来持续查找新文章,当发现时,通过 REST API 触发 ML 逻辑。更多关于此功能的信息,请参阅 ZenML 文档中的此教程

  3. 定时触发:另一种常见方法是将流水线安排在固定间隔内运行。例如,根据您的用例,您可以将流水线设置为每天、每小时或每分钟运行一次。大多数编排器(包括 ZenML)都提供了 cron 表达式界面,您可以在其中定义执行频率。以下是 ZenML 中的一个示例,将流水线设定为每小时运行一次:

    Schedule(cron_expression="* * 1 * *")
    

对于 LLM Twin 的用例,我们选择了手动触发,因为没有其他组件来利用 REST API 触发。同时,由于数据集是从 ZenML 配置中定义的一组静态链接生成的,按计划运行它们没有意义,因为每次运行结果都是相同的。

但是,项目的一个潜在后续步骤是实现一个监视器,监视新的文章出现。当找到新文章时,它会生成新的配置并通过 REST API 触发流水线。另一种选择是将监视器实现为一个额外的流水线,并利用定时触发器每天查找新数据。如果找到新数据,则执行整个 ML 系统,否则停止。

结论是,一旦您可以通过单个命令手动触发所有 ML 流水线,就可以快速将其适应到更高级和更复杂的场景。

触发下游流水线

为了简化流程,我们按顺序串联了所有流水线。具体来说,当数据收集流水线完成后,它会触发特征流水线。当特征流水线成功完成时,它会触发数据集生成流水线,依此类推。您可以使逻辑更加复杂,例如将指令数据集生成流水线设定为每日运行,检查 Qdrant 向量数据库中的新数据量,只有在新数据足够时才启动。从这里开始,您可以进一步调整系统参数并优化它们以降低成本。

为了一次性触发所有流水线,我们创建了一个聚合所有流水线的主流水线,将所有内容集中到一个入口点:

@pipeline
def end_to_end_data(
    author_links: list[dict[str, str | list[str]]], … # 其他参数…
) -> None:
    wait_for_ids = []
    for author_data in author_links:
        last_step_invocation_id = digital_data_etl(
            user_full_name=author_data["user_full_name"], links=author_data["links"]
        )
        wait_for_ids.append(last_step_invocation_id)
    author_full_names = [author_data["user_full_name"] for author_data in author_links]
    wait_for_ids = feature_engineering(author_full_names=author_full_names, wait_for=wait_for_ids)
    generate_instruct_datasets(…)
    training(…)
    deploy(…)

为了保持函数简洁,我们将逻辑添加到计算特征这一步为止。不过,正如上面代码片段所示,您可以轻松地将指令数据集生成、训练和部署逻辑添加到父流水线中,以实现端到端的流程。这样,您可以自动化从数据收集到模型部署的整个过程。

要运行端到端流水线,请使用以下 poe 命令:

poetry poe run-end-to-end-data-pipeline

我们实现的方式并非最佳方法,因为它将所有步骤压缩到单个大型流水线中(通常我们希望避免这种情况),如图 11.20 所示。通常,您希望保持每个流水线相对独立,并使用触发器启动下游流水线。这种方式使系统更易于理解、调试和监控。

image.png

遗憾的是,ZenML 云的免费试用版限制最多只能创建三个流水线。由于我们有更多的流水线,因此通过将所有步骤压缩到一个流水线中来避免此限制。但如果您计划自建 ZenML 或购买其许可证,您可以将一个流水线独立地从另一个流水线中触发,正如下面代码片段所示,其中我们在数据收集 ETL 后触发了特征工程流水线:

from zenml import pipeline, step

@pipeline 
def digital_data_etl(user_full_name: str, links: list[str]) -> str:
    user = get_or_create_user(user_full_name)
    crawl_links(user=user, links=links)
    trigger_feature_engineering_pipeline(user)

@step 
def trigger_feature_engineering_pipeline(user):
    run_config = PipelineRunConfiguration(…)
    Client().trigger_pipeline("feature_engineering", run_configuration=run_config)

@pipeline
def feature_engineering(author_full_names: list[str]) -> list[str]:
    … # ZenML steps

采用这种方法,每个流水线将独立运行,一个流水线按顺序触发下一个流水线,正如本节开头所描述的。请注意,这个功能并非 ZenML 独有,而是编排工具中的常见功能。我们迄今为止学习的原则仍然适用,只是与工具的交互方式有所不同。

提示监控

我们将使用 Opik(来自 Comet ML)来监控提示。然而,正如本章前面提到的 LLMOps 部分所述,我们关注的不仅仅是输入提示和生成的答案。我们希望记录从用户输入到最终结果可用的整个追踪过程。

在深入讨论 LLM Twin 用例之前,先来看一个简单示例:

from opik import track
import openai
from opik.integrations.openai import track_openai

openai_client = track_openai(openai.OpenAI())

@track
def preprocess_input(text: str) -> str:
    return text.strip().lower()

@track
def generate_response(prompt: str) -> str:
    response = openai_client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

@track
def postprocess_output(response: str) -> str:
    return response.capitalize()

@track(name="llm_chain")
def llm_chain(input_text: str) -> str:
    preprocessed = preprocess_input(input_text)
    generated = generate_response(preprocessed)
    postprocessed = postprocess_output(generated)
    return postprocessed

result = llm_chain("Hello, do you enjoy reading the book?")

上述代码片段简化展示了大多数 LLM 应用程序的结构。llm_chain() 主函数接受初始输入并返回最终结果,围绕实际的 LLM 调用包含了预处理和后处理函数。使用 @track() 装饰器,我们记录了每个函数的输入和输出,最终将其聚合成一个完整的追踪记录。这样,我们可以访问初始输入文本、生成的答案以及所有中间步骤,便于在 Opik 仪表板中调试潜在问题。

最后一步是将必要的元数据附加到当前追踪记录中,如以下代码所示。可以通过调用 update() 方法轻松实现,将追踪记录标记或添加元数据(如输入 token 数):

from opik import track, opik_context

@track
def llm_chain(input_text):
    # LLM chain 代码
    # ...
    opik_context.update_current_trace(
        tags=["inference_pipeline"],
        metadata={
            "num_tokens": compute_num_tokens(…)
        },
        feedback_scores=[
            {
                "name": "user_feedback",
                "value": 1.0,
                "reason": "The response was valuable and correct."
            },
            {
                "name": "llm_judge_score",
                "value": compute_llm_judge_score(…),
                "reason": "Using an LLM Judge for runtime metrics."
            }
        ]
    )

您可以进一步扩展此思路并记录不同的反馈评分,最常见的是询问用户生成的答案是否有价值和正确。另一种选择是通过启发式方法或 LLM 评估来自动计算各种指标。

最后,来看如何将提示监控添加到我们的 LLM Twin 项目中。首先参考图 11.21,并回顾我们的模型服务架构。我们有两个微服务:LLM 和业务微服务。LLM 微服务作用范围较小,它仅接受包含用户输入和上下文的提示作为输入并返回通常已后处理的答案。因此,业务微服务是实现监控流水线的正确位置,因为它协调端到端流程。具体来说,Opik 的实现将在第 10 章开发的 FastAPI 服务器中进行。

image.png

由于我们的实现已经是模块化的,因此使用 Opik 可以轻松记录用户请求的端到端追踪:

from opik import track

@track
def call_llm_service(query: str, context: str | None) -> str:
    llm = LLMInferenceSagemakerEndpoint(…)
    answer = InferenceExecutor(llm, query, context).execute()
    return answer

@track
def rag(query: str) -> str:
    retriever = ContextRetriever()
    documents = retriever.search(query, k=3 * 3)
    context = EmbeddedChunk.to_context(documents)
    answer = call_llm_service(query, context)
    return answer

rag() 函数是应用程序的入口点,其他处理步骤则在 ContextRetrieverInferenceExecutor 类中执行。通过为 call_llm_service() 函数添加装饰器,我们可以清晰地捕获发送到 LLM 的提示和其响应。

为追踪记录增加更多细化,可以进一步装饰其他包含预处理或后处理步骤的函数,例如 ContextRetrieversearch 函数:

class ContextRetriever:
    …
   
    @track
    def search(
        self,
        query: str,
        k: int = 3,
        expand_to_n_queries: int = 3,
    ) -> list:
        query_model = Query.from_str(query)
        query_model = self._metadata_extractor.generate(query_model)
        … # 其他实现内容

甚至可以深入到检索优化方法,例如自查询元数据提取器,以增加细化程度:

class SelfQuery:
    @track
    def generate(self, query: str) -> str:
        …
        return enhanced_query

开发者需要决定应用程序的调试和分析所需的细化程度。尽管详细的监控很重要,但监控过多会增加噪声,使得手动理解追踪记录变得困难。因此,找到适当的平衡很关键。一个好的做法是先追踪最关键的函数,如 rag()call_llm_service(),并在需要时逐步增加细化。

最后一步是将有价值的元数据和标签附加到追踪记录中。为此,我们可以进一步增强 rag() 函数,如下所示:

@track
def rag(query: str) -> str:
    retriever = ContextRetriever()
    documents = retriever.search(query, k=3 * 3)
    context = EmbeddedChunk.to_context(documents)
    answer, prompt = call_llm_service(query, context)
    trace = get_current_trace()
    trace.update(
        tags=["rag"],
        metadata={
            "model_id": settings.HF_MODEL_ID,
            "embedding_model_id": settings.TEXT_EMBEDDING_MODEL_ID,
            "temperature": settings.TEMPERATURE_INFERENCE,
            "prompt_tokens": compute_num_tokens(prompt),
            "total_tokens": compute_num_tokens(answer),
        }
    )
    return answer

需要持续监控的三个主要方面:

  1. 模型配置:包括 LLM 和 RAG 层中使用的其他模型。最重要的是记录模型 ID,同时也可以记录影响生成效果的重要参数,如温度。
  2. Token 总数:持续分析输入提示生成的 token 数量和总 token 数量的统计数据非常重要,因为这会显著影响服务成本。例如,如果生成的总 token 数量平均值突然增加,这可能是系统存在问题的信号,需要进一步调查。
  3. 每个步骤的持续时间:跟踪每个步骤的持续时间有助于发现系统中的瓶颈。如果某个请求的延迟异常大,可以快速访问报告,帮助找到问题的源头。

告警

使用 ZenML,您可以快速在任何您喜欢的平台上实现告警系统,例如电子邮件、Discord 或 Slack。例如,可以在训练流水线中添加回调函数,以在流水线失败或训练成功完成时触发通知:

from zenml import get_pipeline_context, pipeline

@pipeline(on_failure=notify_on_failure)
def training_pipeline():
    …
    notify_on_success()

实现通知功能很简单。如以下代码片段所示,您需要从当前栈中获取 alerter 实例,按需构建消息,并将其发送到您选择的通知渠道:

from zenml.client import Client

alerter = Client().active_stack.alerter

def notify_on_failure() -> None:
    alerter.post(message=build_message(status="failed"))

@step(enable_cache=False)
def notify_on_success() -> None:
    alerter.post(message=build_message(status="succeeded"))

ZenML 和大多数编排工具都简化了告警功能的实现,因为它是 MLOps/LLMOps 基础设施中的关键组件。

总结

在本章中,我们首先通过理论部分介绍了 DevOps 的基础知识。接着,我们讨论了 MLOps 的核心组件和原则。最后,通过引入提示监控、保护机制和人机反馈等策略,我们展示了 LLMOps 与 MLOps 的区别。我们还简要讨论了大多数公司为什么会避免从头训练 LLM,而是通过提示工程或微调来优化模型以适应特定用例。在本章的理论部分结尾,我们学习了 CI/CD/CT 流水线的概念、ML 应用程序的三个核心维度(代码、数据、模型),并认识到在部署后实施监控和告警层的重要性,以应对模型退化。

接下来,我们学习了如何将 LLM Twin 的流水线部署到云端,理解了基础设施,并一步步完成了 MongoDB、Qdrant、ZenML 云以及支持应用所需的所有 AWS 资源的部署。最后,我们学习了如何将应用容器化并将 Docker 镜像推送到 AWS ECR,以便在 AWS SageMaker 上执行应用程序。

最后一步是为 LLM Twin 项目添加 LLMOps。我们首先使用 GitHub Actions 实现了 CI/CD 流水线,然后利用 ZenML 制定了 CT 策略。

最后,我们展示了如何使用 Comet ML 的 Opik 实现监控流水线,并使用 ZenML 实现告警系统。这些是将 MLOps 和 LLMOps 添加到任何基于 LLM 的应用程序中的基本支柱。

本书介绍的框架可以快速应用到其他 LLM 应用中。即使我们以 LLM Twin 为例,大多数策略都可以适配到其他项目。通过更改数据和稍微调整代码,我们可以构建一个全新的应用。别忘了,数据是新的石油!

完成本章后,我们学会了如何构建一个端到端的 LLM 应用程序,从数据收集和微调开始,到部署 LLM 微服务和 RAG 服务。在本书中,我们的目标是提供一个思维框架,帮助您在生成式 AI 领域中解决实际问题。现在,祝您在探索和构建之旅中好运!

参考文献

  1. GitLab. (2023, January 25). What is DevOps?
  2. Huyen, C. (2024, July 25). Building a generative AI platform
  3. Lightricks customer story: Building a recommendation engine from scratch
  4. What is LLMOps
  5. Google Cloud. (2024, August 28). MLOps: Continuous delivery and automation pipelines in machine learning
  6. Ml-ops.org. (2024a, July 5). MLOps Principles
  7. Ml-ops.org. (2024b, July 5). MLOps Principles
  8. Ml-ops.org. (2024c, July 5). Motivation
  9. Mohandas, G. M. (2022a). Monitoring machine learning systems
  10. Mohandas, G. M. (2022b). Testing Machine Learning Systems: Code, Data and Models
  11. Preston-Werner, T. (n.d.). Semantic Versioning 2.0.0
  12. Ribeiro, M. T., Wu, T., Guestrin, C., & Singh, S. (2020, May 8). Beyond Accuracy: Behavioral Testing of NLP models with CheckList
  13. Wandb. (2023, November 30). Understanding LLMOps: Large Language Model Operations
  14. Zenml-Io. (n.d.). GitHub—zenml-io/zenml-huggingface-sagemaker: An example MLOps overview of ZenML pipelines from a Hugging Face model repository to a deployed AWS SageMaker endpoint