拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Uni LLM Bench:自托管轻量级 LLM API 基准测试平台实战

Uni LLM Bench:自托管轻量级 LLM API 基准测试平台实战

要说清楚 Uni LLM Bench 是什么,得从我自己的一个真实困惑说起。过去一年多我一直在做多模型接入的业务,要同时评估好几家大模型 API 的好坏:谁的响应快、谁的高峰期不拉胯、谁的成本划算、谁的结构化输出不容易翻车。市面上的公开榜单和厂商控制台都看过、用过,但始终差一层——它们回答不了"我的业务场景下,哪个 API 最适合当生产后端"。后来我干脆自己动手,做了 Uni LLM Bench 这个自托管的轻量化 LLM API 基准测试平台。

这篇文章会把它的定位、指标设计、核心实现、部署踩坑和真实测试数据完整拆开讲一遍。如果你正在做 LLM 技术选型、需要对比多家 API 服务,或者想给团队搭一套可复用的模型评测体系,这篇文章应该能帮你省下不少弯路。

1. 为什么放着现成榜单不用,偏要自己搭一套测试平台

1.1 公开榜单回答不了你的业务问题

Open LLM Leaderboard 这类公开榜单,在模型能力评估上确实有价值,但它的定位是回答"哪个模型的通用能力更强",而不是"哪个 API 服务更适合作我的后端"。这两个问题之间隔着很重要的一层东西:工程化性能和服务品质。

API 服务不是模型本身。模型权重只决定理论上的能力上限,你在客户端感受到的延迟和稳定性,还取决于推理引擎的优化选型、服务端并发架构、负载均衡策略、限流配置,甚至当天的服务端负载。同一个模型,不同厂商托管,体验差异可能很大;同一个厂商,白天和夜里测试,数据也可能完全不同。

公开榜单基本不反映这些变量。榜单评测往往是离线的、批次式的,在一个受控环境里跑固定数据集;而真实 API 调用是在共享资源池里进行的,会遇到排队、抢占、限流。我曾经遇到过某榜单排名很好的模型,接进生产后高峰期 P99 延迟直接飙到十几秒。厂商为了控制成本设置了激进的排队策略,低峰期数据极其漂亮,一到业务高峰就被打回原形。这种情况,光看榜单是永远发现不了的。

1.2 厂商自带监控数据的局限

各家开放平台都有自己的 usage 面板和调用统计,但这些数据有两个天然限制。

第一是口径不统一。有的厂商按服务端处理时间统计延迟,有的按客户端全链路时间统计,还有的默认不统计排队时间。靠这些数据做跨厂商对比,就像拿米尺量身高、拿卷尺量体重,量出来都是数字,但没法互相换算。第二是不可定制。厂商仪表盘只展示它认为重要的指标,你没法让它针对你的业务 prompt 去跑一套评测,也没法把不同厂商的数据拉到同一张表里做归一化对比。

更麻烦的是,厂商面板的数据视角是"历史事后回顾"。某一天某段时间服务质量大幅劣化,事后在面板上看到的只有一堆事后聚合的曲线,你很难定位到具体是哪个时段、哪种负载、哪类请求受的影响。而要主动探测这层问题,唯一可靠的办法就是在外部发起持续可控的请求,这正是基准测试平台该干的事。

1.3 自托管带来的三种自由

自托管基准测试平台的定位很清楚:既不替代公开榜单,也不替代厂商控制台,而是增加一层"以我的业务场景为准、以统一口径为准"的测试层。我把它总结为三种自由。

场景自由:测试用例完全自定义,用生产环境里真实的 prompt 模板和数据,而不是通用知识题。你在业务里用的是 RAG 检索增强、JSON 结构化输出、多轮对话,就用这些场景去测。口径自由:所有供应商统一按同一套指标定义和采样规则计算,p50、p95、成功率、成本全部对齐,结果天然可比。数据自由:原始请求响应日志留在自己的数据库里,不依赖任何第三方平台,事后想怎么分析、怎么审计都可以。自托管的数据资产是属于自己的,这一点在需要内部合规审计的场景里尤其重要。

