Skip to content

如何评估测试集?

本文讨论如何衡量 LLM 测试集的覆盖率与质量,以及新增测试用例能否为已有测试集合带来有效增量。

问题和粗思考

(手写的)

在测试LLM的时候,需要构造些测试用例,也就是golden test,然后有很多具体的方法来构造, 比如手工构造,或者用deepeval的synthesizer去构造。

于是这里自然而然的产生了一个问题,当通过各种方法构造出测试用例后,如何衡量构造测试集的质量? 在过去的软件工程,通常这是在代码review的过程里来判断的,而现在LLM出现了,自然而然希望寻求自动化的评测过程。

一个随想:评测xx的质量带来了新一层的评测,这是否是一种递归,过去人的参与解决了这种递归,机器可能只能尽可能通过确定性检查项逃避这种递归。 而且其实表现怎么样是有大致的“感受”的,

那么这个需求对应的场景很具体:

  1. 判断已有测试用例的覆盖率
  2. 在添加新的测试用例时,如何评判新添加测试用例对已有测试集合的提高程度

这里有已有测试集,以及产品原始文本(产品所有信息的集合),这两者并不是严格相同的。

代码的覆盖率往往讨论覆盖的代码执行路径,插桩统计即可完成,而LLM产品侧的覆盖率是带着语义的测试, 一旦涉及到语义的覆盖率就带来了巨大的不确定性。

讨论覆盖率就必须评估已有文件,从现有状态陈述构造出问题-答案对,这个思路和构造golden test同构的, 问题就变成了构造出来的测试集多大程度上可以代表原始文本,这是个条件概率下的判断,构造出的测试用例作为可信的代理指标。

但这个方法有缺陷,如果用synthesizer生成所有的golden测试用例,并使用他们,那么覆盖率(在当前代理指标下)就到100%了, 这显然是不对的,代理信号一定有损失,所以当前方法显然有局限。

注:以下全部为ai生成的内容,但我阅读审阅并精简修改。

研究综述

本文关心的问题是:给定描述产品能力、业务规则和运行约束的原始文本,和golden testset

  1. 如何判断测试集的覆盖率
  2. 新加测试是否有效,还是语义重复

这不是一个已经存在统一定义和标准工具的问题。DeepEval、Ragas 等框架可以从文档生成测试 数据,也能评估单个测试结果,但截至目前没有提供一个经过充分验证的“业务语义覆盖率”。 相邻研究分布在自动问题生成、文本信息覆盖、需求追踪、model-based testing、RAG 测试集 覆盖、自然语言推理和数据集多样性等领域。下面按照这些方法究竟把什么当作覆盖基准来整理。

分析文本

定义覆盖率

覆盖率首先是一个“选择表示”的问题。

代码覆盖率能够定义 line coverage、branch coverage,是因为程序已经具有明确的语法结构和 执行语义。

自然语言文本没有天然的最小语义单位。同一段文字可以被表示为多种模式:

  • 句子或段落;
  • 信息单位;
  • 原子命题;
  • 实体和关系组成的知识图;
  • 条件、动作、结果、异常组成的业务规则;
  • 从文本中可能提出的问题;
  • 真实用户可能触发的业务场景。

不同的表示代表了不同的衡量标准,“文本覆盖率”不是脱离表示而独立存在 的客观数字。任何方法都至少包含下面三个步骤:

  1. Unitization:把原始文本转换为一组可计数或可加权的单位。
  2. Alignment:判断一个测试用例覆盖了哪些文本单位。
  3. Aggregation:把局部匹配汇总成覆盖率,并处理单位重要性、重复和不确定性。

句子与段落

文本分析方法意味着如何理解文本,通常来讲伴随着对原始文档的拆分和处理。 并且按照已有的内容来做基准去衡量测试集合的覆盖率。 以下提供了几种可能方法,实际使用可以单独使用或者混合使用交叉验证。

最简单的方法是把句子、段落、标题或列表项作为 source units,然后把 Golden 映射回这些 单位:

source-unit coverage
  = 被至少一个 Golden 覆盖的 source unit
    / 全部 source units

这类方法的优点是:

  • 分母直接来自原始文本,不需要先生成一套 reference questions;
  • 单位能够绑定稳定的 source span,结果容易解释;
  • 文档变化后可以根据 source span 做增量更新;
  • 容易建立一个便宜、确定性较强的 baseline。

