本文作者在研究了诸多公司如何部署生成式AI应用之后,发现并归纳了这些平台的相似之处。本文概述了生成式AI平台的常见组件、功能以及实现方法。
题图由 SiliconCloud 平台生成
生成式AI平台是一个非常复杂的系统。本文将从最简单的架构开始,并逐步添加更多组件。在最简单的形式中,应用程序接收查询并将其发送到模型。模型生成回答并将其返回给用户。当前形式下,没有防护措施、增强上下文或优化。模型API指的是第三方API(例如OpenAI、Google、Anthropic)和自托管的API。
从这里开始,可以根据需要添加更多组件。本文所讨论的是常见顺序,但你不必完全遵循。如果你的系统运行良好,可以跳过某些组件。在开发的每个步骤中,评估都是必要的。
通过让模型访问外部数据源和信息收集工具来增强输入的上下文。
设置防护措施以保护系统和用户。
添加模型路由和网关,以支持复杂的Pipeline并增强安全性。
用缓存优化时延和成本。
添加复杂的逻辑和写入操作,以将系统能力最大化。
可观测性使我们可以对系统进行监控和调试,而编排则涉及将所有组件链接在一起。这两个部分都是平台的基本组成部分,我们将在文章最后进行讨论。
本文重点讨论了部署AI应用的总体架构,讨论了构建这些组件所需的组件和注意事项,并不涉及如何构建AI应用,因此不讨论模型评估、应用评估、提示工程、微调、数据标注指南或RAG的分块策略。(这些主题会在即将出版的《AI工程》一书中详细介绍。)
1
第一步:增强上下文
平台的初步扩展通常涉及添加机制,使系统能够为查询补充必要的信息。收集相关信息的过程被称为上下文构建。
许多查询需要上下文才能得到解答。上下文中包含的相关信息越多,模型对其内部知识的依赖就越少,因为模型的内部知识可能由于训练数据和训练方法的局限性而不可靠。研究表明,访问上下文中的相关信息可以帮助模型生成更详细的回答,同时减少幻觉 (Lewis et al., 2020)。
例如,询问“Acme的fancy-printer-A300能否每秒打印100张?”如果模型能获取fancy-printer-A300的规格信息,它就能够给出更好的回答。(感谢Chetan Tekur提供的例子。)
对于基础模型来说,上下文构建相当于传统机器学习模型中的特征工程。两者的目的相同:为模型提供处理输入所需的信息。
上下文学习(从上下文中学习)是一种持续学习的形式。它使模型能够持续整合新信息以做出决策,从而防止模型过时。例如,使用上周数据训练的模型,如果不包含新信息,就无法回答本周的查询。通过用最新信息(如fancy-printer-A300的最新规格)更新模型的上下文,模型可以保持最新状态,并能够回答超出其训练截止日期的查询。
RAG
最有名的构建上下文的模式是RAG,即检索增强生成(Retrieval-Augmented Generation)。RAG由两个部分组成:一个生成器(例如语言模型)和一个检索器,后者从外部来源检索相关信息。
检索并不仅限于RAG。它是搜索引擎、推荐系统、日志分析等系统的核心。许多为传统检索系统开发的检索算法都可以用于RAG。
外部存储源通常包含非结构化数据,例如备忘录、合同、新闻更新等,这些可以统称为文档。一个文档可能包含10个词元,也可能包含100万个词元。直接检索整个文档可能会导致上下文过长。RAG通常要求将文档分割成可管理的块,其大小可以根据模型的最大上下文长度和应用的时延要求来确定。(要了解有关分块和最佳块大小的更多信息,请参阅Pinecone、Langchain、Llamaindex和Greg Kamradt的教程。)
一旦外部存储源中的数据被加载并分块,检索就主要通过两种方法进行:
- 基于术语的检索
这种检索可以像关键词搜索一样简单。例如,查询“transformer”,检索所有包含该关键词的文档。更复杂的算法包括BM25(利用TF-IDF)和Elasticsearch(利用倒排索引)。
基于术语的检索通常用于文本数据,也同样适用于具有文本元数据(如标题、标签、字幕、评论等)的图像和视频。
- 基于嵌入的检索(也称为向量搜索)
可以使用嵌入模型(如BERT、sentence-transformers,以及OpenAI或Google提供的专有嵌入模型)将数据块转换为嵌入向量。给定一个查询,通过向量搜索算法检索与查询嵌入最接近的数据。
向量搜索通常被看作是近邻搜索,使用近似最近邻(ANN)算法,如FAISS(Facebook AI Similarity Search)、Google的ScaNN、Spotify的ANNOY和hnswlib(Hierarchical Navigable Small World)。ANN-benchmarks网站使用四个主要指标对多个数据集上的不同ANN算法进行比较,并考虑了索引和查询之间的权衡。
召回率:算法找到近邻的比例。
每秒查询数(QPS):算法每秒能处理的查询数量。这对高流量应用至关重要。
构建时间:构建索引所需的时间。如果需要频繁更新索引(例如数据发生变化),这个指标非常重要。
索引大小:算法创建的索引大小,这对于评估其可扩展性和存储需求至关重要。
这种方法不仅适用于文本文档,还适用于图像、视频、音频和代码。许多团队甚至尝试总结SQL表和Dataframe,然后使用这些摘要生成用于检索的嵌入。
基于术语的检索比基于嵌入的检索更快且成本更低。它在初始状态下就能够表现良好,这是一个很有吸引力的点。BM25和Elasticsearch在行业中都得到了广泛使用,并成为了更复杂检索系统的强有力的基准。尽管基于嵌入的检索计算开销较大,但随着时间的推移,这一点可以得到显著改进,最终超越基于术语的检索。
生产环境中的检索系统通常结合了几种方法。结合基于术语的检索和基于嵌入的检索被称为混合搜索。
一种常见的模式是顺序处理。首先,使用廉价但精度较低的检索器(如基于术语的系统)获取候选文档。然后,使用更精确但成本较高的机制(如k近邻)从这些候选文档中找到最合适的。第二步也被称为重排序(reranking)。
例如,针对“transformer”这个词,可以检索所有包含“transformer”这个词的文档,无论是关于电气设备、神经网络架构还是电影。之后,可以使用向量搜索从这些文档中找到与transformer查询真正相关的那一部分。
上下文重排序与传统搜索重排序不同之处在于,项目的精确位置并不那么重要。在搜索中,排名(例如第一还是第五)至关重要。而在上下文重排序中,文档的顺序仍然重要,因为它会影响模型处理它们的效果。研究表明,模型可能更好地理解上下文中开头和结尾的文档(Liu et al., 2023)。然而,只要文档被包含在内,其顺序的影响就不如在搜索排名中那么显著。
另一种模式是集成。检索器通过根据查询与文档的相关性评分进行排名。可以同时使用多个检索器获取候选文档,然后将这些不同的排名结合起来生成最终排名。
带有表格数据的RAG
外部数据源也可以是结构化的,例如Dataframe或SQL表。从SQL表中检索数据与从非结构化文档中检索数据有很大不同。给定一个查询时,系统的工作流程如下。
文本到SQL:根据用户查询和表的模式,确定需要哪种SQL查询。
SQL执行:执行SQL查询。
生成:基于SQL结果和原始用户查询生成回答。
在“文本到SQL”这一步中,如果有多个可用的表且其模式无法全部放入模型上下文中,你可能需要一个中间步骤来预测每个查询应使用哪些表。“文本到SQL”可以由用于生成最终回答的同一模型完成,也可以由众多专门的“文本到SQL”模型中的一个完成。
Agentic RAG
互联网是一个重要的数据来源。像Google或Bing API这样的网页搜索工具可以让模型访问丰富的、最新的资源,以便为查询收集相关信息。例如,针对“今年谁赢得了奥斯卡?”这个查询,系统会搜索最新的奥斯卡信息,并利用这些信息生成最终的回答返回给用户。
基于术语的检索、基于嵌入的检索、SQL执行和网页搜索都是模型可以用来增强其上下文的方法。可以将每个动作视为模型可以调用的函数。能够整合外部动作的工作流程也被称为“Agentic”。其架构如下图所示。
» 动作与工具 «
一个工具可以允许一个或多个动作。例如,一人搜索工具可能允许两个动作:按姓名搜索和按电子邮件搜索。不过,这两者的差别很小,所以许多人会将动作和工具交替使用。
» 只读动作与写入动作 «
从外部来源检索信息但不更改其状态的动作称为只读动作。赋予模型写入动作的能力,例如更新表中的值,能够让模型执行更多任务,但也带来了更多风险,这一点之后讨论。
查询重写
用户的查询 (Query )通常需要重写,以增加获取正确信息的可能性。阅读以下对话。
用户:John Doe最近一次在我们这里购买东西是什么时候? AI:John从我们这里购买了一顶Fruity Fedora帽子,是两周前,2030年1月3日。用户:那Emily Doe呢?
最后一个问题“How about Emily Doe?” 是不清晰的。如果直接使用这个查询来检索文档,可能会得到不相关的结果。需要重写这个查询,才能反映用户实际在问什么。新的查询应该具有独立的意义。最后一个问题应该重写为“When was the last time Emily Doe bought something from us?”(Emily Doe最近一次从我们这里购买东西是什么时候?)
查询的重写通常由其他AI模型完成,使用类似于“根据以下对话,重写用户的最后输入,以反映用户实际在问什么”的提示。
查询重写可能会很复杂,尤其是在一些需要进行身份解析或结合其他知识的情况下。如果用户问“How about his wife?”(他的妻子呢?),你首先需要查询数据库,以确定他的妻子是谁。如果你没有这方面的信息,重写模型应承认这个查询无法解决,而不是凭空编造一个名字,否则可能会导致错误的回答。
2
第2步:设置防护措施
防护措施有助于降低AI风险,不仅保护用户,也保护开发者。防护措施应在任何可能出现故障的地方都进行设置。本文讨论了两种类型的防护措施:输入防护措施和输出防护措施。
输入防护措施
输入防护措施通常用于防止两类风险:向外部API泄露私人信息,以及执行会损害系统的恶意提示(如模型越狱)。
向外部API泄露私人信息
这种风险特指在使用外部模型API时需要将数据发送到组织外部。比如,员工可能会将公司的机密或用户的私人信息复制到提示中,并将其发送到模型所在的服务器。
这一点先前曾有一个典例:三星员工将三星的专有信息输入到ChatGPT中,意外泄露了公司的机密。虽然并不清楚三星是如何发现这次泄露的,以及泄露的信息如何被用来对付三星,但这次事件十分严重,导致三星在2023年5月禁止使用ChatGPT。
在使用第三方API时,没有完全防止潜在泄露的万全之策。然而,可以通过防护措施来减轻这些风险。可以使用现有的众多工具之一来自动检测敏感数据。你可以指定需要检测哪些敏感数据。常见的敏感数据类别包括:
- 个人信息(身份证号、电话号码、银行账户)。
- 人脸识别信息。
- 与公司知识产权或专有信息相关的特定关键词和短语。
许多敏感数据检测工具使用AI来识别潜在的敏感信息,例如判断字符串是否类似于有效的家庭住址。如果发现查询中包含敏感信息,你有两个选择:屏蔽整个查询或移除其中的敏感信息。例如,可以用占位符[PHONE NUMBER]来屏蔽用户的电话号码。如果生成的回答中包含此占位符,可以使用一个PII可逆字典,将该占位符映射回原始信息,以便能够揭秘它,如下图所示。
模型越狱
尝试越狱AI模型,使其说出或做出不当的事,已经成为了一种网络娱乐活动。虽然让ChatGPT发表一些有争议的言论可能会让一些人感到好笑,但如果你的客服聊天机器人(带有你的品牌名称和标志)做出同样的事情,那就一点也不好笑了。对于那些可以访问工具的AI系统来说,这一点尤其危险。想象一下,如果用户找到了一种方法来让你的系统执行破坏数据的SQL查询,那将会多么糟糕。
为了解决这个问题,应该首先在系统上设置防护措施,以确保任何有害的操作都无法自动执行。例如,任何可以插入、删除或更新数据的SQL查询在没有人工批准的情况下都不能执行。虽然增加这种安全性可保护系统,但缺点是可能会减慢系统的运行速度。
为了防止应用程序发表不该说的离谱言论,可以为应用程序定义一些超出其职责范围的话题。例如,如果应用程序是一个客服聊天机器人,它就不应该回答政治或社会问题。一个简单的方法是过滤掉那些包含通常与有争议话题相关的预定义短语的输入,比如“移民”或“反疫苗”。更复杂的算法则使用AI来分类输入内容是否涉及预定义的限制话题。
如果系统中有害的提示非常少见,可以使用异常检测算法来识别异常提示。
输出防护措施
AI模型具有概率性,这使得其输出并不总是可靠。通过设置防护措施,可以显著提高应用程序的可靠性。输出防护措施主要有两个功能:
- 评估每次生成的回答质量。
- 指定处理不同故障模式的策略。
输出质量评估
为了捕捉不符合标准的输出,需要了解失败模式是什么样的。以下是一些失败模式的例子以及捕捉它们的方法。
- 空白回答。
- 格式错误的回答,即未按照预期输出格式生成的回答。例如,如果应用程序期望得到JSON格式的输出,而生成的回答却缺少关闭括号。某些格式(如正则表达式、JSON和Python代码)有专门的验证工具。此外,还有一些用于受限采样的工具,如guidance、outlines和instructor。
- 有害回答,例如带有种族主义或性别歧视内容的回答。可以使用各种检测工具来捕捉这些有害内容。
- 事实不一致的回答,即模型凭空捏造的内容。幻觉检测目前是一个很活跃的研究领域,一些相关的解决方案已经涌现,如SelfCheckGPT(Manakul et al., 2023)和SAFE Search Engine Factuality Evaluator,(Wei et al., 2024)。可以通过为模型提供足够的上下文以及使用思维链等提示技术来减少幻觉的发生。
- 含有敏感信息的回答。这种情况可能出现在以下两种场景:
a.模型在训练时使用了敏感数据,导致其在回答中复述这些信息。b.系统从内部数据库中检索到敏感信息以丰富其上下文,然后将这些敏感信息传递到回答中。这种失败模式可以通过不在模型训练时使用敏感数据以及不允许模型检索敏感数据来防止。输出中的敏感数据可以使用与输入防护措施相同的工具来检测。
- 涉及品牌风险的回答,例如对你的公司或竞争对手的错误描述。一个例子是,由X公司训练的模型Grok生成的回答暗示Grok是由OpenAI训练的,这引发了网络对X公司涉嫌窃取OpenAI数据的猜测。关键字监控可以削弱这种失败模式的影响。一旦识别出涉及品牌和竞争对手的输出内容,可以选择屏蔽这些输出,将其交由人工审核,或者使用其他模型检测这些输出的情感,以确保只返回正确的情感内容。
- 总体质量低下的回答。例如,如果你要求模型写一篇文章,而那篇文章写得非常糟糕,或者你要求模型提供一个低热量蛋糕的食谱,而生成的食谱却含有过多的糖。现在越来越流行使用AI评判者来评估模型回答的质量。这些AI评判者可以是通用模型(如ChatGPT、Claude),也可以是专门训练的打分工具,针对给定的查询对回答给出具体评分。
故障管理
AI模型具有概率性,这意味着如果重复提问,可能会得到不同的回答。许多故障可以通过基本的重试逻辑来消除。例如,如果回答是空的,可以重试X次或直到得到非空的回答。同样,如果回答格式错误,可以反复尝试,直到模型生成正确格式的回答。
然而,这种重试策略可能会带来额外的时延和成本。一次重试意味着API调用次数增加一倍。如果重试是在故障发生后进行的,用户体验到的时延将会翻倍。为了减少时延,可以并行调用。例如,不必等待第一次调用失败后再进行重试,可以同时将该查询发送给模型两次,得到两个回答后,选择其中较好的一个。虽然这种方式增加了冗余API调用的次数,但可以保持时延在可控的范围内。
棘手的查询通常还会依靠人工干预来解决。例如,当一个查询包含特定的关键短语时,你可以将其转交给人工操作员处理。有些团队会使用一个专门的模型来决定何时将对话转交给人工处理,这个模型可能是内部训练的。比如,某个团队的情感分析模型检测到用户开始生气,团队便将对话转交给人工操作员处理。另一个团队则在对话进行到一定次数后转交,以防止用户陷入无限循环。
保护措施的权衡
可靠性与时延的权衡:虽然保护措施的重要性不容忽视,但有些团队认为时延更为重要。他们决定不实施保护措施,因为保护措施可能会显著增加应用程序的时延。然而,这些团队只是少数,大多数团队认为风险增加的成本比时延增加更高。
在流式完成模式下,输出保护措施可能效果不佳。默认情况下,完整的回答会在显示给用户之前完成生成,这可能需要很长时间。在流式完成模式中,新生成的词元会实时传输给用户,从而减少用户等待回答的时间。流式完成模式的缺点是某些回答很难评估,因此不安全的回答可能会在系统保护措施确定其应该被阻止之前就已经传输给用户。
自托管与第三方API的权衡:自托管模型意味着不需要将数据发送给第三方,从而减少了对输入保护措施的需求。然而,这也意味着你必须自己实施所有必要的保护措施,而不是依赖第三方服务提供的保护措施。
我们的平台目前就是这样。保护措施可以是独立的工具或模型网关的一部分,这一点稍后会讨论。如果使用评分器,它们会被归入模型API,因为评分器通常也是AI模型。用于评分的模型通常比用于生成的模型更小且更快。
3
第3步:添加模型路由器和网关
随着应用程序变得越来越复杂并涉及更多模型,出现了两种协助处理多个模型的工具:路由器和网关。
路由器
应用程序可以使用不同的模型来回答不同类型的查询。针对不同查询采用不同的解决方案有几个好处。首先,能够有专业化的解决方案,例如一个模型专注于技术故障排除,另一个模型专注于订阅问题。专业化模型通常比通用模型表现更佳。其次,这可以节省成本。与其将所有查询都路由到一个昂贵的模型,不如将简单的查询路由到便宜的模型上。
路由器通常包含一个意图分类器,用来预测用户的意图。根据预测的意图,查询会被路由以适当的解决方案。以一个客服聊天机器人为例,如果意图是:
重置密码 -> 将此用户路由到密码重置页面。
更正账单错误 -> 将此用户路由到人工操作员。
解决技术问题 -> 将此查询路由到一个针对技术故障排查进行了微调的模型。
意图分类器还可以帮助系统规避一些超出范围的对话。例如,可以用意图分类器预测查询是否超出范围。如果查询被认为是不合适的(例如,如果用户询问你会在即将到来的选举中投票给谁),聊天机器人可以礼貌地拒绝参与,使用一些标准回答(“作为聊天机器人,我没有投票的能力。如果你对我们的产品有疑问,我很乐意帮助。”),而不会浪费一次API调用。
如果系统可以执行多种操作,那么路由器还可以包含一个下一步操作预测器,帮助系统决定接下来采取什么行动。如果查询模糊不清,一种有效的操作是请求澄清。例如,对于“冻结”这一查询,系统可能会问:“你是想冻结你的账户还是在谈论天气?”或者简单地说:“对不起。你能详细说明一下吗?”
意图分类器和下一步操作预测器可以是通用模型,也可以是专门的分类模型。专门的分类模型通常比通用模型小且更快,这使得系统可以使用多个模型而不会造成大量的额外时延和成本。
当把查询路由到具有不同上下文限制的模型时,问题的上下文可能需要相应调整。比如一个1000词元的查询,计划使用一个具有4K上下文限制的模型。系统随后采取一个操作,例如网络搜索,返回8000词元的上下文。你可以将查询的上下文截断以适应最初计划的模型,或者把查询路由到一个有更大的上下文限制的模型。
网关
模型网关是一个中间层,它能够使组织以统一且安全的方式与不同的模型交互。模型网关最基本的功能是使开发人员能够以相同的方式访问不同的模型——无论是自托管模型还是如OpenAI或Google等商业API背后的模型。模型网关让代码维护变得更容易。如果模型API发生变化,只需更新模型网关,而不必更新所有使用该模型API的应用程序。
在最简单的形式中,模型网关是一个统一的封装器,代码示例如下。本示例旨在帮助你了解模型网关如何实现,并不具有实际功能,因为它不包含任何错误检查或优化。
import google.generativeai as genai
import openai
def 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。
随着网关和路由器的添加,我们的平台变得越来越有意思。与评分类似,路由也在模型网关中。与用于评分的模型类似,用于路由的模型通常比用于生成的模型小。
4
第4步:用缓存减少时延
当我和我的朋友Eugene Yan分享这篇文章时,他提到缓存可能是AI平台中最被低估的组件。缓存可以显著减少应用程序的时延和成本。
缓存技术也可以在训练过程中使用,但由于这篇文章是关于部署的,我将重点讨论推理中的缓存。常见的推理缓存技术包括提示缓存、精确缓存和语义缓存。提示缓存通常由使用的推理API实现。在评估一个推理库时,了解它支持哪种缓存机制是很有帮助的。
注意,注意力机制的KV缓存不在本文讨论范围内。
提示缓存
许多应用中的提示具有重叠的文本段。例如,所有查询都可以共享同一个系统提示。提示缓存会存储这些重叠的段落以便重复使用,因此只需要处理一次。不同提示中的常见重叠文本段是系统提示。如果没有提示缓存,模型需要对每个查询都处理系统提示。有了提示缓存,模型只需在第一个查询时处理一次系统提示。
对于具有长系统提示的应用程序,提示缓存可以显著减少时延和成本。如果系统提示有1000个词元,并且应用程序今天生成了100万次模型API调用,那么提示缓存将帮助你每天少处理大约10亿个重复输入的词元!然而,这并非完全免费。与KV缓存一样,提示缓存的大小可能相当大,并且需要很多工程投入。
提示缓存对涉及长文档的查询也很有用。例如,如果许多用户问题都与同一个长文档(如一本书或一段代码库)有关,那么这个长文档可以缓存下来,以供多个问题重复使用。
自从Gim等人在2023年11月引入提示缓存以来,它已经被纳入模型API中。Google宣布Gemini API将在2024年6月提供这一功能,并命名为上下文缓存。缓存的输入词元相比普通输入词元有75%的折扣,但你需要为缓存存储额外付费(截至撰写本文时,费用为每小时每百万词元1美元)。鉴于提示缓存的明显优势,如果未来它变得像KV缓存一样流行,我也不会感到很惊讶。
尽管llama.cpp也有提示缓存功能,但它似乎只缓存完整的提示,并且仅适用于同一聊天会话中的查询。其文档有限,但从阅读代码来看,我猜测在长对话中,它缓存了前面的消息,只处理最新的消息。
精确缓存
如果说提示缓存和KV缓存是基础模型所独有的,那么精确缓存则更加通用且直观。系统可以存储已处理的项目,以便在处理相同项目时重复使用。例如,如果用户要求模型总结某个产品,系统会检查缓存中是否已有该产品的摘要。如果有,就取出这个摘要。如果没有,则生成摘要并缓存该摘要。
精确缓存还用于基于嵌入的检索,可以避免冗余的向量搜索。如果查询已经在向量搜索缓存中存在,则获取缓存的搜索结果。如果没有,则为该问题执行向量搜索并缓存结果。
缓存在需要多步操作(如思维链)和/或耗时操作(如检索、SQL执行或网络搜索)的查询中特别有效。
精确缓存可以使用内存存储来实现,以便快速检索。然而,由于内存存储是有限的,缓存也可以使用像PostgreSQL、Redis或分层存储这样的数据库来平衡速度和存储容量。制定一个驱逐策略对管理缓存大小和维持性能至关重要。常见的驱逐策略包括最近最少使用(LRU)、最不常使用(LFU)和先进先出(FIFO)。
缓存一个查询的时间长短取决于该查询被再次调用的可能性。用户特定的查询,例如“我最近的订单状态如何”,不太可能被其他用户重复使用,因此不应缓存。同样,缓存诸如“天气怎么样?”这样的时间敏感查询也意义不大。一些团队会训练小型分类器来预测某个查询是否应该被缓存。
语义缓存
与精确缓存不同,语义缓存不要求传入的查询与缓存中的查询完全相同。语义缓存允许重复使用相似的查询。想象一下,一个用户问“越南的首都是哪里?”模型生成了答案“河内”。随后,另一个用户问“越南的首都城市是哪里?”,虽然这个问题多了一个“城市”一词,但实际上是相同的问题。语义缓存的理念是,系统可以重复使用答案“河内”,而不是从头开始计算新问题。
语义缓存只有在你有可靠的方法来判断两个查询在语义上是否相似时才有效。常用的方法是基于嵌入的相似性,其工作原理如下:
对查询使用嵌入模型生成其嵌入表示。
使用向量搜索找到与当前查询嵌入最接近的缓存嵌入。假设这个相似度分数是X。
如果X超过了设定的相似性阈值,则认为缓存的查询与当前查询相同,并返回缓存的结果。如果不超过,则处理当前查询,并将其与嵌入和结果一起缓存。这种方法需要使用向量数据库来存储缓存查询的嵌入表示。
与其他缓存技术相比,语义缓存的价值更加不稳定,因为其组件容易出现故障。它的成功依赖于高质量的嵌入表示、功能完备的向量搜索以及可靠的相似度度量。设置正确的相似性阈值也可能是个棘手的挑战,通常需要大量的尝试和调整。如果系统错误地将传入的查询视为与另一个查询相似,返回的缓存回答可能是错误的。
此外,语义缓存因涉及向量搜索可能会很耗时且需要密集的计算。向量搜索的速度和成本取决于缓存嵌入数据库的大小。
如果缓存命中率较高,即大部分查询可以通过利用缓存结果有效回答,那么语义缓存可能仍然值得使用。然而,在引入语义缓存的复杂性之前,务必评估与其相关的效率、成本和性能风险。
添加缓存系统后,平台如图所示。KV缓存和提示缓存通常由模型API提供商实现,因此未在此图中显示。如果必须将它们可视化,我会将它们放在模型API框中。我在图中新增了一条箭头,用于将生成的回答添加到缓存中。
5
第5步:添加复杂逻辑并执行写入动作
我们之前讨论的应用程序流程相对简单。基础模型生成的输出大多直接返回给用户(除非未通过保护措施)。然而,应用程序的流程可以更加复杂,包含循环和条件分支。模型的输出还可以用于触发写入动作,例如撰写邮件或下订单。
复杂逻辑
模型的输出可以有条件地传递给另一个模型,或者作为下一步输入的一部分反馈给同一个模型。这个过程会持续进行,直到系统中的某个模型决定任务已完成,并且应该向用户返回最终的回答。
这种情况可能会在赋予系统规划能力和决策能力时出现。举个例子,“规划巴黎周末行程”这一问题。模型可能首先生成一系列潜在活动的列表:参观埃菲尔铁塔、在咖啡馆午餐、游览卢浮宫等。然后,每个活动可以再次反馈给模型,以生成更详细的计划。例如,“参观埃菲尔铁塔”可能会促使模型生成子任务,如检查开放时间、购买门票和寻找附近餐馆。这个迭代过程会一直持续,直到创建出一个全面详细的行程。
我们的基础设施现在有一条箭头指向生成的回答,并将其反馈给上下文构建,这又会反馈给模型网关中的模型。
写入操作
用于上下文构建的动作是只读动作,它们允许模型从数据源读取信息以收集上下文。但系统也可以执行写入操作,对数据源和现实世界进行更改。例如,如果模型输出:“发送一封邮件给X,内容为Y”,系统将调用动作 send_email(recipient=X, message=Y)。
写入操作使系统的能力大幅提升,可以将整个客户外联流程自动化:研究潜在客户、寻找联系方式、起草邮件、发送初次邮件、读取回答、跟进、提取订单、将新订单更新到数据库等。
然而,赋予AI自动改变我们生活的能力的前景是一件有些可怕的事情。就像不应该让实习生有权删除生产数据库一样,同样也不应该允许一个不可靠的AI发起银行转账。对系统能力和安全措施的信任至关重要。我们需要确保系统受到保护,避免系统被恶意行为者操纵而执行有害动作。
AI系统和其他软件系统一样,容易受到网络攻击,但它们还有另一个弱点:提示注入(prompt injection)。提示注入是指攻击者操纵输入提示,使模型表现出不良行为。可以将提示注入视为针对AI而非人类的社会工程攻击。
许多公司都很担心的一种情景是:他们将AI系统接入内部数据库,而攻击者诱使该系统泄露数据库中的私人信息。如果系统拥有这些数据库的写入权限,攻击者可能会诱使系统破坏数据。
任何要利用AI的组织都需要认真对待安全和保障问题。然而,这些风险并不意味着AI系统永远不应该在现实世界中采取行动。AI系统可能会失败,但人类也会失败。如果我们能让人们相信机器可以带我们进入太空,我希望有一天,安全措施足够完善,使我们可以信任自主的AI系统。
可观测性
虽然我将可观测性放在了一个独立的部分中,但它应该从一开始就集成到平台中,而不是作为事后才添加的内容。可观测性对于所有规模的项目都至关重要,系统越复杂,其重要性就越大。
相比其他部分,本节提供的信息最少。一篇文章不可能涵盖可观测性的所有细节。因此,我将简要概述监控的三大支柱:日志、追踪和指标。文章不会涉及具体内容或用户反馈、漂移检测和调试。
指标
谈到监控时,大多数人会想到指标。要跟踪哪些指标取决于想要监控系统的哪些方面,这通常根据应用来特定的。不过,通常有两种类型的指标需要跟踪:模型指标和系统指标。
系统指标会反应整个系统的状态。常见的系统指标包括吞吐量、内存使用情况、硬件利用率和服务可用性/正常运行时间。这些指标在所有软件工程应用中都是通用的。在本文中,我将重点关注模型指标。
模型指标评估模型的性能,例如准确性、有害性和幻觉率。应用Pipeline中的不同步骤也有各自的指标。例如,在RAG(检索增强生成)应用中,检索质量通常通过上下文相关性和上下文精度来评估。向量数据库可以通过其索引数据所需的存储空间以及查询数据所需的时间来评估。
模型输出的失败方式可能是多种多样的。识别这些问题并制定监控它们的指标至关重要。例如,可能需要跟踪模型超时的频率、返回空回答的次数或生成格式错误回答的情况。如果担心模型泄露敏感信息,也需要找到跟踪这种情况的方法。
与长度相关的指标(如问题长度、上下文长度和回答长度)有助于理解模型的行为。某个模型是否比另一个更冗长?某些类型的问题是否更可能导致冗长的回答?这些指标对于检测应用程序中的变化尤其有用。如果平均问题长度突然减少,可能表明存在需要调查的潜在问题。
与长度相关的指标对于跟踪时延和成本也很重要,因为较长的上下文和回答通常会增加时延并带来更高的成本。
跟踪时延对于理解用户体验至关重要。常见的时延指标包括:
第一个词元生成时间(TTFT):生成第一个词元所需的时间。
词元间时间(TBT):每个词元生成之间的间隔时间。
每秒生成词元数(TPS):生成词元的速率。
每个输出词元生成时间(TPOT):生成每个输出词元所需的时间。
总时延:完成一次回答所需的总时间。
还需要跟踪成本。与成本相关的指标包括问题数量和输入、输出词元的数量。如果使用的API有速率限制,跟踪每秒请求数非常重要,以确保API速率在分配的限额内并避免潜在的服务中断。
在计算指标时,可以选择抽样检查或全量检查。抽样检查涉及对数据子集进行采样,以快速识别问题,而全量检查则评估每个请求,以全面了解性能。选择取决于系统的需求和可用资源,结合两者可以提供平衡的监控策略。
在计算指标时,确保它们可以按相关维度进行细分,如用户、发布版本、提示/链条版本、提示/链条类型和时间。这种粒度有助于理解性能变化并识别具体问题。
日志
记录日志的理念很简单:记录一切。记录系统配置,记录问题、输出以及中间输出。记录每个组件的启动、结束以及发生崩溃的时间等。在记录日志时,确保为其添加标签和ID,以帮助了解日志来自系统的哪个部分。
记录一切意味着日志的数量可能会迅速增长。许多用于自动日志分析和日志异常检测的工具都是由AI驱动的。
虽然手动处理日志是不可能的,但每日手动检查生产数据对于了解用户如何使用应用程序是有帮助的。Shankar等人发现,随着开发人员与更多数据的交互,他们对好坏输出的看法会发生变化,这使得他们能够重新编写提示,以增加获得优质回答的机会,并更新评估流程以捕捉不良回答。
追踪
追踪指的是详细记录请求在各种系统组件和服务中的执行路径。在AI应用程序中,追踪揭示了从用户发送问题到最终回答返回的整个过程,包括系统采取的操作、检索的文档以及发送给模型的最终提示。追踪还应显示每个步骤所需的时间及其相关成本(如果可以测量的话)。例如,下图是一个Langsmith追踪的可视化示例。
理想情况下,应该能够逐步追踪每个问题在系统中的转换过程。如果某个问题失败了,应该能够精确定位出问题的具体步骤:是处理错误、检索的上下文不相关、还是模型生成了错误的回答。
AI Pipeline编排
AI应用程序可能会相当复杂,涉及多个模型、从多个数据库检索数据以及访问各种工具。编排器能指定如何将这些不同的组件组合(串联)在一起,以创建端到端的应用程序流程。
从高层次上讲,编排器通过两个步骤进行工作:组件定义和串联(也称为流水线)。
组件定义。你需要告诉编排器你的系统使用了哪些组件,例如模型(包括生成、路由和评分模型),系统可以从中检索数据的数据库以及系统可以执行的操作。与模型网关的直接集成可以帮助简化模型的引入,而一些编排器工具则希望成为网关。许多编排器还支持与评估和监控工具的集成。
串联(或流水线)。你需要告诉编排器从接收用户问题到完成任务这一过程中系统采取的步骤顺序。简而言之,串联就是函数组合。以下是一个串联(流水线)的示例:
处理原始问题。
根据处理后的问题检索相关数据。
将原始问题和检索到的数据结合起来,创建一个符合模型期望格式的提示。
模型根据提示生成回答。
评估该回答。
如果回答被认为是优质的,就返回给用户;如果不满意,则将问题转交给人工操作员。编排器负责在步骤之间传递数据,并可以提供工具,帮助确保当前步骤的输出符合下一步骤的预期格式。
在设计对时延要求严格的应用程序的串联(流水线)时,应尽量并行执行任务。例如,如果你有路由组件(决定将问题发送到哪里)和PII删除组件,它们可以同时进行操作。
目前已经有许多AI编排工具,如LangChain、LlamaIndex、Flowise、Langflow和Haystack。每个工具都有自己的API,所以我不会在这里展示实际代码。
尽管在开始项目时直接跳到使用编排工具是一件有吸引人的事情,但我建议首先在不使用工具的情况下构建应用程序。任何外部工具都会带来额外的复杂性。编排器可能会抽象掉系统工作方式的重要细节,使得理解和调试系统变得困难。
随着应用程序开发过程的推进,你可能会发现编排器能让工作更轻松。在评估编排器时,以下三个方面需要注意:
集成和扩展性:评估编排器是否支持已经使用或可能在未来采用的组件。例如,如果想用Llama模型,需要检查编排器是否支持。由于有太多模型、数据库和框架,任何编排器都不可能支持所有东西。因此,还需要考虑编排器的扩展性。如果它不支持特定组件,修改它的难度有多大?
支持复杂的串联:随着应用程序复杂性的增加,你可能需要管理涉及多个步骤和条件逻辑的复杂串联。支持高级功能(如分支、并行处理和错误处理)的编排器将帮助你高效地管理这些复杂性。
易用性、性能和可扩展性:考虑编排器的用户友好性。寻找直观的API、全面的文档和强大的社区支持,能显著降低你和团队的学习难度。同时,避免使用那些会发起隐藏API调用或给应用程序带来时延的编排器。此外,确保编排器在应用程序数量、开发人员数量和流量增长时,能够有效扩展。
6
结论
这篇文章从基本架构出发,逐步添加了应对不断增长的应用复杂性的组件。每个新增的组件都有各自的好处和挑战,需要仔细考虑和实施。
虽然组件的分离对于保持系统的模块化和可维护性很重要,但这种分离是灵活的。组件之间存在许多重叠。例如,模型网关可以与守护机制共享功能。缓存可以在不同的组件中实现,例如在向量搜索和推理服务中。
这篇文章比我预期的要长得多,但仍有许多细节我未能进一步探讨,特别是在可观测性、上下文构建、复杂逻辑、缓存和守护机制方面。我将在即将出版的《AI工程》一书中会深入探讨这些部分。
这篇文章也没有讨论如何部署模型,因为我的假设是大多数人都会使用第三方API提供的模型。《AI工程》一书中也会有一章来专门讲解推理和模型优化。
版权声明
本文为本站原创内容,转载需注明文章来源(https://www.eiefun.com),另:文中部分素材可能会引用自其他平台,如有侵权或其它,请联系 admin@eiefun.com,我们会第一时间配合删除