前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9264
  • 远程编码Agent的权限对话框死锁问题
  • DeepSeek V4 Pro正式版发布:多项测试逼近Fable 5
  • Raycast 0.71引入屏幕感知功能:AI可直接理解当前窗口上下文
  • 复现2200篇ICML论文的实战总结
  • Vercel AI SDK 新增 ACP 协议兼容 Harness 层
  • GLM 5.2 Eve模型:百万上下文免费用
  • Gemini 3.7 Flash登陆AI Gateway,折扣50%
  • Exa神经网络搜索引擎登陆Vercel Agent市场
  • OpenAI被黑背后:AI Agent失控的三个技术根源
  • Cursor Agent 环境预构建:启动速度提升 3 倍
  • Simon Willison测评:DeepSeek V4 Pro推理能力差异化显著
  • 四步法区分Flaky Test与真实回归
  • AI工具调用测试夹具设计的四大原则
  • EXL2量化:分数位宽如何实现精确显存匹配
  • ExecuTorch移动端推理:三阶段导出流程详解
  • LLM输出快照测试失败不等于回归
  • 事件流处理架构核心:分区与状态的成本推导
  • OpenCode 评测:终端原生 AI 编程 Agent 体验
  • AWS ECS自动扩缩容:正确用队列深度替代CPU利用率
  • AI Agent 内容工厂实战:19 个 Agent 日产 53 篇内容
  • LLM 测试去 flaky:按根因分组而非按测试名
  • AI 编程双车道工作流:量大任务用便宜模型,重大判断需人工审核
  • AI 输出日期格式暗坑:DD/MM 与 MM/DD 歧义为何难发现
  • Claude Chrome扩展升级:侧边栏对话与全平台历史同步
  • LLM 从简历提取工作经历的结构化schema设计
  • LLM 从简历提取学历的结构化schema设计
  • 模型别名指向的模型下线后会发生什么
  • Cursor 推出专用 AI Agent Git 托管服务 Origin
  • LLM 多轮对话截断策略的迁移实践
  • Context Caching 迁移的真实要求
  • Prompt 变更 Code Review 审查清单:六个关键维度
  • Cloudflare Vectorize 定价与维度计费陷阱详解
  • Anthropic引入AI输出水印,部分用户不满
  • Claude各模型知识截止日期详解:训练数据与可靠 cutoff 的差异
  • Claude Message Batches API:24小时窗口内50%折扣的异步批处理
  • Claude API强制输出JSON的规范做法:不用JSON Mode
  • Claude交错思考模式:工具调用后如何继续推理
  • Claude Extended Thinking原理:budget_tokens与max_tokens共享配额
  • Claude API 并行工具调用机制与禁用选项详解
  • ShieldFont:用字体特征对抗AI爬取
  • KEDA 按队列长度扩缩容 Celery 工人
  • CodeRabbit 推变更管理:PR 成 SDLC 瓶颈
  • Celery + Redis 队列化 K8s 推理架构实战
  • 用合同测试拦截第三方 API 破坏性变更
  • AI 能否生成真正的粤语而非书面中文
  • MCP 协议让 AI 操控 Minecraft:61 个工具覆盖移动/建造/战斗
  • Eval 分数骗人:AI 模型金丝雀指标应看 token 消耗/截断率/工具调用
  • 引用文献解析的正确姿势:分段策略比单次全量提取更可靠
  • 本地跑 BGE 向量模型:normalize_embeddings 是关键参数
  • 供应链攻击泄露数千亿字节凭证:2500 名 AI 包用户中招
  • AWS Bedrock 按token计费常见陷阱:单位换算与实时查询方法
  • 已加载 51 / 9264
8.0
热点
AI SCORE
编程提效2026-08-13 07:05

LLM 测试去 flaky:按根因分组而非按测试名

dev.to · AI#LLM测试#测试工程#Flaky
Editor brief · 编辑速览