1.4 什么情况下不需要自建

不是所有场景都需要自建评测平台。如果只是临时比较两三个模型的答案质量,写个脚本加人工看结果就够了。但如果你需要持续跟踪多个 API 服务的质量变化、每次供应商调价或模型升级后重新评估、把评测结果沉淀为路由决策依据,那么一套轻量化的自托管平台就是值得投入的基础设施。Uni LLM Bench 的"轻量化"正是从这种需求出发:不搞 K8s、不搞微服务、不搞分布式任务队列,一个进程加一个 SQLite 文件就能跑。

2. 轻量化设计:Uni LLM Bench 到底在测什么

2.1 六维指标,守住可比性的底线

基准测试平台最怕指标定义不统一。同样的 API,用不同的采样方式能得出完全相反的结论。我把指标拆成六个维度,每个维度对应一类实际诉求,评测结果页分别展示,不做加权平均。

首Token延迟(TTFT):从发起请求到收到第一个 token 的时间。对聊天机器人、交互式应用是生死指标,直接决定用户感知的响应速度。计算方式是请求发出到首个 chunk 到达的时间差,需要连续采样多轮取分位数。端到端延迟:从发起请求到完整响应结束的时间。它受输出长度影响很大,所以必须配合第四个指标一起看——一个模型输出 2000 token 的端到端延迟,和一个输出 200 token 的模型比总延迟,没有意义。

生成吞吐(Tokens/s):端到端延迟除以输出 token 数,即每秒生成速率。这里要强调一个容易被忽略的事实:生成速率并不恒定。长文本的后期速度可能会明显下降,平台测的是整段输出的平均吞吐,对纯生成型任务的体验评估很有参考价值。成功率:非 HTTP 错误、非网络超时的请求比例。这个指标很容易"造假"——把超时阈值设得足够长,几乎所有请求都能成功,但真实用户不会等那么久。所以平台在报告里会同步展示超时阈值,即"在 X 秒超时口径下的成功率"。

成本(每千 token 单价):输入和输出 token 分开计价,输出 token 通常贵数倍。结合生成量就能算出"完成一次业务请求的平均推理成本"。公开榜单完全没有这个维度,但它往往是生产选型时最先拍板的那一个。结构稳定性:对强制 JSON 输出、函数调用、固定格式回答的断言通过率。这个维度最容易出问题——很多模型"能力很强",但在严格的 JSON Schema 下会出现字段缺失、类型错误、括号闭合错乱。做生产接入时,这个指标甚至比答案正确率更关键。

2.2 任务维度:不是笼统问"谁的模型更强"

我把用例分成五类,每类有独立的指标权重和断言方式,单独出报告,不混在一个总分里比较。知识问答:看回答是否包含预期事实关键词、语义是否一致;代码生成:看是否通过预置的语法检查或单元测试;结构化输出:看 JSON Schema 校验是否通过,这是我最常用的一类;长上下文摘要:看输入窗口利用率、长文本下的延迟衰减和关键信息保留率;多轮对话:看连续多轮不跑偏、上下文记忆是否准确。

这样设计的核心原因是:写代码更强的模型和做摘要更强的模型,根本没有可比性。混在一起算总分,只会让评测失去决策参考价值。

2.3 轻量化在技术选型上意味着什么

技术栈上我选择了 Go 加 SQLite。Go 的部署形态最舒服:编译出单二进制,跑在任何 Linux 机器上都不需要运行时依赖。前端是一个单页应用,所有代码打包进同一个二进制,通过 HTTP 提供页面和 API。SQLite 在评测数据规模下完全够用——评测结果是万级行的水平,远没到它的瓶颈。系统默认通过 Docker Compose 编排启动,两条命令就能起整套环境,也支持裸二进制运行。没有消息队列、没有 Redis、没有对象存储,这些运维负担对个人项目和中小团队都是不必要的。

