Day 10 / 共 20 天 · 第 2 周收官

多维评测 Eval + CI 阻断

昨天(Day 09)讲了 Agent 的「记忆」怎么让它越用越准。今天回答一个更根本的问题:怎么证明它真的准?怎么量化"这个 Agent 靠不靠谱"、怎么防止改一版 prompt 就偷偷变差?答案是像跑单元测试一样,给 Agent 跑一套"标准考卷"打分,不及格就拦合并。这是第 2 周可信底座的收官,为下周(D11 起)进入运行时打好"质量能被度量"的地基。

📍 你在 20 天里的位置(可信底座 → 运行时 → 精读)
D09 记忆 D10 评测 D11 安全底座 D12 server 运行时 D13 CLI/MCP D14 精读 sre-rca D15 全家福+元 Agent
💡 用一个类比先兜住今天(延续「盖楼/物业」世界观) 评测就是新房交付前的验房:物业请来验房师(scorer),拿着一张验收清单(golden set),对着标准答案(ground_truth)逐项打钩——水电通不通、防水漏不漏、门窗正不正。不是给一个笼统的"85 分",而是分项验收(多维评分),哪项不合格一眼看出。验收不过,物业就不发钥匙(CI 阻断合并)。而"离线验房"之外还有"入住后日常巡检"(在线评测 ObservableScorer)——交付把下限、巡检抓意外。今天 8 讲就是这套验房流程。
L01

为什么 Agent 必须评测

🤔 痛点:不评测会出什么事故?你把 sre-rca 的 synthesizer prompt 改了一个字,本地跑了两条 case 看着"还行"就合了。上线一周后 SRE 抱怨:"最近根因老是给错方向"。你回滚排查半天,才发现那次改动让 top1 命中率从 0.8 掉到 0.4——但当时没有任何信号拦住你。这就是"没有验房就发钥匙"的后果:住进去才发现漏水。

传统代码有单元测试:输入固定,输出对就 PASS。但 Agent 输出的是"自然语言 + 大模型推理结果"——同一个问题两次回答可能措辞都不一样,没法简单 assert 相等

可如果不评测,你改一句 prompt、换个模型,根本不知道 Agent 变好了还是变差了。所以框架把评测做成一等公民:准备一套"标准考卷"(golden set),给 Agent 跑一遍,从多个维度打分,不及格就拦住不让合并。代码在 packages/ai-trust-toolkit/src/ai_trust_toolkit/eval/

AGENTS.md 里有条硬规矩:新 Agent 必须先有评测样本 + Critic,才能合并。评测不是可选项,是准入门槛。
L02

golden set 与评测样本(数据结构走读)

"标准考卷"就是一批 EvalSample,存成 JSONL 文件(每行一个 JSON 样本),放在每个 agent 的 data/eval_samples.jsonl。先看它的真实 dataclass 定义(eval/schemas.py:61):

# eval/schemas.py:61
@dataclass
class EvalSample:
    id: str
    description: str
    input: dict[str, Any]              # 初始 state 的输入(trace_id / service_name / user_question)
    ground_truth: GroundTruth          # 标准答案(人工标注)
    mock_responses: dict[str, list[str]] | None = None  # 喂给 FakeLLM 的固定回复
👶 逐字段翻译id = 这道题的编号;input = 把哪些字段塞进图的初始 state(相当于"题干");ground_truth = "参考答案"(下面细讲);mock_responses = 预先录好的"假大模型该怎么答"——键是 system prompt 里的关键词(如"Trace 专家"),值是该节点每次调用返回的字符串序列。有了它,评测走 FakeLLM 就能几秒跑完、每次分数完全一样。

真实样本长这样(来自 apps/sre-rca-agent/data/eval_samples.jsonl 第 1 行,已折叠 mock_responses):

