← 文章 / AI技术
Chip Huyen 2小时前 · 2026-08-31 13:44:57 · 1 阅读

构建一个生成式 AI 平台

在研究了企业部署生成式 AI 应用的方式之后,我发现它们的平台有许多共通之处。本文将概述一个生成式 AI 平台的常见组件、它们的功能以及实现方式。我会尽量让架构保持通用,但某些应用可能会有所差异。整体架构如下图所示。

Overview of a genai platform


这是一个相当复杂的系统。本文会从最简单的架构开始,逐步增加更多组件。最简单的形式下,你的应用接收一个查询并发送给模型,模型生成响应后返回给用户。没有安全护栏,没有上下文增强,也没有优化。这里的 Model API 涵盖了第三方 API(如 OpenAI、Google、Anthropic)以及自托管 API。

Overview of a genai platform


在此基础上,你可以根据需要添加更多组件。本文讨论的添加顺序较为常见,但你不必完全照搬。如果某个组件对你的系统并非必需,也可以跳过。不过,在开发的每个阶段,评估都是必不可少的。

  1. 通过让模型访问外部数据源和信息收集工具,来增强输入到模型中的上下文。
  2. 设置安全护栏,保护你的系统和用户。
  3. 加入模型路由器和网关,以支持复杂流水线并加强安全性。
  4. 利用缓存优化延迟和成本。
  5. 添加复杂逻辑和动作,最大化系统能力。

可观测性(用于监控系统状态和排查问题)和编排(将所有组件串联起来)是平台中两个至关重要的组件,我们会在文章最后讨论它们。

» 本文不涉及的内容 «

本文聚焦于部署 AI 应用的整体架构,讨论所需的组件以及构建这些组件时的考量。本文不讲解如何构建 AI 应用,因此不会涉及模型评估、应用评估、Prompt 工程、微调、数据标注指南,或 RAG 的分块策略。这些主题都会在我即将出版的新书《AI Engineering》中详细讨论。

第一步:增强上下文

平台最初的扩展通常是加入机制,让系统能够用必要信息来补充每个查询。收集相关信息的过程叫做上下文构建。

许多查询需要上下文才能回答。上下文中的相关信息越丰富,模型就越不需要依赖其内部知识——而这些知识受训练数据和训练方法所限,可能并不可靠。研究表明,在上下文中提供相关信息能帮助模型生成更详尽的回答,同时减少幻觉(Lewis 等人, 2020)。

举个例子,对于查询“Acme 的 fancy-printer-A300 能否打印 100pps?”,如果给模型附上 fancy-printer-A300 的规格说明,它的回答就会更准确。(感谢 Chetan Tekur 提供这个例子。)

对基础模型而言,上下文构建相当于传统机器学习中的特征工程。两者目的相同:让模型在处理输入时获得必要的信息。

上下文学习,即从上下文中学习,是一种持续学习的形式。它使模型能够持续整合新信息来做判断,避免变得过时。例如,一个基于上周数据训练的模型,如果不把新信息纳入上下文,就无法回答本周的问题。只要用最新信息(例如 fancy-printer-A300 的最新规格)更新模型的上下文,模型就能与时俱进,回答其训练截止日期之后的问题。

RAG

上下文构建最广为人知的模式是 RAG(Retrieval-Augmented Generation,检索增强生成)。RAG 由两个组件构成:生成器(例如语言模型)和检索器,后者负责从外部数据源中取回相关信息。

Overview of a genai platform


检索并非 RAG 独有,它同样是搜索引擎、推荐系统、日志分析等系统的核心。许多为传统检索系统设计的算法,都可以直接用在 RAG 中。

外部知识源通常包含非结构化数据,比如备忘录、合同、新闻等,这些统称为"文档"。一份文档可能只有 10 个 token,也可能长达 100 万个 token。如果直接整篇检索,会导致上下文长度不可控。因此 RAG 一般会先把文档切分成"大小合适的段落(chunk)",具体长度取决于模型的最大上下文长度和应用对延迟的要求。关于分块策略和最佳块大小的更多信息,可以参考 PineconeLangchainLlamaindex 以及 Greg Kamradt 的教程。

外部知识源的数据被加载并切分后,主要通过以下两种方式完成检索。

  1. 基于词项的检索
    最简单的形式就是关键词搜索:给定查询词"transformer",返回所有包含该关键词的文档。更复杂的算法包括 BM25(基于 TF-IDF)和 Elasticsearch(基于倒排索引)。

    基于词项的检索通常用于文本数据,但同样适用于带有文字元数据(标题、标签、字幕、评论等)的图片和视频。

  2. 基于向量的检索(也称为向量搜索)
    先用 embedding 模型(如 BERTsentence-transformers,或 OpenAI、Google 提供的专有 embedding 模型)把数据段落转成向量。检索时,向量搜索算法会找出与查询向量最接近的那些数据。

    向量搜索通常被理解为最近邻搜索,所用算法多为近似最近邻(ANN),比如 FAISS(Facebook AI Similarity Search)、Google 的 ScaNN、Spotify 的 ANNOY 以及 hnswlibHierarchical Navigable Small World)。
    ANN-benchmarks 网站从四个核心指标出发,在多个数据集上对比了不同的 ANN 算法,并权衡索引构建与查询之间的取舍。

    • 召回率(Recall):算法找到的最近邻占全部最近邻的比例。
    • 每秒查询数(QPS):算法每秒能处理的查询数量,对高流量应用尤为关键。
    • 构建时间(Build time):构建索引所需的时间。如果索引需要频繁更新(例如数据经常变化),这个指标就尤其重要。
    • 索引大小(Index size):算法生成的索引体积,直接关系到可扩展性和存储开销。