在我看来,轻量化的底线是:部署复杂度不能超过一个小团队能维护的极限。任何需要专人维护的组件,都不应该出现在一个基准测试平台的默认架构里。

3. 核心实现:把一次评测跑出可信的数据

3.1 用例组织:YAML 作为唯一事实来源

评测用例统一用 YAML 定义,一个最小用例文件的结构大概是这样的:

suite: production_faq tasks: - name: product_pricing type: factoid_qa provider_group: [provider_a, provider_b] prompt: | 用户询问订阅定价策略,请用不超过50字回答。 产品说明:{{ dataset.product_info }} input: datasets/pricing.jsonl assertions: - type: include_keywords keywords: ["分区定价", "月付"] - type: json_format schema: schemas/pricing_reply.json samples: 5

这里值得展开的是变量替换机制。真实业务场景里的 prompt 很少是孤立问题,通常包含上下文信息、用户信息、历史对话。平台用数据集文件加模板注入的方式,每一行 JSON 记录运行时逐条填进 prompt。这样能在贴近生产负载的前提下,保持用例的可复用性——同一套用例模板,换一个数据集就能测出不同业务场景下的表现。

provider_group 是另一个关键设计。同一组用例要在多个 API 服务上分别执行,结果才能横向对比。底层抽象一个 Provider 接口,统一支持标准对话模式、流式传输和 JSON mode 三个能力。各家 API 的差异在适配器层消化掉。这一层花了我不少精力,因为有的厂商把 JSON mode 做成独立参数,有的要求你在 prompt 里写"output as json",有的兼容 OpenAI 格式,有的字段命名完全自成一派。没有这层适配,跨厂商评测几乎没法自动化。

3.2 并发与限流:评测工具最容易踩的坑

并发模型我用了异步事件循环,单进程就能撑出很高的并发连接数,对轻量化部署很友好。但并发不能无限开,每个 provider 都有限流,硬冲只会收获一堆 429。平台按 provider 配置了令牌桶参数,包含速率和容量,比如速率 2 个请求/秒、容量 5。执行器在请求前取令牌,取不到就排队等待。

这里要纠正一个观念:评测的目的不是在最短时间内把所有请求打完,而是在模拟真实负载下测出每个 API 的真实表现。可控的、非突发的请求注入模型反而更接近生产实际。限流处理也不能止步于令牌桶。某些厂商的响应头里带 x-ratelimit-remaining 之类的信息,平台会把原始请求日志完整记录下来,评测结束后可以回看:这个 provider 到底有没有限流、在什么请求速率下限流开始发生。这些数据对后续生产环境的配额评估非常有价值。

3.3 指标计算:样本、分位数和异常值的博弈

指标计算里最大的坑是拿平均数下结论。网络延迟和 LLM 生成时间都呈长尾分布,平均数的参考价值很小。平台默认报告 p50、p90、p95、p99,平均数和标准差仍然保留,但不作为主要展示项。默认每个用例采样 3 轮,3 轮以上分位数才比较稳定。这个轮数都是可配置的:对 SLA 敏感的场景建议 5 轮以上,对成本敏感的场景 1 轮也能看粗粒度结论。

重试策略也值得细化。429、503 这类状态码是暂时性错误,是否重试会直接影响成功率。平台把重试行为显式配置化,默认不重试——评测的初衷是暴露真实稳定性。允许开启最多 2 次退避重试,但重试成功的请求会单独标记。报告里区分"一次成功率"和"重试后成功率"两个指标。这个设计在生产里非常实用:一次成功率反映服务真实健康度,重试后成功率反映最终用户体验,两个数据都看到,才能做出正确的容灾判断。

3.4 断言与质量评估:不只是"答得好不好"