调用模型的测试失败通常按因聚集而非独立,用签名聚合替代逐条报告可快速定位真实根因。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

当一个调用模型的测试套件变红时,往往是一团团地变红:一个速率限制、一个糟糕的部署、一个模型别名迁移,然后四十个测试同时失败。把这读成四十个问题,是每个测试报告的默认行为——但这错了,而且错得离谱,白白浪费数小时。

为什么按测试名分组不起作用

测试报告按运行器认识的东西组织:文件、类、测试名。这种组织方式假设失败是独立的——对于单元测试来说这是个不错的假设,但对于所有通过同一个 HTTP 客户端访问同一个提供商的测试来说,这是一个糟糕的假设。当共享依赖晃动时,每个触及它的测试都会失败,而报告把它们呈现为无关的,因为它们的名字无关。

后果不仅仅是浪费阅读时间。它扭曲了每一个下游数字:你的 flake 计数会因四十个本身没问题的测试而飙升,你的分类待办队列充斥着重复项,而同一次运行中发生的真正单测回归被淹没在噪声中。按原因分组才能让这次运行变得可读——一次事故、一行、四十个受影响的测试,任何因不同原因失败的测试立即凸显出来。

签名由什么构成

签名是一个从失败中计算出的短字符串,具有这样的特性:具有相同根本原因的两个失败产生相同的字符串,而具有不同原因的两个失败产生不同的字符串。值得纳入的字段,按优先级排序:

HTTP 状态码,如果有的话。429 和 500 是不同的事故,即使 resulting exception text 完全相同。在测试 harness 中捕获它——它不在 traceback 里。

提供商的错误类型或代码。rate_limit_exceeded、overloaded_error、context_length_exceeded 及其等价物是最具区分度的字段,而且是稳定的字符串。

异常类名。AssertionError 对比 ValidationError 对比传输超时,是大多数报告从未做出的第一次拆分。

断言位置。最内层属于你自己代码的栈帧——文件和方法名,不要行号,因为行号随每次编辑移动,会让同一分组在不同的 commit 之间碎片化。

服务的模型 ID。同一断言在两个不同模型上失败是两个问题。

必须排除的:测试名、完整消息、请求 ID 或任何时间戳。把测试名放进去,每个分组就恰好只有一个成员——又回到起点了。

需要关注的权衡是:签名可以太粗糙,也可以太细。只对异常类做哈希会把套件中所有的 AssertionError 塌缩成一个大组,这是一行毫无用处的数据。好的签名的检验标准是行为性的,而非美学性的:当你看最大的那个组时,每个成员都应该有相同的修复方案。如果两个成员需要由不同的人来修复,就加一个字段;如果两个组需要由同一变更来修复,就去掉一个。

归一化规则

消息既是有效区分信息所在之处,也是所有熵所在之处,所以需要在哈希之前对它做归一化。六条替换规则完成了大部分工作,而且顺序很重要——先做具体的,再做一般的。

import re

SUBS = [
    (re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
                r"[0-9a-f]{4}-[0-9a-f]{12}\b", re.I), "<uuid>"),
    (re.compile(r"\b(req|chatcmpl|msg)_[A-Za-z0-9]{6,}\b"), "<id>"),
    (re.compile(r"\b\d{4}-\d{2}-\d{2}[T ][\d:.]+Z?\b"), "<ts>"),
    (re.compile(r"0x[0-9a-f]+", re.I), "<addr>"),
    (re.compile(r"'[^']{40,}'"), "'<long>'"),   # inlined prompts and completions
    (re.compile(r"\b\d+\b"), "<n>"),          # do this LAST
]

def normalise(message: str) -> str:
    text = message.strip().split("\n")[0][:400]
    for pattern, replacement in SUBS:
        text = pattern.sub(replacement, text)
    return text