这种检索方式不仅适用于文本文档,同样适用于图像、视频、音频和代码。很多团队甚至会先对 SQL 表和 dataframe 做摘要,再用摘要生成向量用于检索。

相比基于向量的检索,基于关键词的检索更快、成本更低,开箱即用效果也不错,是一个很有吸引力的起点。BM25 和 Elasticsearch 在业界被广泛使用,也是更复杂检索系统强有力的基线。基于向量的检索虽然计算开销大,但经过持续优化后,往往能显著超越基于关键词的检索。

一个生产级的检索系统通常会组合多种方法。把基于关键词的检索和基于向量的检索结合在一起,就叫做混合检索

一种常见的做法是串行组合:先用便宜但精度一般的检索器(例如基于关键词的检索)召回候选集,再交给更精确但更耗时的机制(比如 k 近邻)从中挑出最优结果。第二步也叫做重排序(reranking)。

例如,给定关键词"transformer",你可以检索出所有包含该词条的文档,无论它们涉及的是电气设备、神经网络架构,还是电影。然后再用向量搜索在这些文档中找出真正与"transformer"查询相关的部分。

上下文重排序与传统搜索重排序的区别在于,条目的精确位置不那么关键。在搜索中,排名(比如第一名还是第五名)至关重要。而在上下文重排序中,文档的顺序仍然重要,因为它会影响模型处理文档的效果。论文 Lost in the middle(Liu 等人,2023)指出,模型可能更容易理解位于上下文开头和结尾的文档。不过,只要文档被包含进来,其顺序带来的影响相比搜索排序就要小得多。

另一种模式是集成(ensemble)。前文提到,检索器通过相关性分数对文档排序。集成方法会同时使用多个检索器各自获取候选结果,再将不同的排序结果合并,生成最终排序。

面向表格数据的 RAG

外部数据源也可以是结构化的,比如 dataframe 或 SQL 表。从 SQL 表中检索数据与从非结构化文档中检索有显著不同。给定查询时,系统工作流程如下:

  1. Text-to-SQL:根据用户查询和表结构,确定需要执行的 SQL 语句。
  2. 执行 SQL:运行该 SQL 查询。
  3. 生成回答:根据 SQL 返回结果和原始用户查询生成回复。
GenAI 平台概览


在 text-to-SQL 这一步,如果可用表太多、所有 schema 没法全部塞进模型上下文,就可能需要插入一个中间步骤,预先预测每个查询该查哪些表。Text-to-SQL 既可以用最终生成回复的那个模型来做,也可以用专门的 text-to-SQL 模型。

智能体式 RAG

互联网是重要的数据来源。像 Google 或 Bing 这样的搜索 API 可以让模型获取丰富且最新的资源,为每个查询收集相关信息。例如,面对"今年奥斯卡谁获奖了?"这个问题,系统会搜索最新的奥斯卡信息,并据此生成最终回复给用户。

基于关键词的检索、基于向量的检索、SQL 执行以及网页搜索,都是模型可以用来增强自身上下文的操作。你可以把每个操作理解为模型可以调用的一段函数。能够调用外部操作的工作流也称为智能体(agentic)工作流。整体架构如下图所示。

生成式 AI 平台概览

» 操作 vs. 工具 «

一个工具可以支持一个或多个操作。例如,一个人员搜索工具可能支持两个操作:按姓名搜索和按邮箱搜索。不过两者差别很小,所以很多人把操作工具混用。

» 只读操作 vs. 写入操作 «

从外部源读取信息但不改变其状态的操作称为只读操作。赋予模型写入操作(例如更新表中的数据)能让模型完成更多任务,但也会带来更多风险,这点稍后会讨论。

查询改写

用户输入的查询经常需要改写,以提高获取到正确信息的概率。看下面这段对话:

用户:John Doe 上次从我们这里买东西是什么时候?
AI:John 两周前,也就是 2030 年 1 月 3 日,从我们这里买了一顶 Fruity Fedora 帽子。
用户:Emily Doe 呢?

"Emily Doe 呢?"这个问题本身是有歧义的。如果直接拿这个查询去检索文档,很可能会得到无关的结果。需要把它改写成用户真正想问的内容,改写后的查询应当能独立表达完整含义。"Emily Doe 呢?"应该被改写为"Emily Doe 上次从我们这里买东西是什么时候?"。

查询改写通常由其他 AI 模型完成,使用的 prompt 类似:"给定以下对话,请将用户最后一次输入改写为用户真正想问的内容。"

