第6章 工程实践——真实场景大不同

第6章 工程实践——真实场景大不同

  通过前面章节的学习,相信读者应该已经具备了一定NLP算法应用开发能力,虽然需要借助大语言模型,但这也是一种能力,毕竟用户并不关心产品后面用了什么技术。一款产品或应用开发完成后,接下来就是要面向市场和客户了,这中间有非常多的工作要做,即便我们只是为整套产品或服务提供一个接口,那也有许多要考虑的因素。

  在之前的“相似匹配”和“句词分类”章节,我们在应用部分提及到一些开发应用要注意的事项,不过它们大多和具体的应用相关。本章我们将从整体角度介绍利用大语言模型接口开发一款要上线面向市场的应用或服务时应该考虑的内容。我们重点关注四个方面,首先是评测,它是能否上线的标准;然后是安全,它是上线不得不考虑的话题;最后是网络请求,涉及一些网络请求方面的设计和技巧。

6.1 评测:决定是否上线的标准

6.1.1 为什么评测

  在我们之前的章节中,除了“句词分类”中的微调部分外,几乎没有涉及这个话题,但其实这非常重要。首先,我们来看为什么需要评测。在工程开发中,我们都知道有一个测试工程师的职位,主要的职责就是对产品的各个功能进行各种各样的测试,以保证其功能正常、符合预期。我们在服务上线前往往也会对自己的接口进行压测,看能否达到上线标准。

  对一个算法模型来说,这一步是类似的,只不过它要评测的是模型输出的内容是否符合我们的预期目标。理论上来说,我们永远都能找到模型预测错误的负例,所以实际上线后的结果也不可能百分之百正确。这是算法模型与其他功能开发不太一样的地方,因为它本质上是一个根据已有数据学习到一种”策略“然后去预测新数据的过程。

  测试往往需要一批数据集,这批数据集应该尽量和真实场景接近,但一定不能包含在训练数据中。评测一个模型其实就是评测它在”未见过“样例上的效果。实际场景中,我们往往会在整个训练数据集中先切出去一块作为测试集,它的分布与训练集是一致的。测试集的数量一般为整个数据集的20%,但这个不是必须的,可以视具体情况增加或减少测试集。对一个语言模型来说,比较重要的一点是,构造模型输入和长度截断的方法应和训练集一致。通过模型得到输出后,我们需要将输出和标准答案进行对比,然后统计相关数据,最终得到评价指标。

6.1.2 NLU常用评测指标

  不同的任务往往会采用不同的评测指标,对于NLU任务来说,一般我们会使用精准率(precision,P)、召回率(recall,R)、\(F_1\)等指标衡量,它们一般可以通过表6-1所示的混淆矩阵计算得到。

表6-1 评测用混淆矩阵

真实情况 预测结果正例 预测结果负例
正例 真正例(true positive,TP) 假负例(false negative,FN)
负例 假正例(false positive,FP) 真负例(true negative,TN)

相关指标计算如式(6.1)、式(6.2)和式(6.3)所示。

\[P = \frac{\text{TP}}{\text{TP} + \text{FP}} \qquad (6.1) \\ R = \frac{\text{TP}}{\text{TP} + \text{FN}} \qquad (6.2) \\ F_1 = \frac {2\text{PR}}{\text{P}+\text{R}} \qquad (6.3) \]

  精准率和召回率在通常情况下是一种权衡关系,提高精准率会降低召回率,反过来也一样,\(F_1\)值则综合考虑了两者。举个例子,假设我们有一个垃圾邮件分类器,测试集数量为100,其中垃圾邮件(此处为正例)为20封,其余80封为正常邮件。模型预测结果为30封正例,但其实只有15封是真正的正例。也就是说,30封垃圾邮件中有15封被正确识别,另外15封其实是正常邮件,被误识别为垃圾邮件;同时,模型预测结果为70封的负例中,有5封其实是垃圾邮件,未被模型正确识别。此时,混淆矩阵如表6-2所示。