{
  "id": "db-pool-001",
  "description": "service-A 连接池打满 + 慢查询",
  "input": {
    "trace_id": "trace-db-pool-001",
    "service_name": "service-A",
    "user_question": "service-A 14:23 突然变慢,DB 连接池打满"
  },
  "mock_responses": {
    "Trace 专家":   ["{\"evidence_ids\":[\"trace-1\"], \"hint\":\"SELECT span 8.2s\", ...}"],
    "Metric 专家":  ["{\"evidence_ids\":[\"metric-1\"], \"hint\":\"connection_pool_usage 98%\", ...}"],
    "最终综合节点": ["{\"hypotheses\":[{\"category\":\"DB_POOL_EXHAUSTED\", ...}], \"confidence\":\"高\"}"],
    "质检员":      ["{\"critique_passed\": true, \"rule_results\": {...}}"]
  },
  "ground_truth": {
    "primary_category": "DB_POOL_EXHAUSTED",
    "candidate_categories": ["DB_POOL_EXHAUSTED", "DB_SLOW_QUERY"],
    "keywords_in_hypothesis": ["service-A", "连接池", "98%"],
    "min_supporting_specialists": 3,
    "overall_confidence_at_least": "高"
  }
}

再看"参考答案" GroundTruth 的字段(schemas.py:21)——每个字段就是评测的一把尺子:

# eval/schemas.py:21
@dataclass
class GroundTruth:
    primary_category: str = ""                       # 期望的第一根因 category
    candidate_categories: list[str] = field(default_factory=list)   # 期望进 Top-3 的集合
    keywords_in_hypothesis: list[str] = field(default_factory=list) # 结论文本必须含的关键词
    min_supporting_specialists: int = 2              # 至少几位专家印证
    overall_confidence_at_least: str = "中"          # 置信度至少几级(高>中>低)
    detail: dict[str, Any] = field(default_factory=dict)  # v0.6+:扩展字段桶
EvalSample(一道题) id · description input {…} ← 题干(初始 state) mock_responses {…} ← 假答案 ground_truth(参考答案) primary_category / candidates keywords / min_specialists min_confidence / detail 桶 load_jsonl from_dict 的巧思 已知 key → 对应字段 别名兼容:top1_category→primary_category 未知 key → 自动塞进 detail 桶 → 一套结构复用给 RCA + coding agent
图注:EvalSample 的数据结构与 from_dict 的"未知 key 塞 detail 桶"设计。

load_jsonl()schemas.py:84)逐行读、跳过空行和 # 注释行,每行 json.loads 后交给 from_dict。而 GroundTruth.from_dictschemas.py:44)里藏着关键一手:

# eval/schemas.py:44
@classmethod
def from_dict(cls, d: dict) -> GroundTruth:
    detail = dict(d.get("detail") or {})
    for k, v in d.items():
        if k not in _KNOWN_GT_FIELDS:      # 不认识的 key
            detail.setdefault(k, v)        # 自动收进 detail 桶
    return cls(
        primary_category=d.get("primary_category") or d.get("top1_category", ""),  # 别名兼容
        ...
    )
💡 设计取舍①:为什么要一个 detail「杂物桶」?朴素做法是给每种 agent(RCA、coding、doc-check…)各定义一个 GroundTruth 类。但那样评测框架就要认识 N 种答案结构。这里的选择是:只定一套 GroundTruth,RCA 用得上的字段列出来当"一等公民",其它 agent 特有的字段(如 coding agent 的 golden_patch_countgolden_hunk_count)统统塞进 detail。coding scorer 自己去 ground_truth.detail 里取。代价是 detail 里的东西没有类型检查;收益是一套 schema 通吃所有 agent,样本文件格式统一。
⚠️ 易错点:from_dictprimary_category 还兼容了别名 top1_categorycandidate_categories 兼容 top3_categoriesschemas.py:52-56)。所以你在样本里写 top1_category 也能被认出来——但团队里最好统一一种写法,否则 grep 样本时两种名字都要搜。
L03

对话型:5 维评分(FiveDimScorer)

像 sre-rca 这种"给结论"的 agent,用 FiveDimScorerscorers.py:48)打 5 个布尔维度——每一维过没过:

🎯
top1_hit

第一根因命中标准答案

🥉
top3_hit

Top-3 里命中候选之一

📏
confidence

置信度校准达标

👥
specialists

足够多专家支撑

🔑
keywords

关键词命中率≥50%

现在钻进 FiveDimScorer.score()scorers.py:54)看它逐维怎么算。第一步是从图的最终 state 里把结论抠出来:

# eval/scorers.py:60
score = EvalScore(sample_id=sample_id)
conclusion = final_state.get(self.conclusion_field) or {}          # 默认取 state["conclusion"]
hypotheses = conclusion.get("hypotheses") or conclusion.get("top_root_causes") or []

然后 5 个维度一个个填。挑最关键的三维看:

# ① top1_hit —— 第一条 hypothesis 的 category 对不对(scorers.py:65)
top1_cat = _get_hypothesis_field(hypotheses[0] if hypotheses else None, "category")
score.top1_hit = top1_cat is not None and _category_match(top1_cat, ground_truth.primary_category)

# ③ confidence —— 用 {低:1,中:2,高:3} 把中文置信度变成可比的数(scorers.py:76)
actual_conf   = conclusion.get("confidence") or conclusion.get("overall_confidence") or "低"
actual_rank   = CONFIDENCE_RANK.get(str(actual_conf), 1)
expected_rank = CONFIDENCE_RANK.get(ground_truth.overall_confidence_at_least, 2)
score.confidence_meets = actual_rank >= expected_rank

# ⑤ keywords —— 半数以上关键词命中算过(scorers.py:89)
text_blob = (_flatten_text(hypotheses) + _flatten_text([conclusion])).lower()
if not ground_truth.keywords_in_hypothesis:
    score.keywords_meets = True                       # 没要求关键词 → 直接算过
else:
    hits = sum(1 for kw in ground_truth.keywords_in_hypothesis if kw.lower() in text_blob)
    ratio = hits / len(ground_truth.keywords_in_hypothesis)
    score.keywords_meets = ratio >= 0.5
👶 逐行大白话① 拿到 Agent 排第一的猜测,把它的 category 跟标准答案比——_category_match 会把大小写、下划线、连字符全抹平再比(scorers.py:321),所以 "db_pool" 能匹配 "DB_POOL_EXHAUSTED"。③ 中文"高/中/低"没法直接比大小,先用字典 CONFIDENCE_RANKscorers.py:18)翻译成 3/2/1,再比"实际 ≥ 要求"。⑤ 把结论里所有文字拍平成一个大字符串(_flatten_text),数关键词命中几个,命中一半以上就算过。
💡 本质:5 维就是验房清单上的 5 个勾验房师不会只写"房子 80 分",而是逐项打钩:水(top1_hit 第一根因对不对)、电(top3_hit 候选里有没有)、防水(confidence 置信度别虚高)、门窗(specialists 有没有足够专家撑腰)、墙面(keywords 关键词覆盖够不够)。哪一项没打钩,你立刻知道是哪面墙漏水,而不是对着一个模糊总分发愁。
Agent 这次输出 top1 = db_pool 置信度 = 高 2 个专家支撑 命中关键词 1/2 对答案 验房师 FiveDimScorer ✅ top1_hit ✅ top3_hit ✅ confidence ✅ specialists ⚠ keywords 0.5 keywords 0.5 < 0.6 阈值 → 这一维不达标
图注:多维打分让"退步"能被精确定位到某一维(这里是 keywords 掉到阈值以下),而不是只看到一个模糊的总分下降。
为什么用"多维布尔"而不是"一个总分"? 因为不同维度反映不同问题:top1 不中但 top3 中,说明"方向对但排序不准";关键词全中但置信度虚高,说明"过度自信"。分维度看,你能精确知道是哪一项退步了,而不是只看到一个模糊的总分下降。
⚠️ 设计取舍②:_category_matchscorers.py:321)故意做成大小写不敏感 + 去下划线/连字符 + 允许子串a in e or e in a)。好处:容忍 LLM 输出 "db pool"、样本写 "DB_POOL_EXHAUSTED" 这种格式差异,不会误判。代价:可能"过宽"——比如 "pool" 会匹配上 "DB_POOL_EXHAUSTED"。这是"宁可放过、不可错杀"的评测哲学:评测是回归护栏,不追求学术级精确,只要能稳定抓到"明显退步"即可。
⚠️ 易错点:keywords 维在关键词列表为空时直接判过scorers.py:91)。也就是说样本里 keywords_in_hypothesis 留空,这一维永远绿灯——不是 bug,是"这道题不考关键词"的显式表达。别误以为"绿了就代表命中了"。
L04

