Press "Enter" to skip to content

在13分钟内使用Hamilton构建一个易维护和模块化的LLM应用程序堆栈

LLM应用是数据流,使用专门设计的工具来表达它们

LLM堆栈。使用正确的工具,如Hamilton,可以确保您的堆栈不会变得难以维护和管理。来自pixabay的图像。

本文与Thierry Jean合作撰写,最初发表于此处。

在本文中,我们将分享开源框架Hamilton如何帮助您为大型语言模型(LLM)应用堆栈编写模块化且易于维护的代码。Hamilton非常适合描述任何类型的数据流,而构建LLM驱动的应用程序时,正是在进行数据流处理。通过Hamilton,您可以获得强大的软件维护人机工程学,同时还能够轻松替换和评估应用程序组件的不同提供者/实现。免责声明:我是Hamilton包的作者之一。

我们将通过一个示例向您介绍一个典型的LLM应用程序工作流程,该工作流程用于使用一些文本知识填充向量数据库。具体而言,我们将涵盖从网络中提取数据、创建文本嵌入(向量)并将其推送到向量存储器的过程。

堆栈概述。来自作者的图像。

LLM应用程序数据流

首先,让我们描述一下典型的LLM数据流由哪些部分组成。应用程序将接收到一个小的数据输入(例如文本、命令),并在更大的上下文(例如聊天记录、文档、状态)中进行操作。这些数据将通过不同的服务(LLM、向量数据库、文档存储等)进行传递,执行操作,生成新的数据工件,并返回最终结果。大多数用例在迭代不同输入时会重复执行此流程。

一些常见的操作包括:

  • 将文本转换为嵌入
  • 存储/搜索/检索嵌入
  • 查找最接近嵌入的邻居
  • 检索嵌入的文本
  • 确定传递到提示中所需的上下文
  • 使用相关文本中的上下文提示模型
  • 将结果发送到另一个服务(API、数据库等)
  • 将它们链接在一起!

现在,让我们将上述内容考虑在生产环境中,并想象一下用户对应用程序的输出不满意,您想找出问题的根源。您的应用程序记录了提示和结果。您的代码允许您找出操作的顺序。然而,您无法知道问题出在哪里,系统产生了不良输出… 为了解决这个问题,我们认为有必要拥有数据工件的血统以及生成它们的代码,这样您就可以快速调试此类情况。

为了增加LLM应用程序数据流的复杂性,许多操作是非确定性的,这意味着您无法重新运行或逆向工程操作以重现中间结果。例如,调用API生成文本或图像响应很可能是不可再现的,即使您可以访问相同的输入和配置(您可以通过温度等选项来缓解这种情况)。这也适用于某些向量数据库操作,例如“查找最近的”操作,其结果取决于当前存储在数据库中的对象。在生产环境中,几乎不可能快照数据库状态以使调用可再现。

出于这些原因,采用灵活的工具来创建强大的数据流至关重要,这些工具使您能够:

  1. 轻松插入各种组件。
  2. 看到组件之间的连接方式。
  3. 添加和自定义常见的生产需求,如缓存、验证和可观察性。
  4. 根据您的需求调整流程结构,无需强大的工程技能。
  5. 与传统的数据处理和机器学习生态系统连接。

在本文中,我们将概述Hamilton如何满足1、2和4点的要求。我们将您引用到我们的文档中了解3和5点的内容。

目前的LLM应用程序开发工具

LLM空间仍处于起步阶段,使用模式和工具正在快速发展。虽然LLM框架可以帮助您入门,但目前的选项尚未经过生产测试;据我们所知,没有任何已建立的技术公司在生产环境中使用当前流行的LLM框架。

不要误会,一些工具可以快速创建概念验证!然而,我们觉得它们在两个特定领域存在不足:

1. 如何对LLM应用程序的数据流进行建模。我们坚信,“操作”数据流更适合以函数的形式建模,而不是通过面向对象的类和生命周期。函数更简单易于理解、测试和修改。面向对象的类可能变得非常复杂,增加了心理负担。

当出现错误时,面向对象的框架需要您深入对象的源代码才能理解错误。而使用Hamilton函数时,清晰的依赖关系可以告诉您在哪里查找,并帮助您推理发生了什么(下面会详细介绍)!

