
当我几周前发布了 Neuron AI 官方路由包后,我收到了来自许多开发者的相同问题,只是措辞不同:它能将困难的请求发送给强大模型,而将简单的请求发送给便宜模型吗? 这是一条最自然的规则。也是我无法干净地编写这条规则的那条规则,这让我有些烦恼。
路由器为您提供了一个干净的地方来做出这个决策。您注册几个提供者,设置一条规则,代理永远不会知道它是在与代理对话。但规则必须返回一个提供者名称,而要做到这一点,您首先需要一个“困难”的定义,这个定义必须存在于代码中。这就是之前没有人做的那部分。那个词在对话中承担了大量工作,但没有在编辑器中承担任何工作。
如果您四处寻找,所有绕过方法都有相同的形状。有些人按提示长度进行路由,理论上认为更长的提示更困难。在实践中,一条关于意大利合同法的单行问题短而确实困难,而一个您想要总结的长日志则微不足道。长度衡量的是打字,而不是难度。
其他人会保留一个关键词列表,将包含“legal”“code”或“calculate”的任何内容路由到高级层。这在短期内有效。然后您会永远维护一个词典,它会错过您未预见到的每种措辞,并且对未硬编码的语言中的提示完全没有意见。
最诚实的尝试是在回答之前让一个 LLM 对提示的难度进行评级。它甚至效果相当好。问题在于,您现在为了决定是否进行模型调用而支付了一次模型调用,并在等待它。这会向您试图使其更便宜的精确路径添加延迟和成本。对于每一次请求都在运行的东西来说,这是一种错误的权衡。
新的包 neuron-core/llm-classifier 采取了不同的立场。它构建一个小型分类器,该分类器读取传入的提示并返回 0 到 1 之间的难度分数,其中 0 表示您的模型认为这很容易,1 表示它们挣扎。那里最重要的词是 your。该分数不是对抽象中什么困难的通用猜测。它是根据您实际路由的模型学习的,因此它反映了您的阵容认为困难的东西,这是您决定使用哪个模型回答时唯一相关的内容。
它在纯 PHP 中运行。唯一的要求是 ext-mbstring。没有 Python sidecar 需要部署,没有 GPU,没有推理服务器坐在您的应用旁等待凌晨三点重启。训练一次,离线完成。评分在微秒级别,在进程中运行,在您打开任何提供者套接字之前。在每个请求上,您得到一个数字,而这个数字不会花费您任何东西。
composer require neuron-core/llm-classifier
心理模型是两个在非常不同时间发生的行为,包会正确地将其分开。
第一个是校准。这是在哪里向分类器教授对您的任务和您的模型来说什么是简单、什么是困难,它只发生一次,离线,从脚本或控制台命令中完成。输出是一个单一的 model.bin 文件,您会将其与代码一起提交。当您的模型改进或您的价格变化时,您使用新的阵容重新运行校准并替换文件。没有任何关于这个的东西会进入请求路径。
第二个是评分,而这是唯一在您的实时应用程序中运行的部分。您一次加载 model.bin,理想情况下在引导时或在 Octane、RoadRunner 或 FrankenPHP 工作器内部,然后在每个请求上调用它以获取分数。训练时间和运行时间永远不会相互触及,这正是您想要的属性,因为它会在每个推理调用之前运行。
如果您在想几百个示例提示如何变成一个数字,简短版本是词首先变成数字。包使用一个免费的可下载的 fastText 词向量字典,将每个词映射到一个包含 300 个数字的列表,这些数字捕捉其含义,因此“buy”和“purchase”会靠近一起,而“king”和“carburetor”会远离。每个提示被减少为这些数字的一个平均指纹,而这个指纹是分类器的唯一输入。您不会直接触摸任何数学。您提供提示、答案和一个对它们进行评级的方法,而分类器会找出哪些模式困难。字典中您的实际数据使用的部分会被烘焙到 model.bin 中,因此运行时不需要原始 fastText 文件。
您不需要自己组装数据集就能看到它工作,我也不推荐从这里开始。包附带了一个可立即使用的就绪数据集,源自公开的 RouterBench 基准,一个分层采样的大约 1,845 个提示,每个提示都带有预计算的难度标签。由于难度已经知道,这个路径不需要模型面板、不需要评级者,也不必进行任何 API 调用。您只需要 fastText 向量和几秒钟的 CPU 时间。
# 1) 一次性的:下载 fastText 向量
curl -O https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.en.300.vec.gz
gunzip cc.en.300.vec.gz
mv cc.en.300.vec storage/
# 2) 运行校准,将生成 storage/model.bin
php script/routerbench.php
这就是您的第一个经过训练的模型。加载它并对提示进行评分只需两行。
RouterBench 记录了对于 ~36k 个提示,每个最常用的 11 个 LLM(来自 OpenAI、Anthropic、Mistral 等常见提供者)是否正确回答了 RouterBench。如果您使用该模型的子集,它已经是一个可靠的数据集。我们将其转换为一个可立即使用的就绪数据集来训练您的分类器。
use NeuronCore\Classifier\Classifier;
$scorer = Classifier::load('storage/model.bin');
$score = $scorer->overall($userPrompt); // 0 = easy, 1 = hard
overall() 为您提供一个数字与阈值进行比较。在底层,它是每个能力分数的最大值,而不是平均值,这种选择是故意的。一个提示在某些方面困难而在其他五个方面微不足道,应该被视为困难,而平均值会悄悄淡化它。
这就是我一直等待编写的那部分。分数本身只是一个数字。一旦路由器可以对它采取行动,它就会变得有用,而布线很小。这是使用路由器的 CallbackRule 的明确版本,使每一步可见。
use NeuronAI\Router\Rules\DifficultyRule;
use NeuronCore\Classifier\Classifier;
class MyAgent extends Agent
{
protectedfunction provider(): AIProviderInterface
{
// 一次加载分类器(例如在应用引导时或在长时间运行的工作器内)。
$scorer = Classifier::load('storage/model.bin');
return RouterProvider::make()
->addProvider('mini', new OpenAI(key: 'OPENAI_API_KEY', model: 'gpt-4o-mini'))
->addProvider('4o', new OpenAI(key: 'OPENAI_API_KEY', model: 'gpt-4o'))
->addProvider('o1', new OpenAI(key: 'OPENAI_API_KEY', model: 'o1'))
->setRule(
(new DifficultyRule($scorer))
->outOfDomain('o1', coverage: 0.4) // 不熟悉的提示 → 最有能力
->easy('mini', maxScore: 0.33) // overall() < 0.33 → 便宜且快速
->medium('4o', maxScore: 0.70) // overall() < 0.70 → 全面的平衡者
->hard('o1') // 否则 → 最有能力
);
}
}
现在路由器附带了一个 DifficultyRule,它包装了完全相同的模式。您向它提供已加载的分类器和您的提供者,它会为您执行覆盖保护和阈值路由,整个上面的块会折叠成路由器上的单个规则。
据我所知,这是第一次将提示难度分类器与生产框架在纯 PHP 中布线,这是我悄悄感到满意的部分。
只有两个东西需要调整,您使用数据而不是直觉来调整它们。难度截止点(上面的 0.33 和 0.70)决定简单在哪里结束和困难在哪里开始。覆盖截止点(0.4)决定提示有多不熟悉才会停止信任分数。设置它们的方法是记录真实流量的三个东西:难度分数、覆盖率和您将选择的提供者,然后调整直到您对平衡感到满意。如果便宜模型回答开始出错,请降低困难阈值以更多请求爬到更强的模型。如果出域提示泄漏到便宜层,请提高覆盖截止点。您不是在猜测。您在阅读自己的日志。
在很长一段时间内,PHP 中“哪个模型应该回答这个?”的实际答案要么是静态选择,要么是一堆您手动维护的字符串匹配。现在有一个经过测量的答案,成本只有微秒,并且来自您的模型,它可以插入到已经作为框架一部分的路由器中。在它真正重要的地方保持相同的质量,在其他地方保持更小的账单,并且在运行时没有延迟。
该包是 neuron-core/llm-classifier,它是 MIT 许可的,并且 RouterBench 数据集在盒子里,因此您可以在喝完咖啡前有一个工作模型。
现在训练您的第一个 LLM 分类器:https://github.com/neuron-core/llm-classifier