AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

成功率、恢复率与人工介入

我的学习 →

正在读取学习记录…

成功率回答“这批任务有多少完成了”;恢复率回答“遇到故障的任务中,有多少靠自动机制恢复”;人工介入率回答“这批任务有多少需要人参与”。这些比例看起来都像一个百分数,分母却不同。

像医院的三个数字:全部患者中治愈多少、出现并发症的患者中恢复多少、全部患者中有多少需要会诊。把每个分母圈出来,再讨论高低,才能避免拿一个数字替另一个数字回答问题。

先约定一次任务执行是什么

一位用户提交一次退款任务,助手内部查单两次、重试一次,仍是一项任务的一次执行。attempts 记录工具尝试次数,用于分析重试与成本,不能把它拆成三项新任务来算成功率。若为了评测随机模型对同一任务重复跑三轮,那是三个trial(执行轮次),要保留任务编号与轮次编号。

下面八行是预置执行记录。fault 表示执行中是否遇到故障;human 表示是否发生人工介入;terminal 为success、failed或pending(尚待结案);attempts 是工具尝试数。m1至m3直接自动完成;m4/m5遇故障后自动完成;m6经人工完成;m7人工介入后仍失败;m8没有故障但正在等待人工结案。

口径先锁定:统计本批全部八次执行,包括pending,并把pending单独报告。这样“还没处理完”不会因为被删出分母而把成功率抬高。若要另算“已结案成功率”,可以另列6/7,但必须写明它不包含待结案样本。

哪些指标共享分母,哪些不共享

总成功率的分子包含人工完成;自动成功率要求成功且没有人工介入。人工介入率的分子包括介入后成功、失败和待结案三种情况。它描述人力参与,不等于人工完成率。

自动恢复率采用“自动恢复成功的遇故障执行数 ÷ 全部遇故障执行数”。分子要求遇故障、最终成功、无人介入,分母则保留人工解决和最终失败的故障执行。这个口径能看出恢复机制替用户自动兜住了多少问题。

全部八次任务执行、四次遇故障执行与三次人工介入执行分别形成指标分母,自动恢复为二除以四
图1:先选中符合口径的执行集合,再数成功条件;工具尝试数另行统计。

从空目录保存并运行

在 PowerShell 中进入你平时使用的学习目录,再创建本节文件夹并进入:

Bash
mkdir l7-metrics
cd l7-metrics

用编辑器新建文件,把下面完整代码保存为当前文件夹的 metrics_lab.py。保存类型选“所有文件”,编码选 UTF-8;资源管理器开启“文件扩展名”,核对文件名以 .py 结尾。数据已经写在代码里,可以直接运行。

Python
"""对固定八次任务执行计算成功、自动恢复、人工介入和待结案。"""

from copy import deepcopy


# 一行是一项任务的一次执行,attempts记录内部工具尝试数,不展开成新任务。
RUNS = [
    {"id": "m1", "fault": False, "human": False, "terminal": "success", "attempts": 1},
    {"id": "m2", "fault": False, "human": False, "terminal": "success", "attempts": 1},
    {"id": "m3", "fault": False, "human": False, "terminal": "success", "attempts": 1},
    {"id": "m4", "fault": True, "human": False, "terminal": "success", "attempts": 2},
    {"id": "m5", "fault": True, "human": False, "terminal": "success", "attempts": 3},
    {"id": "m6", "fault": True, "human": True, "terminal": "success", "attempts": 2},
    {"id": "m7", "fault": True, "human": True, "terminal": "failed", "attempts": 1},
    {"id": "m8", "fault": False, "human": True, "terminal": "pending", "attempts": 1},
]


def fraction(numerator, denominator):
    """零分母返回None,展示时写N/A,避免伪造0%或100%。"""
    return None if denominator == 0 else numerator / denominator


