☰
LLM API选型:延迟和TPS远远不够,你需要一套工程化体检工具
2026/11/3 8:37:40 网站建设 项目流程

1. 为什么延迟和 TPS 远远不够用

1.1 一个被误用的性能指标

做 LLM 应用的人,几乎都经历过这样的场景:选型阶段拉一张表,把几家 API 的延迟和 TPS 列出来,谁快选谁。上线之后发现,快的那家输出格式乱七八糟,JSON 里混着解释性文字,函数调用参数缺字段,长上下文一过 8K 就开始胡言乱语。这时候才回头翻文档,发现当初那张表里压根没测过这些。

延迟和 TPS 是基础设施层面的指标,它们回答的是“这条链路通不通、快不快”,但回答不了“这个模型能不能稳定地完成我的任务”。这两件事之间的差距,比大多数人想象的要大得多。一个模型可以在 P99 延迟 800ms、TPS 120 的情况下,把结构化输出任务做成一团浆糊;另一个模型慢一倍,但每次都能给你干净的 JSON。如果你的业务是批量数据抽取、Agent 工具调用、代码生成,后者才是你真正需要的。

我们内部从 2023 年下半年开始密集做模型选型和切换,前后对接过十几家 API 供应商,踩过的坑基本可以编一本手册。最核心的教训就是:延迟和 TPS 只能用来排除明显不可用的选项,真正决定选型的是任务级的能力表现。而任务级表现,必须用贴近真实业务的测试集去测,不能靠感觉,也不能靠厂商自己发的 benchmark 分数。

1.2 厂商 benchmark 的水分在哪里

厂商发布的 benchmark 分数,参考价值有限,原因不复杂。第一,测试集污染。很多经典测试集在训练阶段就被模型见过了,分数虚高是常态。第二,测试条件不透明。temperature 设多少、few-shot 给了几个、system prompt 怎么写的、失败重试算不算,这些细节直接决定分数高低,但厂商报告里往往一笔带过。第三,也是最关键的,厂商测的任务和你的任务不一样。MMLU 考的是知识广度,HumanEval 考的是函数级代码生成,但你的业务可能是“从客服对话里抽取 12 个字段并输出指定 JSON schema”,这个任务在任何一个公开 benchmark 里都找不到对应项。

我们曾经遇到过一家供应商,公开分数很漂亮,实际接入后发现它在中文长文本摘要任务上会把关键数字改掉——不是幻觉,是“顺手润色”导致的数值漂移。这种问题任何通用 benchmark 都测不出来,只有用你自己的业务数据跑一遍才会暴露。

1.3 我们决定自己造一套体检工具

市面上的 LLM 评测工具不少,但大多偏向学术研究或者模型训练侧,面向 API 消费方的、开箱即用的工程化工具反而稀缺。我们要的东西很具体:输入一组 API 配置和一组测试用例,输出一份能直接拿去做决策的报告。报告里不只有总分,还要有分任务类型的通过率、失败案例的具体表现、token 消耗和成本的估算、以及不同参数配置下的稳定性对比。

这套工具我们内部用了大半年,迭代了四个大版本,现在决定开源出来。它不是那种“跑个分就完事”的脚本集合,而是一套完整的体检流程:用例管理、并发调度、结果判定、报告生成、回归对比。下面我把设计思路、核心实现和实操经验完整拆开讲。

2. 工具整体设计与核心思路拆解

2.1 体检和 benchmark 的本质区别

先把概念理清楚。Benchmark 通常指在固定测试集上跑分,追求的是可比较性和可复现性,学术味比较重。体检这个词是我们故意选的,因为它更贴近实际使用场景:你不是在给模型排名,你是在判断这个模型能不能胜任你的工作。体检的维度更宽,包括能力、稳定性、成本、合规性,而且必须结合具体业务。

这个定位差异直接决定了工具的设计取向。第一,测试用例必须支持自定义,而且要足够方便地导入业务数据。第二,判定逻辑必须可插拔,因为不同任务的“正确”标准完全不同——分类任务看标签匹配,抽取任务看字段级准确率,生成任务可能要靠另一个模型来打分。第三,必须支持多轮对话和工具调用,因为这是当前 LLM 应用的主流形态,单轮问答的测试价值越来越低。第四,报告要能直接回答“选哪个”的问题,而不是丢一堆原始数据让你自己分析。