Overview of a genai platform

查询改写可能变得很复杂,尤其是当你需要进行身份解析或融合其他知识时。例如,用户问"他的妻子呢?",你就得先查数据库弄清楚"他"是谁、"他的妻子"是谁。如果信息缺失,改写模型应该承认这个问题无法回答,而不是凭空编造一个名字,导致最终给出错误答案。

第二步:设置安全护栏

安全护栏有助于降低 AI 带来的风险,既保护用户,也保护你们开发者自身。凡是可能出现故障的地方,都应该部署护栏。本文讨论两类护栏:输入护栏和输出护栏。

输入护栏

输入护栏主要用于防范两类风险:将私密信息泄露给外部 API,以及执行恶意提示导致系统被攻破(模型越狱)。

将私密信息泄露给外部 API

这一风险在使用外部模型 API 时尤为突出,因为你需要把数据发送到组织之外。例如,员工可能把公司机密或用户的隐私信息粘贴到提示词里,一并发送给模型托管方。

早期最知名的案例之一是三星员工将公司专有信息输入 ChatGPT,不慎泄露了公司机密。三星是如何发现这次泄露的、泄露的信息又被如何利用,目前尚不清楚。但此事严重到促使三星在 2023 年 5 月禁用 ChatGPT

使用第三方 API 时,没有任何方法能彻底杜绝信息泄露。但你可以借助护栏来降低风险:使用现成的敏感数据自动检测工具,由你指定需要检测的敏感数据类型。常见的敏感数据类别包括:

  • 个人身份信息(身份证号、电话号码、银行账户)。
  • 人脸图像。
  • 与公司知识产权或机密信息相关的特定关键词和短语。

许多敏感数据检测工具会用 AI 来识别潜在的敏感信息,比如判断一个字符串是否像一个有效的家庭住址。如果检测到某次查询中包含敏感信息,你有两个选择:阻止整次查询,或者将敏感信息从查询中移除。例如,可以将用户的电话号码替换为占位符 [PHONE NUMBER]。如果生成的回复中包含这个占位符,再借助一个 PII 可逆字典,将占位符映射回原始信息,从而完成反掩码处理,如下图所示。

Overview of a genai platform

模型越狱

试图让 AI 模型越狱、说出或做出不当行为,已经成了网络上的一种"娱乐活动"。让 ChatGPT 发表争议性言论,或许还能博人一笑;但如果挂着你的品牌名和 logo 的客服聊天机器人也这样,那可就不那么好玩了。对于那些能够调用工具的 AI 系统来说,这种风险尤其危险。可以想象一下,如果有人找到方法让系统执行了一条破坏数据的 SQL 查询,后果会有多严重。

要防范这类问题,首先应当为系统设置护栏,确保任何有害操作都不会被自动执行。例如,所有涉及插入、删除或更新数据的 SQL 查询,都必须经过人工审批后才能执行。这种额外的安全措施带来的代价是会让系统的响应速度变慢。

为了避免应用发表不该说的出格言论,可以为它定义"超范围"的话题。比如,如果你的应用是一个客服聊天机器人,它就不应当回答政治或社会类问题。一种简单的方法是过滤掉那些包含预定义敏感词的输入,例如"移民"或"反疫苗"等通常与争议性话题相关的短语。更复杂的方案则使用 AI 来判断输入是否属于某个预设的限制性话题。

如果有害提示在你的系统中比较少见,可以使用异常检测算法来识别异常提示。

输出护栏

AI 模型本质上是概率性的,因此其输出并不可靠。可以通过设置护栏来显著提升应用的可靠性。输出护栏主要有两个功能:

  1. 评估每一次生成内容的质量。
  2. 针对不同的失败模式制定应对策略。

输出质量评估

要捕捉不符合标准的输出,首先需要了解失败的表现形式。以下是一些常见的失败模式及其检测方法。

  1. 空响应

  2. 格式错误的响应,即未按预期格式输出。例如,应用期望 JSON 格式,但生成的响应缺少闭合括号。可以使用针对特定格式的校验器,如正则、JSON 和 Python 代码校验器。还有一些用于约束采样的工具,例如 guidance、outlines 和 instructor。

  3. 有害响应,例如带有种族歧视或性别歧视的内容。这类响应可以使用众多的有害内容检测工具来捕捉。

  4. 事实不一致的响应,即模型产生的幻觉。幻觉检测是一个活跃的研究领域,已有 SelfCheckGPT(Manakul 等,2023)和 SAFE(Search Engine Factuality Evaluator,Wei 等,2024)等解决方案。可以通过为模型提供充分的上下文以及使用思维链等提示技术来缓解幻觉问题。幻觉的检测与缓解将在我的新书 AI Engineering 中进一步讨论。

  5. 包含敏感信息的响应。这种情况可能发生在以下两种场景中:
    1. 模型在训练时使用了敏感数据,并在推理时将其复述出来。
    2. 系统从内部数据库检索敏感信息以丰富上下文,然后将敏感信息传递给输出。

    可以通过不在敏感数据上训练模型,以及从根本上不允许模型检索敏感数据来避免这类失败。输出中的敏感信息可以使用与输入护栏相同的工具来检测。

  6. 品牌风险类回答,例如错误描述你的公司或竞争对手的回答。一个典型例子是:X 公司训练的模型 Grok 曾生成声称自己由 OpenAI 训练的回答,引发了网友对 X 偷用 OpenAI 数据的怀疑。这类问题可以通过关键词监控来缓解。一旦发现涉及你品牌和竞争对手的输出,你可以直接屏蔽、交给人工审核,或者借助其他模型来检测这些输出的情感倾向,确保只返回符合预期的情感。

  7. 普遍糟糕的回答。比如你让模型写篇文章,结果写得很差;或者你让它给一个低卡蛋糕食谱,结果生成的食谱糖分高得离谱。现在很流行用 AI 评委来评估模型回答的质量。这些 AI 评委可以是通用模型(比如 ChatGPT、Claude),也可以是专门训练的评分模型,能针对给定的问题给出具体的分数。

