在团队频道收到 CI 红色警报后,先用免费模型+LMonkeyCode 对失败进行分类(infra/code/flake/test_logic),再决定读哪条日志。
CI 因为与你的改动无关的原因变红。共享 runner 上的端口冲突、期望时钟走动的测试、缺失的依赖镜像、过期的缓存、终于抓到真实回归的断言。当每个失败都以原始堆栈跟踪的形式涌入频道时,昂贵的不是失败本身;而是决定打开哪条日志。在任何人读一行日志之前,先做一个小小的分类步骤就可以整理好队列。
下面的脚本使用 MonkeyCode 的免费模型访问和免费服务器选项,所以你可以在定时任务中运行它而无需添加基础设施。披露:本文是作为 MonkeyCode 产品推广的一部分撰写的。
一张分诊卡,不是根因裁决
目标不是解释失败。目标是生成一张分诊卡,包含三个字段,帮助你决定下一步读什么:类别(category)、置信度(confidence)、建议的下一步(suggested_next_step)。
用这些类别开始:
模型提出其中一个标签只是一份草稿,不是最终决定。脚本将这份草稿与一些你已经知道的关键词的确定性提示合并,然后打印出一张卡片,让你在打开完整日志之前扫一眼。
可复用的分诊脚本
脚本接受一个包含失败 CI 日志片段的目录,对每个文件的最后 4000 个字符调用模型,并添加正则提示,这些提示只能提升关注级别,永远不会降级。将 classify_log 存根替换为你有权限访问的端点。
# flake_triage.py
import json
import re
from dataclasses import dataclass
from pathlib import Path
CATEGORIES = {
"infra": "network, port, timeout, memory, permission, connection refused",
"code": "import error, syntax error, missing module, config parse, build failure",
"flake": "race, order dependence, retry works, timing, clock, sleep",
"test_logic": "assertion mismatch, expected value changed, invalid input fixture",
"ambiguous": "log truncated, unrelated failure, no stack, generic error",
}
PROMPT = """Classify this CI failure log excerpt.
Categories: infra, code, flake, test_logic, ambiguous.
Return JSON with fields:
category, confidence (0-1), reason (one sentence), suggested_next_step (one sentence).
Do not claim a specific root cause beyond the log.
LOG:
{log}
"""
def classify_log(log: str) -> dict:
# Replace with the model endpoint available to you.
raise NotImplementedError
def rule_hints(log: str) -> dict:
hints = []
if re.search(r"connection refused|timed out|address already in use", log, re.I):
hints.append("infra")
if re.search(r"ImportError|ModuleNotFoundError|SyntaxError", log):
hints.append("code")
if re.search(r"flaky|retry|race|deadlock|eventually", log, re.I):
hints.append("flake")
return {"rule_hints": hints}
def triage_one(failure_blob: str) -> dict:
model = classify_log(failure_blob)
hints = rule_hints(failure_blob)
# Model output is a draft; rule hints can add a competing hypothesis,
# but they never override a lower-confidence label.
if model.get("confidence", 0) < 0.6:
model["suggested_next_step"] = "read the full log before deciding"
return {"model": model, "rule_hints": hints}
def main(log_dir: Path):
logs = sorted(log_dir.glob("*.log"))
for log_path in logs:
text = log_path.read_text(errors="ignore")[-4000:]
result = triage_one(text)
m = result["model"]
print(f"{log_path.name}\t{m.get('category')}\t{m.get('confidence')}")
print(f" reason: {m.get('reason')}")
print(f" next: {m.get('suggested_next_step')}")
if m.get("confidence", 0) < 0.5 or result["rule_hints"]:
print(f" hints: {result['rule_hints']}")
if __name__ == "__main__":
main(Path("ci_logs"))
rule_hints 函数存在的原因是处理那些你已知会在环境中反复出现的词模式。如果日志说 address already in use,模型可能高置信度地称之为 infra,也可能低置信度地称之为 ambiguous。但无论如何,提示会保持可见,这样读卡片的人就可以挑战这个标签。
不是每个模型标签都值得同等的响应。用这张决策表把卡片转化为行动:
这张表故意保守。模型说 flake 并不意味着有权将该 issue 关闭为 flaky。它是在归咎责任之前测试确定性的提示。
用最近二十个失败来校准它
取二十个你已经知道真正根因的已关闭 CI 失败。运行脚本对保存的日志进行分类,并将提议的类别与真实情况对比。按类别计算精确率,而不只是总体准确率。你经常会发现 infra 和 code 很容易分类,因为它们的特征很明显,而 flake 和 test_logic 则很嘈杂。这个本地数字比任何外部基准都更有用,因为它反映了你自己的技术栈、runner 镜像和测试习惯。
当你更换模型端点时,重新运行同样的校准集。不同的指令调优模型即使在类别上达成一致,也会在置信度上产生分歧,而且你从一个端点学到的置信度阈值可能无法干净地迁移到另一个端点。
尾日志是有损的。如果只传递最后 4000 个字符,一个原因出现在前 500 行的失败看起来可能像是 ambiguous。
类别会重叠。竞态条件可能表现为连接被拒绝,缺失依赖可能看起来像 test_logic 失败。卡片是用来排序注意力的,不是用来宣称根因的。
脚本默认不访问你的测试代码或 diff。这是刻意的:它应该保持廉价和低上下文。如果你想获得更好的标签,只添加失败的测试名称和 diff 摘要,而不是整个仓库。
日志可能包含环境名称、文件路径和秘密。将它们发送到第三方模型端点是一个数据处理决策,需要与分享任何其他 CI 产物一样的审查。
不要基于模型标签自动关闭、自动重试或自动分配。此工作流中唯一的自动部分是为人审阅而进行的列表排序。
谁应该跳过此工作流
有合规或隐私规则禁止将 CI 日志发送到外部模型端点的团队。
小团队——每个红构建已经被同一个人阅读;添加分类反而是繁文缛节而非杠杆。
CI 非常稳定、flake 很少见、失败类别从文件名就能明显看出的仓库。
期望一步到位完成全自动根因分析或自动修复的团队。此工作流给你的是阅读顺序,不是修复方案。
从五个最近的失败 job 和每个的一行总结开始。重点不是减少日志;而是用更好的顺序去读它们。如果你已经通过 MonkeyCode 获得了免费模型访问,将脚本指向 CI 产物在一个免费服务器上,让它起草第一遍;人类仍然拥有最终决定权。