它的问题也很直接:

  • 一个句子可能包含多条独立规则;
  • 一条规则可能跨越多个句子;
  • 标题、定义、例子和关键限制会被赋予相同权重;
  • 两个问题映射到同一句,不代表它们覆盖了相同的业务行为;
  • 相似度匹配容易忽略否定、数值边界和条件变化。

AURA-QG 论文: 是目前与这个思路最接近的公开工作。它把普通段落拆成句子,把较短的列表整体作为necessary Information Units,同时建立列表项、连续句子窗口等 optional Information Units。问题先经过 bi-encoder 检索,再由 cross-encoder 重排.

若问题能够可靠映射到一个 IU,则相应的 necessary IU 被标记为 covered。最终 Coverage 是被覆盖的 necessary IU 占全部 necessary IU 的比例。

AURA-QG 的贡献是证明了一条可实现路径:

document -> information units -> question/IU alignment -> set coverage

但它主要服务于 factoid、FAQ、remembering 和 understanding 类型的问题。论文本身明确 说明,它没有处理 application、analysis、evaluation、复杂推理,以及表格、图表和图片; 它也承认 structural entropy 不能衡量 conceptual breadth。它对人工偏好的验证只有 24 组 question-set comparison:Coverage 与人工多数票一致 19 次,即 79%。这足以说明方法 值得作为 baseline,但不足以证明句子/list block 是业务语义的正确分母。

因此,这类方法适合回答:

测试问题是否触及了原文的大部分信息表面?

不适合直接回答:

测试集是否覆盖了所有业务条件、异常流程和行为差异?

原子命题

句子级分母过粗时,可以把文本拆成能够独立判断真假的最小命题。PropSegmEnt 论文 的研究动机 就是:一个句子往往表达多个 proposition,这些 proposition 可能拥有不同的真值,因此 句子级 entailment 会掩盖局部差异。

退款超过七天仍未完成时,商户可以联系客服,但不能重复发起退款。

例如以上案例可以被拆成:

P1:退款存在超过七天仍未完成的状态。
P2:在该状态下,商户可以联系客服。
P3:在该状态下,商户不能重复发起退款。

这时可以定义:

proposition coverage
  = 被现有 Golden 覆盖的原子命题
    / 从文档提取出的全部有效原子命题

相对于直接生成 Golden,原子命题有几个优势:

  • 它仍然接近原文,而不是提前引入用户语气、persona 和问题形式;
  • 每个命题可以强制绑定证据片段;
  • 可以用 textual entailment 检查命题是否真的来自原文;
  • 更容易发现一个句子中只覆盖了一半信息的情况;
  • 更适合作为后续问题生成、业务规则抽取和覆盖分析的公共中间表示。

但“原子性”本身并不完全客观。同一内容可以按不同粒度拆解;代词、时间条件、跨句依赖和 隐含前提可能使单个命题失去上下文。自动提取还会出现:

  • omission:漏掉原文命题;
  • over-splitting:把不可独立成立的信息错误拆开;
  • under-splitting:多个条件仍混在同一个命题中;
  • hallucination:加入原文没有表达的条件;
  • duplication:同一命题被不同措辞重复抽取。

因此,命题提取器本身必须作为一个待评估的测量工具,而不能直接视作真值。至少需要检查:

  • proposition 对 source span 的 entailment precision;
  • 在人工标注小样本上的 proposition recall;
  • 不同 prompt、模型、seed 下的稳定性;
  • 对否定、阈值和条件变更的敏感度;
  • 多次提取时新增命题数量的饱和曲线。

Synthesizer 抽样

使用 DeepEval Synthesizer 从文档生成 Golden,并不是一个错误方向。更准确的理解是: Synthesizer 定义了一个由文档、模型、prompt 和配置共同决定的问题分布。

document + generator configuration
  -> distribution of possible probes

可以把这个过程写成一个条件抽样问题。定义:

  • \(D\):原始文档;
  • \(\theta\):生成配置,包括模型、prompt、temperature、evolution 类型和过滤规则;
  • \(q\):一个生成出来的问题;
  • \(T\):当前待评估的 Golden testset;
  • \(\operatorname{Cover}(T,q)\):当前测试集是否已经覆盖问题 \(q\) 所代表的语义场景。

那么一次生成可以写成:

\[ q \sim G(\,\cdot\mid D,\theta) \]

