使用 JavaScript 和 Hugging Face 推理 API 编排小型语言模型 (SLM)

社区文章 发布于 2024 年 6 月 4 日

英文版

大家好!

我将展示如何使用 Hugging Face 推理 API、一个 Space、Docker 和不到 500 行 JavaScript 代码来编排多个小型 LLM,让它们在一个小型交互式神经网络模拟器中回答我关于尝试的问题。

您可以在这篇 IATalking 博客文章中查看结果。移动滑块并尝试减小误差;您将看到一个 AI 根据您的尝试(和尝试历史)生成评论。这些评论来自 Phi3、Llama、Mystral 等模型,它们都很小,只有几十亿个参数。

每当发送新的请求时,我将在此处展示的代码会选择其中一个模型。如果一个模型开始出现故障,那么下次发出请求时,它被选择的几率就会降低。因此,我可以通过使用不同的模型和创建响应变体来实现 LLM 的一种“高可用性”,此外还可以调整单个 LLM 的温度,使其在每次请求时都更具创意。

本文将详细介绍我是如何做到这一点的,并解释所涉及的文件。

Space

您可以在 Space 中找到此模型的代码:Jay Trainer。API 响应链接为:https://iatalking-jaytrainer.hf.space/error?error=123

我将其命名为 Jay Trainer,以纪念 Jay Alammar (https://jalammar.github.io/),他创作了原始文章并提出了构建一个简单神经网络模拟器以促进学习的想法。他的文章非常棒:神经网络基础视觉交互指南

查看文件,您会注意到以下内容

  • 一个 Dockerfile
  • docker-compose.yml
  • server.js
  • README.md

image/png

这是一个 Docker Space,我选择 Docker 是因为它简单。Hugging Face 允许您使用 Docker 部署 API,这非常棒,因为它带来了无限的可能性:您可以用您喜欢的语言部署 API!

如果您查看 Dockerfile,您会看到一个标准的 Node.js 服务器:image/png

docker-compose.yml 只是为了方便我的本地测试。在推送到 Hugging Face 之前,我只需使用 `docker compose up` 就可以启动相同的代码。这省去了我每次输入“docker -p etc. etc.”的麻烦。

README.md 只包含 Hugging Face 所需的内容,而真正的主角是 `server.js`。所有的逻辑和端点代码都在这里!

可用 API

该 API 在 server.js 中实现,是一个 Express 应用程序。创建了以下端点:

  • /error
    这是最常用的功能:您传入错误号和尝试历史记录,它会返回一个文本!
  • /
    这是主端点。它只会显示一条简单的消息,表明服务正在运行!
  • /models
    这是一个调试端点。通过它,我可以查看所涉及的 LLM 的历史和执行统计信息。这让我能够知道哪个模型生成了更多错误,哪个模型执行了更多次,以及一些其他仅用于跟踪和调试的信息!
  • /test
    这个端点只是用来测试 Express 是否正常工作并响应。

服务器初始化

服务器启动时会进行一些配置,其中最主要的是定义一个包含要使用的模型列表的对象

image/png

全局常量 MODELS 将包含我的模型!每个目标都是一个将要使用的 LLM 的“id”。这里的 id 是我为这段代码定义的内部标识符,而不是 Hugging Face 的 ID。它只是一种“别名”以方便识别。

因此,对于它们每一个,我都需要定义:模型名称和提示模板。名称是 Hugging Face 中独一无二的名称,格式为 ORG/MODEL。这是您在模型卡片中找到的名称。

image/png

`prompt` 是一个我需要为此模型生成提示时调用的方法。每个模型可能都有不同的提示格式,所以我不能对所有模型都使用相同的提示。通过这个函数,我可以通过相同的调用创建一个动态生成提示的机制:我只需要调用 `prompt()` 方法并传入我想要的文本。

我从未研究过 Hugging Face Transformers 的代码,但它很可能遵循与 apply_chat_template 方法相同的理念。在这里,这只是一个非常简单的过程版本!

因此,只需这两个成员,我就可以轻松地添加或删除 LLM!我选择硬编码是为了控制和简化事情。但我可以将其外部化到一个文件或 API 中,以便在运行时包含或删除 LLM。由于这只是一个简单的 PoC,我暂时选择保持简单。

最后,重要的是要记住,代码还对环境变量 HF_TOKEN 进行了验证,这是 Hugging Face 的令牌。此令牌在 Space 中配置为 secret。在本地测试中,我生成一个测试令牌,并在我的 docker compose up 中使用它,在我的 shell 中创建一个环境变量。这又是 docker compose 为我带来的另一个便利!

模型初始化

在服务器启动时,对于 MODELS 中的每个模型,我将生成第三个属性:stats。在这里,我们将拥有 LLM 的一些执行指标,例如执行总数、错误总数以及它生成错误的百分比。

image/png

属性描述如下:

  • 总计
    执行总数。
  • 错误
    一个表示低质量总数的值。
  • 份额
    错误份额。这将根据错误值计算。
  • 错误率
    错误百分比:错误/总数。
  • 正常率
    正常百分比(错误率的相反):1 - 错误率。

然后,在这一部分,我将所有 LLM 组装成一个数组,存储在变量 ModelList 中。

代码的其余部分专门用于 Express 端点和逻辑函数,这些将在下面详细说明。

端点 /error

这是主要端点。上面的页面调用此端点。它期望以下参数:

  • error
    用户当前尝试的错误值。

  • 尝试次数
    尝试历史。它们是逗号分隔的错误数字。

image/png

代码首先对参数值进行一些验证。基本上,我确保 `error` 是一个数字,并且 `tentativas` 是一个逗号分隔的数字列表。任何不符合这些条件的情况,我都会返回给用户。这样做的目的是避免提示注入,因为(您很快就会看到)我将直接连接这些值。

然后,它将调用 `Prompt()` 函数(我们稍后会讲到),该函数将构建提示并使用 Hugging Face 推理 API 请求 LLM。

该函数将返回 LLM 的响应。这里将进行一些验证。

image/png

在这些验证中,最重要的是只提取 LLM 文本直到“结束”标记。您会看到我要求 LLM 始终以我称之为 |fim| 的标记结束文本。这是为了让它返回一个点,表示它已经生成了正确的文本。从那以后,无法保证。因此,我只会获取 |fim| 之前的结果。

我进行的另一个验证是字符数量。在这里,我假设每字最多 8 个字符,由于我将默认值设置为 20 个字,我简单地计算 20*8。

稍后您将了解原因,但请注意,有些地方我们正在更改响应模型的错误变量。这是我创建的一种机制,用于“惩罚”那些偏离我预期质量标准的响应。

这个验证有很多漏洞,可以做得更好。但是,再次强调:这只是一个用于博客的小型 PoC,目的是学习如何更多地使用 API 并与小型 LLM 交互。因此,我没有做太多超出基本的事情。

prompt() 函数

`prompt` 函数是在每次使用 `/error` 端点时调用的函数。

image/png

该函数的任务是根据我遇到的错误来构建提示。我的目标是根据用户的尝试生成消息。他正试图生成一个小于 450 的值,所以我根据当前值生成一个提示。我的想法是尝试让 LLM 变得有趣,如果用户离目标值很远或很近,就和他开玩笑。如果他接近目标值,我就会生成一个更具激励性而不是有趣的提示。

我本可以把所有东西都放在一个提示中,但我认为这会在多个层面效率低下。首先,我将为不会使用的条件生成示例。例如,如果错误是 2000,就没有必要发送提示来解释它生成激励性消息。我只需要生成玩笑消息的提示(定义为任何大于 2000 的值)。

这样,我节省了小型 LLM 的上下文,它有局限性!这样做的另一个优点是它更精确。示例中使用的 token 越少,它生成与我的示例相似的 token 的可能性就越大。当我这样做时,响应的差异是惊人的。它开始生成与错误范围的示例更接近的内容,而不是我全部放在一起。在这里,纯粹的提示工程帮助我从 LLM 中提取出最好的结果。

image/png

正如我之前提到的,除了字符限制(我暂时将其固定为 20 个单词)之外,我还要求它以“|fim|”结尾。添加这个标记进一步过滤了它在开始的 20 个单词之后出现幻觉的情况。它通常在“|fim|”之后开始出现幻觉。由于我获取了该标记之前的所有内容,这大大减少了消息与上下文无关的情况。

image/png

然后,一旦我的提示准备好,我就会调用 `GetModelAnswer` 函数,该函数负责技术部分,即选择最佳 LLM 并管理 LLM 未响应的情况!

image/png

GetModelAnswer() 函数

此函数负责将我的提示发送到 MODELS 列表中之一的 LLM。

想法很简单:如果用户没有在参数中明确选择模型,那么我将尝试选择最佳模型并调用它。如果它失败,我将尝试下一个,直到选项用尽!

image/png

我们很快就会看到 `UpdateProbs`,它负责计算要调用的最佳模型。在循环中,它将重复最多与列表中的模型数量(我们在初始化时加载的 `ModelList` 数组,如上所示)等效的次数,它将获取代表我们模型的对象。

image/png

它构建 Hugging Face 推理 API 的 URL(这可能是一个环境变量,以防有一天它发生变化)。还记得我们在 MODELS 常量中定义的“name”吗?是的,我们就是在这里使用它的!因此,它必须是完全相同的名称。

然后,它将构建要发送到 Hugging Face API 的数据。

image/png

推理 API 有很多选项。在我们的例子中,我们需要使用 `inputs`,也就是我们的提示。这里请注意,我正在调用当前模型的 `prompt` 方法,它将为该模型构建特定的提示(还记得吗,我们上面提到过它!)。多亏了这个方法,我可以根据每个 LLM 的需求格式化提示。

`parameters` 和 `options` 键是我硬编码的配置:最大 70 个令牌和 50% 的温度,以避免过多的幻觉,但在调用之间仍有变化。

最后,一个简单的 `fetch` 用于向 Hugging Face API 发送请求并等待响应!这里也可以添加一些处理,例如超时等,但我选择保持简单。

请注意,此时,我还会在该模型的统计数据中增加请求总数。这将有助于生成成功概率。

此外,我将测量它运行所需的时间(以毫秒为单位)。我很快就会用它来惩罚或奖励模型。

image/png

如果响应是错误(不是 HTTP 200),我们增加错误计数器并尝试列表中的下一个模型。我将重复此过程,直到收到 HTTP 200 或直到我尝试过的模型用完为止。这个 IF 中的所有逻辑都只是为了这个:获取列表中的下一个模型并重复整个循环!

在下面,我进行了一些质量检查:如果响应时间大于 2.5 秒,我会稍微增加错误变量。或者,如果响应时间小于 900 毫秒,我会稍微减少错误。这样,我就可以根据响应时间来惩罚或奖励模型。您会发现这会影响最常选择的模型。我可以在这里添加更多的检查。只需操纵每个模型的错误变量值,这将在定义最佳模型的算法中体现出来。

如果返回 HTTP 200,则表示我得到了响应,然后我直接返回,这会结束我的循环!

UpdateProbs 函数

此函数包含用于确定最佳 LLM 的逻辑。这里的目标是选择最有可能生成高质量响应的模型。成功的可能性由每个模型的 `stats.erros` 属性控制。该值是相对于 `stats.total`(尝试总数)的百分比。这里的想法是:如果 `stats.erros` 等于 `stats.total`,那么模型生成低质量响应(或失败)的可能性为 100%。

为了决定哪个模型是最好的,我们计算错误总数,并在所有模型之间分配一个百分比。例如,我们考虑 3 个 LLM:Gemini、Phi3 和 Llama。假设尝试和错误如下:

  • Gemini,错误 = 2,尝试 = 2
  • Llama,错误 = 0,尝试 = 2
  • Phi3,错误 = 1,尝试 = 2

请注意,Gemini 的准确率为 0%(2 次尝试中有 2 个错误),Llama 的准确率为 100%,Phi3 的准确率为 50%。
因此,如果您分配总命中数,您将得到以下结果:

  • Gemini: 0% ( 0/(0+100+50) )
  • Phi3: 33% ( 50/(0+100+50) )
  • Llama: 66% ( 100/(0+100+50) )

在 0% 到 100% 之间选择一个随机数。选择第一个涵盖随机数范围的模型。例如,如果选择 30%,您可以选择 Phi3,因为它涵盖 0 到 33% 的范围。如果选择 35%,则只有 Llama 合适,因为它涵盖 33% 到 100% 的范围。

通过这种方式,我们能够优先选择最准确的模型。如果两个 LLM 的机会相等,一个小技巧会使其中一个被选中。

image/png

因此,我们能够根据模型的性能随机选择最佳模型,在这种情况下,性能是通过一些简单的检查来衡量的。但代码已准备好允许更多的指标(只需在达到这些额外的错误条件时增加错误计数)。

其他端点

其他端点,例如 /models,仅用于调试。因此,我认为不需要额外的解释。您可以直接在代码中查看或实时访问。

image/png

希望您喜欢,并且这个实现能够帮助您在 Hugging Face 上生成更多使用多个 LLM 的想法!!!

如有任何疑问,请随时联系我!

X: @IATalking
领英:@rodrigoribeirogomes

社区

注册登录 发表评论