失败处理

AI 模型具有概率性,这意味着同样的问题再问一次,可能会得到不同的回答。很多失败可以通过基本的重试逻辑来缓解。比如,如果回答为空,就重试若干次,直到得到非空回答;或者如果回答格式有误,就重试直到模型输出正确格式。

不过,这种重试策略会带来额外的延迟和成本。每多一次重试,API 调用量就翻倍。如果等失败后再重试,用户感受到的延迟也会翻倍。为了降低延迟,可以并行发起调用:对于每个问题,不是等第一次请求失败后再重试,而是一开始就把同一个问题同时发给模型两次,拿到两个回答后择优选用。这样虽然增加了冗余的 API 调用次数,但能把延迟控制在可接受范围内。

让人类来处理棘手的查询也是很常见的做法。比如,当查询中包含特定关键词时,可以转交给人工客服。也有团队会用一个专门的模型(可能是内部训练的)来判断何时应该把对话转交给人工。举例来说,有团队在情感分析模型检测到用户情绪变得愤怒时,就会把对话转给人工;也有团队在对话轮次超过一定数量后就把对话转给人工,以避免用户在无限循环中越陷越深。

护栏的权衡

可靠性与延迟的权衡:虽然大家都承认护栏很重要,但有些团队告诉我延迟对他们来说更关键,因此选择不部署护栏,因为护栏会显著增加应用的响应时间。不过这只是少数派,大多数团队认为增加风险带来的代价要大于延迟带来的影响。

输出护栏在流式补全模式下可能效果不佳。默认情况下,模型要先生成完整响应再展示给用户,这往往需要较长时间;而在流式补全模式下,新生成的 token 会边生成边推送给用户,大大缩短等待时间。但代价是难以对部分响应做评估,不安全的响应可能在护栏判定需要拦截之前就已经推送给了用户。

自托管与第三方 API 的权衡:自托管模型意味着你不用把数据发送给第三方,从而降低了对输入护栏的需求。但这也意味着你必须自行实现所有必要的护栏,而不能依赖第三方服务提供的护栏。

我们的平台现在看起来是这样:护栏既可以是独立工具,也可以是模型网关的一部分,后面会再讨论。如果使用了评分器,会把它归到模型 API 这一类下,因为评分器通常也是 AI 模型。用于评分的模型一般比用于生成的模型更小、速度更快。

生成式 AI 平台概览

第三步:加入模型路由器和网关

随着应用变得越来越复杂、涉及的模型越来越多,业界涌现出了两类工具来帮助你管理多个模型:路由器和网关。

路由器

一个应用可以使用不同的模型来响应不同类型的查询。针对不同查询采用不同方案有几个好处。首先,这样可以使用专门化的方案,比如一个模型专门处理技术故障排查,另一个专门处理订阅问题。专门化模型的表现通常优于通用模型。其次,这有助于降低成本。可以把简单的查询路由到更便宜的模型上,而不是把所有查询都路由到昂贵的模型。

路由通常由意图分类器组成,用于预测用户的意图。根据预测出的意图,将查询路由到相应的方案。例如,对于客服聊天机器人,如果意图是:

  • 重置密码 → 将用户引导到密码重置页面。
  • 纠正账单错误 → 将用户转接给人工客服。
  • 排查技术问题 → 将查询路由到专门微调过的故障排查模型。

意图分类器还可以帮助系统避免超出范围的对话。例如,可以让意图分类器预测查询是否超出范围。如果判定查询不合适(例如用户询问你会把票投给谁),聊天机器人可以使用预设回复礼貌地拒绝(如"作为聊天机器人,我没有投票权。如果您有产品方面的问题,我很乐意为您解答。"),从而避免浪费一次 API 调用。

如果系统支持多种操作,路由可以包含一个下一步操作预测器,帮助系统决定下一步该采取什么操作。当查询存在歧义时,询问澄清是一个合理的操作。例如,对于"冻结"这个查询,系统可能会问"您是想冻结账户,还是在说天气?",或者简单地说"抱歉,能详细说说吗?"。