这里的 G 不是“原始文本中全部可能问题”的真实分布,而是 Synthesizer 在给定文档和配置后 诱导出的条件分布。改变模型、prompt、temperature 或 evolution 配置,都会得到不同的 G。 seed 通常用于取得这个分布中的不同样本,而不应被误认为业务语义空间本身的一部分。

如果 Synthesizer 还会过滤不清晰、不可回答或不忠实的问题,那么最终留下来的 probe 实际 来自进一步的条件分布:

\[ G_{\mathrm{valid}}(q \mid D,\theta) = G\bigl(q \mid D,\theta,\operatorname{Valid}(q,D)=1\bigr) \]

将条件分布展开后,可以写成带归一化常数的形式:

\[ G_{\mathrm{valid}}(q \mid D,\theta) = \frac{ G(q \mid D,\theta)\, \mathbf{1}\!\left[\operatorname{Valid}(q,D)=1\right] }{ Z(D,\theta) } \]

其中:

\[ Z(D,\theta) = \Pr_{q\sim G(\cdot\mid D,\theta)} \left[\operatorname{Valid}(q,D)=1\right] \]

\(\mathbf{1}[\cdot]\) 是指示函数:条件成立时取 1,否则取 0。这个过滤会改变采样分布。例如复杂但 容易被判为“不清晰”的问题可能被系统性删除。因此,最终覆盖率只能解释为“对通过过滤的 生成问题分布的覆盖”,不能自动推广到所有可由原文提出的问题。

在这个条件分布下,当前测试集 T 的 generator-conditioned coverage 可以严格定义为:

\[ C_G(T \mid D,\theta) = \Pr_{q\sim G_{\mathrm{valid}}(\cdot\mid D,\theta)} \left[\operatorname{Cover}(T,q)=1\right] \]

也就是:

\[ C_G(T \mid D,\theta) = \mathbb{E}_{q\sim G_{\mathrm{valid}}(\cdot\mid D,\theta)} \left[ \mathbf{1}\!\left[\operatorname{Cover}(T,q)=1\right] \right] \]

它是一个条件概率或条件期望,不是对未知的“全部业务场景数量”的直接计数。

实际运行时无法枚举 G_valid,只能用 Monte Carlo 样本估计。独立生成 n 个 held-out probes:

\[ q_1,q_2,\ldots,q_n \overset{\mathrm{iid}}{\sim} G_{\mathrm{valid}}(\,\cdot\mid D,\theta) \]

样本估计量是:

\[ \widehat{C}_G(T\mid D,\theta) = \frac{1}{n} \sum_{j=1}^{n} \mathbf{1}\!\left[\operatorname{Cover}(T,q_j)=1\right] \]

例如独立生成 100 个有效 probe,其中 73 个对应的语义场景已被 T 覆盖,则当前估计值为 0.73。这个数字仍应同时报告采样数量和置信区间;如果 probes 来自同一个 batch、同一个 source unit 或存在去重/重写依赖,它们不满足严格独立同分布假设,应按 source unit 做 cluster bootstrap,而不能直接把 100 个问题都当作独立观测。只有在 iid 假设近似成立时, 才可以使用下面的标准误近似:

\[ \operatorname{SE}\!\left(\widehat{C}_G\right) \approx \sqrt{ \frac{ \widehat{C}_G(1-\widehat{C}_G) }{n} } \]

从这个分布反复生成问题,相当于使用一批随机探针对原始文本进行测量。于是可以定义:

generator-conditioned probe coverage
  = 独立生成的有效 probes 中
    能被当前测试集覆盖的比例

这个数字表示:

当前测试集覆盖了多少生成器认为可能由该文档产生的问题概率质量。

它不表示:

当前测试集覆盖了原始文本中的全部潜在业务场景。

QA-based summarization evaluation 已经使用过相近思想。QAEval 从 reference/source 生成问题, 再检查 candidate 是否能够回答,以估计二者的信息重合。QuestEval 进一步使用双向 question generation 和 QA,在不依赖人工 reference summary 的情况下估计信息 precision 与 recall。

这些工作说明:使用生成问题作为信息覆盖的代理测量有研究基础。但测量结果是整个 pipeline 的合成结果:

question generation recall
  x question faithfulness
  x alignment accuracy
  x answer/coverage decision accuracy

如果 Synthesizer 总是围绕容易表达的事实生成问题,它会系统性忽略复杂条件;如果生成器 反复采用相同模板,样本数量增加也不会扩展语义支持集。