2.2 四层架构的取舍

工具整体分成四层:用例层、调度层、判定层、报告层。这个分层不是拍脑袋定的,是被实际需求逼出来的。

用例层负责管理测试集。我们支持三种来源:内置的通用用例集、从 JSONL/CSV 导入的自定义用例、以及从线上日志采样生成的用例。第三种最实用,因为线上日志里的输入分布才是真实的,人工构造的用例往往过于“干净”。用例的 schema 设计上,我们保留了metadata字段,用来标记任务类型、难度、业务标签,方便后续按维度切分报告。

调度层负责并发执行和重试。这里最大的坑是速率限制。不同供应商的限流策略差异极大,有的按 RPM 限,有的按 TPM 限,有的两者叠加,还有的会在突发流量时直接返回 429 而不给Retry-After。我们的做法是自适应并发:初始并发设低,根据成功率和响应时间动态调整,遇到 429 就指数退避并降低并发。这套逻辑比固定并发慢一些,但稳定性好太多,尤其是在跑几百条用例的时候。

判定层是最需要灵活性的部分。我们内置了五种判定器:精确匹配、包含匹配、JSON schema 校验、字段级抽取比对、LLM-as-judge。前四种是确定性的,快且便宜;第五种用于开放式生成任务,成本高但覆盖面广。判定器可以组合使用,比如先做 schema 校验,通过了再用 judge 评内容质量。

报告层输出 Markdown 和 JSON 两种格式。Markdown 给人看,JSON 给 CI 流水线用。报告里除了总分,还有按任务类型、按难度、按输入长度的分维度统计,以及失败用例的完整记录——包括输入、期望输出、实际输出、判定理由。这部分是排查问题的关键,没有失败详情,报告就是废纸。

2.3 为什么不做成 SaaS

有朋友问过为什么不直接做个在线服务。原因有两个。第一,测试数据往往包含业务敏感信息,很多团队不愿意上传到第三方。第二,体检是低频但重度的操作,跑一次可能几十分钟,做成 SaaS 的服务器成本不划算,而且网络抖动会干扰延迟测量。本地部署、命令行调用、结果落盘,这个形态最符合实际使用习惯。我们也提供了 Docker 镜像,方便在 CI 里跑。

3. 核心细节解析与实操要点

3.1 测试用例怎么设计才有价值

用例设计是整套工具里最考验经验的部分。我的建议是分三层来组织。

第一层是冒烟用例,20 到 30 条,覆盖最基本的连通性和格式正确性。这层用例的特点是判定简单、执行快,用来快速排除完全不可用的 API。比如“返回合法 JSON”“不输出 markdown 代码块包裹”“函数调用参数完整”这类。

第二层是能力用例,100 到 300 条,按任务类型分组。这层是体检的主体,需要覆盖你业务里所有重要的任务形态。以我们自己的业务为例,分了信息抽取、意图分类、多轮改写、代码生成、工具调用五个组,每组 30 到 60 条。用例的来源是线上日志采样加人工标注,保证输入分布真实。

第三层是压力用例,专门测边界情况。超长输入、特殊字符、多语言混合、指令冲突、诱导性 prompt,这些用例平时用不到,但一旦线上触发就是事故。我们有一组用例专门测“当 system prompt 和 user 指令矛盾时模型听谁的”,这个在 Agent 场景里非常关键。

注意:用例的期望输出不要写得太死。比如抽取任务,如果期望输出是完整 JSON,判定时应该做字段级比对而不是字符串比对,否则字段顺序不同就会误判。我们早期就吃过这个亏,白白浪费了一轮测试。

3.2 并发调度的参数怎么定

并发数不是越大越好。我见过有人直接把并发开到 100,结果一半请求 429,剩下的一半因为服务端排队延迟暴涨,测出来的数据完全不可信。

我们的默认策略是这样的:初始并发 4,每完成 20 个请求评估一次,如果成功率高于 95% 且 P95 延迟没有明显上升,并发加 2;如果出现 429 或成功率低于 90%,并发减半并进入 30 秒冷却。上限默认 32,可以通过配置调整。这套策略在大多数供应商上都能跑到接近限流阈值的稳定状态。