coding 型:6 维加权(CodingAgentScorer)

像 spec-executor 这种"改代码"的 agent,评价标准不同——用 CodingAgentScorerscorers.py:107)打 6 个加权浮点维度(权重之和=1.0):

维度衡量
tool_success工具调用成功率
build_pass改完能否通过构建/测试
patch_count补丁数量是否合理
iterations迭代轮数(越少越好)
risk_distribution改动风险分布
change_minimality改动是否最小化(可选用 Haiku 当裁判)

权重是写死的常量(scorers.py:22),build_pass(能不能通过构建)占最重的 0.30——因为对 coding agent 来说"改完能跑"是硬指标:

# eval/scorers.py:22
CODING_DIM_WEIGHTS: dict[str, float] = {
    "tool_success": 0.20, "build_pass": 0.30, "patch_count": 0.15,
    "iterations": 0.15, "risk_distribution": 0.10, "change_minimality": 0.10,
}   # sum == 1.0

# score() 里加权求和 → total(scorers.py:153)
total = round(sum(dims[k] * self.weights[k] for k in self.weights), 4)
return EvalScore(sample_id=sample_id, dim_scores=dims, total=total,
                 total_passed=total >= self.slo)   # 默认 SLO=0.6
💡 设计取舍③:为什么这套用「加权浮点」,而 RCA 用「布尔」?RCA 的问题是"对/错分明"——第一根因要么命中要么没命中,用布尔最清晰。coding agent 的问题却是"程度"——改动比标准答案多改了 30% 算轻微超标、多改 3 倍算严重超标,这种连续的"好坏程度"用 0.0~1.0 的浮点分才表达得出来,最后加权成一个 total 卡 SLO。两套 scorer 都实现同一个 BaseScorer Protocol(scorers.py:37),run_eval 不关心用了哪套;EvalScore.all_passedschemas.py:142)靠"dim_scores 是否非空"自动切换判据——非空走 total_passed,空则走 5 维全 True。

change_minimality(改动是否最小化)有两种裁判模式(scorers.py:166):默认 heuristic 纯算数(实际 hunk 数 vs golden hunk 数的超出比例),或 haiku 让便宜 LLM 当裁判。而 haiku 模式最能体现工程化的谨慎——它有三层回退

# eval/scorers.py:177  _haiku_minimality_judge
def _haiku_minimality_judge(self, state, gt) -> float:
    try:
        from ai_trust_toolkit.llm import get_llm
        from langchain_core.messages import HumanMessage, SystemMessage
    except ImportError:
        return self._score_minimality_heuristic_fallback(state, gt)   # 回退①:没装 langchain
    if not self._minimality_prompt:
        return self._score_minimality_heuristic_fallback(state, gt)   # 回退②:prompt 文件缺失
    llm = get_llm("haiku")
    try:
        resp = llm.invoke([SystemMessage(...), HumanMessage(...)])
        return _extract_score_from_text(str(resp.content))
    except Exception:
        return self._score_minimality_heuristic_fallback(state, gt)   # 回退③:LLM 调用失败