要避免“自己生成、自己达到 100%”的回环,应把生成用途分开:

原始文档
  -> candidate generation:生成可能加入测试集的 Golden
  -> measurement generation:独立生成只用于评估的 held-out probes

measurement probes 不进入被评估测试集,并应使用不同 seed、prompt,最好还包括不同模型。 更严格的做法是 leave-one-generator-out:由 G1 生成 candidate tests,由 G2/G3 生成测量 探针。如果 G1 产生的测试只能覆盖 G1 自己的表达风格,而无法覆盖其他生成器产生的有效 问题,说明覆盖提升主要来自 generator bias。

还应按 source unit 分层生成,而不是让生成器自由决定问题分布。例如每个 source unit 生成相同数量的 held-out probes,先计算每个 unit 内的覆盖,再在 units 之间汇总。这样 一个容易生成问题的段落不会因为产出几十个相似问题而取得过高权重。

这个分层条件抽样可以进一步写成:

\[ U(D)=\{u_1,u_2,\ldots,u_K\} \]

其中 u_i 是从文档 D 得到的 source unit。先选择一个 unit,再在该 unit 的条件下生成:

\[ q_{ij} \sim G_{\mathrm{valid}}(\,\cdot\mid D,u_i,\theta) \]

为每个 unit 定义权重 w_i,并要求:

\[ w_i\ge 0, \qquad \sum_{i=1}^{K}w_i=1 \]

那么分层后的总体覆盖定义为:

\[ C_{\mathrm{stratified}}(T\mid D,\theta) = \sum_{i=1}^{K} w_i\, \Pr_{q\sim G_{\mathrm{valid}}(\cdot\mid D,u_i,\theta)} \left[\operatorname{Cover}(T,q)=1\right] \]

若每个 unit 生成 m_i 个 probe,其样本估计量为:

\[ \widehat{C}_{\mathrm{stratified}} = \sum_{i=1}^{K} w_i \left( \frac{1}{m_i} \sum_{j=1}^{m_i} \mathbf{1}\!\left[\operatorname{Cover}(T,q_{ij})=1\right] \right) \]

当所有 source units 同等重要时,可以设:

\[ w_i=\frac{1}{K} \]

如果目标是生产代表性或风险覆盖,也可以让 w_i 来自生产频率、业务严重度或人工优先级。 但此时指标的名字和解释必须相应改变,例如 production-weighted probe coverage 或 risk-weighted probe coverage,不能再称为未经加权的文本覆盖率。

自由生成与分层生成估计的是两个不同目标:

自由生成:
  按 Synthesizer 自己的偏好分布估计概率质量覆盖。

按 source unit 分层生成:
  控制生成器的主题偏好,估计各文本单位上的平均覆盖。

两者都值得保留。前者能观察生成器自然产生的问题空间;后者更接近“原始文本各部分是否都 被平等检查”。它们的差距本身也是一个有意义的诊断信号:差距很大,说明 Synthesizer 的 自然采样分布对部分文本单位存在明显偏置。

Embedding 与主题

上面提到的文本拆分和分析在尝试定义离散的命题,而我们实际上可以避免定义命题, 而是把原文 chunk 和测试问题放在同一 embedding space。

几何计算

几何计算实际测量了什么? 设文档被切成 \(N\) 个 chunks,其向量为:

\[ X=\{x_1,x_2,\ldots,x_N\} \]

测试集包含 \(M\) 个问题,其向量为:

\[ Y=\{y_1,y_2,\ldots,y_M\} \]

可以先对文档向量 \(X\) 做 K-means,得到文档语义簇:

\[ D_1,D_2,\ldots,D_K \]

然后把每个测试问题分配到最近的文档 cluster。第 \(k\) 个 cluster 附近的问题数量为:

\[ n_k = \sum_{j=1}^{M} \mathbf{1} \left[ \operatorname*{arg\,min}_{\ell} d(y_j,\mu_\ell)=k \right] \]

其中 \(\mu_\ell\) 是第 \(\ell\) 个文档 cluster 的质心。\(n_k\) 可以回答:

哪些文档主题附近聚集了较多测试问题,哪些主题附近问题较少?

还可以比较文档 units 与测试问题在各 cluster 上的数量占比:

\[ p_D(k)=\frac{|D_k|}{N}, \qquad p_Q(k)=\frac{n_k}{M} \]