表6-2 示例混淆矩阵

真实情况 预测结果正例=30 预测结果负例=70
正例=20 TP=15 FN=5
负例=80 FP=15 TN=65

对应的指标如式(6.4)、式(6.5)和式(6.6)所示。

\[P = \frac{15}{15+15} = 0.5 \qquad (6.4) \\ R = \frac{15}{15+5} = 0.75 \qquad (6.5) \\ F_1 = \frac{2 \times 0.5 \times 0.75}{0.5+0.75} = 0.6 \qquad (6.6) \]

  在这个例子中,应该20封垃圾邮件,我们找到了15封,召回率就是15/20,但是我们一共预测出了30封,精度就只有15/30。这个场景下,没有识别出来垃圾邮件是可接受的,但如果把用户的正常邮件识别为垃圾邮件,那用户肯定有意见。所以,这就要保证精度很高,换句话说,只要是被标记为垃圾邮件的就一定是垃圾邮件(虽然可能会漏掉一些垃圾邮件)。这时候要求的概率阈值就比较高,比如95%,只有大于该阈值我们才判断为垃圾邮件。这也就意味着,本来模型判断为垃圾邮件的,有可能因为概率没有达到95%而被标记为正常邮件。此时,精度非常高,但同时我们会漏掉很多垃圾邮件,导致召回率下降。反过来,如果召回率上升,意味着更多正常邮件可能被识别为垃圾邮件,精准率下降。我们给出调整后的混淆矩阵示例,如表6-3所示,读者不妨自己重新计算指标。

表6-3 调整后的混淆矩阵

真实情况 预测结果正例=10 预测结果负例=70
正例=20 TP=10 FN=10
负例=80 FP=0 TN=80

  当然,如果我们的模型能够让这些被识别为负例的正例的概率提高到阈值之上,换句话说这时候的模型效果更好了(更加肯定垃圾邮件是垃圾邮件),那么精准率和召回率就会同时提升,\(F_1\)也会跟着提升。如果能达到这种理想情况那肯定是最好的,但现实往往需要权衡,毕竟有些样本实在是太难辨认了。

  如果是多分类,则需要分别计算每一个类别的指标,然后综合,有两种综合方法。

  • macro方法:先计算每个类别的精准率和召回率,取平均后,再计算\(F_1\)值。
  • micro方法:先计算混淆矩阵元素的平均,再计算精准率、召回率和\(F_1\)值。

  各个类别的重要性相对平衡时,可以使用macro方法;更关心总体性能而不是每个类别的性能时,可以使用micro方法。以类别样本不均衡来举例说明,如果你想要平等地看待每个样本,无论类别均衡不均衡,所有样本都一视同仁,可以选择micro方法;但是如果你觉得类别是平等的,样本多的和少的类别应该平等地看待,那可以选择macro方法。

6.1.3 NLG常用评测指标

  在NLU中,无论是句子分类、Token分类还是其他匹配问题,一般都有一个标准答案。但NLG任务不太一样,因为是生成式的,所以很难保证输出的内容和标准答案一模一样,更不用说,很多任务根本也没有标准答案。生成式摘要、翻译等任务往往都有参考答案,我们至少还有个比对的标准;但像生成式写作、自由问答、对话这种,很多时候则需要针对性的设计评测指标。下面我们针对有参考答案和没有参考答案的任务分别举个和大语言模型相关的例子来进行说明。

  有参考答案的任务,我们以生成式文本摘要为例,它要求模型在给定一段文本后,能用几句话概述这段文本的主要内容和思想,具体可以参考“文本生成”一章。因为这种任务有参考答案,所以我们经常采用相似度来衡量,即计算生成的摘要与参考答案之间的相似度。 这个做法和我们之前在”相似匹配“一章的做法一样。或者,我们也可以单独训练一个二分类模型,判断生成的内容和参考答案是否相似。更进一步,我们可以细化到Token粒度进行评估,从语义角度评估,常用的方法是BERTScore;从字面量角度评估,常用的方法是BLEU和ROUGE。

  BERTScore借助BERT这样的预训练模型计算Token的Embedding,然后计算生成的内容和参考答案Token之间的相似度,并进一步根据式(6.7)和式(6.8)计算精准率和召回率。