意图分类器和下一步操作预测器可以是通用模型,也可以是专门的分类模型。专门分类模型通常比通用模型小得多、速度快得多,让系统能够同时使用多个模型,而不会带来明显的延迟和成本开销。

把查询路由到上下文长度各不相同的模型时,可能需要相应地调整查询的上下文。假设有一条 1,000 token 的查询,原本要发给上下文上限为 4K 的模型,但系统在执行某项操作(例如网页搜索)后,带回了 8,000 token 的上下文。此时你可以选择截断查询上下文以适配原先指定的模型,也可以把查询重新路由到上下文更大的模型。

网关

模型网关是一个中间层,让组织能够以统一、安全的方式对接不同的模型。它最基础的功能,就是让开发人员能以相同的方式访问各种模型——无论是自托管模型,还是 OpenAI、Google 等商用 API 后面的模型。模型网关让代码维护变得更加轻松:如果某个模型的 API 发生变化,你只需要更新模型网关,而不必挨个修改所有调用该模型 API 的应用。

Overview of a genai platform

最简单的情况下,模型网关就是一个统一的封装层,如下面的代码示例所示。这个示例只是为了让你大致了解模型网关的实现思路,并不保证可以直接运行——里面既没有错误处理,也没有性能优化。

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)

模型网关的核心作用是访问控制与成本管理。与其把组织级的 token 直接发给每个想调用 OpenAI API 的人——这样很容易泄露——不如只让他们通过模型网关来访问,从而形成一个集中可控的访问入口。网关还可以实现细粒度的权限控制,指定哪些用户或应用可以使用哪些模型。此外,网关能够监控并限制 API 调用量,防止滥用并有效控制成本。

模型网关还可以实现降级策略,以应对限流或 API 故障(后者其实很常见)。当主 API 不可用时,网关可以将请求路由到备用模型、短暂等待后重试,或以其他方式优雅地处理失败。这样能保证应用持续平稳运行,不会被打断。

既然请求和响应已经流经网关,这里就很适合顺带实现负载均衡、日志和数据分析等功能。部分网关服务还提供缓存和安全护栏(guardrails)能力。

网关实现起来相对简单,市面上有很多现成方案。比如 Portkey 的 gatewayMLflow AI Gateway、WealthSimple 的 llm-gatewayTrueFoundryKong 以及 Cloudflare

加入网关和路由之后,我们的平台越来越有意思了。和评分一样,路由也放在模型网关里。和评分模型类似,路由模型通常也比生成模型小。

生成式 AI 平台概览

第 4 步:用缓存降低延迟

我把这篇文章给朋友 Eugene Yan 看时,他说缓存可能是 AI 平台中最被低估的组件。缓存能显著降低应用的延迟和成本。

缓存技术也能用在训练阶段,但这篇文章讨论的是部署,所以这里只讲推理缓存。常见的推理缓存技术包括 prompt cache、exact cache 和 semantic cache。Prompt cache 一般由你所用的推理 API 实现,在评估推理库时,了解它支持哪些缓存机制会很有帮助。

注意力机制中的 KV cache 不在本文讨论范围内。

Prompt cache

应用中的许多 prompt 都有重叠的文本片段,例如所有查询可以共用同一个系统 prompt。Prompt cache 把这些重叠片段存起来复用,只需处理一次。系统 prompt 是不同 prompt 中最常见的重叠片段:没有 prompt cache 时,模型每次查询都要重新处理系统 prompt;有了 prompt cache,系统 prompt 只在第一次查询时处理一次就够了。

对于系统提示词较长的应用,提示缓存可以显著降低延迟和成本。如果你的系统提示词有 1000 个 token,而应用每天产生 100 万次模型 API 调用,那么启用提示缓存每天可少处理大约 10 亿个重复的输入 token!不过,这并非完全免费。提示缓存和 KV cache 一样,体积可能相当庞大,需要投入不少工程精力。

提示缓存同样适用于涉及长文档的查询。例如,如果许多用户查询都围绕同一份长文档(比如一本书或一个代码库),这份长文档就可以被缓存,在不同查询间复用。

提示缓存自 2023 年 11 月由 Gim 等人提出以来,已被集成到模型 API 中。Google 于 2024 年 6 月宣布在 Gemini API 中提供该功能,命名为 context cache。缓存的输入 token 价格是常规输入 token 的 25 折(即 75% 折扣),但你需要额外支付缓存存储费用(截至本文撰写时,每小时每百万 token 1.00 美元)。鉴于提示缓存的显著优势,它未来变得和 KV cache 一样普及,我丝毫不会感到意外。

llama.cpp 也提供了 prompt cache,但似乎只能缓存完整的提示词,且仅在同一个聊天会话内的查询中生效。它的文档非常有限,但从代码来看,我的猜测是:在长对话中,它会缓存之前所有消息,只对最新一条消息进行处理。

精确缓存

如果说提示缓存和 KV cache 是基础模型独有的机制,那么精确缓存(exact cache)则更为通用和直接。系统把已处理过的内容存下来,以便在后续请求完全相同的内容时直接复用。例如,当用户让模型对某件商品生成摘要时,系统会先查缓存,看这件商品的摘要是否已有缓存。有则直接取出,没有则生成摘要并写入缓存。