\(p_Q(k)\gg p_D(k)\) 表示问题对该 cluster 过度集中,\(p_Q(k)\ll p_D(k)\) 表示相对稀疏。 这仍然测量的是测试注意力分配,而不是 cluster 内部的信息覆盖。

但它只是 question density / cluster occupancy,不是覆盖率。一个 cluster 即使有一百个 问题,这些问题也可能全部围绕 cluster 中同一个局部事实,其他内容仍未被触及。

要观察 cluster 内部是否存在空白,应该把方向反过来:对每个文档 chunk,寻找离它最近的 测试问题:

\[ r_i = \min_{1\le j\le M}d(x_i,y_j) \]

如果使用 cosine similarity,也可以写成:

\[ s_i = \max_{1\le j\le M} \cos(x_i,y_j) \]

\(r_i\)\(s_i\) 直接给出的仍然只是 proximity profile

  • 哪些文档 chunk 附近存在语义相近的问题;
  • 哪些 chunk 到所有测试问题都很远;
  • 各 cluster 内的最近距离分布;
  • 新增测试前后,文档到测试集的距离是否缩短。

这个方向不能反过来。如果计算的是“每个问题到最近文档的距离”,主要测到的是问题是否与 文档相关或可由文档回答,而不是文档内容是否被测试集覆盖。

距离代理

距离不能直接等同于覆盖。要从 \(r_i\) 得到一个比例,必须额外引入判定规则。例如设阈值 \(\tau\),把距离小于阈值的文档 chunk 当作“被覆盖”:

\[ \widehat{C}_{\tau} = \frac{ \sum_{i=1}^{N} w_i\, \mathbf{1}[r_i\le\tau] }{ \sum_{i=1}^{N}w_i } \]

其中 \(w_i\) 是文档 chunk 的权重。如果所有 chunks 同等重要,则 \(w_i=1\)。这个指标更准确 的名字是:

embedding neighborhood coverage,或 thresholded semantic coverage proxy。

同样可以计算单个 cluster 内的代理覆盖:

\[ \widehat{C}_{k,\tau} = \frac{ \sum_{x_i\in D_k} w_i\,\mathbf{1}[r_i\le\tau] }{ \sum_{x_i\in D_k}w_i } \]

\(n_k\) 回答“附近有多少问题”,而 \(\widehat{C}_{k,\tau}\) 回答“该 cluster 中有多少文档 单位落在至少一个问题的阈值邻域内”。后者更接近覆盖,但仍然依赖 embedding、chunk 和 \(\tau\) 三层代理假设。

也可以不做硬阈值,而是使用随距离衰减的核函数:

\[ \widehat{C}_{\mathrm{soft}} = \frac{ \sum_{i=1}^{N}w_i\,k(r_i) }{ \sum_{i=1}^{N}w_i } \]

例如:

\[ k(r)=\exp\left(-\frac{r^2}{2\sigma^2}\right) \]

这种 soft score 会让离测试问题越近的文档单位获得越高分。它适合比较同一 embedding 模型、 同一分块方法和同一参数下两个测试集的相对变化,但不应被解释成“真实业务场景已经覆盖了 百分之多少”。

从原始文本到这个数字,实际经过了三层代理假设:

原始文本 chunk
  -> 假设 chunk 是合适的语义单位
embedding distance
  -> 假设向量距离保留了与测试有关的语义差异
距离阈值或核函数
  -> 假设足够近就表示已经被测试覆盖

任何一层不成立,最终数字都可能偏离真正想测量的业务覆盖。

  • 观察指标:分别报告聚簇密度、最近距离和经校准的覆盖代理;三者不能相互替代,也不能由问题数量直接推出覆盖率。

Proxy 值选择

阈值 \(\tau\) 不应根据 embedding 分布任意选择。可以抽取一批 (document unit, question) pair,由人标注:

covers
partially covers
related but not covered
unrelated

然后用这些标注:

  • 画距离对 covers/not-covered 的 precision-recall curve;
  • 根据希望保证的 precision 或 recall 选择 \(\tau\)
  • 在独立验证集上报告误判率;
  • 比较不同 embedding、cross-encoder 和 NLI judge;
  • 检查分数对 \(\tau\)、K-means 的 \(K\) 和 chunk 粒度是否敏感。

更可靠的 pipeline 通常是:

embedding / cluster
  -> 高召回地找出 candidate question-unit pairs
cross-encoder 或 NLI / LLM judge
  -> 判断是否真的覆盖相同命题或行为