2. 自定义/扩展。不幸的是,一旦超出当前框架所提供的“易于”操作范围,您需要具备强大的软件工程技能才能修改当前框架。如果这不是一个选择,这意味着您可能会在特定的自定义业务逻辑上超出框架的范围,这可能会导致您维护的代码表面积比起初不使用框架时要大。

关于这两个问题的更多信息,我们建议您参考相关讨论(hacker news, reddit)中用户的详细发言。

虽然Hamilton不能完全取代当前的LLM框架(例如,没有“agent”组件),但它具备满足LLM应用程序需求的所有构建块,两者可以并行工作。如果您想要一种干净、清晰和可自定义的方式来编写生产代码,集成多个LLM堆栈组件,并监控您的应用程序,那么让我们继续下一节吧!

使用Hamilton构建

Hamilton是一个用于描述Python中数据流的声明性微框架。它不是一个新的框架(已有3.5年历史),多年来一直用于生产建模数据和机器学习数据流。它的优势在于以一种简单易懂的方式表达数据和计算的流程(就像DBT对于SQL的作用一样),非常适合支持LLM应用程序的数据和计算需求的建模。

The Hamilton paradigm in a picture. Instead of procedural assignment, you instead model it as a function. The function name is an “output” you can get, while the function input arguments declare dependencies on what’s required to be computed. Image by author.

Hamilton的基本原理很简单,它可以以多种方式进行扩展;您不必了解Hamilton也可以从本文中获得价值,但如果您有兴趣,可以参考以下内容:

  • tryhamilton.dev-在浏览器中的交互式教程!
  • 在Hamilton中进行Pandas数据转换,只需5分钟
  • 在10分钟内了解Hamilton的血统+Hamilton
  • Hamilton + Airflow用于生产

我们的例子

为了帮助您建立一些心理背景,想象一下这种情况。您是一个小型数据团队,负责创建一个LLM应用程序与您组织的文档进行“聊天”。您认为从功能、性能、许可证、基础设施需求和成本等方面评估候选架构是很重要的。最终,您知道您组织的主要关注点是提供最相关的结果和良好的用户体验。评估这一点的最佳方法是构建一个原型,测试不同的堆栈并比较它们的特性和输出。然后,当您过渡到生产环境时,您将希望系统可以轻松维护和审查,以始终提供出色的用户体验。

考虑到这一点,在本示例中,我们将实现LLM应用程序的一部分,具体是数据摄取步骤,用于对知识库进行索引,其中我们将文本转换为嵌入向量并将其存储在向量数据库中。我们使用几个不同的服务/技术来实现这一模块化。主要步骤如下:

  1. 从HuggingFace Hub加载SQuAD数据集。您可以将其替换为预处理文档的语料库。
  2. 使用Cohere API、OpenAI API或SentenceTransformer库对文本条目进行嵌入。
  3. 将嵌入向量存储在向量数据库中,可以是LanceDB、Pinecone或Weaviate。

如果您想了解更多关于嵌入和搜索的信息,我们将读者引导至以下链接:

  • 解释文本嵌入 – Weaviate
  • 如何使用Pinecone进行语义搜索

在我们讲解这个示例的过程中,对您来说,考虑/记住以下内容将会很有用:

  • 将我们所展示的与您目前的工作进行比较。看看Hamilton是如何让您在不需要明确的LLM中心化框架的情况下策划和构建项目的。
  • 项目和应用程序结构。了解Hamilton如何强制执行一种结构,使您能够构建和维护一个模块化的堆栈。
  • 对迭代和项目持久性的信心。结合以上两点,Hamilton使您能够更轻松地在生产中维护LLM应用程序,无论由谁编写。

让我们从可视化开始,给您一个概述:

Pinecone + Sentence transformer堆栈的Hamilton DAG可视化。图片作者:

使用pinecone和sentence transformers时,LLM应用程序的数据流将如下所示。使用Hamilton来理解事物之间的关联就像在Hamilton驱动程序对象上调用display_all_functions()一样简单。

模块化代码

让我们使用示例来解释Hamilton中实现模块化代码的两种主要方法。

@config.when

Hamilton的重点是可读性。不解释@config.when的作用,您可能已经能够判断出这是一个条件语句,只有在谓词满足时才会包含在内。下面是使用OpenAI和Cohere API将文本转换为嵌入向量的实现。