⚠️ 边界/易错点:注意这三个 return fallbackscorers.py:182 / 190 / 213)。LLM-as-judge 是"锦上添花",绝不能因为它挂了就让整个评测崩——没装依赖、prompt 文件不在、模型调用抛异常,任一环节出问题都静默退回纯算数,评测照跑。这就是可信工程化的一贯做派:越是"可选增强"的东西,越要保证它失败时系统不受影响。
为什么默认是 heuristic 不是 haiku? heuristic 零 LLM 调用、零成本、结果确定——适合 CI 高频跑。haiku 模式每条样本多花约 500 token,判得更准但引入了成本和(哪怕已被 FakeLLM 摁住的)不确定性,留给"想更细看改动质量"时手动开。默认省钱、按需增强。
L05

run_eval:批量跑考卷

把样本、图、scorer 交给 run_eval()runner.py:17)就能跑批:

加载golden set 并发跑图每题 ainvoke 打分scorer.score 汇总EvalReport
samples = EvalSample.load_jsonl("data/eval_samples.jsonl")
report = await run_eval(graph, samples, scorer=FiveDimScorer())
report.print_summary()              # 漂亮打印各维通过率

# CI 里用阈值判定,不达标就退出码非 0
if not report.meets_thresholds(EvalThresholds(top1=0.5, top3=0.7)):
    raise SystemExit(1)

现在看 run_eval 的真实实现(runner.py:17)——它跟具体 agent 完全解耦,你只要把编译好的 graph 传进来。核心是这段并发跑批:

# eval/runner.py:42
semaphore = asyncio.Semaphore(concurrency)          # 控制同时跑几条(默认 1=串行)

async def _run_one(idx: int, sample: EvalSample) -> EvalScore:
    async with semaphore:                           # 拿到令牌才开跑
        try:
            initial = initial_state_builder(sample)         # sample.input → 初始 state
            config  = config_builder(sample, idx)           # 每条一个独立 thread_id
            final   = await graph.ainvoke(initial, config=config)   # 真跑图
            return scorer.score(sample_id=sample.id, final_state=final,
                                ground_truth=sample.ground_truth)   # 打分
        except Exception as e:
            err_score = EvalScore(sample_id=sample.id)
            err_score.error = f"{type(e).__name__}: {e}"    # 挂了只记这条的 error
            return err_score

scores = await asyncio.gather(*[_run_one(i, s) for i, s in enumerate(samples)])
👶 三行看懂控制流(1) gather 把 N 条样本同时发起;(2) 每条进 _run_one 前先 async with semaphore 抢令牌——令牌数 = concurrency,所以最多 N 条一起跑、多了排队,防止把下游打爆;(3) 每条被一整个 try/except 包住,一条炸了只把异常记进这条自己的 EvalScore.error,其余照跑——绝不会一条烂样本掀翻整场评测。
load_jsonl N 条样本 Semaphore concurrency 个令牌 ainvoke→score ✅ ainvoke→score ✅ 炸了→记 error,不影响别人 gather EvalReport 每条样本一个 try/except → 单条失败隔离,整场评测不崩
图注:run_eval 的控制流——Semaphore 限并发、gather 聚合、单条 try/except 隔离故障。
📝 举个例子:一次跑批的输入→输出 输入:data/eval_samples.jsonl(比如 20 条 golden 样本)+ build_rca_graph() + FiveDimScorer()
run_eval 并发把 20 题各喂进图跑一遍、逐题打 5 维分 → 汇总
输出(report.print_summary()):top1=0.85 top3=0.95 confidence=0.80 specialists=0.90 keywords=0.70 → 对照阈值 top1≥0.5, top3≥0.7… 全过 → meets_thresholds() = True → CI 退出码 0,放行合并。
⚠️ 设计取舍④:concurrency 默认 1(串行)runner.py:24)。为什么不默认拉满?因为评测最看重"结果可复现"——串行跑,节点里的共享 mock、日志顺序、资源占用都最稳定、最好排查;想加速再显式调大。默认求稳、按需提速,是评测工具的合理默认值。而且 config_builder 给每条样本一个独立 thread_id=f"eval-{id}-{idx}"runner.py:75),并发时各自的 checkpoint 记忆互不串。
L06