人工标注样本
  -> 校准阈值并测量判断器误差

研究与边界

2025 年的一篇 RAG semantic test coverage 预印本采用了相近思路:

  • 对文档切块并编码;
  • 对文档 embedding 做 K-means;
  • 过滤明显无关的测试问题;
  • 对每个文档块寻找最近的测试问题;
  • 汇总 basic coverage、cluster-weighted coverage;
  • 为低覆盖 cluster 提取主题并建议新问题。

相关资料:Methodological Framework for Quantifying Semantic Test Coverage in RAG Systems

该论文把平均相似度等结果称为 coverage,但从测量有效性上,更准确的说法是 embedding-based soft coverage proxy。它可以快速找到没有测试问题靠近的文档区域,也 适合比较两个测试集谁更接近文档整体分布,却不能可靠区分:

  • 条件成立与不成立;
  • 允许与禁止;
  • 阈值前、阈值点和阈值后;
  • 相同主题下不同 expected behavior;
  • 单轮明确问题与多轮歧义;
  • RAG 命中与错误检索导致的不同系统路径。

因此它更适合作为 gap discovery 和粗粒度 distribution coverage,而不是业务等价性的 最终判定。SemHash、近似去重和 Vendi Score 也属于这一层:它们可以发现表面重复和多样性 趋势,但无法独立证明新增测试具有新的业务判别意义。

Model-based testing

方法

前面的方法把句子、命题或 embedding 区域作为覆盖对象,但“提到同一件事”不等于“验证了 同一种业务行为”。Model-based testing 采用另一条路径:先根据需求建立系统行为的抽象 模型,再从模型生成测试或把已有 Golden 映射到模型,最后计算模型元素的覆盖。

自然语言需求
  -> 业务场景的结构化表示
  -> 可枚举的模型元素与覆盖准则
  -> Golden 与模型元素对齐
  -> 覆盖结果

这种方法把“业务场景覆盖”变成可以计算的问题。例如状态机中的一次状态转移、决策表中的 一条规则、流程图中的一个异常分支,都可以成为明确的覆盖单位。自然语言 use case 生成 acceptance tests 的研究也采用类似路线:先提取实体、语义角色、前置条件和流程,再构造 scenario path 与 test oracle。UMTG

这条路线的第一个难点不是覆盖公式,而是 semantic modeling:如何把自然语言中的业务 语义转换为合适的结构。这里不存在一个适用于所有文本的通用 schema。表示方法取决于什么 因素会改变系统的预期行为,而表示方法又决定覆盖率的分母。

表示选择

文本表达的业务语义 更合适的表示 可以测量的覆盖
明确状态和事件驱动的生命周期 FSM / labeled transition system 状态、转移、guard、非法转移、关键路径
多项条件共同决定一个结果 Decision table / DMN 规则、条件组合、边界、冲突
多角色、预先定义的端到端流程 Workflow graph / BPMN 活动、分支、异常流、消息和路径
顺序不固定、由事件和人工判断推进的案例 Case model / CMMN 可用活动、milestone、事件和约束
权限、禁止和属性约束 Policy model / ABAC permit/deny 规则、属性组合、规则冲突
始终成立或最终必须发生的性质 Invariant / temporal logic 性质、触发条件及反例路径
计算公式、静态配置和跨实体关系 Constraint / relational model 输入分区、数值边界、不变量和关系约束

FSM 适合订单、退款、审批等强状态业务。它至少需要表示当前状态、事件、guard、动作和下一 状态;测试分母也自然落在状态、转移和路径上。FSM 测试研究已经形成 state、transition、 transition-pair 和路径等不同强度的覆盖准则,但这些准则只适用于确实具有状态机语义的系统。 FSM coverage 综述 · 基于需求的 FSM 测试

没有明显生命周期的条件规则更适合 decision table。比如“地区、账户类型和风险等级共同决定 退款资格”,重点是条件组合和结果,而不是虚构状态转换。OMG 的 DMN 专门用于精确定义业务决策和决策表,并明确设计为 与流程模型配合,而不是替代流程模型。

涉及参与者协作和预先定义顺序时,可以使用 BPMN;如果活动顺序由持续变化的案例信息和人工判断决定, CMMN更合适。两者的差异说明“弱状态流程”也不能简单压成 FSM:有些业务关注固定控制流,有些关注当前可执行的活动和约束。

