last30days-skill 的评估治理:为什么搜索质量评估是手动门槛而非每个 PR 的 CI 门禁
【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skill
本文基于 last30days-skill 仓库中的架构决策记录 search-quality-eval-manual-by-default-2026-05-10.md,系统讲解该项目为何刻意不把evaluate_search_quality.py这个"基线 vs 候选版本"的搜索质量 A/B 评估器接入每个 PR 的 CI:三个核心阻塞因素(实时 API 依赖、成本与延迟、LLM 判分的非确定性)如何共同决定了"手动默认"的评估策略,以及配套的源码实现、操作命令与评审规范。读完本篇,你将掌握在混合了实时数据源与 LLM 判定的项目中,如何设计评估门禁、如何运行手动质量评估,以及"离线确定性评估进 CI + 实时评估手动触发"的分层治理模式。
背景:evaluate_search_quality.py是什么
skills/last30days/scripts/evaluate_search_quality.py 是一个可选的本地评估脚本,用于在基线版本(baseline revision)与候选版本(candidate revision)之间,对一组固定的评审主题(reviewer topics)做检索质量对比。它产生两类指标:
- 确定性重叠指标:
Jaccard重叠度、retention(对基线的保留率)、按来源(per-source)的条数与重叠; - LLM 判分指标:由 Gemini 作为裁判模型对结果做 0–3 分的相关性标注后,计算
Precision@5、nDCG@5与跨判分池的来源覆盖召回率(source-coverage recall)。
它的定位在 docs/search-quality-eval.md 中被明确写为:"不是用户侧运行时的一部分,默认也不需要进入 CI"。也就是说,这个脚本天然具备"回归捕捉器"的外观——见到评估器就想接入每个 PR 自动运行,是大多数团队的直觉反应。而本项目的决策恰好相反:刻意不把它接进每个 PR 的 CI。
决策核心:为什么 CI-on-every-PR 是错误默认
决策文档给出了三条阻塞属性,这三条共同构成了"实时 API + 非确定性"的组合困境:
1. 实时 API 依赖(Live API access)
候选版本要真正跑出结果,引擎必须实际运行——意味着真实的 ScrapeCreators 调用、真实的 Reddit 抓取、真实的 YouTube 搜索。在 CI 中运行,要么需要注入生产凭据(在自动化流水线里长期持有生产密钥),要么依赖一套 record/replay 的录制回放 fixture 集合——而外部 API 的响应结构几乎会立即漂移,fixture 随之失效。这条属性可以从源码中得到印证:evaluate_search_quality.py 的run_last30days()直接以子进程方式在 git worktree 中运行完整的last30days.py引擎并解析其 stdout JSON,没有任何网络录制/回放层;create_eval_env()(同文件 L313-L327)还会白名单式地透传OPENAI_API_KEY、XAI_API_KEY、SCRAPECREATORS_API_KEY、GOOGLE_API_KEY等真实凭据环境变量,说明这条评估路径设计上就假定在线运行。
2. 成本与延迟(Cost and latency)
一次完整的评估会跨 N 个评审主题把流水线跑 N 次(每个主题还要跑基线 + 候选两个版本,即 2N 次完整引擎运行)。乘以每个 PR(包括纯文档 PR)之后,花费是实打实的,墙钟时间也会把 CI 从约 30 秒推到好几分钟。当前默认主题池来自 fixtures/eval_topics.json,包含 8 个带query_type与选题理由(rationale)的主题(对比类、how_to、breaking_news、product、opinion、prediction、concept、factual 各型);当该文件缺失时,代码内还内置了 6 个兜底主题(见 L31-L42)。--timeout默认为 240 秒/次运行,2N 次运行的累计墙钟与 API 开销对"每个 PR 无条件跑一遍"来说并不划算。
3. 判分路径的非确定性(Non-determinism in the judging path)
LLM 判分指标对评审有价值,但依赖当日裁判模型的行为。从源码看,判分请求固定temperature: 0并要求 JSON 输出(L215-L234),但这只压住了采样随机性,压不住模型版本漂移与同一输入不同日期的重打分差异。决策文档的判断很直白:一个每 20 个 PR 就因裁判对某条结果重新打分而误挂 1 个 PR 的评估器,比完全没有评估更糟——它教会贡献者"重试"而不是"阅读结果"。
值得注意的是,脚本对判分结果做了按裁判模型隔离的缓存:judgments 缓存在<output-dir>/judgments/<slug>.json,只有当缓存中记录的judge_model与本次--judge-model完全一致时才复用;模型变化时宁可丢弃旧缓存重判,若此时又缺少 Gemini API key,则返回空判分并向 stderr 明确告警"该主题的指标将全为零"(L283-L310)。这种"不静默产出错误数字"的工程处理,恰恰说明团队深知判分结果的模型敏感性——同一套逻辑用在 CI 自动门禁上,就成了不稳定信号。
确定性指标也不是"真相"
最后一个常被忽略的点:即使用户只关心确定性的重叠指标,它们也只是回归守卫而非正确性度量。一个能提升 Jaccard 重叠的改动可能同时劣化综合(synthesis)质量;一个降低重叠的改动反而可能是有意的改进。因此,连确定性一侧也不安全地用于自动 fail PR。这也是为什么--mock、--quick等降成本开关(L511-L523)存在——它们服务于人工评估时的快速迭代,而不是为 CI 门禁降档。
评估器工作原理:源码级拆解
理解评估器的运行机制,有助于理解为什么它"能手动跑、不适合自动跑"。
双 worktree A/B 架构
main()(L526-L584)先通过resolve_repo_dir()把--baseline/--candidate两个 git 引用解析为仓库目录:
- 标签为字面量
WORKTREE时,直接复用当前检出目录(即"候选 = 我手上这份代码"); - 其他引用(如
main、某个 tag、commit)则通过git worktree add --detach <tmpdir> <rev>创建临时 detached worktree(L370-L386),评估结束在finally块中统一git worktree remove --force清理。
这正是决策文档中手动评估命令的底层形态:
LAST30DAYS_PYTHON=python3.13 \ python3 skills/last30days/scripts/evaluate_search_quality.py \ --baseline main --candidate HEAD参数默认值(来自build_parser()):--baseline HEAD~1、--candidate WORKTREE、--output-dir tmp/search-quality、--limit 20、--timeout 240;另有--search(限定来源)、--quick、--mock、--judge-model、--topics-file。裁判模型的默认值取 lib/providers.py 中的常量GEMINI_FLASH_LITE = "gemini-3.1-flash-lite";Gemini key 的解析顺序为GOOGLE_API_KEY→GEMINI_API_KEY→GOOGLE_GENAI_API_KEY(环境变量优先,其次本地 config,见 L195-L203),可用GEMINI_MODEL类配置项或--judge-model覆盖。
输出形态守卫与指标实现
run_last30days()会对旧版本引擎探测--json-profile支持:只要检出的引擎源码中出现该参数就显式传--json-profile=raw,并在解析结果后做形状守卫——若 payload 带schema_version却没有ranked_candidates,说明引擎意外吐出了 agent 导出 profile,脚本直接报错而不是给空列表打零分(L359-L366)。这种"失败要大声"的风格贯穿全脚本,与决策文档"CI 信号质量 > 覆盖率"的立场一致。
指标实现均为标准检索度量(L136-L192):
jaccard(A,B) = |A∩B| / |A∪B|,两集皆空记 1.0;retention(A,B) = |A∩B| / |A|,基线为空记 1.0(衡量候选版本相对基线"丢了多少");precision_at_k统计 top-k 中判分 ≥2("relevant and useful" 及以上)的比例;判分刻度 0=跑题/明显差、1=弱或边缘、2=相关有用、3=高度相关最佳之一(build_judge_prompt(),L237-L270);ndcg_at_k使用DCG = Σ (2^grade − 1) / log2(index+1),理想 DCG 由判分池(baseline ∪ candidate 的并集,去重后)内分数降序取前 k 得到;source_coverage_recall衡量:判分 ≥2 的条目所覆盖的"好来源"集合中,候选排名命中了多少。
summarize_topic()(L403-L437)把上述指标按 baseline / candidate / stability 三段组织,最终write_summary()输出<output-dir>/metrics.json与summary.md(含| Topic | Base P@5 | Cand P@5 | Base nDCG@5 | Cand nDCG@5 | Jaccard | Retention |对比表);单主题失败不中断整体,而是记入failures列表写进输出,退出码在存在失败时为 1(L475-L584)。
干净的评估环境
docs/search-quality-eval.md 还记录了环境隔离细节:脚本 shell out 到last30days.py时强制干净的 env 认证路径——透传XAI_API_KEY、OPENAI_API_KEY、SCRAPECREATORS_API_KEY,但刻意不透传浏览器 Cookie 的 X 认证,使评估运行保持在"无弹窗"路径上;同时从评估PATH中剥离node并给yt-dlp包一层--ignore-config,避免旧版本引擎继承本地浏览器 Cookie 配置。这与源码中create_eval_env()只允许PATH/LANG/LC_ALL/TMPDIR与白名单凭据键(L49-L61)、并清空LAST30DAYS_CONFIG_DIR的做法互为印证。
决策指引:四条操作准则
决策文档的 Guidance 部分给出四条准则,其中前三条是当前规范,第四条是重审条件:
1. 评估器保持可用,只是不自动
脚本对维护者和贡献者始终可运行。两种触发方式:
- 评审者请求:当 PR 落在检索 / 排序 / 综合路径且风险值得时,reviewer 手动请求一次评估运行;
- 贡献者自跑:提交前在本地先跑,获取前置信号。
verify_v3.py(skills/last30days/scripts/verify_v3.py)中把评估器登记为 v3 验证 bundle 的一环(EVALUATOR = SKILL_ROOT / "scripts" / "evaluate_search_quality.py"),说明"手动但常备"在工具链中是真实落地的,而非口头约定。
2. 标准 PR CI 门禁保持确定性与契约化
进入自动门禁的只能是"同一输入两次运行给出同一答案"的检查:离线安全的pytest、插件契约检查、版本一致性契约、ruff/lint。输出质量评估(quality-of-output assessment)整体位于该循环之外。
3. 中间地带是workflow_dispatch,不是自动 PR 门禁
如果维护者想要"GitHub 触发但不让每个 PR 承担实时 API 成本"的评估,正确形态是手动派发的工作流(workflow_dispatch)或标签触发(label-gated)的工作流——而不是无条件在pull_request:上运行的工作流。核心原则一句话:成本旋钮留在人手里。
4. 若评估器能变为离线确定性,则重审该决策
阻塞项是"实时 API + 非确定性"的组合。未来如果脚本能基于静态 fixture 计算有意义的 Jaccard / retention 指标(无实时 API 调用、无 LLM 判分),决策翻转,它将成为默认 CI 的候选。文档明确要求跟踪这一条件、条件满足时重审。
落地状态:该"重审条件"正在被另一条路线兑现
从当前仓库结构看,第四条"重审条件"并没有让团队放弃在 CI 里做质量评估,而是由另一套离线评估体系承接:
- .github/workflows/validate.yml 的
evaljob 在每个 PR 上运行uv run pytest tests/eval -x -s; - 该离线评估套件基于录制的 HTTP fixture 回放(
lib/http.py处重放),从不调用 LLM 或网络,度量引用落地(citation grounding)、时效合规(recency compliance)、聚类一致性(cluster coherence)、覆盖与确定性,对照 tests/eval/baseline.json 的地板值做硬基线检查,详见 docs/reference/eval.md; - 因此当前治理格局是双层:离线确定性的研究质量基线进 CI(每个 PR 都有分数表与硬检查),而依赖实时 API 与 LLM 判分的 A/B 评估器(
evaluate_search_quality.py)保持手动。
这恰好验证了决策文档的框架:CI 门禁 = "契约形状 + 确定性",质量评估 = 人工判断触发。
实操规范:什么该合并、什么不该
决策文档最后把指引翻译成可执行的评审规则:
| 场景 | 处置 |
|---|---|
PR 把evaluate_search_quality.py接入默认 validate.yml 工作流 | 不合并 |
PR 添加workflow_dispatch触发器或标签门控的运行 | 合并 |
| Review 检索/排序类改动,diff 显示可能劣化质量 | 主动请求手动评估运行,不要指望 CI 替你捕捉 |
一次完整手动评估的可复制形态(结合 docs/search-quality-eval.md 的推荐用法):
# 基本形式:基线 origin/main vs 当前检出 uv run python skills/last30days/scripts/evaluate_search_quality.py # 指定版本与参数 uv run python skills/last30days/scripts/evaluate_search_quality.py \ --baseline main \ --candidate HEAD \ --per-source-limit 5产出位于tmp/search-quality/(--output-dir可改):metrics.json供程序化对比,summary.md的对比表供人工评审直接阅读。判分缓存位于<output-dir>/judgments/,切换--judge-model会自动失效旧缓存——阅读旧报告时务必确认报告所用裁判模型与缓存记录一致。
两条来自用户文档的诚实提醒值得保留:Jaccard与 retention 是回归守卫而非真相指标;Precision@5与nDCG@5的上限取决于判分池质量,它们帮助对比版本,但不能替代更大规模的人工标注基准。
小结
这条架构决策的可迁移经验有三点:
- 门禁的选择标准是信号的可靠性,而非覆盖的完整性——一个 5% 误挂率的自动质量门,比没有门更伤害团队对 CI 的信任;
- "实时 API + LLM 判分"组合天然不适合无条件自动触发,成本旋钮(
workflow_dispatch/ label-gated)应留在人手里; - 重审条件应当被显式写出并跟踪——本仓库通过另建离线 fixture 回放评估套件(tests/eval)部分兑现了"离线确定性化"路径,使 CI 拿到了确定性质量基线,而 A/B 实时评估器继续以手动形态服务高风险评审。
对检索 / 排序 / 综合类改动,正确的验证姿势是:CI 的离线基线全绿 + 视 diff 风险手动跑一次evaluate_search_quality.py并人工阅读summary.md,而不是等待某个自动门禁替你把关。
【免费下载链接】last30days-skillAI agent skill that researches any topic across Reddit, X, YouTube, HN, Polymarket, and the web - then synthesizes a grounded summary项目地址: https://gitcode.com/GitHub_Trending/la/last30days-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考