重试策略上,429 和 5xx 都重试,最多 3 次,退避时间用指数加随机抖动,基础 2 秒。超时不重试,因为超时往往意味着请求已经到达服务端并开始计费,重试会造成重复消耗。这一点很多人不注意,跑一次测试烧掉双倍 token。

参数默认值说明
initial_concurrency4初始并发数
max_concurrency32并发上限
success_threshold0.95提升并发的成功率门槛
cooldown_seconds30触发限流后的冷却时间
max_retries3最大重试次数
backoff_base2.0退避基础秒数

3.3 判定器的实现细节

精确匹配和包含匹配实现简单,不展开。重点说 JSON schema 校验和 LLM-as-judge。

JSON schema 校验我们用的是标准 JSON Schema 规范的一个子集,支持类型检查、必填字段、枚举值、数值范围。实际使用中发现,模型输出的 JSON 经常有小毛病:尾随逗号、单引号、注释、markdown 代码块包裹。所以校验前要先做一轮清洗,把代码块标记剥掉,把常见语法错误修掉,再解析。清洗逻辑要记录日志,因为清洗率本身就是一个重要指标——如果某个模型 30% 的输出都需要清洗,说明它的格式遵循能力有问题。

LLM-as-judge 的坑更多。第一,judge 模型本身有偏好,会倾向于给长回答打高分。第二,judge 的 prompt 极其敏感,换个措辞分数就变。第三,成本高,跑一轮 judge 的钱可能比被测模型本身还贵。我们的做法是:judge prompt 里明确给出评分维度和锚点示例,要求输出结构化评分而不是自由文本,并且用两个不同的 judge 模型交叉验证,分歧大的用例标记出来人工复核。

提示:judge 模型不要用和被测模型同源的,否则会有一致的偏见。比如测 A 家的模型,judge 就用 B 家的。

3.4 成本估算怎么做才准

Token 成本估算看着简单,实际很容易算错。输入 token 和输出 token 的单价通常不同,缓存命中的输入 token 又有折扣,有些供应商还有阶梯定价。我们的做法是分别统计输入、输出、缓存命中三类的 token 数,乘以对应的单价。单价从配置文件读,不硬编码,因为价格变动频繁。

还有一个容易忽略的点:失败请求的 token 消耗。有些供应商在返回错误时也会计费,有些不会。这个差异在跑大批量测试时影响不小。我们在报告里单独列出失败请求的 token 消耗,方便判断。

4. 实操过程与核心环节实现

4.1 环境准备与安装

工具是 Python 写的,要求 3.10 以上。依赖不多,主要是 httpx 做异步请求、pydantic 做数据校验、rich 做终端输出、typer 做命令行。安装方式有两种,pip 直接装或者从源码装。

# 方式一:pip 安装 pip install llm-healthcheck # 方式二:源码安装 git clone https://github.com/example/llm-healthcheck.git cd llm-healthcheck pip install -e .

配置文件用 YAML,放在项目根目录或者通过--config指定。最小配置只需要填 API 的 base_url、api_key、model 名称。api_key 支持从环境变量读,避免写进配置文件。

providers: - name: provider-a base_url: https://api.example-a.com/v1 api_key: ${PROVIDER_A_KEY} model: model-x max_concurrency: 16 - name: provider-b base_url: https://api.example-b.com/v1 api_key: ${PROVIDER_B_KEY} model: model-y max_concurrency: 8 test_suites: - smoke - extraction - tool_calling judge: provider: provider-b model: model-y sample_rate: 0.3

4.2 跑第一轮体检

配置好之后,一条命令启动。

llm-healthcheck run --config config.yaml --output reports/

执行过程会在终端实时显示进度,包括已完成用例数、当前并发、成功率、平均延迟。跑完之后在reports/目录下生成带时间戳的 Markdown 和 JSON 报告。

第一次跑建议先用冒烟用例集,确认连通性和基本格式没问题,再跑完整用例集。完整用例集跑一轮的时间取决于用例数量和并发,我们 300 条用例、并发 16 的情况下大概 8 到 12 分钟。

4.3 读懂体检报告

报告的结构是这样的:顶部是总览,列出每个 provider 的总通过率、平均延迟、P95 延迟、总 token 消耗、估算成本。中间是分任务类型的通过率对比表。底部是失败用例详情。