def summarize(runs):
    """口径固定为本批全部任务执行;pending保留在总分母中并单列。"""
    ids = set()
    for row in runs:
        if row["id"] in ids:
            raise ValueError("任务执行编号重复")
        ids.add(row["id"])
        if type(row["fault"]) is not bool or type(row["human"]) is not bool:
            raise ValueError("故障和介入标记必须是布尔值")
        if row["terminal"] not in ("success", "failed", "pending"):
            raise ValueError("未知任务状态")
        if type(row["attempts"]) is not int or row["attempts"] < 1:
            raise ValueError("尝试次数必须是正整数")
    total = len(runs)
    successes = sum(row["terminal"] == "success" for row in runs)
    auto_successes = sum(row["terminal"] == "success" and not row["human"] for row in runs)
    faults = sum(row["fault"] for row in runs)
    recovered = sum(row["fault"] and row["terminal"] == "success" and not row["human"]
                    for row in runs)
    human = sum(row["human"] for row in runs)
    human_success = sum(row["human"] and row["terminal"] == "success" for row in runs)
    pending = sum(row["terminal"] == "pending" for row in runs)
    return {
        "success": (successes, total), "auto_success": (auto_successes, total),
        "auto_recovery": (recovered, faults), "human": (human, total),
        "human_success": (human_success, human), "pending": (pending, total),
        "attempts": sum(row["attempts"] for row in runs),
    }


def display(pair):
    numerator, denominator = pair
    value = fraction(numerator, denominator)
    return f"{numerator}/{denominator}=" + ("N/A" if value is None else f"{value:.1%}")


def main():
    result = summarize(deepcopy(RUNS))
    print(f"任务执行数={len(RUNS)} 工具尝试总数={result['attempts']}")
    for name in ("success", "auto_success", "auto_recovery", "human",
                 "human_success", "pending"):
        print(f"{name}: {display(result[name])}")
    no_faults = [row for row in RUNS if not row["fault"]]
    print(f"无故障子集/auto_recovery: {display(summarize(no_faults)['auto_recovery'])}")


if __name__ == "__main__":
    main()

回到刚才的 PowerShell,确认文件已经保存,再运行:

Bash
ls
python -X utf8 metrics_lab.py

程序输出:

TXT
任务执行数=8 工具尝试总数=12
success: 6/8=75.0%
auto_success: 5/8=62.5%
auto_recovery: 2/4=50.0%
human: 3/8=37.5%
human_success: 1/3=33.3%
pending: 1/8=12.5%
无故障子集/auto_recovery: 0/0=N/A

手算八次执行的各个比例

第一行是8次任务执行、12次工具尝试。尝试数来自1 + 1 + 1 + 2 + 3 + 2 + 1 + 1 = 12,成功率仍以8为分母。

success 包含m1至m6,共6次成功,所以6 ÷ 8 = 75.0%。auto_success 排除人工完成的m6,共5次,5 ÷ 8 = 62.5%。两个数差一项,能看出总体成绩中的人工贡献。

auto_recovery 的分母是m4、m5、m6、m7四次遇故障执行;其中m4/m5自动完成,分子为2,2 ÷ 4 = 50.0%。m6经人工成功,仍在分母中但不进入自动恢复分子;不能把分母改成所有8次执行,也不能只剩两次自动完成的故障执行。

human 是m6、m7、m8,共3次介入,3 ÷ 8 = 37.5%。human_success 只在这3次介入执行中看完成,只有m6,1 ÷ 3 ≈ 33.3%。pending 为m8一项,1 ÷ 8 = 12.5%,说明这个批次还会随着结案更新。

最后一行筛出没有故障的m1、m2、m3、m8。这组没有遇故障样本,恢复率为0/0,程序显示N/A(无法计算)。它既不能显示0%表示“完全恢复不了”,也不能显示100%表示“全部恢复”。

把一次人工完成改成自动恢复

在 RUNS 中只把m6的 "human": True 改为False,保存并运行 python -X utf8 metrics_lab.py。m6仍遇故障、仍成功,所以总成功率保持6/8;自动成功变成6/8;自动恢复变成3/4=75.0%;人工介入变成2/8=25.0%;人工完成变成0/2=0.0%。待结案与工具尝试数不变。恢复True后再运行测试。