质量评估最容易滑向玄学。我的做法是给断言分三档。规则断言最便宜、快、稳定,包括关键词、正则、JSON Schema、代码编译运行检查,能覆盖的场景绝不上复杂的评估。语义相似度解决规则覆盖不了的表述不确定问题,用本地小型嵌入模型计算输出向量与期望答案的余弦相似度,设定阈值。这里有个经验:采样嵌入要配置在本地推理,不要为每次评测专门调一次 LLM,否则评测成本会直接翻倍。

LLM-as-judge 是最后的选择,谨慎使用。Judge 模型本身有偏差,同一个回答用不同 judge 打分可能差很多,而且 Judge 调用会显著增加评测成本和延迟。只推荐在规则断言和相似度都覆盖不了的主观类任务上使用,比如文案风格、逻辑清晰度,并且每次 Judge 评测必须做双向盲测,把候选输出随机打乱再让 judge 打分,避免位置偏见。

断言失败的请求会进入独立的 failed 队列,不直接丢弃,方便事后人工复核到底是模型输出问题还是断言写得太苛刻。这个能力很重要——相当比例的评测失败,原因其实在断言本身。

4. 自托管部署的落地细节与典型踩坑

4.1 容器化与数据目录

部署层面,Uni LLM Bench 的目标是一条命令起服务、一个 SQLite 文件存数据、环境变量配好就跑。我用 Docker Compose 组装了两个容器:bench-server 承载 API 和评测执行器,bench-web 承载前端。Go 编译出的单二进制镜像很小,启动秒级。SQLite 挂一个 volume,开启 WAL 模式。Nginx 负责前端静态文件和反向代理,整个编排文件约 200 行。

数据目录里留了一个 runs/ 文件夹,每次评测的完整原始请求与响应 JSON 都会落盘。这个设计的意图很朴素:自托管的意义之一就是永远保有 raw data,事后想做任何更深层的分析都有据可查。如果你不想用 Docker,裸二进制也完全能跑,一条命令加一个 config.yaml 即可,SQLite 文件自动创建在 data.db。这也是我判断轻量化是否达标的底线——部署复杂度不能超过一个小团队能维护的极限。

4.2 401 Unauthorized:最迷惑的 API Key 问题

这个坑值得单独写。同一个 key 在 curl 脚本里一切正常,放进平台后却一直报 401 incorrect api key。排查一圈后发现原因五花八门。

环境变量被 shell 转义吃掉了一部分字符。key 以 sk- 开头,后面跟着大小写字母和连字符,在.env 文件里没有用单引号包住时,某些符号会被 shell 解析掉。.env 文件读进来时把 BOM 或行尾的 CRLF 带进去了,Windows 下编辑过的文件尤其容易中招——用 hexdump 检查才发现变量值末尾多了个 0x0d。还有一类特殊情况:部分服务账户级 key 带 IP 白名单或组织限制,测试机出口地址不在白名单内,返回的同样是 401,但问题根本不在 key 本身,而在网络策略。最后一个是复制粘贴时混入前导或尾部空格,肉眼完全看不出来。

排查这类问题,我建议按这样的顺序走:先写一个最小的 HTTP 请求脚本,直接从同一套环境变量文件里读 key,发一个最简单的对话请求;然后把平台日志里的请求头原文 dump 出来,和脚本的请求头逐字段对比,看 Authorization 头是否完全一致。没有对比,就永远在猜。平台侧我后来把密钥存储改成了两种模式,一种是环境变量注入,一种是 UI 托管并加密存入 SQLite。团队协作场景下我默认推荐 UI 托管模式,配置入口直接填 key 和备注,平台启动后会用状态标识提示哪些 provider 的 key 还没配好。

4.3 400 上下文超限:用例和模型窗口的匹配问题

做长上下文测试时,平台会碰到 400 错误,错误消息类似“this model's maximum context length is 1048576 tokens”,不同模型的上下文窗口各不相同。这个错误本身好理解,难的是在评测中优雅处理。