阈值与 CI 阻断:不达标合不了

EvalThresholdsschemas.py:98)定义每一维的及格线,默认:top1=0.5、top3=0.7、confidence=0.7、specialists=0.7、keywords=0.6。跑完得到的 EvalReport 会先把每条样本的布尔汇总成"通过率",再逐维对照阈值:

# eval/schemas.py:172  某一维的通过率 = 这一维为 True 的样本数 / 总样本数
def rate(self, dim: str) -> float:
    attr_name = {"top1":"top1_hit", "top3":"top3_hit", "confidence":"confidence_meets",
                 "specialists":"specialists_meets", "keywords":"keywords_meets"}[dim]
    return sum(1 for s in self.scores if getattr(s, attr_name)) / len(self.scores)

# eval/schemas.py:222  任何一维通过率低于阈值 → 整体不达标
def meets_thresholds(self, thresholds: EvalThresholds) -> bool:
    s = self.summary()
    return all(s[dim] >= getattr(thresholds, dim)
               for dim in ("top1","top3","confidence","specialists","keywords"))
👶 关键区别:这里比的是"通过率"不是"单条对错"rate("top1") 算的是"20 条样本里有几条 top1 命中",比如 17/20 = 0.85。meets_thresholdsall(...) 要求五维的通过率都过线,只要有一维(比如 keywords 掉到 0.55 < 0.6)没过,整个函数返回 False,CI 就 SystemExit(1) 拦下合并。注意它是群体通过率的门槛,不是"每条样本都必须满分"——允许个别难题失手,但整体水平不能滑坡。

这个能力被接进了 CI 流水线(Day 18 细讲)——有一道专门的闸门 ci-gate-eval

🚫

ci-gate-eval:评测不达标 → 阻止 merge

PR 里改了某个 agent,CI 自动对它重跑评测,任一维掉到阈值以下,流水线直接失败,合并按钮点不动。PR 模式只跑变更的 agent,省时间。

👶 小白:大模型每次输出都不一样,那评测分数岂不是每次跑都飘?CI 怎么可能稳定拦得住?

👨‍🏫 老师:好问题。秘密在样本里的 mock_responses——评测走的是 FakeLLM(Day 11),假大模型对每道题返回固定回复,所以同一份代码跑 100 遍分数完全一样。飘的是真模型,评测里被摁成了确定值。这样"分数变了"就一定是你的代码/prompt 变了,而不是模型随机抖动——CI 才敢拿它当合并闸。

👶 小白:那 PR 里改了 5 个 agent,每次都全量重跑 21 个 agent 的考卷不会很慢吗?

👨‍🏫 老师:不会。ci-gate-eval 在 PR 模式下只跑被改动的那几个 agent的考卷,主干才全量。加上全程 FakeLLM 不联网,几秒就出结果。

这就是"防退步"的护城河:你改 prompt、换模型、重构节点——只要让 Agent 的核心能力退步了,CI 立刻拦下来。评测样本和业务代码放在同一个仓库、同一个 PR,所以能力和考卷永远同步演进。这是把"Agent 质量"纳入工程化管控的关键一步。
L07

在线评测 & 回归检测

除了 CI 里用 golden set 的"离线评测",框架还有两个进阶能力:

ObservableScorer — 在线评测(scorers.py:381)

golden set 需要人工标答案,成本高、覆盖有限。ObservableScorer 不依赖标准答案(也 不调 LLM,纯统计),从线上真实调用的 envelope 信号算 5 个 SLO 维度。默认阈值写在 OBSERVABLE_SLO_DEFAULTSscorers.py:372),看它怎么判"违约":

# eval/scorers.py:372  在线 SLO 默认阈值
OBSERVABLE_SLO_DEFAULTS = {
    "success_rate": 0.85,        # status="success" 占比 · 跌破触红
    "critic_end_rate": 0.10,     # Critic 拦下(不敢下结论)占比 · 涨过触红
    "p99_latency_ms": 30000.0,   # 延迟 p99 · 涨过触红
    "p99_cost_usd": 0.50,        # 成本 p99 · 涨过触红
    "failure_no_LLM_rate": 0.02, # 预算/限流类硬失败占比
}