\[P = \frac{1}{|{\hat x}|} \sum_{\hat x_j} \max_{x_i \in x} \text{SimArray} \qquad (6.7) \\ R = \frac{1}{|x|} \sum_{x_i} \max_{\hat x_j \in \hat x} \text{SimArray} \qquad (6.8) \]

其中,\(x\)是参考答案的Token,\(\hat{x}\)是生成内容的Token。得到精准率和召回率,就可以进一步根据式(6.3)计算得到\(F_1\)值。我们举个具体的例子,如下所示。

ref = "我爱伟大祖国"
hyp = "我爱祖国"

# emd_r shape => (1, 6, 768)
# emd_h shape => (1, 4, 768)
sim = emd_r @ emd_h.transpose(1, 2)
# sim shape => (1, 4, 6)
sim = array([
    [[0.9830, 0.5148, 0.5112, 0.5310, 0.4482, 0.4427],
     [0.4854, 0.9666, 0.9402, 0.5899, 0.8704, 0.3159],
     [0.4613, 0.8755, 0.9184, 0.5819, 0.9397, 0.3576],
     [0.4456, 0.3135, 0.3572, 0.5036, 0.3696, 0.9722]
    ]
])

  上面我们给定参考答案为ref,生成的内容为hyp,通过BERT等预训练模型首先得到每个Token的向量表示(其中,1为批大小、768为向量维度、6和4为Token数),然后通过矩阵乘法得到相似数组为sim。接下来我们根据式(6.7)和式(6.8)分别计算精准率和召回率,如下所示。

p = 1/4 * sim.max(axis=2).sum()
r = 1/6 * sim.max(axis=1).sum()

  根据得到的精准率和召回率可以进一步计算\(F_1\)值。可以看出,精准率是\(\hat x\)中的每个Token匹配到\(x\)中的一个Token,即匹配到0.9830、0.9666、0.9387和0.9722四个值,对应的位置在\(x\)中的Token是“我”、“爱”、“祖”、“国”。而召回率是\(x\)中的每个Token匹配到\(\hat x\)中的一个Token,“伟大”两个字没有匹配到,但“伟”字分数还可以(0.9402),“大”的分数就只有0.5899了。总的来说,这种方法是一种从Token的语义角度来对比生成内容和参考答案的评测方法。

  和BERTScore不同的是,BLEU和ROUGE是按照字面量是否完全相同这样去比较的,这里的字面量一般会选择N-Gram,N一般都会选择多个同时使用(比如1、2、3、4)。它们的算法有不少细节,但是如果从简单直观的角度理解,前者衡量有多少个生成内容的Gram出现在参考答案中,后者衡量多少个参考答案的Gram出现在生成内容中。前者类似精准率,后者则类似召回率,他们也可以使用式(6.3)计算得到一个\(F_1\)值。不过单独使用也没问题,这一般取决于具体的任务,比如文本摘要就常用ROUGE分数,因为相对而言,我们更加关注生成的摘要有没有概括到给定文本,即有没有覆盖参考答案。而翻译任务则常用BLEU分数,因为我们更加关注翻译出来的内容是不是正确。

  以上这些评测方法都有很多现成的工具包,只要安装后即可方便使用,读者实际使用时可自行通过搜索引擎搜索。

  没有参考答案的任务,我们以文案生成为例,这是非常适合大语言模型的一个任务。具体来说,就是给定大量商品和用户属性,让模型针对该用户就给定商品生成一段有说服力的销售文案或推荐话术。这一类任务一般都需要人工进行评估,当然,我们也可以让更好的大语言模型充当人工角色,我们要做的是设计评价指标和标准。这个标准一般视需求而定,但是一般会重点考虑以下因素。

  • 准确性。生成的内容是否包含关键必要信息(比如商品名称、价格等),这些信息是否有误。
  • 流畅性。生成的内容读起来是否通顺、合理、有逻辑。这里的流畅不仅指字面上的流畅,还包括语义上的流畅。
  • 生动性。生成的内容是否有吸引力,能够让用户产生购买欲。这个要求有点高,不过也并非不可能达到。

  我们可以针对这每一项进行打分,可以采用多人打分取平均;也可以用来对多个模型或服务商进行评测,选择最合适的。需要再次强调的是,指标的设计务必要结合实际场景,综合权衡成本和效果,不必追求非常全面。