总览部分要重点看两个东西。一是通过率和延迟的权衡,如果 A 通过率 92% 延迟 1.2s,B 通过率 88% 延迟 0.6s,选哪个取决于你的业务对延迟的敏感度。二是成本,有时候通过率高的模型成本反而低,因为它不需要重试。

分任务类型的对比表最能暴露问题。我们遇到过一家供应商,总体通过率 90%,看起来不错,但拆开看发现工具调用任务通过率只有 55%,其他任务都在 95% 以上。这种结构性短板在总分里被掩盖了,只有分维度看才能发现。

失败用例详情是排查的起点。每条记录包含输入、期望输出、实际输出、判定结果和判定理由。我习惯先按失败类型分组,看看是格式问题、内容问题还是拒答问题,再针对性分析。

4.4 回归对比

体检不是跑一次就完事。模型会更新,供应商会调整,你的业务需求也会变。所以工具支持把两次报告做对比,输出差异。

llm-healthcheck diff reports/run-20240101.json reports/run-20240201.json

对比报告会标出哪些用例从通过变成失败(回归),哪些从失败变成通过(改进),以及整体指标的变化。回归用例要重点关注,因为模型更新导致的回归往往很隐蔽,不对比根本发现不了。

我们内部的做法是每周跑一次全量体检,结果存档。一旦发现回归,立刻排查是模型侧的问题还是我们 prompt 侧的问题。这个习惯帮我们避免了好几次线上事故。

5. 常见问题与排查技巧实录

5.1 延迟数据波动大怎么办

延迟波动大通常有三个原因。第一,网络抖动。本地网络到 API 端点的链路质量直接影响测量结果,建议在稳定的网络环境下跑,或者用云服务器跑。第二,服务端负载波动。同一个 API 在不同时间段的延迟可能差一倍,建议在固定时间段跑,或者跑多轮取中位数。第三,并发设置不当。并发过高会导致服务端排队,延迟数据失真。

我们的做法是:每轮体检跑三次,取中位数;延迟统计里同时给出 P50、P95、P99,不只看平均值;如果 P99 和 P50 差距超过 5 倍,在报告里标记出来,提示可能存在长尾问题。

5.2 通过率忽高忽低

通过率不稳定,首先要看是不是 temperature 设置的问题。temperature 大于 0 时,同一个输入多次执行结果可能不同。体检场景下建议 temperature 设为 0,追求可复现性。如果业务本身需要 temperature 大于 0,那就在报告里注明,并且每个用例跑 3 次取多数结果。

其次要看是不是判定器太严格。比如 JSON 校验,如果模型输出字段顺序不同就判失败,那通过率会虚低。检查判定逻辑是否做了必要的归一化。

还有一个隐蔽的原因:供应商侧的模型版本悄悄更新了。有些供应商不通知就切换模型版本,导致行为变化。这种情况只能靠定期体检和回归对比来发现。

5.3 工具调用任务怎么测

工具调用是当前最容易出问题的任务类型,也是最难测的。我们的测试用例格式是这样的:给定一组工具定义和一段用户输入,期望模型输出正确的工具名和参数。判定时比对工具名是否匹配、参数是否完整、参数值是否正确。

常见失败模式有几种。一是工具名拼错或大小写不对。二是参数缺失,尤其是可选参数被漏掉。三是参数类型错误,比如该传数字传了字符串。四是该调用工具却直接回答了。五是该直接回答却调用了工具。这五种失败模式我们在报告里分别统计,方便定位。

注意:不同供应商的工具调用格式差异很大,有的用 OpenAI 的 function calling 格式,有的用自己的格式。工具里做了适配层,但适配层本身也可能有 bug,测试前先用一两条用例验证适配层是否正确。

5.4 长上下文测试的坑

长上下文测试最容易踩的坑是“中间迷失”。模型在长上下文里,对开头和结尾的信息记得牢,中间部分容易忽略。测试用例要专门设计成关键信息在中间位置,才能测出这个问题。

另一个坑是 token 计数。不同供应商的 tokenizer 不同,同样一段文本的 token 数可能差 20%。测试时要按供应商自己的 tokenizer 来算,不能用统一的估算。工具里集成了几个主流 tokenizer,配置里指定用哪个。

