Scorer 与 Metric:打分、按 epoch 归并、聚合成指标
30 秒导读: 求解侧(见 02-solver-taskstate)跑完后,每条样本手里握着一个模型
output。评判侧要回答:这条对不对?它是一条两级归并的流水线——先把「每条样本、每个 epoch」的输出判成一个Score,再按sample_id把同一样本的多个 epoch 归并成一条,最后跨所有样本聚 合成accuracy=0.82、stderr=0.03这种能写进日志、能对比的指标数字。
本章只讲评判侧。样本怎么跑出 output 归 01-eval-loop;打完的分怎么落进日志、怎么在 transcript 里回放归 06-log-transcript-sandbox。
1. 这是什么(零基础也能懂)
一句话定义: 评判侧 = 把「模型说了什么」变成「一个能统计的数字」的那套东西。
解决什么问题: 假设你在评一个数学题库,跑了 500 道题、每道重复答 4 遍(4 个 epoch)。你现在有 2000 段模型输出。你想知道的其实就一个数:正确率是多少、误差有多大。从 2000 段自然语言到「82% ± 3%」这两个数字,中间要跨三道坎:
| 坎 | 谁来跨 | 干的事 |
|---|---|---|
| 一段输出对不对? | Scorer(打分器) | 把一条样本一个 epoch 的 output 判成一个 Score |
| 同一题答了 4 遍,算哪次? | ScoreReducer(epoch 归并器) | 按 sample_id 把 4 个 Score 归并成 1 个 |
| 500 题合起来是多少? | Metric(指标) | 跨样本把一堆 Score 聚合成一个数 |
用起来什么样: 定义一个任务时,你把打分器和它要算的指标一起声明——指标是「挂在打分器上」的:
# 示意,非源码:一个最小任务的评判侧声明
from inspect_ai import Task, task
from inspect_ai.scorer import match, accuracy, stderr
@task
def my_task():
return Task(
dataset=[...],
solver=[...],
scorer=match(location="end"), # 打分器:看输出结尾是否等于 target
)
# match 内部已经声明了 metrics=[accuracy(), stderr()],
# 所以跑完自动得到 accuracy 和 stderr 两个数字。
一句话直觉: 把它当成一条打分—求平均的装配线。Scorer 是流水线最前端的质检员(逐件判合格/不合格),Reducer 是把「同一件产品的多次质检」并成一个结论,Metric 是最后把全批次的合格率算出来贴到质检报告上。
2. 顶层全景(评判侧怎么转)
2.1 一张流水线图
先说怎么读这张图:从上到下是数据流,左边标注每一步手里数据的「粒度」(一条是什么)。
粒度 评判侧流水线 在哪
─────────────────────────────────────────────────────────────────────
每样本×每epoch ┌─ TaskState + Target ─┐
的 output │ (求解侧交付) │
└──────────┬───────────┘
▼
① Scorer.__call__ scorer/_scorer.py:Scorer
判成一个 Score
│
每样本×每epoch [ Score ] value/answer/explanation
一个 Score │ scorer/_metric.py:Score
▼
② ScoreReducer scorer/_reducer/reducer.py
按 sample_id 归并 (mean / max / pass_at_k ...)
│
每样本一个 Score [ SampleScore ] scorer/_metric.py:SampleScore
(跨 epoch 已合) │
▼
③ Metric scorer/_metric.py:MetricProtocol
跨样本聚合 (accuracy / stderr / grouped ...)
│
整个 scorer 一组 [ EvalMetric ... ] _eval/task/results.py
最终指标 │ 装进 EvalScore → EvalResults
▼
写进日志
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Score | 一次打分的结果:值 + 抽取的答案 + 解释 + 元数据 | scorer/_metric.py:83 |
SampleScore | 给 Score 套上 sample_id / 样本元数据 / 打分器名 | scorer/_metric.py:165 |
Target | 被打分对象要比对的「标准答案」 | scorer/_target.py:4 |
Scorer | 协议:(state, target) -> Score,评判的核心接口 | scorer/_scorer.py:34 |
value_to_float | 把 Score.value(可能是 "C"/"I"/文字)翻成 float | scorer/_metric.py:199 |
ScoreReducer | 把同一样本多 epoch 的 list[Score] 归并成一个 Score | scorer/_reducer/types.py:7 |
Metric | 协议:list[SampleScore] -> Value,跨样本聚合 | scorer/_metric.py:259 |
eval_results | 总装:调 reducer 、调 metric、拼成 EvalResults | _eval/task/results.py:88 |
2.3 主线走一遍(高层)
- 求解侧把每条样本、每个 epoch 的
TaskState(含output)和Target交给 Scorer。 - Scorer 判出一个
Score(值 + 答案 + 解释)。 reduce_scores按sample_id分组,对每组用配置的ScoreReducer归并成一条(_eval/task/results.py:659)。- 归并后的每条
SampleScore交给每个 Metric,聚合成accuracy、stderr等数字。 - 数字装进
EvalScore.metrics,连同 reducer 名、样本计数一起进日志。
3. 核心原理(逐个机制,由浅入深)
3.1 打分的数据模型:Score、Value 与 value_to_float
要解决的小问题: 打分器五花八门——有的答「对/错」,有的给 0.87 的连续分,有的一次返回一组子分。得有一个统一的容器装下这些,还得有办法把它们都变成能求平均的数。
Score 装什么。 Score 是个 pydantic 模型,四个可读字段 + 一段编辑历史:
| 字段 | 意思 |
|---|---|
value | 分值本身,类型是 Value(见下) |
answer | 从模型输出里抽出来的那段答案(可选) |
explanation | 为什么给这个分(可选,常存判分模型的完整回复) |
metadata | 附加信息 |
history | 分数被人工改过的编辑轨迹 |
依据:scorer/_metric.py:83(Score),字段定义在 :86-99。
Value 是三态的。 一个分可以是标量、可以是一列、可以是一张表——这决定了下游归并和聚合要分三条岔路走:
Value = 标量 "C" / 0.87 / True / 3
| 列表 (list) [0.8, 0.6, 1.0] —— 一条样本多个子分
| 字典 (dict) {"fluency": "C", "accuracy": "I"} —— 命名子分
依据:scorer/_metric.py:49(Value 类型别名)。Score 提供 as_float() / as_dict() / as_list() 一组便捷读法(:132-156),内部靠 _as_scalar() 强制取标量,取不到就抛错(:158)。
四个约定字母。 字符串打分器爱用四个单字母,value_to_float 把它们翻成分数:
| 字面量 | 常量 | 翻成 float |
|---|---|---|
"C" correct | CORRECT | 1.0 |
"I" incorrect | INCORRECT | 0.0 |
"P" partial | PARTIAL | 0.5 |
"N" noanswer | NOANSWER | 0.0 |
依据:常量在 scorer/_metric.py:36-46。value_to_float() 是个工厂,返回一个 to_float 闭包;它还顺手认 "yes"/"true"→1、"no"/"false"→0、纯数字串直接转,遇到 list/dict 打警告返回 0:
# 真实实现节选,scorer/_metric.py:228 to_float
def to_float(value: Value) -> float:
if isinstance(value, int | float | bool):
return float(value)
elif value == correct:
return 1.0
elif value == partial:
return 0.5
elif value == incorrect or value == noanswer:
return 0
elif isinstance(value, str):
... # 认 yes/true/no/false 和数字串
这就是「文字判分」和「数值统计」之间的翻译层——所有 Metric 想把 Score 变成数,都要过它。
未打分的哨兵值。 有时打分器判不了(拒答、解析失败),但你仍想保留上下文。Score.unscored() 造一个 value=NaN 的分:NaN 是全套 reducer 和 metric 都认得的「跳过我」信号(scorer/_metric.py:101 unscored)。下游到处能看到 math.isnan(...) 的过滤(如 _eval/task/results.py:390)。
ScoreEdit 与编辑历史。 history 字段存的是 ScoreEdit 列表,支持事后人工改分(比如复核 UI 里把一个误判从 I 改成 C),UNCHANGED 是「这个字段不动」的哨兵。依据:scorer/_metric.py:64(ScoreEdit)、:60(UNCHANGED)。
3.2 Scorer 协议与注册:打分器怎么定义、指标怎么挂上去
要解决的小问题: 打分器要能被名字创建(从日志里恢复、从命令行指定),还要携带「我该算哪些指标」这份信息。
协议本体极简。 一个 Scorer 就是「吃 state 和 target,吐一个 Score」的异步函数:
# 真实定义,scorer/_scorer.py:34 Scorer
@runtime_checkable
class Scorer(Protocol):
async def __call__(self, state: TaskState, target: Target) -> Score | None:
...
@scorer 装饰器做三件事(关键)。 它不只是注册,还把 metrics 塞进 registry 元数据:
- 强制打分函数是 async(不是就抛
TypeError,_scorer.py:173)。 - 把
{SCORER_METRICS: metrics}连同其它元数据打进 registry(_scorer.py:184)。 - 注册名字,让
scorer_create(name)能按名重建(_scorer.py:116)。
所以 match()、choice() 这些内置打分器,函数签名上都顶着 @scorer(metrics=[accuracy(), stderr()])——指标是声明在打分器上的,这就是为什么第 1 节里只写了 scorer=match() 却自动得到了两个指标。下游 scorer_metrics(scorer) 从 registry 把这份 metrics 取回来(_scorer.py:246),ScorerInfo.from_scorer 再拆出来喂给结果装配(_eval/task/results.py:60)。
Target:被比对的标准答案。 Target 就是一个字符串序列的薄包装,.text 把多个拼成一个字符串:
# 真实定义,scorer/_target.py:4 Target
class Target(Sequence[str]):
def __init__(self, target: str | list[str]) -> None:
self.target = target if isinstance(target, list) else [target]
@property
def text(self) -> str:
return "".join(self.target)
多个 target 常用于「任一命中即算对」——比如 match 逐个 target 试,命中一个就返回 CORRECT(scorer/_common.py:26)。
3.3 内置打分器:五种「怎么判对」
要解决的小问题: 从「结尾字符串相等」到「让另一个模型来判」,判对的方式差别巨大。Inspect 内置了一梯队,由简到繁:
| 打分器 | 怎么判 | 文件:符号 |
|---|---|---|
match / includes | 字符串匹配(结尾/任意/精确/包含),可选数值归一 | scorer/_match.py:9 / :46 |
pattern | 正则抽答案,再和 target 比 | scorer/_pattern.py:47 |
choice | 多选题,比对选中的字母集合 | scorer/_choice.py:45 |
model_graded_qa / _fact | 让判分模型给 GRADE:C/P/I | scorer/_model.py:87 / :29 |
math | 用 sympy 判数学表达式等价 | scorer/_math.py:683 |
下面挑三个有代表性的讲清「不显然的地方」。
① 字符串匹配的数值坑(match)。 天真做法 "25".endswith("5") 是 True,但 25 ≠ 5。所以 numeric=True 时走的是另一条路:抽出数字、归一化、按数值相等比,而不是字符串后缀比:
# 真实实现节选,scorer/_common.py:61 match_str
if numeric and _is_number(t):
# 目标是数:从 value 里抽数、两边归一、按数值比,
# 不再走下面的文本比较(否则 "25".endswith("5") 会误判)
v = strip_numeric_punctuation(v)
t = normalize_number(t)
...
它还会剥掉货币符号 $€£、千分位逗号(scorer/_match.py:19-30 的 docstring 明说了这套规则),但故意不剥百分号——60% 到底是 60 还是 0.6 有歧义,所以不猜。
② 模型判分与提示注入防御(model_graded_qa)。 让另一个模型当裁判:把「题目 + 提交答案 + 评分标准 + 判分指令」拼成一个 prompt,发给判分模型,再用正则从回复里抠出 GRADE: C。
抠取用的默认正则藏着两个巧思:
# 真实定义,scorer/_model.py:302 DEFAULT_GRADE_PATTERN
DEFAULT_GRADE_PATTERN = (
rf"(?is).*(?<!\w)GRADE(?!\w){_GRADE_SPACING}:{_GRADE_SPACING}([CPI])"
)
- 开头
.*是贪婪的(配 DOTALL):强制匹配到最后一个GRADE: X。指令要求裁判把结论放最后,所以思维链里早先出现的、或被提交答案「注入」进来的 GRADE 都不算数。 (?<!\w)GRADE(?!\w)给 GRADE 加词边界:downgrade:这种普通词不会被误当成判决。
更狠的是,neutralize_structural_delimiters 会把数据里的 [BEGIN DATA]/[END DATA] 改写成带连字符的形式,防止模型输出伪造这些结构标记来越狱判分 prompt(scorer/_model.py:367)。这是判分侧的安全边界,值得单独记一笔。
传一组 model 进去时,model_graded_qa 会退化成 multi_scorer(..., "mode")——多个裁判各判一次、取众数(scorer/_model.py:151)。
③ 数学等价(math)。 判「\frac{1}{2} 和 0.5 相等」不能靠字符串。math() 是一条五段流水线:预处理 unicode → 抽 \boxed{} 内容 → LaTeX 归一 → 转成 sympy 能解析的串 → 用 sympy 的 equals() 或近似相等判定(scorer/_math.py:834 check_answers)。它是整个 scorer 目录里工程量最大的一支,依赖可选的 sympy(缺了就抛 pip_dependency_error,:702)。
3.4 按 epoch 归并:ScoreReducer
要解决的小问题: 同一道题跑了 4 个 epoch,得到 4 个 Score。做统计前得先把它们并成一条——但「并」有很多种口径:平均?取最好?多数表决?4 次里至少 1 次对?
归并器就是 list[Score] -> Score。 协议只有一个方法(scorer/_reducer/types.py:7)。内置一梯队,注册名即用法:
| reducer | 归并口径 | 符号 |
|---|---|---|
mean | 平均分(默认) | reducer.py:41 mean_score |
median | 中位数 | reducer.py:63 median_score |
mode | 众数(投票) | reducer.py:12 mode_score |
max | 取最高 | reducer.py:206 max_score |
at_least_k | ≥ k 次达标即算 1 | reducer.py:86 at_least |
pass_at_k | k 次里至少 1 次对的概率估计 | reducer.py:121 pass_at |
pass_k | k 次全对的概率估计 | reducer.py:167 pass_k |
三态 Value 各归各的。 每个 reducer 内部都先看第一条有效分是标量/列表/字典,再分派到 _count_scalar / _count_list / _count_dict(或统计版 _compute_*_stat)。列表按下标逐位归并、字典按 key 逐键归并。NaN(未打分)一路被 _is_reducible 过滤掉(reducer.py:569)。
pass@k 的 NaN 陷阱(巧妙处)。 pass_at_k 有个短路:剩下的都错也不够 k 个失败时直接返回 1.0。但如果 NaN 过滤后有效 epoch 少于 k,这个短路会吐出假的 1.0。代码专门先挡了一道:
# 真实实现节选,scorer/_reducer/reducer.py:138
if total < k:
# NaN 过滤后有效 epoch 不足 k,pass@k 无定义,
# 返回 NaN 哨兵而不是短路产生的假 1.0
return float("nan")
名字里带 k 的动态注册。 pass_at/at_least 这类用 setattr(reduce, REDUCER_NAME, f"pass_at_{k}") 把 k 编进名字(reducer.py:162)。反过来,create_reducers("pass_at_5") 用正则把尾部 _5 拆成 k=5——但只在字面名不是已注册 reducer 时才拆,免得把自定义的 top_5 误拆(registry.py:148)。validate_reducer 还会在 k > epochs 时对内置 reducer 报错(registry.py:180)。
默认是 mean。 任务没配 reducer 时,结果装配用 mean_score() 兜底(_eval/task/results.py:357 _reduced_views)。
3.5 跨样本聚合:Metric
要解决的小问题: 归并后每条样本一个分,现在要把 500 条压成几个数字。
Metric 就是 list[SampleScore] -> Value。 注意入参从 Score 升级成了 SampleScore——因为像分组、聚类这种指标需要读样本的元数据。旧签名 list[Score] 仍兼容,靠 is_metric_deprecated 类型嗅探区分(_eval/task/results.py:594)。
# 真实定义,scorer/_metric.py:259 MetricProtocol
@runtime_checkable
class MetricProtocol(Protocol):
def __call__(self, scores: list[SampleScore]) -> Value:
...
内置指标: 都在 scorer/_metrics/ 下,每个都是薄薄一层 numpy:
| metric | 算什么 | 符号 |
|---|---|---|
accuracy | 全体 to_float 后求比例 | _metrics/accuracy.py:15 |
mean | 全体求均值 | _metrics/mean.py:11 |
stderr | 均值标准误(CLT),可选聚类版 | _metrics/std.py:51 |
std / var | 样本标准差 / 方差 | _metrics/std.py:148 / :183 |
bootstrap_stderr | bootstrap 重采样标准误 | _metrics/std.py:16 |
grouped | 按元数据分组各算一遍,再出「all」 | _metrics/grouped.py:15 |
accuracy 内核就三行——但注意它专门挡了空列表除零(accuracy.py:30):
# 真实实现节选,scorer/_metrics/accuracy.py:29
def metric(scores: list[SampleScore]) -> float:
if not scores:
return 0.0 # 空列表返回 0 而非除零崩溃
total = 0.0
for item in scores:
total += to_float(item.score.value)
return total / float(len(scores))
@metric 与 epoch 契约(较新、易忽略)。 装饰器有个 scores 参数,声明这个指标想吃「归并前」还是「归并后」的分:
scores= | 拿到的输入 |
|---|---|
"auto"(默认) | 归并后的分(除非显式关掉 reducer) |
"reduced" | 明确要每样本一条(reducer 跑完) |
"unreduced" | 每样本每 epoch 一条(每个 epoch 当独立观测,如 frequency()) |
依据:scorer/_metric.py:383(MetricScores)、:412 装饰器。结果装配据此把指标拆成 reduced / unreduced 两个「视图」分别算(_eval/task/results.py:195-227)。
分组指标返回字典(巧妙处)。 grouped(metric, group_key) 按样本元数据里的 key 把样本切成组,对每组跑一遍内层 metric,返回一个 {组名: 值} 字典,外加一个「all」聚合;它甚至挡了「某个组名恰好叫 all」的碰撞(grouped.py:76)。字典型返回值下游会被展开成多个 EvalMetric——这是 dict 型 Value 的用途之一。
4. 深入实现:一条分从 Score 到 EvalResults
要解决的小问题: 把上面三级串起来,看总装到底怎么拼。入口是 eval_results(_eval/task/results.py:88)。
4.1 两级归并的代码位置
eval_results() results.py:88
└─ 每个 scorer:
compute_eval_scores_for_views() results.py:173 —— 决定 reduced/unreduced 视图
├─ reduce_scores(reducer) results.py:659 ← ① 按 epoch 归并
│ 按 sample_id 分组 → reducer 归并成一条
└─ compute_eval_scores() results.py:232
└─ scorer_for_metrics() results.py:377 ← ② 跨 样本调 metric
call_metric() results.py:580
① 按 epoch 归并 发生在 reduce_scores:按 str(sample_id) 分组,对每组 reducer([score...]) 归并成一条 SampleScore(results.py:659)。
② 跨样本聚合 发生在 scorer_for_metrics:先把 NaN(未打分)样本滤掉并计数 unscored_samples(results.py:388),再对每个 metric 调 call_metric。
4.2 指标返回值的三种落地
scorer_for_metrics 里,metric 返回的 Value 按形状落成不同数量的 EvalMetric(results.py:418-447):
| metric 返回 | 落地 |
|---|---|
| 标量 | 一个 EvalMetric |
dict(如 grouped) | 每个 key 一个 EvalMetric |
| list | 每个下标一个 EvalMetric(名字带 -1/-2 后缀) |
4.3 字典型 Score 走另一条装配路
如果打分器本身返回 dict 型 value(命名子分),且指标也按 dict 声明,则走 scorers_from_metric_dict(results.py:467):它把每个子 key 抽成标量分、对该 key 跑指标,每个 key 产出一个独立的 EvalScore——相当于「一个打分器展开成多个」。它还支持 fnmatch 通配 key(resolve_glob_metric_keys,results.py:622)。
4.4 最终产物
每个 (scorer, reducer) 组合产出一个 EvalScore,里面装着 metrics: dict[str, EvalMetric]、reducer 名、scored_samples / unscored_samples 计数(results.py:450)。所有 EvalScore 收进 EvalResults.scores。归并后的每样本分另外收进 EvalSampleReductions 供回放(results.py:203)。这些结构怎么进日志文件,见 06-log-transcript-sandbox。
5. 巧妙之处(可借鉴的技术)
- 文字/数值的翻译层集中在一处。 所有「C/I/yes/0.87」到 float 的转换只有
value_to_float一个入口(scorer/_metric.py:199),Metric 和 Reducer 都复用它。想改判分口径,改一个工厂参数即可。 - NaN 作为全局「未打分」哨兵。 从
Score.unscored()(_metric.py:101)到 reducer 的_is_reducible(reducer.py:569)到结果 装配的math.isnan过滤(results.py:390),一条哨兵贯穿三级,不用额外的 Optional 包装。 - 判分正则「绑最后一个 GRADE」防注入。
DEFAULT_GRADE_PATTERN用贪婪.*+ 词边界,让思维链和被提交内容里的伪 GRADE 都失效(_model.py:302);再叠一层结构标记中和(_model.py:367)。评测框架里少见的把「裁判被越狱」当威胁模型处理。 - pass@k 的不足样本护栏。 有效 epoch < k 时返回 NaN 而非短路假 1.0(
reducer.py:138),避免 NaN 过滤悄悄抬高指标。 - 归并前先校验形状一致。 dict/list 型分在归并前会检查所有 epoch 的 key 集合 / 长度一致,不一致直接报错而不是悄悄丢数据(
reducer.py:477、:510)。
6. 边界与局限
value_to_float遇到 list/dict 只会打警告返回 0(_metric.py:247)。想统计复合分,得让 Metric 自己拆(如grouped那样),不能指望默认转换。math()依赖可选的 sympy,未安装直接抛错(_math.py:702);且它的 LaTeX 归一里有若干数据集特化的硬编码(如把F_{30}直接替换成斐波那契值832040,_math.py:339),迁到别的题库要留意。- 模型判分的可靠性取决于裁判模型和 prompt;
partial_credit只在用默认指令时生效(自定义指令要自己写 P 的说明,_model.py:119docstring 明示)。 - 旧签名 Metric(
list[Score])仍支持但已废弃,靠运行时类型嗅探区分(results.py:594),嗅探失败会当成 deprecated 处理。
7. 横向对比
同属评测框架,评判侧的设计取舍差异:
- 谁定义「对」:Inspect 把 Scorer 做成可注册、可组合的一等公民,并把 metrics 声明挂在 scorer 上;许多轻量评测库把「判分」和「统计」揉在一个函数里,难以按 epoch 归并或换指标口径。
- 多次采样的处理:Inspect 显式分「按 epoch 归并(Reducer)」和「跨样本聚合(Metric)」两级,
pass@k/pass^k这类需要多次采样的指标是 reducer 而非 metric——这让「同题多答」的统计语义清晰。
具体与本 shelf 兄弟项目的逐项对照,见总库 doc 的「评判与聚合」原理条目。
8. 代码地图(导航索引)
| 主题 | 文件 | 关键符号 |
|---|---|---|
| 打分数据模型 | src/inspect_ai/scorer/_metric.py | Score、SampleScore、ScoreEdit、Value、value_to_float |
| 分值常量 | src/inspect_ai/scorer/_metric.py | CORRECT、INCORRECT、PARTIAL、NOANSWER |
| Scorer 协议与注册 | src/inspect_ai/scorer/_scorer.py | Scorer、scorer、scorer_metrics、ScorerSpec |
| 评判目标 | src/inspect_ai/scorer/_target.py | Target |
| 字符串匹配打分 | src/inspect_ai/scorer/_match.py、_common.py | match、includes、str_match_scorer、match_str |
| 正则打分 | src/inspect_ai/scorer/_pattern.py | pattern、match_first、match_all_groups |
| 多选打分 | src/inspect_ai/scorer/_choice.py | choice、_score_target |
| 模型判分 | src/inspect_ai/scorer/_model.py | model_graded_qa、model_graded_fact、DEFAULT_GRADE_PATTERN、neutralize_structural_delimiters |
| 数学等价打分 | src/inspect_ai/scorer/_math.py | math、extract_answer、check_answers |
| epoch 归并器 | src/inspect_ai/scorer/_reducer/reducer.py | mean_score、max_score、mode_score、at_least、pass_at、pass_k |
| reducer 注册 | src/inspect_ai/scorer/_reducer/registry.py | score_reducer、create_reducers、validate_reducer |
| reducer 协议 | src/inspect_ai/scorer/_reducer/types.py | ScoreReducer、ScoreReducers |
| Metric 协议 | src/inspect_ai/scorer/_metric.py | MetricProtocol、Metric、metric |