6.2 安全:必须认真对待的话题

  安全是指模型生成的内容不应该包含任何偏见、敏感、风险等内容。这是产品的生命线,安全问题不过关,产品必然会被下线。所以,作为服务提供方,如果我们要使用生成式语言模型,当它在效果上被验证可以达到要求后,接下来要重点关注的就是内容安全。

6.2.1 前后处理

  前处理和后处理是我们能想到的最直观的解决方案。前处理是指将用户的输入传递给模型前先过一遍风险检查,这里可以是一个模块或外部接口(国内很多厂商都提供了类似接口)。如果用户的输入是有风险的,就直接返回预设好的回复,不再传给模型接口生成回复。后处理是指对模型生成的内容进行风险检查,如果检测到风险内容,则将该内容屏蔽,或直接返回预设好的回复。

  前后处理我们可以只做一侧,也可以两侧同时做,这中间的区别就是多一次接口调用。多一次接口调用意味着响应时间变得更长,同时这方面的费用也会增加。另外需要注意的是,如果是流式输出,由于Token是一个一个吐出来的,可能需要在一句话结束时就对其进行风险检查。

  关于风险检查模块或接口,可能只是简单的一个布尔值,表示是否有风险,也可能会收到更加细致的信息,提示调用方是哪个方面的风险,以及置信度多高。我们可以根据实际需要选择合适的接口,或自主研发相应的模块。

6.2.2 提示词

  由于大语言模型本身具备极强的理解能力,所以我们可以在每次输入的提示词中描述我们对输出内容的要求。对于包含上下文的场景(比如之前介绍过的文档问答),可以限制其必须基于给定上下文内容进行回复。另外,也可以在提示词中限制输出长度(注意,是提示词中而不是接口参数),虽然有时候不太管用,但对于一个理解能力很好的语言模型来说还是有效果的。而且,限制长度还能节省费用。

  通过提示词控制生成内容操作比较简单,一般情况下都能达到要求,但依然有几种情况需要特别注意。

  • 给定的上下文本身就是风险内容。此时,给予上下文进行的回复自然也有可能是风险内容。
  • 模型本身的知识是不完备的,它并不一定能理解所有的风险,尤其是每个不同用户期望和认识的“风险”。此时,它认为自己的输出没问题,但在我们的场景下可能不合适。
  • 不排除有人恶意引导模型输出风险内容。这就如同网络骇客一样,大模型骇客也会利用模型缺陷对其进行攻击。

  最后要强调的是,即使我们不考虑上面这几种情况,也在提示词中做了各种控制,模型依然有可能会输出不期望的回复,尤其是类似temperature这种控制生成内容多样性的参数设置的不太保守时。因此,根据实际需要有可能需要和其他方法结合起来一起使用。