问题现象可能原因排查方向
延迟 P99 远高于 P50服务端排队或长尾请求降低并发,检查是否有超长输入
通过率波动超过 5%temperature 非 0 或模型版本变化固定 temperature,做回归对比
工具调用失败率高格式适配问题或模型能力不足先验证适配层,再换模型对比
JSON 校验失败率高输出含代码块或语法错误检查清洗逻辑,统计清洗率
成本估算偏差大缓存命中或失败计费未计入分别统计三类 token,核对账单

5.5 几个我踩过的坑

第一个坑:早期版本没有做请求去重,同一个用例因为重试被跑了多次,token 消耗翻倍。后来加了请求指纹,相同输入在短时间内只跑一次。

第二个坑:判定器的异常没有捕获,某个用例的判定器抛异常导致整轮测试中断。现在所有判定器都包了异常处理,判定失败记为“判定错误”而不是“用例失败”,两者在报告里分开统计。

第三个坑:报告里的成本估算用的是硬编码单价,供应商调价后没更新,导致估算严重偏低。现在单价全部从配置文件读,并且报告里注明单价的来源和日期。

第四个坑:并发调度的冷却逻辑早期写成了全局冷却,一个 provider 触发限流导致所有 provider 都暂停。现在冷却是按 provider 隔离的。

6. 工具选型与扩展思路

6.1 和现有评测框架的关系

有人问这套工具和 lm-evaluation-harness、promptfoo 这些是什么关系。定位不同。lm-evaluation-harness 偏学术,主要跑公开 benchmark,适合模型训练侧评估。promptfoo 偏 prompt 工程,适合对比不同 prompt 的效果。我们这套工具偏工程选型和上线前体检,关注的是 API 消费方的实际需求:多供应商对比、成本估算、稳定性、回归监控。三者可以互补,不冲突。

6.2 怎么接入自己的业务数据

最实用的方式是从线上日志采样。导出最近一周的请求日志,按任务类型分层抽样,人工标注期望输出,转成工具的用例格式。这个过程听起来麻烦,但一次投入长期受益。我们第一批用例就是这么来的,300 条用例花了两天标注,之后每次模型选型都直接复用。

用例格式是 JSONL,每行一条,字段包括id、task_type、input、expected、metadata。input支持多轮对话格式,expected可以是字符串、JSON 对象或者判定器配置。

{"id": "ext-001", "task_type": "extraction", "input": [{"role": "user", "content": "从以下文本抽取姓名和电话:张三,13800138000"}], "expected": {"name": "张三", "phone": "13800138000"}, "metadata": {"difficulty": "easy", "source": "prod-log"}}

6.3 后续可以扩展的方向

工具目前覆盖了文本类任务,多模态任务的支持还在规划中。图片输入、音频输入的测试用例格式和判定逻辑都需要重新设计,尤其是判定部分,图片生成的判定比文本难得多。

另一个方向是和 CI 集成。现在已经有命令行接口和 JSON 输出,理论上可以接进 CI 流水线,每次模型配置变更时自动跑一轮冒烟用例。这个我们内部已经在用,效果不错,能拦住大部分低级错误。

还有一个想法是做在线监控,把体检能力做成常驻服务,定期抽样线上请求做质量评估。这个复杂度高一些,涉及数据脱敏和采样策略,暂时没做。

7. 一些实操心得

跑了几十轮体检之后,我最大的体会是:测试用例的质量比工具本身重要得多。工具再好,用例设计得不对,测出来的结论就是错的。我见过有人用一堆“1+1 等于几”这种用例去测模型,然后得出“两个模型差不多”的结论,这种测试毫无意义。

第二个体会是:不要迷信单次结果。模型行为有随机性,服务端状态有波动,单次测试的结论可能是噪声。重要决策前至少跑三轮,看结果的稳定性。

第三个体会是:失败用例比成功用例更有价值。成功用例告诉你模型能做什么,失败用例告诉你模型在什么情况下会翻车。后者才是选型决策的关键依据。所以报告里的失败详情一定要认真看,不要只看总分。

最后一个建议:体检要常态化。模型在变,业务在变,一次体检的结论保质期很短。把体检纳入常规流程,每次模型切换或版本更新都跑一轮,才能持续保证质量。我们现在的节奏是每周全量一次,每次配置变更冒烟一次,这个频率供参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询