这项变化表示同一成功原先依赖人工,现在改为自动取得;它没有增加总成功数。若只汇报总成功率,这项自动化改善就会被藏起来。

报告时保留计数与观察窗口

每个比例同时给出分子、分母、任务状态定义和采样窗口。只看两条故障任务的100%恢复,和看两千条故障任务的同样比例,证据强度相差很大。真实模型多轮执行还应按任务家族检查波动,并结合样本量考虑区间估计;

若只取已结案的执行,成功率变成6/7≈85.7%,它可以作为辅助视角,但不能悄悄替换本批6/8。若要计算“接受过恢复尝试的故障执行”的恢复率,则另设字段记录恢复尝试,另列指标名称与分母。不同口径都可以有用途,关键是让数字能回到记录。

Anthropic 对task、trial与重复执行指标的说明有助于区分任务与执行轮次,以及一次成功和多次稳定成功。真实评测可把同样的统计函数接到实际事件记录。

保存测试并检查关键条件

8项测试检查总体与自动成功、故障分母、两种人工指标、pending保留、任务与工具尝试分开、无故障N/A、空批次N/A与重复编号。 将下方代码保存为同一文件夹的 test_metrics_lab.py,运行:

Bash
ls
python -X utf8 -m unittest -v test_metrics_lab.py

看到 Ran 8 tests 与 OK 表示这些固定条件全部通过。若找不到文件,先用 dir 核对当前文件夹及两份文件名;若出现具体断言失败,先恢复正文中的代码与预置数据,再对照该项测试的条件。

完整测试:test_metrics_lab.py
Python
"""核对每个指标的分母,尤其恢复与人工成功不混用。"""

from copy import deepcopy
from pathlib import Path
import sys
import unittest

sys.path.insert(0, str(Path(__file__).parent))
import metrics_lab as lab


class MetricsTests(unittest.TestCase):
    def test_success_includes_human_success(self):
        result = lab.summarize(lab.RUNS)
        self.assertEqual(result["success"], (6, 8))
        self.assertEqual(result["auto_success"], (5, 8))

    def test_recovery_denominator_is_faulted_runs(self):
        self.assertEqual(lab.summarize(lab.RUNS)["auto_recovery"], (2, 4))

    def test_human_denominators_are_distinct(self):
        result = lab.summarize(lab.RUNS)
        self.assertEqual(result["human"], (3, 8))
        self.assertEqual(result["human_success"], (1, 3))

    def test_pending_remains_in_total(self):
        self.assertEqual(lab.summarize(lab.RUNS)["pending"], (1, 8))

    def test_attempts_do_not_expand_task_denominator(self):
        result = lab.summarize(lab.RUNS)
        self.assertEqual(result["attempts"], 12)
        self.assertEqual(result["success"][1], 8)

    def test_no_faults_produce_na_recovery(self):
        rows = [row for row in lab.RUNS if not row["fault"]]
        self.assertEqual(lab.display(lab.summarize(rows)["auto_recovery"]), "0/0=N/A")

    def test_empty_batch_has_no_percentage(self):
        self.assertEqual(lab.display(lab.summarize([])["success"]), "0/0=N/A")

    def test_duplicate_ids_are_rejected(self):
        rows = deepcopy(lab.RUNS)
        rows[-1]["id"] = "m1"
        with self.assertRaises(ValueError):
            lab.summarize(rows)


if __name__ == "__main__":
    unittest.main()
课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 10项任务中5项自动成功、2项失败、3项转人工,自动成功率是多少

10项任务中5项自动成功、2项失败、3项转人工,自动成功率是多少?

对应知识点:L7-metrics

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

自动恢复率的分母为什么和总体成功率不同?人工完成的故障任务计入哪里?

对应知识点:L7-metrics

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

4项任务出现故障,其中2项无人介入自动完成、1项人工完成、1项失败,写表达式计算自动恢复率。

对应知识点:L7-metrics

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

两批成功率都是80%,一批5条另一批500条,还要展示什么?

对应知识点:L7-metrics

图片查看器
100%

文章配图 · 按 Esc 关闭