精确缓存还可用于基于嵌入的检索,以避免重复进行向量搜索。如果某条查询的向量搜索结果已在缓存中,就直接取出;否则就执行向量搜索并将结果写入缓存。

缓存对于需要多步推理(例如思维链)和/或耗时操作(例如检索、SQL 执行、网页搜索)的查询尤为有用。

精确缓存可以用内存存储来实现,以获得快速读取。但内存容量有限,因此也可以用 PostgreSQL、Redis 等数据库,或者分层存储来实现缓存,在速度和容量之间取得平衡。淘汰策略至关重要,能控制缓存规模、保障性能。常见的淘汰策略包括 LRU(最近最少使用)、LFU(最不经常使用)和 FIFO(先进先出)。

一个查询该缓存多久,取决于它被再次调用的概率。像"我最近的订单状态如何"这种针对单个用户的查询,其他用户几乎不会再问,不值得缓存。同样,像"今天天气怎么样"这种时效性很强的问题也没太大缓存意义。有些团队会训练一个小型分类器来预测某条查询是否值得缓存。

语义缓存

和精确缓存不同,语义缓存不要求新查询与已有缓存完全一致,而是允许复用语义相近的查询。假设有用户问"越南的首都是哪里?",模型给出答案"河内"。之后另一个用户问"越南的首都城市是哪座?",本质上是同一个问题,只是多了个"城市"二字。语义缓存的思路就是:直接复用"河内"这个答案,不必再从头处理这条新查询。

语义缓存的前提是,你能可靠地判断两条查询在语义上是否相似。一种常见做法是基于嵌入向量的相似度匹配,流程如下:

  1. 用嵌入模型为每条查询生成向量。
  2. 通过向量检索,找出与当前查询向量最接近的缓存向量。假设它们的相似度为 X。
  3. 如果 X 超过你设定的相似度阈值,就认为这条缓存查询和当前查询是同一个,直接返回缓存结果;否则就正常处理当前查询,并把查询、向量和结果一起写入缓存。

这种方案需要一个向量数据库来存储缓存查询的向量。

与其他缓存技术相比,语义缓存的价值更值得商榷,因为它的许多组件都容易出问题。它的效果取决于高质量的 embedding、正常运作的向量搜索,以及可靠的相似度度量。如何设置合适的相似度阈值也是个难题,往往需要大量反复试验。一旦系统误判新查询与某条已有查询相似,从缓存返回的响应就会出错。

此外,语义缓存由于涉及向量搜索,既耗时又算力密集。这部分向量搜索的速度和成本取决于缓存 embedding 数据库的规模。

如果缓存命中率较高——也就是说,大量查询能通过复用缓存结果得到有效回答——那么语义缓存或许还是值得的。不过,在引入语义缓存所带来的复杂性之前,务必要先评估它在效率、成本和性能方面的风险。

加入缓存层之后,平台架构如下图所示。KV cache 和 prompt cache 通常由模型 API 服务方实现,因此图中未画出;如果一定要可视化展示,我会把它们放在 Model API 框里。图中还新增了一条箭头,表示将生成的回答写入缓存。

生成式 AI 平台概览

第 5 步:加入复杂逻辑与写操作

到目前为止,我们讨论的应用流程都比较简单。基础模型的输出基本上直接返回给用户(除非没通过安全护栏)。然而,实际的应用流程可能更复杂,包含循环和条件分支。模型的输出还可以触发写操作,比如撰写邮件或下单。

复杂逻辑

模型的输出可以有条件地传递给下一个模型,或者作为输入再次反馈给同一个模型,这个过程会一直持续,直到系统中某个模型判定任务已完成,并将最终结果返回给用户。

这种效果在让系统具备自主规划、决定下一步该做什么的能力时就会显现。比如面对"规划一个巴黎周末行程"这样的查询,模型可能先生成一份潜在活动清单:参观埃菲尔铁塔、在咖啡馆吃午餐、游览卢浮宫等等。然后这些活动可以逐一反馈给模型,生成更详细的计划。例如,"参观埃菲尔铁塔"可以促使模型生成子任务:查看开放时间、购票、找附近餐厅。这个迭代过程持续进行,直到生成一份完整详尽的行程。

此时我们的基础设施中多了一条回路:生成的回答反馈到上下文构建环节,再由上下文构建环节送回模型网关中的各个模型。

生成式 AI 平台总览

写操作

用于上下文构建的动作属于只读操作,它们让模型从数据源读取信息以收集上下文。但系统也可以执行写操作,直接修改数据源甚至影响现实世界。例如,模型若输出"给 X 发送邮件,内容为 Y",系统就会调用 send_email(recipient=X, message=Y) 这一动作。

写操作让系统的能力大幅扩展,可以帮你自动化整个客户开发工作流:调研潜在客户、查找联系方式、起草邮件、发送首封邮件、阅读回复、跟进沟通、提取订单、将新订单更新到数据库等等。