# eval/scorers.py:441  score_aggregate 里判一维是否违约(以成功率为例)
success_rate = status_distribution.get("success", 0.0)
if success_rate < self.slos["success_rate"]:
    severity = "red" if success_rate < self.slos["success_rate"] - 0.1 else "yellow"
    breaches.append(SloBreach(dim="success_rate", actual=success_rate,
                              threshold=self.slos["success_rate"], severity=severity))
👶 大白话它把一批线上 case 的 envelope 拿来做统计:成功率、Critic 拦截率、延迟 p99、成本 p99、硬失败率。每一维跟阈值比,超标就往 breaches 里塞一条 SloBreach,还分 yellow(黄,轻微超)red(红,超得离谱) 两档严重度。空 case 列表直接返回全 0(scorers.py:403),不会除零崩。
⚠️ 设计取舍⑤:ObservableScorer 的类注释里明确写着 "SHALL NOT 调 LLM(纯 statistics)"scorers.py:382)。为什么定死不许?因为在线评测要对海量线上 case 跑、要频繁跑,一旦引入 LLM 判分,成本和延迟都会失控,还会把"评测"本身变成一个可能出错的推理过程。这里的取舍是:golden set 离线评测用 LLM 也行(量小),在线评测只准用确定性统计(量大)

RegressionDetector — 回归检测

对比"这次评测"和"历史基线",自动找出哪一维退步了。配合仓库里的 eval-replay skill(Day 19):给一个线上出问题的 case_id,能拉出历史输入重放、对比 5 维评分、定位是哪一维掉了。

离线 vs 在线评测:离线 = 拿固定考卷考(可控、可复现、进 CI);在线 = 观察真实流量表现(覆盖真实分布、发现考卷没覆盖的问题)。两者互补——离线保下限,在线抓意外。
🎯 一句话口诀(评测五步)备卷 → 跑图 → 打分 → 卡阈值 → 拦合并」——golden set 备考卷、run_eval 跑图、scorer 打分、meets_thresholds 卡阈值、ci-gate-eval 拦合并。能把这五步讲给同事听,今天就通关了。
L08

今日小结 + 动手

🧠 第 2 周收官自测

  • Agent 为什么不能用普通单测、要专门评测?(输出是自然语言,没法 assert 相等)
  • golden set / ground_truth / mock_responses 各是什么?
  • 对话型 5 维、coding 型 6 维分别评什么?为什么用多维而不是一个总分?
  • 评测怎么进 CI 阻断合并?(meets_thresholds + ci-gate-eval)
  • 离线评测和在线评测(ObservableScorer)的区别?

✋ 动手

# 1. 看 eval 模块清单
sed -n '1,61p' packages/ai-trust-toolkit/src/ai_trust_toolkit/eval/__init__.py

# 2. 看一个真实的 golden 样本文件
head -3 apps/sre-rca-agent/data/eval_samples.jsonl

# 3. 读 5 维评分器
sed -n '48,110p' packages/ai-trust-toolkit/src/ai_trust_toolkit/eval/scorers.py

# 4. 亲自跑一遍 sre-rca 的评测
uv run python apps/sre-rca-agent/scripts/run_eval.py

# 5. 跑 eval 单测
uv run pytest packages/ai-trust-toolkit/tests/test_eval.py -q
第 3 周预告 · Day 11 起:可信底座讲完了!第 3 周转向"运行时与实战"。Day 11 补齐 toolkit 最后几块拼图——@safe_tool_resultget_llm/set_llm_factory 厂商无关注入、FakeLLM 测试基础设施,它们是前面所有"测试友好、厂商无关"的技术底座。
← Day 09 Memory Day 11 · 安全工具 & LLM 注入 & FakeLLM →