由于@config.when装饰器和相同的函数名embeddings前面有双下划线(__cohere__openai),Hamilton将识别这两个函数作为替代实现。它们的函数签名不必完全相同,这意味着采用不同的实现是简单明了的。

embedding_module.py

对于这个项目,在同一个文件中使用@config.when装饰器实现所有嵌入服务是有意义的,因为每个服务只有3个函数。然而,随着项目的复杂性增加,函数可以移动到单独的模块中,并采用下一节的模块化模式。还要注意的一点是,每个这些函数都可以独立进行单元测试。如果您有特定的需求,将其封装在函数中并进行测试是很容易的。

切换Python模块

下面是Pinecone和Weaviate的向量数据库操作的实现。请注意,这些片段来自pinecone_module.pyweaviate_module.py,并注意函数签名的相似性和差异。

pinecone_module.py和weaviate_module.py

使用Hamilton,数据流通过函数名和函数输入参数进行连接。因此,通过为类似操作共享函数名称,这两个模块可以轻松互换。由于LanceDB、Pinecone和Weaviate的实现位于单独的模块中,每个文件的依赖关系减少了,使其更简短,提高了可读性和可维护性。每个实现的逻辑都清晰地封装在这些命名函数中,因此对于每个相应的模块来说,单元测试是容易实现的。单独的模块强化了它们不应同时加载的概念。当发现多个具有相同名称的函数时,Hamilton驱动程序实际上会抛出一个错误,以帮助强制执行这个概念。

驱动程序的影响

运行Hamilton代码的关键部分是在run.py中找到的Driver对象。除了CLI和一些参数解析的代码之外,我们得到:

run.py的代码片段

Hamilton驱动程序负责执行和控制数据流,通过上述代码片段可以看到它通过三种机制实现了模块化:

  1. 驱动程序配置。这是一个在实例化时驱动程序接收到的包含常量信息的字典,比如要使用哪个API,或者嵌入式服务的API密钥。这与能够传递JSON或字符串(例如Docker容器、Airflow、Metaflow等)的命令平面很好地结合在一起。具体来说,这是我们指定要使用哪个嵌入API的地方。
  2. 驱动程序模块。驱动程序可以接收任意数量的独立的Python模块来构建数据流。在这里,vector_db_module可以被替换为我们要连接的所需向量数据库实现。还可以通过importlib动态导入模块,在开发和生产环境中都很有用,并且还可以通过配置驱动数据流实现的方式来启用。
  3. 驱动程序执行。final_vars参数确定应该返回什么输出。您无需重新组织代码来改变所需的输出。假设我们想要在数据流中调试某些内容的情况,可以通过将其名称添加到final_vars来请求任何函数的输出。例如,如果您有一些中间输出需要调试,可以很容易地请求它,或者完全停止在该位置执行。请注意,驱动程序在调用execute()时可以接收inputsoverrides值;在上面的代码中,class_name是一个执行时的input,它指示我们要创建的嵌入对象以及在我们的向量数据库中存储它的位置。

模块化总结

在Hamilton中,实现可互换组件的关键是:

  1. 定义具有相同名称的函数,然后
  2. 使用@config.when进行注解,并通过传递给驱动程序的配置选择要使用的函数,或者
  3. 将它们放在单独的Python模块中,并将所需的模块传递给驱动程序。

因此,我们刚刚展示了如何使用Hamilton插入、替换和调用各种LLM组件。我们不需要解释什么是面向对象的层次结构,也不需要您具有丰富的软件工程经验来理解(我们希望如此!)。为了实现这一点,我们只需匹配函数名称和它们的输出类型。我们认为这种编写和模块化代码的方式比当前的LLM框架更易于理解。

实践中的Hamilton代码

为了证明我们的论点,以下是我们观察到的使用Hamilton代码进行LLM工作流的一些实际影响:

CI/CD

可以交换模块/@config.when的能力也意味着在CI系统中进行集成测试非常容易,因为您可以根据需要灵活地交换/隔离数据流的各个部分。

协作

  1. Hamilton所提供的模块化功能可以轻松地跨团队边界进行镜像。函数名称和它们的输出类型成为一个合同,确保可以进行精确的更改并对更改充满信心,并通过Hamilton的可视化和谱系功能(如我们所见的初始可视化)了解到下游依赖关系。例如,清楚地知道如何与向量数据库进行交互和消费。
  2. 代码更改更容易审查,因为流程由声明性函数定义。更改是自包含的;因为没有需要学习的面向对象层次结构,只需要修改一个函数。任何“自定义”都是Hamilton默认支持的。

