Skip to content
大语言模型应用测试体系
概述
LLM 应用测试与传统测试的差异
传统应用的测试建立在“确定性”之上:同一个函数传入同样的参数,返回结果可以精确预测。以 Jest 风格的断言为例,expect(output).toBe(expected) 完全有效,因为输出值可以被枚举和比较。
LLM 应用改变了这种假设。一个基于大语言模型的应用通常包含以下不确定因素:
- 模型输出具有随机性。即使温度参数设为 0,许多模型仍不能保证逐字一致的输出。
- 同一个 Prompt 模板,配上不同的用户输入,输出空间接近无限,无法像传统接口那样枚举。
- Prompt 措辞的微小变化、模型版本升级、上下文长度变化,都会导致输出风格或语义发生变化。
- 输出是否“正确”,往往不是一个布尔值,而是一个语义判断。
这些特征使得测试体系需要增加一层面向语义和生成质量的测试手段。这套体系不是用 LLM 测试来替代传统测试,而是在传统测试之上,补上针对 LLM 行为的新层次。
测试金字塔在 LLM 应用中的形态
传统测试金字塔的层级仍然适用,但每一层的内容发生了变化。
从底部到顶部:
确定性单元测试
针对不依赖模型的纯函数:输出解析器、JSON 提取器、文本切分器、关键词匹配逻辑、工具函数。这些测试仍然使用精确断言。Prompt 行为测试
直接对 Prompt 模板或 LLM 调用逻辑进行测试。测试中使用 mock 模型或少量真实模型调用,验证模型输出是否符合预期的行为模式。模型评估
使用一个独立的评测集,在真实模型上运行完整链路,计算指标分数,判断当前 Prompt、模型版本和参数配置的整体质量。端到端验证
覆盖从用户输入到最终响应的完整链路,包括外部 API 调用、数据库查询、工具调用等。这部分用例数量通常少,但覆盖面广。
Prompt 测试、模型评估与自动化验证的分工
三者解决的是不同阶段的问题。
- Prompt 测试发生在开发期。开发者编写一个 Prompt 模板后,需要快速知道它是否在主要场景下行为正确。这类测试追求速度和覆盖度,不追求统计意义上的质量评分。
- 模型评估发生在选型和迭代期。当需要比较两个 Prompt 版本、两个模型或两组参数时,评测集和指标提供可量化的对比依据。
- 自动化验证发生在交付期。将前两者嵌入持续集成流水线,使得每次变更都自动触发测试和评估,防止质量回归。
三者不是替代关系。例如,一次代码变更可能同时需要:运行快速 Prompt 测试保证基本行为正常,运行小规模评测保证指标不下降,再执行依赖 mock 的端到端用例验证流程畅通。
测试用例设计
输入空间与输出空间的拆解
设计测试用例的第一步,是拆解被测应用的输入空间和输出空间。
LLM 应用的输入通常包含以下几个部分:
- 用户输入:用户的自然语言消息。
- 系统指令:即 System Prompt,通常隐藏,但影响所有输出。
- 检索上下文:RAG 应用中从知识库检索到的文本片段。
- 对话历史:多轮对话中之前的交互内容。
- 配置参数:温度、最大生成 token 数、模型名称等。
输出空间则包括:
- 回答文本
- 结构化输出(JSON、代码、SQL)
- 工具调用序列
- 拒绝回答或追问澄清
将输入和输出拆开后,才能针对每个维度设计用例。例如,对于一个支持 JSON 输出的客服助手,至少要覆盖以下组合:
js
const cases = [
{
id: 'normal-json-query',
input: '你好,请问你们几点开门?',
system: '你是一个客服助手,总是输出 JSON 格式。',
expect: 'valid-json',
},
{
id: 'empty-input',
input: '',
system: '你是一个客服助手,总是输出 JSON 格式。',
expect: 'empty-input-handled',
},
{
id: 'very-long-input',
input: 'a'.repeat(5000),
system: '你是一个客服助手,总是输出 JSON 格式。',
expect: 'length-managed',
},
];以上 expect 字段值是行为标签,不是精确输出。用例的执行逻辑需要把每个标签映射到对应的验证函数,例如 valid-json 对应 JSON 解析检查,length-managed 对应输出长度上限检查。
Prompt 测试用例的覆盖维度
Prompt 测试用例不同于传统单元测试用例,它不依赖精确输出,而是验证模型是否在给定输入下表现出预期的行为。
常用的覆盖维度包括:
- 指令遵循:模型是否按照系统指令行动。例如要求“用一句话回答”,检查输出是否只有一句话。
- 格式约束:模型是否输出合法 JSON、Markdown、代码块。
- 上下文依赖:RAG 场景下,给定检索文本后,模型是否只基于上下文回答。
- 边界输入:空字符串、超长文本、特殊 Unicode、纯标点符号。
- 对抗输入:用户试图绕过系统指令,如“忽略之前的指令”。
- 角色一致性:模型是否始终保持设定角色,不从角色中跳出。
一个用例可以同时覆盖多个维度。但每个用例至少要有一个明确的“预期行为”,否则无法判断测试是否通过。
基于场景的用例组织方式
用例通常按照业务场景组织,而不是按照代码函数组织。例如一个商品问答助手,测试场景可以分为:
- 商品属性查询
- 价格与促销信息查询
- 库存与物流查询
- 售后与退换货政策查询
- 无法回答的模糊问题
每个场景下再扩充具体输入。这样的组织方式便于团队评审时阅读,也便于将真实用户日志转换成测试用例。
评测集与 Golden Answers
评测集的构成与来源
评测集是模型评估的基础。一条完整的评测数据通常包含以下字段,字段名可根据项目约定调整:
id:唯一标识input:用户输入expected_behavior:期望模型的输出行为golden_answer:对准确答案可得的任务,提供参考答案(Golden Answer)retrieved_contexts:RAG 场景下,模型应当使用的检索上下文metadata:来源、创建人、创建时间、适用场景
下面是参考结构:
js
const evalCase = {
id: 'case-001',
input: '这款手机支持 5G 吗?',
expected_behavior: '根据给定的商品参数回答是否支持 5G',
golden_answer: '支持,该手机支持 5G 网络。',
retrieved_contexts: ['商品参数:……'],
metadata: { source: 'user-log', author: 'qa-team', created: '2026-08-11' },
};评测集的来源可以有以下几种:
真实用户日志
从历史对话中抽样,去除隐私信息后转换为评测用例。这是最有价值的来源,因为能反映真实分布。人工构造
根据业务需求编写典型的、边界化的输入。适合覆盖真实数据中尚未出现的场景。历史 Bad Case
在评估和发布后发现的输出质量问题,转为回归用例。合成数据
使用 LLM 根据模板生成大量变体,再经人工筛选。合成数据可以扩充覆盖度,但不能完全替代真实数据。
样本数量与标注质量
评测集并非越大越好。传统机器学习中,更大数据集通常意味着更可靠的指标,但在 LLM 评估中,评测集的构建成本和更新成本高,几十条到几百条经过精心设计的样本往往足以支撑一个早期评测集。
重点在于样本的覆盖质量:
- 核心场景都要有代表样本
- 边界输入和对抗输入要占一定比例
- 每个样本必须经过人工确认,而不是只靠自动生成
标注质量直接影响指标可信度。如果 Golden Answer 本身有误,所有计算出的指标都会失真。建议遵循以下流程:
- 编写标注指南,说明每条 Golden Answer 的期望范围。
- 至少两人独立标注同一批数据。
- 对比标注结果,对不一致的样本讨论后重新标注。
- 定期抽查评测集中的现有数据,防止标注随时间推移不再符合需求。
避免评测集过拟合
评测集会随着迭代不断更新,但存在一个风险:当开发者反复针对同一评测集调整 Prompt 后,指标会变得虚高,而面向新的真实输入时表现依然不佳。
缓解方法:
- 将评测集分为调试集和留存集。开发过程中只使用调试集;留存集在关键节点运行一次,用于检验整体效果。
- 定期从真实日志中抽取新样本加入评测集,替换掉覆盖价值低的旧样本。
- 不要只盯着单个指标。指标上升的同时,也要检查 bad case 是否变成了另一类问题。
断言策略:规则、模型评判与人工抽查
基于规则的断言
规则断言是最稳定的断言方式。它不依赖模型,运行速度快,结果完全可解释。
适用场景:
- 输出必须包含某个关键词或实体
- 输出必须是合法 JSON,且满足特定 Schema
- 输出长度必须在某个范围内
- 输出必须不能包含禁用词
ts
import assert from 'node:assert/strict';
function assertProductIdIncluded(output: string, productId: string) {
assert.ok(
output.includes(productId),
`输出中应包含商品 ID:${productId}`
);
}
function assertValidJSON(output: string) {
const parsed = JSON.parse(output);
assert.ok(parsed.answer !== undefined, 'JSON 中应包含 answer 字段');
}规则断言的局限在于无法判断语义是否正确。模型可能生成一段语法合法、包含所有关键词,但语义完全错误的回答。
LLM-as-judge
当断言标准是“回答是否解决问题”“是否忠实于检索上下文”这类语义判断时,可以用另一个 LLM 担任评判者,即 LLM-as-judge。
一个简化实现如下:
ts
interface JudgeResult {
verdict: 'pass' | 'fail';
reason: string;
}
async function judgeWithLLM(
input: string,
output: string,
rubric: string,
judgeModel: LLMClient
): Promise<JudgeResult> {
const prompt = `
你是评测系统。请根据以下评判标准,判断候选回答是否合格。
===== 评判标准 =====
${rubric}
===== 用户输入 =====
${input}
===== 候选回答 =====
${output}
只输出 JSON,格式:
{"verdict": "pass" 或 "fail", "reason": "简要原因"}`;
const response = await judgeModel.complete(prompt);
return JSON.parse(response.text);
}使用 LLM-as-judge 时,需要注意:
- 评判标准(rubric)写得越具体,评判结果越稳定。例如,将“回答质量”替换为“回答是否直接回答了用户问题,是否基于给定上下文,是否包含未经验证的事实”。
- Judge 模型也受非确定性和自身偏差影响。必要时对同一条输出多次判定,以多数票作为最终结果。
- Judge 模型的能力需要达到一定水平。如果 Judge 模型比被测模型还弱,评判结果不可靠。
人工抽样复核
自动评估器用于批量筛选,不能完全取代人工审查。建议在每轮评估后按以下方式抽样复核:
- 抽样频率:每次完整评估运行后执行一次;每日回归任务至少抽查一次。
- 抽样量:从评测结果中随机抽取 10~20 条输出,另外抽取全部被判为 fail 的输出。
- 复核步骤:由标注人员独立阅读用户输入、检索上下文、模型输出和评估结果,判断自动评估是否准确;将不一致的样本提交团队讨论,必要时修正 Golden Answer 或评判标准,并将该样本加入留存集。
抽样复核的结论应记入评估报告,用于持续衡量自动评估器的可靠性。
自动评估器的可靠性验证
LLM-as-judge 并不是天然可信的。在将自动评估结果作为质量门禁之前,需要验证评估器本身。
验证方法是使用一个已经有人工标注结果的小样本集,将自动评估结果与人工判定进行比较:
ts
const humanLabels = [
{ caseId: 'case-01', label: 'pass' },
{ caseId: 'case-02', label: 'fail' },
// ...
];
const judgeLabels = await Promise.all(
humanLabels.map((item) => runJudgeForCase(item.caseId))
);
const agreement = calculateAgreement(humanLabels, judgeLabels);如果一致性较低,则说明自动评估器不能直接用于质量门禁,需要调整评判标准或更换 Judge 模型。
模型评估指标
常用指标与定义
Ragas 将 RAG 流水线拆分为检索(retriever)和生成(generator)两部分,并提供了面向不同组件的评估指标 [1]。
Faithfulness(忠实性)
Faithfulness 衡量生成答案是否忠于检索上下文。计算方式是:将答案拆分为多个事实性声明,检查每个声明能否由检索上下文推断出来,最终得分为:
text
被上下文支持的声明数 / 总声明数例如,模型输出包含 3 个事实性声明,其中只有 2 个能在给定上下文中找到依据,则该项得分为 2/3 [2]。
Answer Relevancy(答案相关性)
Answer Relevancy 衡量答案是否贴合用户问题。Ragas 的做法是让 LLM 根据答案反向生成若干个假设性问题,再计算这些生成的问题与原始问题在向量空间中的余弦相似度,取平均值作为分数 [2]。
Context Precision(上下文精确率)
Context Precision 衡量检索到的上下文中,相关信息是否排在靠前位置。它反映检索结果排序质量。
Context Recall(上下文召回率)
Context Recall 衡量检索出的上下文是否覆盖了 Golden Answer 所需的信息。需要参考答案才能计算。
其他指标
Ragas 还提供 Context utilization、Context entity recall、Noise Sensitivity、Summarization Score 等指标 [1]。这些指标分别面向 RAG 的不同失败模式,例如检索到无关信息时的抗噪能力,以及摘要任务中的信息保留程度。这些指标多数不需要参考答案,适合开放域评估,但依赖 LLM 作为评判者,存在 judge bias 和计算成本。
按应用类型选择指标
不同应用形式所关注的指标不同。
开放域问答
主要关注 Answer Relevancy 和 Faithfulness。答案需要紧贴问题,且不能编造事实。
RAG 应用
需要一并评估检索和生成两个环节。常用组合:
- Context Precision:检索结果中正确信息是否排在前面
- Context Recall:检索结果是否完整覆盖了所需信息
- Faithfulness:生成答案是否忠于检索上下文
摘要应用
关注 Summarization Score,以及事实一致性。模型不能凭空添加原文不存在的信息。
结构化输出应用
如果输出是 JSON 或代码,指标以格式校验通过率和字段准确率为主,语义指标反而次要。
Bad Case 分析与迭代
指标只能反映整体状况,真正推动质量提升的是 bad case 分析。
当一次评估完成后,筛选出得分较低的输出,按失败模式归类:
- 事实性错误
- 答非所问
- 格式不符合要求
- 错误地拒绝回答
- 检索到了无关内容
对每一类 bad case,判断根因是 Prompt 设计问题、检索问题还是模型能力问题。修复后,将 bad case 加入评测集,形成回归测试。
处理非确定性与外部依赖
输出非确定性的控制与容忍
LLM 输出天然带有随机性。在测试中,可以通过以下方式降低波动:
- 将温度参数设为 0
- 如果模型服务支持,设置 seed 参数
- 固定模型版本,不在测试过程中升级
- 对同一输入多次采样,以多数结果作为输出
但任何方法都不能完全消除非确定性。测试设计上需要容忍一定比例的随机失败:
- 断言只针对核心行为,不针对逐字输出
- 对语义类断言设置多次采样投票
- 如果偶发失败不影响整体指标,在门禁中允许基于多次运行的平均结果
LLM 调用的抽象与 Mock
在单元测试和快速集成测试中,不应该真实调用 LLM。通过抽象接口,可以注入 mock 实现:
ts
interface LLMClient {
complete(input: string, config: ModelConfig): Promise<string>;
}
class RealLLMClient implements LLMClient {
async complete(input: string, config: ModelConfig) {
return callProvider(input, config);
}
}
class MockLLMClient implements LLMClient {
async complete(input: string, config: ModelConfig) {
if (input.includes('天气')) {
return '今天晴,22 度。';
}
if (config.temperature === 0) {
return '这是一个确定性输出。';
}
return '默认回答。';
}
}有了接口抽象后,被测代码不需要知道底层是真实模型还是 mock。快速测试全部走 mock,长时评估才切换到真实模型。
测试数据管理与版本固定
LLM 应用测试涉及几类数据:
- Prompt 模板
- 评测集
- 模型版本与参数配置
- 外部依赖的返回数据
这些数据如果不同步更新,测试结果将无法复现。建议将评测集和 Prompt 模板纳入版本控制,并在评测报告中记录所使用的模型名、模型版本、温度参数和 Prompt 版本。
json
{
"prompt_version": "summary-v3",
"model": "gpt-4o",
"temperature": 0,
"seed": 42
}每次评估时,连同这一份配置信息一起记录。只有当所有输入版本一致时,两次评估结果才有可比性。
自动化验证与 CI/CD 集成
流水线中的测试阶段
LLM 应用的测试不能只有一个阶段。不同阶段的测试成本和置信度不同,建议分成三层:
- 快速层:每次提交或拉取请求时运行。使用 mock LLM,只跑规则断言,不涉及真实模型调用。
- 评估层:拉取请求合入前运行。使用真实模型,运行简化版评测集,覆盖核心场景。
- 回归层:每日或每周运行。使用完整评测集,生成详细指标报告。
一个 GitLab CI 配置示例:
yaml
stages:
- fast
- eval
fast-tests:
stage: fast
script:
- npm run test:unit
- npm run test:prompt-mock
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
regression-eval:
stage: eval
script:
- npm run eval:regression
artifacts:
paths:
- eval-reports/质量门禁设计
质量门禁的作用是对评估结果进行拦截。门禁条件通常有两种:
绝对分数门槛
例如“Answer Relevancy 的平均分不得低于某个阈值”。这种方式直观,但阈值一旦定死,会随模型升级和评测集更新而失真。
相对基线门槛
将本次评估结果与上一次基线对比,如果某个指标下降超过容忍范围,则拦截。这种方式更稳定,不会因为评测集扩展导致全盘失败。
ts
const result = await runEvaluation();
const baseline = await loadBaseline('main');
const dropped = baseline.faithfulness - result.faithfulness;
if (dropped > tolerance) {
throw new Error(`faithfulness 较基线下降了 ${dropped}`);
}门禁中不应该只包含平均分。应同时检查 bad case 数量,以及是否存在新出现的高风险失败模式。
Prompt 版本、模型版本与回归策略
每次 Prompt 修改、模型升级或参数调整,都应当触发一次回归评估。评估结果需要与版本信息绑定,便于追踪。
当完成一次新的 Prompt 迭代后,评估报告不仅显示指标变化,还应能追溯到是基于哪个 Prompt 版本、哪个模型版本产生的。否则,一次质量回退将难以定位原因。
测试成本与缓存策略
成本控制的基本方法
LLM 测试的每一次评估都产生真实调用成本。控制成本的基本思路是减少不必要的调用:
- 将大规模评测放到每日或每周任务,而不是每次提交都运行
- 按变更影响范围选择评测集子集,例如只修改了客服 Prompt,只运行客服相关用例
- 使用便宜的模型作为前筛选,只有通过初筛的样本才使用高成本模型复评
- 对结果进行抽样人工审查,而不是全量人工审查
缓存与结果复用
相同输入和相同配置下,模型输出不需要重复计算。对于快速回归任务,可以使用缓存结果。
ts
const cache = new Map<string, string>();
async function cachedComplete(
client: LLMClient,
input: string,
config: ModelConfig
): Promise<string> {
const key = JSON.stringify({ input, ...config });
const cached = cache.get(key);
if (cached) {
return cached;
}
const output = await client.complete(input, config);
cache.set(key, output);
return output;
}缓存键必须包含所有影响输出的因素:Prompt 版本、模型版本、参数配置、输入文本。任何一项变化都不能复用旧结果。
注意点
- 缓存结果只代表同一配置下的一次采样,不代表模型在该配置下的完整输出分布。对于依赖语义分布的质量判断,应按多次采样的结果进行。
- 缓存不能用于验证新 Prompt 或新模型。当编写新 Prompt、切换模型或调整参数时,必须绕过缓存发送真实请求,否则评估结果是旧输出的回放,不能反映新配置的真实表现。
- 缓存只适合同一配置的重复回归任务,例如定时执行的基线评测。在新配置的首次评估中,应明确关闭缓存。
工具链与框架选型
开源评测工具概览
LLM 评测工具仍在快速发展中,当前常见的开源工具有:
- Promptfoo:面向 Prompt 和模型配置的回归测试工具。通过配置文件声明用例,支持规则断言与模型评判,输出格式化的评测报告。
- Ragas:面向 RAG 流水线的评估库。提供 Faithfulness、Answer Relevancy、Context Recall 等指标。
- DeepEval:一个面向 LLM 应用单元的评估框架,提供类似测试框架的组织方式和多种断言方法。
选型时先明确需求:如果主要做 Prompt 快速回归,Promptfoo 更直接;如果做 RAG 指标评估,Ragas 提供了现成实现;如果需要把断言嵌入已有的 pytest 或用例组织,DeepEval 更接近常规测试框架。
与现有测试体系的整合
工具链不必替代现有测试体系。更常见的整合方式是:
- 单元测试沿用原有测试框架(Jest、Vitest、pytest)
- 模型评估使用独立脚本,将评测结果输出为 JSON 报告
- CI 流水线读取 JSON 报告,决定是否通过
下面是评估脚本输出到 CI 的报告 JSON 示例,指标数值只是占位:
json
{
"run_id": "run-2025-06",
"prompt_version": "qa-v7",
"model": "gpt-4o",
"metrics": {
"faithfulness": 0.82,
"answer_relevancy": 0.75
},
"passed": true
}报告中的数值仅作格式展示,真实阈值由项目根据评测集和业务目标确定;评测报告本身不决定门禁结果,门禁规则写在 CI 脚本中。
注意点
- 不要同时引入多个功能重叠的评测工具。每个工具的评测集、指标定义和 prompt 输入方式不同,同时使用会带来维护成本。
- 工具版本快速变化,指标算法可能有调整。锁定工具版本,并将版本号记录在评估报告中。
- 任何工具生成的指标,都应经过人工抽查验证,避免“工具评分为高、实际质量不行”的情况。
从 0 到 1 的落地路径
小范围试点与基线建立
在一个包含 LLM 的项目中引入测试体系,不要试图一次覆盖所有场景。选择一个核心且边界清晰的场景,例如“商品问答”或“文档摘要”,先完成一个最小闭环。
落地步骤:
- 从当前 Prompt 和模型出发,收集 30~50 条真实输入作为雏形评测集。
- 为每条输入标注预期行为或 Golden Answer。
- 编写一个简单的评估脚本,调用模型,计算两项指标。
- 运行一次评估,得到当前基线。
这一步的目标不是“达到某个分数”,而是让团队能够回答:当前应用的质量状况如何?哪些输入表现最差?
评估报告与团队评审
评估完成后,输出一份结构化报告,至少包含以下内容:
- 本次评估的 Prompt 版本、模型版本和参数
- 各指标得分
- 表现最差的 5~10 个用例
- 失败模式归类
评估结果不能只停留在文档中。建议定期组织团队评审,由开发者、产品和测试共同审查 bad case。评审结论可以分为:
- 需要修改 Prompt
- 需要补充评测集
- 需要更换模型或调整参数
- 属于已知限制,接受该行为
持续迭代机制
测试体系形成之后,需要维护其生命力:
- 每次 Prompt 变更,跑一次评估,将结果记录到版本历史
- 每次发布后发现质量问题,将 bad case 加入评测集
- 每季度从真实日志中抽样,补充评测集的覆盖范围
- 定期评估评测集本身,删除不再适用的旧用例
测试体系不是一次性构建的产物,而是随应用一起演化的基础设施。