然而,让 AI 自动改变我们的生活,这件事想想就令人不安。就像不该让实习生拥有删除生产数据库的权限一样,你也不应该让一个不可靠的 AI 去发起银行转账。系统的能力与其安全措施是否值得信赖至关重要。你需要确保系统能够抵御恶意攻击者,防止他们操纵系统执行有害操作。

AI 系统和一般软件系统一样容易遭受网络攻击,但它还有一个独特的弱点:提示注入(prompt injection)。所谓提示注入,是指攻击者通过篡改输入给模型的提示,诱导模型表现出不良行为。可以把提示注入理解为针对 AI 而非人类的"社会工程学"。

很多公司担心的一种场景是:他们让 AI 系统接入内部数据库,攻击者便借此诱导系统泄露数据库中的私密信息。如果系统还拥有写入权限,攻击者甚至可以让系统篡改数据。

任何想要利用 AI 的组织都必须认真对待安全与防护问题。但这并不意味着 AI 系统永远不该具备在真实世界中采取行动的能力。AI 会出错,人同样会出错。既然我们能让人坐飞机上天,我也希望有朝一日安全机制足够完善,让我们可以放心地把任务交给自主运行的 AI 系统。

生成式 AI 平台概览

可观测性

虽然我单独拿出一节来讲可观测性,但它应当从一开始就融入平台,而不是事后才想起来补上的。可观测性对各种规模的项目都至关重要,而且系统越复杂,它的重要性就越突出。

这一节的内容会比其他章节少一些——毕竟不可能在一篇博客里讲清可观测性的所有细节。因此,我只简要介绍监控的三大支柱:日志(logs)、链路追踪(traces)和指标(metrics),不会深入展开用户反馈、漂移检测、调试等具体话题。

指标

提到监控,大多数人首先想到的就是指标。要追踪哪些指标取决于你想了解系统的哪些方面,这因应用而异。但总体而言,需要追踪两类指标:模型指标和系统指标。

系统指标反映的是整个系统的运行状态,常见的包括吞吐量、内存占用、硬件利用率以及服务可用性/在线时长等。这些指标在所有软件工程场景中都通用,本文就不再赘述,重点放在模型指标上。

模型指标用于评估模型的性能,例如准确率、有害输出率和幻觉率。应用流水线的各个步骤也有各自的指标。以 RAG 应用为例,检索质量通常用上下文相关性和上下文精确度来评估。向量数据库则可以从索引数据所需的存储空间以及查询耗时这两个维度来衡量。

模型输出可能以多种方式出错,因此识别这些问题并制定相应指标来监控它们至关重要。例如,你可能希望追踪模型超时、返回空响应或输出格式异常等情况的频率。如果担心模型泄露敏感信息,也需要想办法进行追踪。

查询、上下文和响应长度这类与长度相关的指标,有助于理解模型的行为特征。比如某个模型是否比其他模型更啰嗦?某些类型的查询是否更容易得到冗长的回答?这些指标在检测应用变化时尤其有用——如果平均查询长度突然下降,可能意味着存在需要排查的潜在问题。

长度相关指标对于追踪延迟和成本也很重要,因为更长的上下文和响应通常会带来更高的延迟和成本。

追踪延迟对于理解用户体验至关重要。常见的延迟指标包括:

  • 首 token 响应时间(TTFT):生成第一个 token 所花费的时间。
  • token 间隔时间(TBT):相邻 token 生成之间的时间间隔。
  • 每秒 token 数(TPS):token 的生成速率。
  • 每个输出 token 的耗时(TPOT):生成每个输出 token 所花费的时间。
  • 总延迟:完成一次响应所需的全部时间。

此外,你还需要追踪成本。成本相关的指标包括查询数量以及输入和输出 token 的总量。如果使用的是有速率限制的 API,追踪每秒请求数也很重要,这样才能确保不超过配额上限,避免服务中断。

计算指标时,可以在抽查和全量检查之间灵活选择。抽查通过采样部分数据来快速发现问题,全量检查则评估每一个请求,从而获得全面的性能视图。具体选哪种,取决于系统的需求和可用资源;将两者结合使用,往往能形成更均衡的监控策略。

在计算指标时,要确保指标能够按相关维度进行拆分,例如用户、版本、prompt/chain 版本、prompt/chain 类型以及时间等。这种细致的拆解有助于理解性能波动,并定位具体问题。

日志

由于这篇文章已经写得很长,而且我在《Designing Machine Learning Systems》一书中已经详细讨论过日志相关内容,这里就简短带过。日志的核心理念很简单:记录一切。系统配置要记录,查询、输出以及中间结果要记录,组件何时启动、何时结束、何时崩溃等都要记录。每一条日志都应当打上标签和 ID,以便后续追溯它在系统中产生于哪个环节。

记录一切意味着日志量会迅速膨胀。目前许多自动化日志分析和日志异常检测工具背后都由 AI 驱动。

