构建生成式 AI 平台 [译]
2024 年 7 月 25 日 • Chip Huyen
在研究了公司如何部署生成式 AI 应用程序后,我注意到它们的平台有许多相似之处。本文概述了生成式 AI 平台的常见组件、它们的功能以及如何实现。我尽量保持架构的通用性,但某些应用程序可能会有所不同。以下是总体架构的样子。
这是一个相当复杂的系统。本文将从最简单的架构开始,并逐步添加更多组件。在最简单的形式中,您的应用程序接收查询并将其发送到模型。模型生成响应并返回给用户。此时没有任何保护措施、增强的上下文或优化。模型 API 框既包括第三方 API(如 OpenAI、Google、Anthropic)也包括自托管的 API。
根据需要,您可以添加更多组件。本文讨论的顺序很常见,但您不需要完全按照这个顺序来。如果您的系统运作良好,可以跳过某个组件。在开发过程中的每一步都需要进行评估。
- 通过让模型访问外部数据源和信息收集工具来增强模型的上下文输入。
- 设置保护措施以保护您的系统和用户。
- 添加模型路由器和网关以支持复杂的管道并增加更多的安全性。
- 使用缓存优化延迟和成本。
- 添加复杂的逻辑和编写操作以最大化系统的能力。
可观察性(允许您通过监控和调试了解系统的可见性)和编排(涉及将所有组件串联在一起)是平台的两个重要组件。我们将在本文的最后讨论它们。
» 本文不包含的内容 «
本文主要讨论部署 AI 应用程序的整体架构。文中将讨论需要哪些组成部分以及在构建这些部分时需要考虑的因素。本文并不涉及如何构建 AI 应用程序,因此不讨论模型评估、应用程序评估、提示词工程、微调、数据标注指南或 RAG 的分块策略。所有这些主题都在我即将出版的书《AI 工程》中涵盖。
第一步:扩充上下文信息
平台的初步扩展通常涉及添加机制,以使系统能够用必要的信息丰富每个查询。收集相关信息被称为上下文构建。
许多查询需要上下文信息才能得到回答。上下文中的相关信息越多,模型对其内部知识的依赖就越少,而内部知识可能由于其训练数据和方法的不确定性而不够可靠。研究表明,获取上下文中的相关信息可以帮助模型生成更详细的响应,同时减少生成虚假信息(Lewis et al., 2020)。
例如,面对“Acme 的高级打印机 A300 能否打印 100pps?”这一查询,如果提供了高级打印机 A300 的规格,模型将能够更好地回答。(感谢 Chetan Tekur 提供的示例。)
对于基础模型来说,上下文构建相当于经典机器学习模型中的特征工程。它们的目的相同:为模型提供处理输入所需的信息。
从上下文中学习是一种持续学习的形式。它使模型能够不断地结合新信息来做出决策,防止其过时。例如,使用上周数据训练的模型无法回答本周的问题,除非将新信息包含在其上下文中。通过用最新信息(如高级打印机 A300 的最新规格)更新模型的上下文,模型可以保持最新状态,并能够回答超过其训练截止日期的查询。
RAGs
最著名的上下文构建模式是 RAG(Retrieval-Augmented Generation)。RAG 由两个组件组成:生成器(如大语言模型)和检索器(retriever),后者从外部来源检索相关信息。
检索不仅仅应用于 RAG。它也是搜索引擎、推荐系统和日志分析等系统的基础。许多为传统检索系统开发的检索算法同样适用于 RAG。
外部数据源通常包含非结构化数据,如备忘录、合同、新闻更新等。这些数据统称为_文档_。文档可以是 10 个 token 也可以是 100 万个 token。直接检索整个文档可能会导致上下文长度过长。RAG 通常需要将文档拆分成_便于处理的块_,这些块的大小可以根据模型的最大上下文长度和应用的延迟要求来确定。要了解更多关于分块和最佳块大小的信息,请参阅 Pinecone、Langchain、Llamaindex 和 Greg Kamradt 的教程。
一旦从外部数据源加载并分块后,检索主要通过两种方法进行。
-
基于术语的检索 这可以简单到关键词搜索。例如,给定查询“transformer”,获取所有包含该关键词的文档。更复杂的算法包括 BM25(利用 TF-IDF)和 Elasticsearch(利用倒排索引)。术语检索通常用于文本数据,但也适用于包含文本元数据(如标题、标签、字幕、评论等)的图像和视频。
-
基于嵌入的检索(也称为向量搜索)您可以使用嵌入模型将数据块转换为嵌入向量,如BERT、sentence-transformers和 OpenAI 或 Google 提供的专有嵌入模型。给定一个查询,通过向量搜索算法检索与查询嵌入最接近的数据。向量搜索通常被视为最近邻搜索,使用近似最近邻(ANN)算法,如FAISS(Facebook AI 相似搜索)、Google 的ScaNN、Spotify 的ANNOY和hnswlib(分层导航小世界)。ANN-benchmarks 网站在多个数据集上比较不同的 ANN 算法,使用四个主要指标,考虑了索引和查询之间的权衡。
- 召回率:算法找到的最近邻的比例。
- 每秒查询数(QPS):算法每秒可以处理的查询数。这对高流量应用至关重要。
- 构建时间:构建索引所需的时间。此指标特别重要,如果您需要频繁更新索引(例如因为您的数据会变化)。
- 索引大小:算法创建的索引的大小,对于评估其可扩展性和存储需求至关重要。
这不仅适用于文本文档,还适用于图像、视频、音频和代码。许多团队甚至尝试总结 SQL 表和数据帧,然后使用这些摘要生成用于检索的嵌入。
基于术语的检索比基于嵌入的检索要快得多且更便宜。它可以很好地开箱即用,使其成为一个有吸引力的起点。BM25 和 Elasticsearch 在业界被广泛使用,并作为更复杂的检索系统的强大基准。基于嵌入的检索虽然计算成本高,但随着时间的推移可以显著改进,以超越基于术语的检索。
一个生产级的检索系统通常结合几种方法。结合基于术语的检索和基于嵌入的检索被称为 混合搜索。
一种常见的模式是顺序的。首先,一个便宜的、精度较低的检索器,如基于术语的系统,获取候选项。然后,一个更精确但更昂贵的机制,如 k 近邻算法,找到这些候选项中最好的。这第二步也称为重新排序。
例如,给定术语“transformer”,你可以获取所有包含单词 transformer 的文档,而不管它们是关于电器设备、神经架构还是电影。然后你使用向量搜索在这些文档中找到实际与 transformer 查询相关的文档。
上下文重新排序与传统搜索重新排序的不同之处在于项目的确切位置不那么重要。在搜索中,排名(例如,第一或第五)是至关重要的。在上下文重新排序中,文档的顺序仍然重要,因为它会影响模型处理它们的效果。根据论文《Lost in the middle》(Liu 等,2023)的建议,模型可能更好地理解上下文开头和结尾的文档。然而,只要文档被包含在内,与搜索排名相比,其顺序的影响就不那么显著了。
另一种模式是集成。请记住,检索器通过将文档按其与查询的相关性评分进行排名。你同时使用多个检索器获取候选项,然后将这些不同的排名结合起来生成最终排名。
使用表格数据的检索增强生成(RAG)
外部数据源也可以是结构化的,例如数据框或 SQL 表。从 SQL 表中检索数据与从非结构化文档中检索数据有显著区别。给定一个查询,系统的工作流程如下。
- Text-to-SQL:根据用户查询和表结构,确定所需的 SQL 查询。
- SQL 执行:执行 SQL 查询。
- 生成:根据 SQL 结果和原始用户查询生成响应。
对于 Text-to-SQL 步骤,如果有许多可用表且其结构不能全部适应模型上下文,您可能需要一个中间步骤来预测每个查询应该使用哪些表。Text-to-SQL 可以由用于生成最终响应的相同模型完成,也可以由众多专门的 Text-to-SQL 模型之一完成。
智能体 RAGs
数据的一个重要来源是互联网。像 Google 或 Bing API 这样的网络搜索工具可以为模型提供一个丰富的、最新的资源,以便为每个查询收集相关信息。例如,针对“今年谁赢得了奥斯卡奖?”这样的查询,系统会搜索最新的奥斯卡奖信息,并使用这些信息生成对用户的最终回答。
基于关键词的检索、基于嵌入的检索、SQL 执行和网络搜索是模型可以采取的扩展其上下文的操作。你可以将每个操作视为模型可以调用的函数。一个可以结合外部操作的工作流程也被称为 智能体。其架构如下所示。
» 操作与工具 «
工具允许一个或多个操作。例如,一个人名搜索工具可能允许两个操作:按姓名搜索和按电子邮件搜索。然而,这两者之间的区别很小,因此许多人将操作和工具互换使用。
» 只读操作与写入操作 «
从外部来源检索信息但不改变其状态的操作是只读操作。给予模型写入操作,例如更新表中的值,使模型能够执行更多任务,但也带来了更多的风险,这将在后续讨论中详细说明。
查询重写
通常需要重写用户查询,以增加获取准确信息的可能性。请考虑以下对话。
用户:John Doe 上次从我们这里买东西是什么时候?AI:John 上次是两周前的2030年1月3日,从我们这里买了一顶 Fruity Fedora 帽子。用户:那 Emily Doe 呢?
最后一个问题,“那 Emily Doe 呢?”,是模糊的。如果你直接用这个查询来检索文档,可能会得到无关的结果。你需要重写这个查询以反映用户实际在问什么。新的查询应该有意义。最后一个问题应该被重写为“Emily Doe 上次从我们这里买东西是什么时候?”
查询重写通常是通过使用其他 AI 模型来完成的,使用类似于“给定以下对话,重写最后一个用户输入以反映用户实际在问什么”的提示词。
给定以下对话,重写最后一个用户输入以反映用户实际在问什么用户:John Doe上次从我们这里买东西是什么时候?AI:John上次是两周前的2030年1月3日,从我们这里买了一顶Fruity Fedora帽子。用户:那Emily Doe呢?
重写后的查询:
Emily Doe上次从我们这里买东西是什么时候?
查询重写可能会变得复杂,特别是当你需要进行身份解析或整合其他知识时。如果用户问“他的妻子呢?”,你首先需要查询数据库以找出他的妻子是谁。如果你没有这个信息,重写模型应该承认这个查询是无法解决的,而不是臆造一个名字,导致错误的答案。
第二步:设置护栏
护栏有助于减少 AI 风险,并保护不仅仅是用户,还有开发者自己。应在可能出现失败的地方设置护栏。这篇文章讨论了两种类型的护栏:输入护栏和输出护栏。
输入护栏
输入护栏通常是为了防止两种风险:向外部 API 泄露私人信息,以及执行可能损害系统的错误提示词(突破模型限制)。
向外部 API 泄露私人信息
这种风险特指使用外部模型 API 时需要将数据发送到组织外部。例如,员工可能会将公司的秘密或用户的私人信息复制到提示词中并发送到模型托管的地方。最著名的早期事件之一是三星员工将三星的专有信息输入 ChatGPT,意外地 泄露了公司的秘密。目前尚不清楚三星是如何发现这一泄漏以及泄漏的信息是如何被用来对付三星的。然而,这一事件足够严重,以至于三星在 2023 年 5 月 禁止使用 ChatGPT。在使用第三方 API 时,没有万无一失的方法来防止潜在泄漏。然而,你可以通过防护措施来减轻这些风险。你可以使用许多可用的工具之一,这些工具可以自动检测敏感数据。由你指定需要检测的敏感数据类型。常见的敏感数据类别包括:
- 个人信息(身份证号码、电话号码、银行账户)。
- 人脸。
- 与公司知识产权或机密信息相关的特定关键词和短语。
许多敏感数据检测工具使用 AI 来识别潜在的敏感信息,例如确定字符串是否类似于有效的家庭地址。如果查询被发现包含敏感信息,你有两个选择:阻止整个查询或从中移除敏感信息。例如,你可以用占位符 [电话号码] 来屏蔽用户的电话号码。如果生成的响应包含这个占位符,使用一个可逆的 PII 字典,将该占位符映射到原始信息,以便你可以取消屏蔽,如下图所示。
模型越狱
试图破解 AI 模型,让它们说出或做出不当行为,已经成为网络上的一种游戏。虽然让 ChatGPT 发表争议性言论对某些人来说可能很有趣,但如果是贴有你的品牌名称和标志的客户支持聊天机器人做同样的事情,就没那么好玩了。对于可以访问工具的 AI 系统来说,这尤其危险。想象一下,如果用户找到一种方法让你的系统执行一个损坏你数据的 SQL 查询,会发生什么。
为了应对这种情况,你应该首先为系统设置防护栏,以确保不会自动执行任何有害操作。例如,任何可以插入、删除或更新数据的 SQL 查询都不能在未经人工批准的情况下执行。这种额外的安全措施的缺点是可能会减慢系统的速度。
为了防止你的应用程序发表不当言论,你可以为应用程序定义超出范围的主题。例如,如果你的应用程序是一个客户支持聊天机器人,它不应回答政治或社会问题。一个简单的方法是过滤掉包含预设短语的输入,这些短语通常与争议性话题相关,例如“移民”或“反疫苗”。更复杂的算法则使用 AI 来分类输入是否属于预定义的受限主题之一。
如果有害提示在你的系统中很少见,你可以使用异常检测算法来识别异常提示。
输出护栏
AI 模型具有概率性的特性,这使得它们的输出不可靠。你可以设置护栏来显著提升应用程序的可靠性。输出护栏有两个主要功能:
- 评估每次生成的质量。
- 指定处理不同故障模式的策略。
输出质量评估
要捕捉不符合标准的输出,首先需要了解失效的表现形式。以下是一些失效模式的示例及其捕捉方法。
-
空白响应。
-
格式错误的响应,未按预期格式输出。例如,如果应用程序期望 JSON 格式,而生成的响应缺少一个结束括号。某些格式有专门的验证器,如 regex、JSON 和 Python 代码验证器。还有一些工具用于约束采样,如指南、框架和导师。
-
有害的响应,例如包含种族主义或性别歧视的言论。这些可以使用多种有害内容检测工具来捕捉。
-
事实不一致的响应,由模型产生的错误信息。错误信息检测是一个活跃的研究领域,有一些解决方案,如SelfCheckGPT(Manakul 等,2023)和SAFE,搜索引擎事实性评估器(Wei 等,2024)。你可以通过为模型提供足够的上下文和提示词技术(如链式思维)来减轻错误信息的产生。错误信息的检测和减轻将在我即将出版的书籍AI Engineering中进一步讨论。
-
包含敏感信息的响应。这种情况有两种可能。
- 你的模型是在敏感数据上训练的,并将其重新输出。
- 你的系统从内部数据库中检索敏感信息以丰富其上下文,然后在响应中传递这些敏感信息。
可以通过不使用敏感数据训练模型以及不允许其检索敏感数据来预防这种失败模式。可以使用与输入防护相同的工具来检测输出中的敏感数据。
-
品牌风险响应,例如误解贵公司或竞争对手的响应。一个例子是 Grok,一个由 X 训练的模型,生成了一个响应暗示 Grok 是由 OpenAI 训练的,导致网络猜测 X 窃取了 OpenAI 的数据。这种故障模式可以通过关键词监控来减轻。当你确定了有关品牌和竞争对手的输出后,你可以阻止这些输出,将它们交给人工审查,或者使用其他模型检测这些输出的情感,以确保只返回正确的情感。
-
一般性糟糕的响应。例如,如果你让模型写一篇文章,而那篇文章写得很差,或者你要求模型提供一个低热量蛋糕的食谱,而生成的食谱含有过量的糖。使用 AI 评审来评估模型响应的质量已经成为一种流行的做法。这些 AI 评审可以是通用模型(如 ChatGPT、Claude),也可以是专门训练的评分模型,根据查询为响应输出具体分数。
失败管理
AI 模型具有概率性,这意味着相同的查询可能在不同时间得到不同的响应。许多失败情况可以通过简单的重试逻辑来解决。例如,如果响应为空,可以重试几次,直到获得非空响应。同样,如果响应格式不正确,可以一直重试,直到获得正确格式的响应。
然而,这种重试策略可能会带来额外的延迟和成本。每次重试都会使 API 调用次数翻倍。如果重试发生在失败之后,用户体验到的延迟也会翻倍。为了减少延迟,可以采取并行调用的方法。例如,对于每个查询,不必等到第一个查询失败后再重试,可以同时向模型发送两次查询,得到两个响应后选择较优的一个。这样虽然增加了 API 调用的冗余,但能够有效控制延迟。
在处理棘手查询时,常常需要依赖人工干预。例如,如果查询中包含特定的关键词,可以将其转交给人工操作员。一些团队使用专门的模型(可能是内部训练的)来决定何时将对话转交给人工操作员。例如,有的团队在情感分析模型检测到用户情绪激动时,将对话转交给人工操作员。还有的团队在对话达到一定轮次后转交,以防用户陷入无限循环。
护栏权衡
可靠性与延迟的权衡:尽管承认护栏的重要性,一些团队告诉我,延迟更重要。他们决定不实施护栏,因为这些措施会显著增加其应用程序的延迟。然而,这些团队是少数。大多数团队发现,增加的风险比延迟带来的成本更高。
输出护栏在流式生成模式下可能效果不佳。默认情况下,整个响应是在显示给用户之前生成的,这可能需要很长时间。在流式生成模式下,新 token 在生成时会即时传输给用户,减少了用户等待查看响应的时间。缺点是很难评估部分响应,因此在系统护栏确定应阻止响应之前,不安全的响应可能已经传输给用户。
自托管与第三方 API 的权衡:自托管模型意味着你不必将数据发送给第三方,从而减少了输入护栏的需求。然而,这也意味着你必须自己实施所有必要的护栏措施,而不是依赖第三方服务提供的护栏。
我们的平台现在是这样的。护栏可以是独立的工具,也可以是模型网关的一部分,如后面讨论的那样。如果使用评分器,它们被归类为模型 API,因为评分器通常也是 AI 模型。用于评分的模型通常比用于生成的模型更小更快。
第三步:添加模型路由器和网关
随着应用程序的复杂性增加并涉及更多的模型,出现了两种类型的工具来帮助开发者处理多个模型:路由器和网关。
路由器
一个应用程序可以使用不同的模型来响应不同类型的查询。针对不同查询提供不同的解决方案有几项好处。首先,这使你可以拥有专门的解决方案,例如一个专门处理技术故障排除的模型和另一个专门处理订阅的模型。专门化的模型可能比通用模型表现更好。其次,这可以帮助你节省成本。将所有查询都路由到一个昂贵的模型,不如将简单的查询路由到更便宜的模型,这样可以帮助你节省成本。
路由器通常由一个意图分类器组成,该分类器预测用户试图做什么。基于预测的意图,查询被路由到适当的解决方案。例如,对于一个客户支持聊天机器人,如果意图是:
- 重置密码 –> 将该用户路由到有关密码重置的页面。
- 纠正账单错误 –> 将该用户路由到人工操作员。
- 排除技术问题 –> 将该查询路由到一个为故障排除微调的模型。
意图分类器还可以帮助你的系统避免超出范围的对话内容。例如,你可以有一个意图分类器来预测查询是否超出范围。如果查询被判断为不适当的(例如,如果用户问你会在即将到来的选举中投票给谁),聊天机器人可以使用预设的回应(“作为一个聊天机器人,我没有投票的能力。如果你有关于我们产品的问题,我很乐意帮助。”)礼貌地拒绝参与,而不会浪费一个 API 调用。
如果你的系统可以访问多个操作,路由器可以涉及一个下一步动作预测模型来帮助系统决定下一步采取什么动作。一个有效的动作是询问澄清,如果查询是模糊的。例如,响应查询“冻结”,系统可能会问,“你是想冻结你的账户还是在说天气?”或只是说,“对不起。你能详细说明吗?”
意图分类器和下一步动作预测模型可以是通用模型,也可以是专门的分类模型。专门的分类模型通常比通用模型小得多且更快,允许你的系统使用多个它们而不会产生显著的额外延迟和成本。
在将查询路由到具有不同上下文限制的模型时,可能需要相应地调整查询的上下文。考虑一个包含 1000 个 token 的查询,它被分配到一个具有 4K 上下文限制的模型中。系统接着执行一个操作,例如网络搜索,返回 8000 个 token 的上下文。在这种情况下,您可以截断查询的上下文以适应最初预定的模型,或者将查询路由到具有更大上下文限制的模型。
模型网关
模型网关是一个中间层,使您的组织能够以统一和安全的方式与不同的模型接口。模型网关的基本功能是使开发人员能够以相同的方式访问不同的模型——无论是自托管模型还是商业 API(如 OpenAI 或 Google)背后的模型。通过模型网关,代码维护变得更加容易。如果模型 API 发生变化,您只需更新模型网关,而不是更新所有使用该模型 API 的应用程序。
在最简单的形式中,模型网关是一个统一的包装器,如下面的代码示例所示。此示例旨在让您了解如何实现模型网关。它并不是功能性的,因为它不包含任何错误检查或优化。
import google.generativeai as genaiimport openaidef openai_model(input_data, model_name, max_tokens):openai.api_key = os.environ["OPENAI_API_KEY"]response = openai.Completion.create(engine=model_name,prompt=input_data,max_tokens=max_tokens)return {"response": response.choices[0].text.strip()}def gemini_model(input_data, model_name, max_tokens):genai.configure(api_key=os.environ["GOOGLE_API_KEY"])model = genai.GenerativeModel(model_name=model_name)response = model.generate_content(input_data, max_tokens=max_tokens)return {"response": response["choices"][0]["message"]["content"]}@app.route('/model', methods=['POST'])def model_gateway():data = request.get_json()model_type = data.get("model_type")model_name = data.get("model_name")input_data = data.get("input_data")max_tokens = data.get("max_tokens")if model_type == "openai":result = openai_model(input_data, model_name, max_tokens)elif model_type == "gemini":result = gemini_model(input_data, model_name, max_tokens)return jsonify(result)
模型网关在访问控制和成本管理方面发挥作用。与其将组织的 OpenAI API 访问令牌分发给每一个想要使用的人(这些令牌很容易泄漏),不如只给人们访问模型网关的权限,从而创建一个集中受控的访问点。网关还可以实现细粒度的访问控制,指定哪个用户或应用程序应该访问哪个模型。此外,网关可以监控和限制 API 调用的使用,防止滥用并有效地管理成本。
模型网关还可以用于实施回退策略,以克服速率限制或 API 故障(后者不幸是常见的)。当主 API 不可用时,网关可以将请求路由到替代模型,稍等片刻后重试,或以其他优雅的方式处理故障。这确保了您的应用程序可以顺利运行而不受干扰。
由于请求和响应已经通过网关流动,因此它也是实现其他功能的好地方,例如负载平衡、日志记录和分析。一些网关服务甚至提供缓存和防护机制。
鉴于网关设备相对容易实现,市面上有很多即用的网关设备。示例包括 Portkey 的网关、MLflow AI Gateway、WealthSimple 的llm-gateway、TrueFoundry、Kong和Cloudflare。
随着添加了网关设备和路由器,我们的平台变得更具吸引力。与评分功能类似,路由功能也存在于模型网关中。与用于评分的模型类似,用于路由的模型通常比用于生成的模型要小。
第四步:使用缓存减少延迟
当我将这篇文章分享给我的朋友 Eugene Yan 时,他说缓存可能是 AI 平台中最被低估的组件。缓存可以显著减少应用程序的延迟和成本。
缓存技术也可以在训练过程中使用,但由于这篇文章是关于部署的,我将专注于推理的缓存。一些常见的推理缓存技术包括提示词缓存、精确缓存和语义缓存。提示词缓存通常由你使用的推理 API 实现。在评估推理库时,了解它支持什么缓存机制是有帮助的。
KV 缓存用于注意力机制不在本讨论范围内。
提示缓存
在许多应用程序中,提示信息会有重叠的文本片段。例如,所有查询都可以共享相同的系统提示。提示缓存会存储这些重叠的片段,以便重复使用,因此您只需要处理一次它们。在不同的提示信息中,系统提示是一个常见的重叠文本片段。没有提示缓存,您的模型需要在每个查询中处理系统提示。有了提示缓存,它只需要在第一次查询时处理系统提示。
对于有长系统提示的应用程序,提示缓存可以显著减少延迟和成本。如果您的系统提示有 1000 个标记,而您的应用程序今天生成了 100 万个模型 API 调用,那么提示缓存将为您每天节省大约 10 亿个重复输入标记!不过,这并不是完全免费的。像 KV 缓存一样,提示缓存的大小可能会非常大,并且需要大量的工程努力。
提示缓存对于涉及长文档的查询也很有用。例如,如果您的许多用户查询都与同一个长文档(如一本书或代码库)有关,那么这个长文档可以被缓存,以供跨查询重复使用。
自从 2023 年 11 月由Gim 等人引入以来,提示缓存已经被集成到模型 API 中。Google 宣布 Gemini API 将在 2024 年 6 月提供此功能,称为*上下文缓存*。缓存的输入标记相比于常规输入标记有 75% 的折扣,但您需要为缓存存储额外付费(截至撰写本文时,为每小时 1.00 美元/百万个标记)。鉴于提示缓存的明显好处,我不会对它像 KV 缓存一样受欢迎感到惊讶。
虽然 llama.cpp 也有prompt cache,但它似乎只缓存整个提示,并且仅适用于同一聊天会话中的查询。它的文档有限,但从代码来看,我猜测在一个长对话中,它缓存了之前的消息,只处理最新的消息。
准确缓存
与提示词缓存和 KV 缓存这些专属于基础模型的缓存不同,准确缓存更加通用且直接。系统会保存已处理的项目,以便在之后再次请求这些项目时可以重用。例如,当用户要求模型总结一款产品时,系统会先检查缓存中是否已有该产品的总结。如果有,则直接获取该总结;如果没有,则生成该产品的总结并将其缓存。
准确缓存也用于基于嵌入的检索,以避免重复的向量搜索。如果传入的查询已经在向量搜索缓存中,则直接获取缓存的搜索结果;如果没有,则执行向量搜索并缓存结果。
对于需要多个步骤(如思维链)和/或耗时操作(如数据检索、SQL 执行或网页搜索)的查询,缓存尤为重要。
可以使用内存存储实现快速检索的准确缓存。然而,由于内存容量有限,还可以使用 PostgreSQL、Redis 或分层存储等数据库来实现缓存,以平衡速度和存储容量。为了管理缓存大小和维护性能,制定缓存淘汰策略至关重要。常见的策略包括最近最少使用(LRU)、最少频繁使用(LFU)和先进先出(FIFO)。
缓存查询的时间长短取决于该查询被再次调用的可能性。例如,像“我最近的订单状态如何”这种用户特定的查询,不太可能被其他用户重用,因此不适合缓存。同样,对于“天气怎么样”这种时间敏感的查询,缓存也没有多大意义。一些团队会训练一个小型分类器来预测某个查询是否应该被缓存。
语义缓存
与精确缓存不同,语义缓存不要求传入的查询与缓存的查询完全一致。语义缓存允许重用相似的查询。例如,一个用户问“越南的首都是什么?”,模型给出的答案是“河内”。之后,另一个用户问“越南的首都_城市_是什么?”,虽然多了一个词“城市”,但其实是同一个问题。语义缓存的理念是,系统可以重用答案“河内”,而不是重新处理这个新查询。
语义缓存需要一个可靠的方法来确定两个查询在语义上是否相似。一个常见的方法是基于嵌入的相似性,其过程如下:
- 使用嵌入模型为每个查询生成嵌入。
- 使用向量搜索找到与当前查询嵌入最接近的缓存嵌入。假设这个相似性得分为 X。
- 如果 X 小于你设定的相似性阈值,缓存的查询被视为与当前查询相同,返回缓存结果。否则,处理当前查询并将其与嵌入和结果一起缓存。
此方法需要使用向量数据库来存储缓存查询的嵌入。
与其他缓存技术相比,语义缓存的价值较为可疑,因为其多个组件容易出现故障。 成功依赖于高质量的嵌入、有效的向量搜索以及可靠的相似性度量。设定合适的相似性阈值也很困难,需要大量的尝试和调整。如果系统错误地将传入查询判定为与其他查询相似,返回的缓存响应将会错误。
此外,语义缓存可能耗时且计算密集,因为它涉及向量搜索。向量搜索的速度和成本取决于缓存嵌入数据库的大小。
如果缓存命中率高,语义缓存可能仍然有价值,这意味着可以通过利用缓存结果有效地回答大量查询。但是,在引入语义缓存的复杂性之前,请确保评估其效率、成本和性能风险。
随着缓存系统的增加,平台如图所示。键值缓存和提示词缓存通常由模型 API 提供者实现,因此它们未在图中显示。如果必须将它们可视化,我会将它们放在模型 API 框中。新增了一个箭头,用于将生成的响应添加到缓存中。
第五步:添加复杂逻辑和写操作
我们迄今为止讨论的应用程序具有相对简单的流程。基础模型生成的输出大部分返回给用户(除非它们未通过防护措施)。然而,应用程序流程可以通过循环和条件分支变得更复杂。模型的输出还可以用于调用写操作,例如撰写电子邮件或下订单。
复杂逻辑
模型的输出可以有条件地传递给另一个模型,或者作为下一步输入的一部分反馈给相同的模型。这种过程会一直进行,直到系统中的某个模型决定任务已完成,并且应该将最终响应返回给用户。
当您赋予系统计划和决定下一步操作的能力时,这种情况就会发生。例如,考虑查询“计划一个巴黎周末行程”。模型可能首先生成一个潜在活动列表:参观埃菲尔铁塔,在咖啡馆午餐,游览卢浮宫等。然后,这些活动中的每一个都可以反馈给模型,以生成更详细的计划。例如,“参观埃菲尔铁塔”可能提示模型生成子任务,如检查开放时间,购买门票,寻找附近的餐馆。这种迭代过程会持续进行,直到创建出一个全面且详细的行程。
我们的基础设施现在有一个箭头将生成的响应指向上下文构建,然后反馈给模型网关中的模型。
写操作
用于上下文构建的动作是_只读操作_。它们允许模型从数据源读取信息来收集上下文。然而,系统也可以执行_写操作_,对数据源和外界进行更改。例如,如果模型输出“向 X 发送一封内容为 Y 的邮件”,系统将调用动作send_email(recipient=X, message=Y)
。
写操作极大地提升了系统的能力。它们可以让你自动化整个客户外联工作流程:研究潜在客户、找到他们的联系方式、撰写邮件、发送首封邮件、阅读回复、跟进、提取订单、用新订单更新数据库等。
然而,赋予 AI 自动改变我们生活的能力是令人担忧的。就像你不应让实习生有权删除生产数据库一样,也不应让不可靠的 AI 发起银行转账。对系统能力及其安全措施的信任至关重要。你需要确保系统免受恶意行为者的攻击,防止他们操纵系统执行有害操作。
AI 系统像其他软件系统一样容易受到网络攻击,但它们还有另一个弱点:提示注入。提示注入是指攻击者通过操纵输入提示词,让模型表现出不良行为。你可以将提示注入看作是对 AI 而非人类的社会工程学。
许多公司担心的是,他们让 AI 系统访问内部数据库后,攻击者会诱使该系统泄露数据库中的私人信息。如果系统对这些数据库有写入权限,攻击者可能会诱使系统破坏数据。
任何希望利用 AI 的组织都需要严肃对待安全问题。然而,这些风险并不意味着 AI 系统永远不应被赋予在现实世界中行动的能力。AI 系统可能会失败,但人类也会失败。如果我们能让人们信任机器将我们送入太空,我希望有一天,安全措施能足以让我们信任自主 AI 系统。
可观测性
虽然我在独立的部分中提到了可观测性,但它应该从一开始就集成到平台中,而不是作为事后补充。无论项目大小,可观测性都至关重要,并且随着系统复杂性的增加,其重要性也会增加。
相比其他部分,这一部分提供的信息最少。在一篇博客文章中无法涵盖所有可观测性的细微差别。因此,我将简要介绍监控的三个支柱:日志、追踪和指标。具体细节如用户反馈、漂移检测和调试等内容则不在本文讨论范围内。
监控指标
在谈论监控时,大多数人首先想到的是指标。要跟踪哪些指标取决于您希望监控系统的哪些方面,这通常是根据具体应用程序而定的。不过,总的来说,有两种类型的指标需要跟踪:模型指标和系统指标。
系统指标反映了整个系统的状态。常见的系统指标包括吞吐量、内存使用情况、硬件利用率以及服务可用性和正常运行时间。这些都是软件工程应用中普遍关注的指标。在本文中,我将重点介绍模型指标。
模型指标用于评估模型的性能,例如准确率、毒性和幻觉率。应用程序管道的不同步骤也有各自的指标。例如,在 RAG 应用中,检索质量通常通过上下文相关性和上下文精度来评估。向量数据库可以根据索引数据所需的存储量以及查询数据所需的时间来进行评估。
模型的输出可能以多种方式失败。识别这些问题并制定相应的监控指标是非常重要的。例如,您可能需要跟踪模型超时的频率、返回空响应的次数或生成格式错误响应的频率。如果您担心模型泄露敏感信息,也需要找到方法进行跟踪。
与长度相关的指标,如查询长度、上下文长度和响应长度,有助于理解模型的行为。某些模型是否比其他模型更冗长?某些类型的查询是否更容易导致冗长的回答?这些指标尤其有助于检测应用程序中的变化。如果平均查询长度突然减少,这可能表明存在需要调查的潜在问题。
长度相关的指标对于跟踪延迟和成本也很重要,因为更长的上下文和响应通常会增加延迟并产生更高的成本。
跟踪延迟对于了解用户体验至关重要。常见的延迟指标包括:
- 第一个 Token 生成时间 (TTFT):生成第一个 Token 所需的时间。
- Token 生成间隔时间 (TBT):每个 Token 生成之间的间隔时间。
- 每秒生成 Token 数 (TPS):Token 生成的速率。
- 每个输出 Token 生成时间 (TPOT):生成每个输出 Token 所需的时间。
- 总延迟:完成响应所需的总时间。
您还需要跟踪成本。与成本相关的指标包括查询次数以及输入和输出 Token 的数量。如果您使用的是带有速率限制的 API,跟踪每秒请求数非常重要,以确保您在分配的限制范围内,并避免潜在的服务中断。
在计算指标时,您可以选择抽查和详查。抽查是对数据子集进行采样以快速识别问题,而详查则是评估每个请求以获得全面的性能视图。选择取决于系统的要求和可用资源,结合两者可以提供平衡的监控策略。
在计算指标时,确保它们可以按相关维度进行细分,例如用户、版本、提示词/链版本、提示词/链类型和时间。这种细粒度有助于理解性能变化并识别具体问题。
日志
由于这篇博客文章篇幅较长,而且我已经在设计机器学习系统和Designing Machine Learning Systems一书中详细讨论了日志,因此这里我会简要说明。日志记录的哲学很简单:记录一切。记录系统配置,记录查询、输出和中间输出。记录组件的启动、结束和崩溃等情况。记录日志时,务必为其添加标签和 ID,以帮助你知道该日志来自系统的哪个部分。
记录一切意味着日志量可能会迅速增长。许多用于自动日志分析和日志异常检测的工具都是由生成式 AI 驱动的。
尽管手动处理日志是不可能的,但每天手动检查生产数据以了解用户如何使用你的应用程序是有益的。Shankar 等人(2024)发现,随着开发人员接触更多数据,他们对好坏输出的感知会发生变化,从而使他们能够重新编写提示词以增加获得良好响应的机会,并更新评估流程以捕捉不良响应。
跟踪
跟踪是指详细记录请求在各个系统组件和服务中的执行路径。在 AI 应用中,跟踪揭示了从用户发送查询到最终返回响应的整个过程,包括系统所采取的动作、检索到的文档以及发送给模型的最终提示词。它还应显示每个步骤所需的时间及其相关的成本(如果可测量)。例如,这是一个 Langsmith 跟踪的可视化图。
理想情况下,您应该能够逐步跟踪每个查询在系统中的转换过程。如果查询失败,您应该能够准确定位到出错的具体步骤:是处理错误、检索到的上下文无关,还是模型生成了错误的响应。
AI 管道编排
一个 AI 应用程序可能相当复杂,由多个模型组成,从多个数据库中检索数据,并可以访问各种工具。编排器帮助你指定如何将这些不同的组件组合(链接)在一起,以创建一个端到端的应用程序流。
总体而言,编排器以两个步骤工作:组件定义和链接。
-
组件定义 你需要告诉编排器你的系统使用了哪些组件,比如生成模型、路由模型和评分模型,你的系统可以从中检索数据的数据库,以及你的系统可以采取的动作。与模型网关的直接集成可以帮助简化模型的引入,一些编排器工具希望成为网关。许多编排器还支持与评估和监控工具的集成。
-
链接 你告诉编排器你的系统从接收到用户查询到完成任务所采取的一系列步骤。简而言之,链接只是函数组合。以下是一个管道的示例。
- 处理初始查询。
- 根据处理过的查询检索相关数据。
- 将初始查询和检索到的数据结合起来,创建一个模型预期格式的提示词。
- 模型根据提示词生成响应。
- 评估响应。
- 如果响应被认为是好的,将其返回给用户。如果不是,将查询路由到人类操作员。
编排器负责在步骤之间传递数据,并可以提供工具,帮助确保当前步骤的输出符合下一步的预期格式。
在为具有严格延迟要求的应用程序设计管道时,尽量并行执行尽可能多的操作。例如,如果你有一个路由组件(决定将查询发送到哪里)和一个个人身份信息移除组件,它们可以同时进行。
目前市面上有许多 AI 编排工具,包括 LangChain、LlamaIndex、Flowise、Langflow 和 Haystack。每个工具都有自己的 API,因此我在这里不会展示具体的代码。
虽然在开始项目时直接使用编排工具很诱人,但建议先尝试不使用这些工具来构建应用程序。 外部工具会增加复杂性。编排工具会抽象掉系统运作的关键细节,使得理解和调试系统变得困难。
随着应用开发的推进,您可能会发现编排工具可以简化工作。以下是评估编排工具时需要考虑的三个方面。
- 集成和扩展性 评估编排工具是否支持您已经使用或将来可能采用的组件。例如,如果您想使用 Llama 模型,请检查编排工具是否支持该模型。考虑到现有的模型、数据库和框架数量之多,不可能有一个编排工具支持所有内容。因此,您还需要考虑编排工具的扩展性。如果它不支持特定组件,修改难度如何?
- 支持复杂流水线 随着应用程序复杂性的增加,您可能需要管理涉及多个步骤和条件逻辑的复杂流水线。支持高级功能如分支、并行处理和错误处理的编排工具将帮助您高效地管理这些复杂性。
- 易用性、性能和可扩展性 考虑编排工具的用户友好性。寻找具有直观 API、全面文档和强大社区支持的工具,这些因素可以显著降低您和团队的学习曲线。避免那些会发起隐藏 API 调用或引入延迟的编排工具。此外,确保编排工具在应用程序数量、开发人员数量和流量增长时能够有效扩展。
结论
这篇文章从一个基本架构开始,然后逐步增加组件以应对日益复杂的应用程序。每个新增的组件都有其自身的优点和挑战,需要仔细考虑和实施。
虽然组件的分离对保持系统的模块化和可维护性很重要,但这种分离是流动的。组件之间有许多重叠。例如,模型网关可以与保护机制共享功能。缓存可以在不同的组件中实现,如在向量搜索和推理服务中。
这篇文章比我预期的要长得多,然而还有许多细节我未能进一步探讨,特别是在可观测性、上下文构建、复杂逻辑、缓存和保护机制方面。我将在即将出版的《AI 工程》一书中深入探讨所有这些组件。
这篇文章也没有讨论如何提供模型服务,因为大多数人将使用第三方 API 提供的模型。《AI 工程》还将有一章专门讨论推断和模型优化。
参考文献和致谢
特别感谢 Luke Metz、Alex Li、Chetan Tekur、Kittipat “Bot” Kampa、Hien Luu 和 Denys Linkov 对本文早期版本的意见反馈。他们的见解大大改进了本文的内容。一切剩余的错误均由我负责。
我阅读了许多公司分享的关于如何采用生成式 AI 的案例研究,以下是我最喜欢的一些案例。
- 构建生成式 AI 产品的思考 (LinkedIn, 2024)
- 我们如何在 Pinterest 构建 Text-to-SQL (Pinterest, 2024)
- 从想法到现实:通过生成式 AI 提升客户支持 (Vimeo, 2023)
- 深入探讨世界上最聪明的邮件 AI (Shortwave, 2023)
- 基于 LLM 的数据实体规模化数据分类 (Grab, 2023)
- 从预测到生成——Michelangelo 如何加速 Uber 的 AI 之旅 (Uber, 2024)