权限规则则应表示成 subject-resource-action-environment-effect 一类的策略模型,并明确 规则合并和冲突处理。XACML 3.0 就是这类访问控制策略语言;用 FSM 表达 permit/deny 规则通常会制造大量没有业务意义的状态。 跨步骤安全性质,例如“退款完成后永远不能再次退款”,也可以独立表示成 invariant 或时序 性质,再检查是否存在违反性质的执行路径。 Property-pattern coverage 展示了怎样围绕这类性质定义 模型测试覆盖。

组合表示

真实业务通常需要组合表示。退款领域可能同时包含:

  • FSM:退款生命周期及合法状态转移;
  • Decision table:是否满足退款资格;
  • Policy model:谁有权提交、审核或撤销;
  • Invariant:退款金额不能超过已支付金额;
  • Workflow graph:商户、平台和支付渠道之间的协作。

因此不应先设计一个万能 schema,再强迫所有文本填入。结构化过程应先判断文本表达的业务 语义,再选择一种或多种表示:

文本语义分类
  -> 选择一种或多种表示
  -> 抽取模型元素并回链 source span
  -> 将 Golden 映射到模型元素
  -> 按表示方法分别报告覆盖

覆盖评测

完成结构化之后,评测分为两层。第一层检查模型是否忠实于原文:模型元素必须能回链到 source span,并区分原文明确陈述、合理推断和未说明的信息。第二层才检查 Golden 是否覆盖 模型元素。一个测试不仅要在文字上相关,还必须构造对应的条件或状态,并用 expected output 验证模型规定的行为。

对任一表示 \(m\),把从文档 \(D\) 中得到的模型元素记为 \(E_m(D)\),Golden testset 记为 \(T\), 可以计算:

\[ C_m(T\mid D) = \frac{ \sum_{e\in E_m(D)} w_e\, \mathbf{1}[\exists t\in T:\operatorname{Covers}_m(t,e)] }{ \sum_{e\in E_m(D)} w_e } \]

关键在于 \(\operatorname{Covers}_m\) 随表示方法变化:FSM 要验证转移和结果,decision table 要命中条件组合,policy model 要验证 permit/deny,invariant 要检查反例。不同表示的覆盖率 不能直接相加,应该作为一组指标分别汇报。

覆盖强度也可以逐步提高。例如 FSM 可以从 state coverage 提高到 transition、guard、 forbidden transition 和 path coverage;decision table 可以从命中每条规则提高到条件组合 和边界覆盖。无论采用哪种模型,都应保留下面的追溯链:

source span -> model element -> Golden -> expected result

这条链路让覆盖数字可以回到具体需求,也能区分三种不同缺口:文档没有描述、结构化模型没有 抽取到,或者 Golden 没有覆盖。

方法边界

Model-based testing 的优势是更接近真正的业务行为,代价是模型本身也可能错。每个模型元素 都应保留 source span、抽取版本和“不明确”状态;文档没有说明的状态、结果或例外不能由 抽取器自行补全。文本到 FSM、decision table 或策略模型的自动抽取,本身需要单独验证, 不能因为模型是结构化的,就把它当成原文真值。

比较方法

可以把目前的方法排列成几个递进层次:

Level 0:表面和主题覆盖
  token、chunk、embedding、cluster

Level 1:信息内容覆盖
  sentence、list block、AURA-QG Information Units

Level 2:命题覆盖
  atomic propositions、entailment

Level 3:业务行为覆盖
  state transition、decision rule、workflow、policy、invariant

Level 4:使用与风险覆盖
  生产流量、事故、对抗输入、未文档化行为

这些层次不是互相排斥的候选答案。低层指标通常更便宜、更稳定,适合快速扫描;高层指标更 接近真正目标,但提取和验证成本更高。一个实际系统可能需要由低到高逐层过滤:

embedding 发现空白区域
  -> source units 建立可追踪分母
  -> atomic propositions 细化复合句
  -> typed business models 表达行为差异
  -> production events 检验外部有效性

区分指标

覆盖率不是测试集质量的全部。

  • 覆盖率:是否触及已知文本单位、命题或业务规则。
  • 丰富度:同一规则下,是否覆盖了会改变预期行为的条件、边界和状态;paraphrase 不算新增丰富度。
  • 代表性:是否接近真实使用分布,尤其是高频和高风险场景。
  • 测试有效性:是否能区分正确和错误实现。

因此,覆盖率达到 100% 只表示当前分母已饱和,不能代表测试已经足够。

Comments