6.2.3 可控生成

  在NLP的细分领域,有一个领域叫可控文本生成(controllable text generation,CTG),主要研究如何控制模型的输出让其更加可控,即输出我们想要的,不输出我们不想要的。可控文本生成的方法很多,由于基本都涉及到模型训练或微调,因此我们不过多深入介绍,感兴趣的读者可以查找并阅读相关文献。简单来看,可以将可控文本生成分成三类。

  • 使用控制Token。一般会在文本开头增加一个控制生成属性的Token,这里的属性可以是情感倾向、主题、风格等等。基于提示词的控制生成也可以算作这一类型,尤其是当模型经过带控制的提示词微调训练后。
  • 使用控制模型。主要体现在生成过程中,使用一个或多个属性分类器对生成过程进行引导。一般做法是保持语言模型的参数不变,更新分类器的参数使其能够判别不同属性,或者区分想要的内容和不想要的内容。生成时使用分类器作为条件,影响语言模型输出时Token的概率分布。
  • 使用反馈控制。典型的代表就是我们第一章“基础科普”中介绍过的RLHF,比如InstructGPT将有帮助、真实性和无害性作为反馈标准影响模型。此外,类似自训练、自学习等方法也可以看作从反馈中学习,这里的反馈可能来自人类,也可能来自模型。这类方法从根本上改变了模型,是彻底的控制。

  对于安全问题,虽然本节内容已经给出了不少防范措施,但我们仍然不建议将大模型直接面向用户。我们实在难以保证其中不出差错,而一旦出差错带来的风险是巨大的。同时,我们也建议在设计上考虑以下一些辅助方案。

  • 建议增加消息撤回机制。即使有发送一些风险内容,但只要能及时撤回,在一定程度上也能将风险降到最低。
  • 建议对用户账号进行严格管控,如果有用户触发风险内容,应及时予以关注。对刻意发送或诱导模型输出风险内容的账号,应马上对其进行限制。
  • 留存所有的对话和消息记录,以备事后查验。

  考虑到服务商可能会提供一些关于安全检查的最佳实践,也建议读者在开发前仔细阅读,尽量遵循文档进行处理。

6.3 网络:接口调用并不总是成功

  由于本书涉及到的大模型都是通过接口使用的,这就避免不了网络请求。本节我们主要介绍和网络请求相关的实践。

6.3.1 失败

  网络请求失败是常见的情况,只要我们的服务代码中有需要通过网络请求第三方接口的,都应该关注这个问题。对于网络请求失败的情况,常用的解决方法是重试。是的,当本次接口调用失败或超时时,服务端应再次发起请求。不过重试并不是简单地只要失败就重新发起请求,这里有非常多的细节需要考虑。

  • 哪些情况需要重试?当我们在调用接口时,并不是对所有的失败均进行重试,比如访问的令牌到期,服务端返回未鉴权错误,此时应该去调用鉴权接口重新获取令牌,而不是一直重试。一般来说,我们可以对因网络超时、服务端错误等导致的失败请求进行重试。
  • 多久重试一次?连续的重试肯定是不合理的,这可能会导致服务端响应更慢。一般我们会随时间增加而相应地增加重试间隔时间,比较常用的是指数级增加重试间隔时间,比如第一次间隔2秒,第二次4秒,第三次8秒,以此类推。
  • 重试多少次停止?可以肯定的是,我们不可能一直重试下去,当一个接口重试几次后依然失败,那可能是服务或网络已经发生了故障。这种情况下,试多少次都是没用的。这个可以根据实际场景选择相应配置,一般最多重试3次即可。

  如果经过以上策略重试后依然失败,此时我们一般会抛出异常,有可能同时向客户端返回预先指定的结果,或者回调某个专门处理接口调用失败的函数。这些一般都需要根据实际需要进行设计。

  重试策略看起来还不错,不过让我们考虑一种特殊情况。假设第三方接口服务真的挂了或网络真的发生了故障,此时,客户端一直在发请求,而服务端则在重试策略下不停重试,结果自然是每个请求都达到重试上限并最终抛出异常。

  对于这种情况,我们一般会加入熔断机制,当失败次数达到某个阈值时,将此服务进行熔断,直接返回预设好的响应或者干脆拒绝请求。这样,后面的请求就不会再调用第三方接口了。实际中,我们往往会返回一个简化版本的处理,比如模型服务挂了,则返回规则结果。这也被称为服务降级。

  熔断也经常主动用在高峰限流场景下,当某个时刻请求突然暴增导致资源不够用时,可以有规划地对一些不重要的服务进行熔断。比如做秒杀活动时,为了保证短时间涌入的大量请求能够得到响应,可以对商品查询、筛选等功能返回缓存或上一次查询的结果。

  熔断机制尤其适合一个接口包含多个请求源,最终返回整合结果的情况。此时,熔断有问题的请求源,可以保证整个接口依然是可用的。熔断一段时间后,可以尝试自动恢复,先放行一定数量请求,如果响应成功则关闭熔断器,否则继续对有问题的请求源保持熔断状态。

  最后,我们强烈建议对服务增加可视化的配置和监控,同时启用告警功能,当任一服务出现问题时,能够及时进行干预 ,保证服务稳定可用。