其中两条值得提醒。把每个整数替换成 <n> 会把"期望 1 次工具调用,得到 0 次"和"期望 1 次工具调用,得到 3 次"塌缩成一组——这通常是你想要的,但偶尔也会掩盖真实的区别;把它放在最后执行,这样更具体的模式先抢走它们的匹配。最后截断到第一行会把模型输出从消息中丢弃,这是整个设计的要点——嵌入在断言消息中的 completion 每次都是唯一的,如果你不把它去掉,会瓦解任何分组。

一个处理 JUnit XML 的脚本

每个值得使用的运行器都会输出 JUnit XML:pytest 用 --junitxml,Vitest 用 --reporter=junit --outputFile,Playwright 用它的 junit reporter。那个文件就是输入,用标准库解析它——无需额外依赖。

让每个 job 把 JUnit XML 输出到一个已知路径并上传为 artifact。即使对通过的 job 也要这样做——那些通过的行是你之后想要的所有比率的分母。

解析每个 <testcase> 并读取其 <failure> 或 <error> 子元素。如果你的运行器输出的是 rerun 方言,也要读取 <flakyFailure> 和 <rerunFailure>:那些是重试过的尝试,它们就是 flake 信号。

计算签名并分组。按最大组优先打印。

import hashlib, sys, xml.etree.ElementTree as ET
from collections import defaultdict

FAILURE_TAGS = ("failure", "error", "flakyFailure", "rerunFailure")

def signature(case, node):
    parts = [
        node.tag,
        node.get("type") or "",
        normalise(node.get("message") or ""),
        case.get("classname") or "",
    ]
    raw = "|".join(parts)
    return hashlib.sha1(raw.encode()).hexdigest()[:10], raw

groups = defaultdict(list)
for path in sys.argv[1:]:
    for case in ET.parse(path).iter("testcase"):
        for node in case:
            if node.tag in FAILURE_TAGS:
                key, raw = signature(case, node)
                groups[key].append((case.get("name"), raw))

for key, members in sorted(groups.items(), key=lambda kv: -len(kv[1])):
    print(f"{len(members):4d}  {key}  {members[0][1][:110]}")
    for name, _ in members[:5]:
        print(f"        {name}")
    if len(members) > 5:
        print(f"        ... and {len(members) - 5} more")

这个脚本故意一次读取多个文件,因为你想要分组的单元是 CI run,而不是 job。分散在八个并行分片上的套件会写入八个 XML 文件,而击中全部八个的速率限制是一次事故;如果按文件分组,会报告成八次。把整个 artifact 目录传进去。

注意 classname 在签名里而测试名不在。包含 classname 让真正不相关的模块中的失败保持分离,同时仍然把一个模块内的二十个测试塌缩在一起;如果你的套件把所有东西都归到一个 class 里,就去掉它,因为它此时没有任何贡献。

输出改变了红色构建对你的要求。一个有四十个成员的组,带有 rate_limit_exceeded 代码,是一次行动:降低套件的并发度——本地绿、CI 红的页面涵盖了这个。一个只有一员的组,带有在你自己代码中特定函数的 AssertionError,才是你真正需要阅读的东西。

把签名持久化到每次尝试旁边,而不是只在当下计算它。一旦它成为一列,那些曾经很难的问题就变得 trivial 了:这个签名本周是新的吗?哪个签名占了套件红灯时间的大部分?周二飙升的那个组之前出现过吗?这是 flake dashboard 已经需要的表多加一个字段,而这个字段让 dashboard 值得构建。

与 rerun 相关的 JUnit 元素是来自 Surefire 的扩展,不是任何单一标准的一部分,支持程度因运行器和 CI 产品而异。在依赖 <flakyFailure> 存在之前,检查你的运行器实际输出什么。

一个追踪随时间变化的 Flaky Prompt 测试的 Dashboard

一个用于排名优先修复哪些 Prompt 测试的 Flakiness Score

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
AI Agent 内容工厂实战:19 个 Agent 日产 53 篇内容
下一篇
AI 编程双车道工作流:量大任务用便宜模型,重大判断需人工审核