测试用例是按业务场景设计的,不是按模型设计的。一个输入 3 万 token 的用例,放到窗口只有 8k 的模型上必然失败。但如果为了照顾小窗口模型,把所有用例都按最低窗口设计,又测不了长上下文能力。我的做法是给用例增加 min_context_window 字段,平台调度时先读取模型的实际上下文窗口,不满足就跳过并标记为 skipped_due_to_window,在报告中单独列一栏,不参与成功率分母。这样不同窗口的模型能在同一份用例集下完成评测,结果呈现也公平。

另一个容易被忽略的点是请求体大小带来的传输耗时。输入上万 token 时,请求序列化和网络传输时间已经不容轻视。平台在计时时区分了"传输耗时"和"服务端处理耗时"两个分段,前者受客户端网络和请求体大小影响,后者才是模型处理能力。评测时建议在同一内网环境运行,才能让不同 provider 的服务端处理耗时具备可比性。

4.4 429、503 和失败重试的记录

重试策略前文提过是可配置的,这里补充落地时的记录细节。平台每次请求都会写一行原始事件流,包含 request_id、provider、模型、阶段(queued/sent/first_token/done/failed)、时间戳、HTTP 状态码、错误详情。评测结束后按 provider 聚合,能清楚看到:多少请求第一次就成功、多少撞了限流、多少在超时阈值处被主动放弃。

我见过太多团队评测时对 429 视而不见,压测报告漂亮得不像真的——因为把限流错误全部默默重试掉了。但真实的限流体验恰恰是用户会在高峰期遇到的,这个数据不该被淹没。把限流相关状态码单独统计,报告里列"限流丢弃率",是我建议接入生产前的必看指标。

4.5 SQLite 在评测压力下的并发写

最后一个部署层的坑是 SQLite 并发写。评测天然是并发任务,每个任务完成都会写结果,SQLite 的默认行为在多连接并发写时会出现 database is locked。解决思路并不复杂:开启 WAL 模式,把写入改成单写者队列,事务保持短小。业务层还做了批量落盘,攒够一批请求后一次性写入,而不是每条请求各写一次。这套组合在本地模拟 500 并发持续评测,锁冲突率降到 0。

SQLite 常被当成玩具,但在评测这个场景下它很够用。数据量就是万级行,远没有到 SQLite 的瓶颈。自托管平台不该一切向数据库看齐,能用 SQLite 解决的问题,就不该背上一个独立数据库服务的运维负担。这个判断,是我做完这个项目后最深的体会之一。

5. 真实数据复盘:一场跨三家 API 供应商的对比测试

5.1 一个典型的评测结果长什么样

理论讲完,上实测。我在固定内网环境里用同一份用例集,对三个供应商跑了一轮 Uni LLM Bench,分别记为服务 A、服务 B、服务 C。数据按具体时段和模型版本而变,这里重点讲解读方法。

用例是 20 个知识问答加 10 个 JSON 输出断言,每个用例跑 5 轮,共 450 个请求。结果大致长这样:

指标服务A服务B服务C
首Token延迟 p50412 ms268 ms520 ms
首Token延迟 p95930 ms810 ms1600 ms
端到端延迟 p503.2 s4.8 s5.1 s
生成吞吐 p5062 tokens/s41 tokens/s38 tokens/s
一次成功率99.6%96.5%99.1%
JSON Schema 通过率98%88%99%
输出端千token成本X 元X+0.4 元X-0.2 元

这张表完美展示了为什么不能只看一个指标。服务 B 的首 Token 延迟最低,但端到端延迟反而最高,说明它首字响应快、后续生成却慢,交互场景的真实体验不一定好。服务 C 价格最低但 p95 延迟偏高,高峰期的 SLA 风险较大。服务 A 表现均衡,适合默认路由。