6.3.2 延迟

  延迟是指接口没有在给定时间内给出响应,但又不会超时失败的情况。延迟比失败更加常见,它不光和当时的网络状况有关,也和网络配置(比如带宽、是否有专用网络等)有关,而且还和接口功能的复杂度、请求和返回的数据量等非网络因素有关。对于大语言模型接口的延迟,我们给出以下实践建议。

  首先,建议针对不同的需求选择不同规模的模型。以OpenAI为例,它提供了多个不同版本的模型,读者可以根据任务难度选择适当的模型。越复杂的模型,一般响应速度也越慢,耗时较长,而且价格还贵。总之,还是我们一直强调的观点,工具只是手段,不是目的,能用简单、低成本方案解决的就不要用复杂的。

  其次,一般大语言模型接口都会提供“停止序列”参数,读者可以关注并配置该参数,以便及时结束模型的输出。同时,我们在本章“安全”一节也提到过,可以在提示词中限制输出的Token数,让答案尽量简短。模型输出的内容越少,响应时间就越快,延迟越低。当然,接口的请求体(request body)也不应该过大,太长的上下文不仅会增加传输时间,而且可能也会增加费用(以OpenAI为例,提示词也是要计费的)。这可能需要一些语义匹配或信息压缩提炼技术,每次只传输最相关的上下文,“相似匹配”和“句词分类”这两章内容均对此有所涉及,此处不再展开讨论。

  第三,针对部分场景应用,可以使用流式输出。流式输出体验比较好,用户很少会感到延迟,因为模型接口一旦接收到请求就开始响应。目前比较常用的服务端方案包括SSE(server-sent events)和WebSocket。SSE是一种基于HTTP的单向通信技术,允许服务器向客户端发送持续的事件流。WebSocket是双工通信技术,允许客户端和服务器建立双向通信通道。SSE更加适合服务端向客户端持续发送数据的情况,而WebSocket则更加适合客户端和服务器实时交互(如对话)的情况。

  第四,考虑使用缓存。这在某些场景下是可以的,比如问答类应用。事实上,只要每次交互的上下文不是动态变化的,都可以考虑使用缓存。

  以上是关于降低大语言模型接口延迟的通用建议,在实际场景中如果遇到延迟问题,建议读者仔细分析服务代码,找到关键瓶颈进行解决。尤其是当接口中包含比较多的网络请求时,任何一个请求都可能成为瓶颈,比如当历史文档非常庞大时,使用向量库或数据库查询相关上下文也会比较耗时。