虽然手动处理所有日志并不现实,但每天抽时间人工审视生产数据仍然很有价值,这有助于你直观感受用户究竟是如何使用你的应用的。Shankar 等人(2024)发现,随着接触的数据越来越多,开发人员对“好输出”和“坏输出”的判断标准也会发生变化——他们既能借此改写 prompt 以提高获得理想回复的概率,也能相应地更新评估流程,捕捉那些不符合要求的回答。

链路追踪

Trace(追踪)是指对一个请求在各个系统组件和服务中的完整执行路径进行详细记录。在 AI 应用中,追踪可以完整呈现从用户发出查询到最终响应返回的全过程,包括系统执行的操作、检索到的文档以及最终发送给模型的 prompt。它还应展示每个步骤所花费的时间及其对应的成本(如果可测量的话)。例如,下面是 Langsmith 的一次追踪可视化。

Overview of a genai platform


理想情况下,你应该能够逐步追踪每个查询在系统中的转换过程。如果某个查询失败了,你应该能够精确定位出问题的具体步骤:是查询处理出错、检索到的上下文不相关,还是模型生成了错误的响应。

AI 流水线编排

一个 AI 应用可能相当复杂,会涉及多个模型、从多个数据库检索数据,并能调用各种各样的工具。编排器(orchestrator)可以帮你指定这些不同组件如何组合(串联)起来,形成端到端的应用流程。

从宏观上看,编排器的工作分为两步:组件定义和串联(即流水线化)。

  1. 组件定义
    你需要告诉编排器你的系统使用了哪些组件,例如模型(包括用于生成、路由和打分的模型)、可供系统检索数据的数据库,以及系统可以执行的动作。与模型网关的直接集成有助于简化模型接入流程,有些编排器本身就希望充当网关的角色。许多编排器还支持与评估和监控工具集成。

  2. 串联(或流水线化)
    你需要告诉编排器系统从接收用户查询到完成任务所执行的步骤顺序。简而言之,串联就是函数组合。下面是一个流水线的示例。

    1. 处理原始查询。
    2. 基于处理后的查询检索相关数据。
    3. 将原始查询与检索到的数据组合,生成符合模型输入格式的 prompt。
    4. 模型根据提示词生成回复。
    5. 评估该回复。
    6. 如果回复质量达标,就返回给用户;如果不达标,则把请求转给人工处理。

    编排器负责在各个步骤之间传递数据,并能提供一些工具,确保当前步骤的输出符合下一步所要求的格式。

在为延迟要求严苛的应用设计流水线时,应尽量并行处理。例如,路由组件(决定把请求发往何处)和 PII 脱敏组件完全可以同时执行。

市面上有很多 AI 编排工具,包括 LangChainLlamaIndexFlowiseLangflowHaystack。每个工具都有自己的 API,这里就不展示具体代码了。

虽然项目初期就迫不及待地引入编排工具很常见,但还是建议先不用编排工具来构建应用。任何外部工具都会带来额外的复杂度。编排器会屏蔽掉系统运行的关键细节,导致理解和调试系统变得困难。

随着应用开发进入后期阶段,你可能会决定引入编排器来减轻负担。在评估编排器时,可以从以下三个方面入手。

  1. 集成性与可扩展性
    评估编排器是否支持你当前在用或未来可能采用的组件。例如,如果想用 Llama 模型,就要看编排器是否支持它。考虑到模型、数据库、框架的数量繁多,编排器不可能面面俱到。因此,还需要关注编排器的可扩展性——当某个组件不被原生支持时,改造成本有多高?
  2. 对复杂流水线的支持
  3. 易用性、性能与可扩展性
    考察编排器的用户友好程度。直观的 API、完善的文档以及活跃的社区支持都能显著降低你和团队的学习成本,因此应优先关注这些方面。避免使用那些会在背后发起隐式 API 调用或给应用引入额外延迟的编排器。同时,还要确保编排器能够在应用数量、开发人员规模和流量持续增长时依然有效扩展。

结语

这篇文章从一个基础架构出发,逐步引入新组件以应对不断增长的应用复杂度。每一次扩展都带来新的收益和挑战,需要仔细权衡和落地。

组件的拆分对保持系统的模块化和可维护性至关重要,但这种拆分并非一成不变,各组件之间存在大量重叠。例如,模型网关可以与 guardrails 共享功能;缓存机制也可能在向量检索和推理服务等多个组件中分别实现。

这篇文章的篇幅已经远超我的预期,但仍有大量细节未能深入展开,尤其是可观测性、上下文构建、复杂逻辑、缓存以及 guardrails 等方面。我会在即将出版的新书 AI Engineering 中逐一深入探讨这些组件。

本文同样没有讨论模型服务的部署问题,前提假设是大多数读者会使用第三方 API 提供的模型。《AI Engineering》中也会专门有一章介绍推理与模型优化。

参考资料与致谢

特别感谢 Luke MetzAlex LiChetan TekurKittipat "Bot" KampaHien LuuDenys Linkov 对本文早期版本提出的反馈。他们的见解极大地提升了文章质量,文中的任何错误均由本人负责。

我阅读了大量企业分享的生成式 AI 落地案例,其中一些我尤为推荐。

原始来源: Chip Huyen

评论 (0)