5.2 数据解读的常见误区

第一个误区是拿 p50 做决策。p50 只代表"多数时候还行",生产上的用户投诉往往来自 p95 甚至 p99 的尾巴。有些服务 p50 只差几十毫秒,p95 却差出一倍,直接用 p50 决策很容易选错。第二个误区是忽略时段因素。我在上午、下午、晚间各跑了一轮,同一家服务晚间高峰的 p95 延迟比白天高出 60% 以上。评测报告的"快/慢"必须带着时间段标注,否则换个时段结论就会反转。第三个误区是把单次请求当成噪音。LLM 生成有随机性,一次请求的响应时间波动可以很大。不采样三五轮,"低延迟""高成功率"都可能只是巧合。

5.3 如何把数据沉淀为路由决策

基于这份报告,我推导了自己的路由规则:交互式请求默认走服务 A,成本敏感型批量任务走服务 C,服务 B 做兜底并用重试策略缓解稳定性问题。这个决策不是一次性结论——供应商调价、模型版本更新、网络波动都会让结论失效。Uni LLM Bench 现在每周自动跑一轮评测,结果进入消息通知,出现异常波动时我再单独复查。

6. 从个人工具到团队基建:还能往哪儿走

6.1 接 CI 做回归监测

把评测做成定时任务或 CI workflow,每次供应商修改模型、调整价格,或我方 prompt 体系升级时,自动触发一轮完整评测。和基线对比,如果 P95 延迟上升超过 20% 或成功率下降超过 1%,就触发告警。这需要平台把每个 provider 的基线快照持久化到 SQLite,CI 里直接拿上一版本的数据做对比。我在实际接入中发现,这类自动回归最大的价值不是发现大故障,而是捕捉"缓慢劣化"——供应商某次推理优化调整后,延迟一点一点攀升,人工看根本觉察不到,机器对比能第一时间拉响警报。

6.2 成本治理:从"花多少钱"到"值不值"

平台记录了每次评测的 token 消耗,并按当前价格自动估算成本。积累一段时间后,你不只能回答"这个月 API 调用花了多少",还能回答"如果切换某个供应商,相同业务量能省多少"。这些数据在跟供应商谈价格时特别好用——不用凭感觉说"你们贵了",而是直接拿出同场景下的量化对比。更进一步,平台可以按用例维度统计单位成本,哪个业务场景是最烧钱的、哪个用例的性价比在下降,一目了然。

6.3 与 LLM 网关联动:测量结果变成路由规则

这是比较进阶的玩法。Uni LLM Bench 负责测量,网关负责转发,评测输出的历史数据可以生成一张"路由健康表"供网关选路。网关收到请求时发现服务 A 近期 p95 劣化,自动把流量切到服务 B;批量任务则始终优先选单位成本最低的 provider。测量平台提供带置信度的数据,路由系统做实时决策,这个组合是兼顾成本与质量的可行路径。

当然,这条路走下去也会遇到新问题。评测请求本身消耗 token 费用,持续压测需要控制频率;LLM-as-judge 的主观任务要防止 judge 模型版本更新造成评分漂移;团队多人使用时,API key 的权限管理和调用审计也需要投入精力。每个问题单独展开都能写一篇,这里先不展开了——等你真正跑起来,自然会走到这一步。

最后分享一个我做这个项目过程中的体会。模型评测这件事,公开榜单替你回答"哪家聪明",厂商面板替你回答"哪家健康",但只有自己搭的平台上,你才能回答对业务真正重要的那个问题——"哪家最适合我的场景"。把这个回答握在自己手里,比看任何报告都踏实。再补一个小技巧:做评测前先把超时阈值、采样轮数、重试策略三个参数用注释写进配置文件的头部。这组参数决定了报告口径,同一份数据如果不带这三个参数,别人根本没法复现你的结论。我吃过一次亏才加上这个习惯,建议你直接抄作业。

返回列表