6.3.3 扩展

  本书迄今为止一直未提到高并发的场景,也默认用户请求不太多,单个账号就可以提供服务,这在现实中是存在的,尤其是新产品或服务还没有很多用户的时候。但一款成功的商业应用用户一定不会少,并发也比较高。此时,对接口服务进行扩展就是我们重点要考虑的。需要说明的是,我们这里主要指横向扩展。

  当我们决定要对服务进行扩展时,首先要做的是了解基本情况和需求,包括日均调用次数、日调用峰值、平均并发数、最大并发数、期望平均响应时长、是否可以使用缓存等。除此之外,还需要了解大模型服务商的相关政策,比如OpenAI对接口调用就有限制,不同模型、不同类型账号限制都不一样。值得说明的是,服务提供商一般都会提供关于扩展的最佳实践,这也是重点要掌握的信息。这些对于我们接下来的方案至关重要,我们期望能够以最低的成本满足业务或用户需要。

  了解完基本情况,我们就可以对需要的资源和成本进行大致估算。资源主要就是账号,建议最好有20%以上的冗余。成本估算可以通过提示词和响应的平均长度,以及日均调用次数进行简单测算。

  对于账号资源,我们一般需要构建一个资源池进行统一管理,当需要调用时,先从资源池中选择一个可用的账号完成本次调用服务。如果需要的账号比较多,或者预估未来调用会增长时,建议一开始就把资源池模块写好,方便日后自由扩展。

  资源池模块应该具备基本的添加、删除、修改功能,建议构建适合企业内部的自定义账号体系,将真实的账号绑定在自定义账号上。这样做的好处是便于管理,而且还方便支持多个不同的大模型服务商。资源池模块还有一项基本功能是报表统计功能,包括不同维度统计的调用方、调用次数、失败率、费用等内容。

  当然,对资源池来说,最重要的功能还是资源调度,简单来说就是如何为每一次调用分配账号。我们应该能够支持多种不同的策略进行调度,比如简单的随机选择账号、按失败率从低到高、按不同功能使用不同服务商等。需要特别注意的是,不要把任何业务相关的东西参杂进来,资源池只负责管理资源。

  大语言模型(或其他模型服务)接口,往往还支持批量(batch)模式,也就是一次发送多条请求,同时获取这些请求的响应。在并发比较高、响应时间要求又不那么高的情况下非常适合。如果是流式输出,响应时间这一项可以忽略,此时建议使用批量模式。

  使用批量模式,需要在用户请求和请求大模型服务商接口之间做一层处理,合并用户请求,批量一次向大模型服务商发起请求,收到反馈后分发到对应的用户请求响应上。具体地,我们可以维护一个队列和最小请求间隔时间,在最小请求间隔时间内,固定窗口大小的请求同时出队进行批量请求。理论上批量大小是不限制的(也意味着我们可以不固定窗口大小),但建议最好选择一个合适的最大窗口值,既能达到最大的吞吐量,又不至于有太高的延迟。注意,这里是“最大值”,因为当并发很低时,队列内不一定有最大窗口值数量的请求。

  如果可以的话,每个窗口的大小最好是2的指数(即1、2、4、8、16、32等),因为服务器一般都是GPU,这样的处理能提升一些效率。当然,大部分服务商应该已经在服务内部做了类似的优化,批大小即使不是2的指数,性能差别应该也几乎可以忽略。

  刚刚我们提到了使用队列,事实上,我们强烈建议将服务端写成队列模式,这样做的好处是,当资源不足时,服务依然是可用的,除非资源和请求数量严重不均衡,这就意味着我们不需要按照并发的峰值去申请资源。如果对服务的响应时长要求没那么高,用户能接受偶尔出现的一定时间的等待,这种方式可以极大地节约资源。

6.4 本章小结

  开发一个演示用的小应用和真正可商用的服务落地之间有非常多的事情要处理。本章我们从评测开始介绍,了解到一个模型服务先得“能用”然后才能讨论上线,我们介绍了针对不同任务的评测方法。接着我们强调了安全对一个大模型相关应用或服务的重要性,并给出了一些处理方案。最后,针对大模型服务接口调用,我们给出了如何更好地构建一个稳定、可靠服务的建议。本章内容都针对真实场景下的应用和服务构建,我们给出了一些工程实践经验,但读者应该清楚,实际中遇到的问题可能远比我们提到的更多。而且,我们也未涉及超大规模分布式处理,这是有意为之的。如果你或你所在的企业已经达到了这种程度,那时一定会有精通算法工程和架构的同事专门负责。总之,真实场景下的工程实践需要考虑更多因素,需要更加仔细地设计,处处充满权衡的艺术。

posted @ 2024-04-21 11:23  3cH0_Nu1L  阅读(67)  评论(0编辑  收藏  举报