调试

当Hamilton出现错误时,可以清楚地知道它映射到的代码是什么,由于函数的定义方式,可以知道在数据流中应该放置它。

以使用cohere的嵌入函数的简单示例为例。如果发生超时或解析响应时出现错误,可以清楚地知道它映射到这段代码,并且根据函数定义,您会知道它在流程中的位置。

@config.when(embedding_service="cohere")def embeddings__cohere(    embedding_provider: cohere.Client,    text_contents: list[str],    model_name: str = "embed-english-light-v2.0",) -> list[np.ndarray]:    """使用Cohere Embed API将文本转换为向量表示(embeddings)    参考:https://docs.cohere.com/reference/embed    """    response = embedding_provider.embed(        texts=text_contents,        model=model_name,        truncate="END",    )    return [np.asarray(embedding) for embedding in response.embeddings]
可视化显示`embeddings`在数据流中的位置。图片作者:自己。

创建模块化LLM堆栈的技巧

在我们结束之前,这里有一些指导您构建应用程序的想法。有些决策可能没有明显的最佳选择,但正确的模块化方法将使您能够在需求发展时高效迭代。

  1. 在编写任何代码之前,绘制工作流程的逻辑步骤的DAG。这为定义不特定于服务的常见步骤和接口奠定了基础。
  2. 识别可以交换的步骤。通过有目的地配置点,您将减少投机泛化的风险。具体而言,这将导致具有较少参数、默认值并且分组到主题模块中的函数。
  3. 如果相关,请将数据流的部分划分为具有少量依赖关系的模块。这将导致较短的Python文件,较少的软件包依赖关系,提高可读性和可维护性。汉密尔顿不关心,并且可以从多个模块构建其DAG。

关闭 & 未来方向

感谢您阅读到这里。我们相信汉密尔顿在帮助每个人表达他们的数据流方面发挥着重要作用,而LLM应用程序只是一个用例!总结我们在本文中传达的信息可以归结为:

  1. 将LLM应用程序视为数据流是有用的,因此使用汉密尔顿非常适合。
  2. 以对象为中心的LLM框架可能不透明,难以扩展和维护您的生产需求。相反,应该使用汉密尔顿的直观声明式风格编写自己的集成。这样做将提高您代码的透明度和可维护性,具有清晰的可测试函数,将运行时错误清晰映射到函数,并内置可视化您的数据流。
  3. 使用汉密尔顿所规定的模块化将使协作更加高效,并为您提供必要的灵活性,以便根据领域的发展速度修改和更改您的LLM工作流。

现在,我们邀请您在这里尝试并修改完整的示例。有一个`README`将解释要运行和开始的命令。否则,我们正在努力通过考虑以下内容来改进汉密尔顿+LLM应用程序体验:

  1. 代理。我们是否可以为普通汉密尔顿数据流提供相同级别的可见性?
  2. 并行化。如何更简单地表达在文档列表上运行数据流的示例。请参阅进行中的PR以了解我们的意思。
  3. 缓存和可观察性的插件。您已经可以在汉密尔顿之上实现自定义的缓存和可观察性解决方案。我们正在致力于为常见组件提供更多的开箱即用的标准选项,例如redis。
  4. 用户贡献的数据流部分。我们看到有可能为特定LLM应用程序用例标准化常见名称。在这种情况下,我们可以开始聚合汉密尔顿数据流,并允许人们根据自己的需求下载它们。

我们想听到您的声音!

如果您对其中任何内容感到兴奋,或有强烈的意见,请加入我们的Slack频道/或在这里留下一些评论!以下是一些获取帮助的资源:

📣 加入我们的Slack社区 – 我们非常乐意帮助回答您可能遇到的问题或帮助您入门。

⭐️ 给我们在GitHub上加星

</

📝 如果你发现了什么问题,请留下你的意见

以下是其他可能会感兴趣的 Hamilton 文章:

  • tryhamilton.dev – 在浏览器中的交互式教程!
  • 在 Hamilton 中进行 Pandas 数据转换,只需 5 分钟
  • 在 10 分钟内学会使用 Lineage + Hamilton
  • Hamilton + Airflow 用于生产环境
Leave a Reply

Your email address will not be published. Required fields are marked *