使用 JavaScript 和 Hugging Face 推理 API 编排小型语言模型 (SLM)
PT-BR
你好!
我将向您展示我是如何使用 Hugging Face 推理 API、一个 Space、Docker 和不到 500 行 JavaScript 代码来编排多个小型 LLM,让它们在一个小型交互式神经网络模拟器中对我尝试的结果进行回应。
您可以在这篇 IATalking 博客文章中看到结果。移动滑块并尝试减少误差;您会看到一个 AI 将根据您的尝试(和尝试历史)生成评论。这些评论来自 Phi3、Llama、Mistral 等模型,它们都是小型模型,拥有几十亿参数。
每当发送新请求时,我将在此处展示的代码会选择其中一个模型。如果某个模型开始出现故障,则下次发出请求时,其被选择的可能性会降低。通过这种方式,我可以实现 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
这是一个 Docker Space,我选择 Docker 是因为它的简单性。Hugging Face 允许您使用 Docker 创建服务,这太棒了,因为它提供了无限的可能性:您可以用您喜欢的语言创建 API 并将其托管在 Space 中!
如果您查看 Dockerfile,您会看到一个典型的 Node.js 服务器:
docker-compose.yml 只是为了方便我的本地测试。在发送到 Hugging Face 之前,我只需使用 `docker compose up` 运行相同的代码。这省去了我输入 "docker -p 等等" 的麻烦。
README.md 中只包含 Hugging Face 所要求的必要条件,而核心是 `server.js`。所有的逻辑和端点代码都在这里!
提供的 API
这个在 server.js 中实现的 API 是一个 Express 应用程序。创建了以下端点:
- /error
这将是使用最多的:您传入错误编号和尝试历史,它将返回一个文本! - /
这是默认端点。它只是显示一条简单的消息,指示服务已启动! - /models
这是一个调试端点。通过它,我可以看到相关 LLM 的历史和执行统计。这让我可以知道谁产生了更多错误,谁被执行得更多,以及其他一些仅用于监控和调试的信息! - /test
这是一个仅仅用于测试 Express 是否真正工作和响应的端点。
服务器初始化
服务器启动时,会进行一些配置,其中最主要的是定义一个包含我想要使用的模型列表的对象。
全局常量`MODELS`将包含我的模型!每个对象都是我为这段代码定义的 LLM 的“ID”,而不是 Hugging Face 的 ID。它只是一个“别名”,方便识别。
因此,对于每一个模型,我需要定义:模型名称和提示模板。名称是 Hugging Face 上独一无二的名称,格式为 ORG/MODEL。这是您在模型卡片中找到的名称。
prompt
是一个方法,当需要为该模型生成提示时,我将调用它。每个模型可以有不同的提示格式,所以我不能对所有模型使用相同的提示。通过此函数,我可以使用相同的调用创建动态生成提示的机制:我只需调用 prompt()
方法并传入我想要的文本即可。
我从未研究过 Hugging Face Transformers 的代码,但很可能这与 apply_chat_template 方法背后的思想相同。这里只是一个非常简单的版本!
因此,只需这两个成员,我就可以轻松添加或删除 LLM!我选择将其硬编码以进行控制和简化。但我本可以将其外部化到文件或 API 中,以便在运行时包含或删除 LLM。由于这是一个简单的 PoC,我目前更倾向于保持简单。
最后,重要的是要记住,代码还对 HF_TOKEN 环境变量进行了验证,它是 Hugging Face 令牌。此令牌在 Space 中配置为秘密。在本地测试中,我生成一个测试令牌,并在我的 docker compose up 中使用它,在我的 shell 中创建一个环境变量。这是拥有一个现成的 docker compose 带来的另一个便利!
模型初始化
在服务器启动时,对于 `MODELS` 中的每个模型,我将生成第三个属性:`stats`。在这里,我们将有一些 LLM 执行指标,例如总执行次数、总错误数以及它产生错误的百分比。
属性描述如下:
- 总计
总执行次数。 - 错误数
一个数字,表示低质量的总数。 - 份额
错误份额。这将根据总错误数计算。 - 错误率
错误百分比:错误数/总数。 - 成功率
成功百分比(与错误率相反):1 - 错误率。
并且,在这一部分,我创建了一个包含所有 LLM 的数组,它在 `ModelList` 变量中。
代码的其余部分专门用于 Express 端点和逻辑函数,这将在下面详细说明。
/error 端点
这是主端点。上面的页面就是调用这个端点。它需要以下参数:
错误
用户当前尝试的错误值。尝试次数
尝试历史。这些是逗号分隔的错误编号。
代码首先验证参数值。基本上,我确保 error 是一个数字,并且 `tentativas`(tentativas = attempts,在葡萄牙语中)是一个逗号分隔的数字列表。任何与此不同的,我都会返回给用户。这样做的目的是避免提示注入,因为(您很快就会看到),我将直接连接这些值。
然后,它将调用 `Prompt()` 函数(我们稍后会讨论),该函数负责通过 Hugging Face 推理 API 组装提示并向 LLM 提问。
该函数将返回 LLM 的响应。这里将进行一些验证。
在这些验证中,最重要的是只提取 LLM 文本直到“结束”标记。您会看到我要求 LLM 总是以我称为 `|fim|`(fim = end,在葡萄牙语中)的标记结束文本。这是为了让它返回一个点,表明到该点为止它生成的文本是正确的。超出该点,就无法保证了。因此,我只会取 `|fim|` 之前的结果。
我做的另一个验证是字符数。在这里,我假设每个单词最多 8 个字符的标准,并且我设置了默认的 20 个单词,所以我简单计算 20*8。
稍后,您将明白原因,但请注意,有些部分我们正在更改响应模型的错误变量。这是我创建的一种机制,用于“惩罚”偏离预期质量标准的响应。
这种验证有很多漏洞,可以做得更好。但再说一次:这只是一个简单的博客 PoC,目标是学习更多地使用 API 并与小型 LLM 交互。因此,我没有超出基础知识的范围。
prompt
函数
prompt
函数是每当使用 /error
端点时都会调用的函数。
此函数的作用是根据我遇到的错误来组装提示。我的目标是根据用户的尝试生成消息。他正试图生成一个低于 450 的值,因此我根据当前值生成一个提示。其目的是,如果用户远离或接近该值,则尝试让 LLM 幽默地与用户开玩笑。如果他接近,我则会生成一个更具激励性而非有趣的消息。
我本可以将所有内容都放在一个提示中,但我认为那样效率会很低。首先,我会为不会使用的条件生成示例。例如,如果错误是 2000,则无需发送解释生成激励性消息的提示。我只需要生成玩笑消息的提示(定义为任何高于 2000 的值)。
这样,我节省了 LLM 的上下文,它很小并且有局限性!这样做的另一个优点是它变得更准确。示例中间的令牌越少,它生成与我的示例相似的令牌的概率就越高。当我这样做时,响应的差异是惊人的。它开始生成更接近错误范围示例的内容,而不是将所有内容都放在一起。在这里,纯粹的提示工程帮助我从 LLM 中提取出最好的效果。
而且,正如我所提到的,除了字符限制(我目前固定为 20 个单词)之外,我还要求它以“|end|”结尾。添加这个标记进一步过滤了在最初 20 个单词后它出现幻觉的情况。它通常在“|end|”之后开始出现幻觉。由于我只获取这个标记之前的所有内容,这大大减少了不相关消息的情况。
然后,一旦我的提示准备好,我就会调用 `GetModelAnswer` 函数,该函数负责技术部分,即选择最佳 LLM 并管理 LLM 未响应时的情况!
GetModelAnswer
函数
此函数负责将我的提示发送到 `MODELS` 列表中的一个 LLM。
想法很简单:如果用户没有在参数中明确选择模型,那么我将尝试选择最佳模型并调用它。如果它失败,我将尝试下一个,直到我用尽所有选项!
我们很快会看到 `UpdateProbs`,它会计算要调用的最佳模型。在循环中,它最多会重复列表中的模型数量(我们上面加载的 `ModelList` 数组)的次数,它将获取代表我们模型的对象。
它会组装 Hugging Face 推理 API 的 URL(如果有一天它会更改,这可能是一个环境变量)。还记得我们在 `MODELS` 常量中定义的“名称”吗?所以,这就是我们将使用它的地方!因此,它必须是完全相同的名称。
然后,它将组装要发送到 Hugging Face API 的数据。
推断 API (https://huggingface.co/docs/api-inference/en/index) 有多种选项。在我们的例子中,我们需要使用 `inputs`,它是我们的提示。这里,请注意我正在调用当前模型的 `prompt` 方法,它将为该模型组装特定的提示(记住,我们上面已经讨论过它了!)。多亏了这个方法,我可以根据每个 LLM 的需求格式化提示。
`parameters` 和 `options` 键是我硬编码的配置:最大 70 个 token,温度为 50%,以避免过多的幻觉,但仍能在调用之间有所变化。
最后,一个简单的 `fetch` 将请求发送到 Hugging Face API 并等待响应!在这里,也值得添加一些处理,例如超时等,但我选择保持简单。
请注意,此时,我还增加了此模型统计数据中的总请求计数器。这将有助于生成准确率概率。
此外,我将测量此操作运行所需的时间(以毫秒为单位)。我很快就会用它来惩罚或奖励模型。
如果响应是错误(与 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% 的范围。
这样,我们就可以优先选择那些命中率最高的模型。如果 2 个 LLM 具有相同的机会,一个小技巧将使其中一个被选中。
因此,我们能够根据模型的性能随机选择最佳模型,在这种情况下,性能是通过一些简单的检查来衡量的。但是代码已经准备好允许更多的指标(只需在满足这些额外的错误条件时增加错误计数)。
其他端点
其他端点,如 /models
,仅用于调试。因此,我认为无需额外解释。您可以直接在代码中查看或实时访问它。
希望您喜欢它,并且此实现可以帮助您为在 Hugging Face 上使用多个 LLM 生成更多想法!!!
如有任何问题,请随时联系!
X:@IATalking
领英:@rodrigoribeirogomes