我把大模型接进CI后,失败用例自动归因,报告直接发飞书

简介: 本文介绍一种AI驱动的CI失败归因方案:通过大模型自动分析pytest失败日志,精准分类环境问题、用例缺陷与代码Bug,并将结构化结论实时推送至飞书。测试排查耗时从2小时降至10分钟,效率提升12倍,全部脚本开源可复用。

以前CI红了,一群人围在屏幕前翻日志猜原因。现在CI红了,飞书里直接弹出一条消息:“本次失败50条,其中45条为环境问题,5条疑似代码Bug,详情见下。”

大家好,我是某互联网公司的测试架构师。

上个月,团队里一个做自动化的同事跟我吐槽:“哥,我每天花在‘看失败报告’上的时间,比写测试脚本还多。”

我问他怎么回事。他说每天CI跑完,少则二三十条失败,多则上百条。点开一看,有的是环境超时,有的是元素定位失效,有的是测试数据被其他用例污染了,真正是Bug的可能就三五条。但为了找出那三五条,他得一条条翻日志、看截图、对代码变更,两三个小时就没了。

我说:“你让大模型替你看。”

他问:“怎么看?”

我说:“CI跑完,把失败日志丢给大模型,让它做归因分类,然后把结论发到飞书。你只看结论。”

两天后,他的CI流水线里多了一个步骤。现在CI跑完,飞书群里直接弹出一条消息:

本次回归失败50条。

环境问题:42条(连接超时/容器启动失败)
用例问题:3条(定位器失效/数据污染)
疑似Bug:5条(断言失败,涉及订单状态不一致) 详情:点击查看完整报告
他跟我说:“我现在每天花在失败排查上的时间,从两小时变成了十分钟。”

今天这篇文章,我把整个方案拆开讲。从CI配置到Prompt模板到飞书机器人,全部可复制。

一、先搞清楚:这个方案的核心逻辑是什么?
传统CI流水线的测试阶段是这样的:

代码提交 → 触发CI → 跑测试 → 生成报告 → 人工看报告 → 人工归因 → 决定下一步
人工看报告、人工归因,是整个链路里最耗时的环节。

我们的方案是在“生成报告”和“人工看报告”之间,插进去一个AI归因节点:

代码提交 → 触发CI → 跑测试 → 生成原始报告 → AI归因分析 → 生成结构化结论 → 飞书推送
AI归因节点做三件事:

收集失败用例的日志、截图路径、相关代码变更
调用大模型对每条失败进行分类(环境/用例/代码Bug/数据/未知)
汇总成结构化报告,通过飞书机器人推送
人工只需要看AI筛出来的“疑似Bug”那几条。

二、准备工作:你需要什么?
环境要求:

一个跑在CI上的测试项目(我们用的是GitLab CI + pytest)
一个飞书群,并创建自定义机器人(拿到webhook地址)
一个大模型API Key(DeepSeek、GLM、通义千问都行,我们用的DeepSeek)
测试框架输出:pytest 建议用 --junitxml=report.xml 生成结构化报告。或者用 pytest-json-report 插件生成JSON,解析更方便。

飞书机器人:在飞书群设置里添加“自定义机器人”,拿到webhook地址,安全设置选“签名校验”或“IP白名单”。我们用的签名校验,后面代码里会体现。

三、核心脚本:失败归因分析器
整个方案的核心是一个Python脚本,放在项目根目录的 ci/analyze_failures.py。

!/usr/bin/env python3

-- coding: utf-8 --

import json
import os
import sys
import xml.etree.ElementTree as ET
from openai import OpenAI

============ 配置 ============

DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY")
FEISHU_WEBHOOK = os.environ.get("FEISHU_WEBHOOK")
FEISHU_SECRET = os.environ.get("FEISHU_SECRET")

client = OpenAI(
api_key=DEEPSEEK_API_KEY,
base_url="https://api.deepseek.com"
)

============ 解析pytest报告 ============

def parse_junit_xml(xml_path):
"""从JUnit XML中提取失败用例信息"""
tree = ET.parse(xml_path)
root = tree.getroot()
failures = []
for tc in root.iter("testcase"):
failure = tc.find("failure")
error = tc.find("error")
if failure isnotNoneor error isnotNone:
node = failure if failure isnotNoneelse error
failures.append({
"classname": tc.get("classname"),
"name": tc.get("name"),
"time": tc.get("time"),
"message": node.get("message", ""),
"text": (node.text or"")[:2000], # 截断,避免token爆炸
})
return failures

============ 收集上下文 ============

def collect_context(failures):
"""收集Git变更、环境信息等上下文"""
import subprocess
try:
git_diff = subprocess.check_output(
["git", "diff", "HEAD~1", "--stat"],
stderr=subprocess.DEVNULL
).decode("utf-8", errors="ignore")
except Exception:
git_diff = "无法获取Git变更信息"

return {
    "failures": failures,
    "git_diff": git_diff[:3000],
    "ci_env": os.environ.get("CI_ENVIRONMENT", "unknown"),
    "timestamp": os.environ.get("CI_PIPELINE_CREATED_AT", ""),
}

============ 调用大模型归因 ============

PROMPT_TEMPLATE = """你是一个资深的测试开发工程师,正在分析CI流水线中失败的测试用例。

请对下面每一条失败用例进行归因分类,只能选择以下类别之一:

  • 环境问题:网络超时、容器启动失败、依赖服务不可用、配置错误等
  • 用例问题:定位器失效、测试数据污染、用例逻辑错误、断言过强/过弱
  • 代码Bug:业务逻辑错误、接口返回不符合预期、状态不一致等
  • 数据问题:测试数据不存在、数据被其他用例修改、数据过期
  • 未知:无法判断

同时请给出:

  1. 一句话归因结论
  2. 置信度(高/中/低)
  3. 如果是代码Bug,给出建议的复现步骤

输入数据:
{context}

请严格输出JSON数组,每个元素包含:
{ {
"name": "用例名称",
"category": "环境问题|用例问题|代码Bug|数据问题|未知",
"conclusion": "一句话结论",
"confidence": "高|中|低",
"reproduce_steps": "如果是代码Bug,给出复现步骤,否则为空字符串"
}}
只输出JSON,不要解释。
"""

def analyze_with_llm(context):
prompt = PROMPT_TEMPLATE.format(
context=json.dumps(context, ensure_ascii=False, indent=2)
)
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
response_format={"type": "json_object"}
)
content = resp.choices[0].message.content
return json.loads(content)

============ 生成飞书报告 ============

def build_feishu_card(results, total_failures):
"""构建飞书消息卡片"""
from collections import Counter
counter = Counter(r["category"] for r in results)
bugs = [r for r in results if r["category"] == "代码Bug"]

# 按类别统计
summary_lines = []
for cat in ["环境问题", "用例问题", "代码Bug", "数据问题", "未知"]:
    count = counter.get(cat, 0)
    if count > 0:
        summary_lines.append(f"**{cat}**:{count}条")

# 疑似Bug详情
bug_lines = []
for b in bugs[:5]:  # 最多展示5条
    bug_lines.append(f"- `{b['name']}`:{b['conclusion']}(置信度:{b['confidence']})")
ifnot bug_lines:
    bug_lines.append("无")

card = {
    "msg_type": "interactive",
    "card": {
        "header": {
            "title": {"tag": "plain_text", "content": "🔴 CI回归失败归因报告"},
            "template": "red"
        },
        "elements": [
            {"tag": "div", "text": {"tag": "lark_md", "content": f"**失败总数**:{total_failures}条\n\n" + "\n".join(summary_lines)}},
            {"tag": "hr"},
            {"tag": "div", "text": {"tag": "lark_md", "content": "**疑似代码Bug**:\n" + "\n".join(bug_lines)}},
            {"tag": "hr"},
            {"tag": "note", "elements": [{"tag": "plain_text", "content": "由AI自动归因,请人工复核疑似Bug项"}]}
        ]
    }
}
return card

============ 发送飞书消息 ============

def send_feishu(card):
import time, hmac, hashlib, base64
timestamp = str(int(time.time()))
string_to_sign = f"{timestamp}\n{FEISHU_SECRET}"
hmac_code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()
sign = base64.b64encode(hmac_code).decode("utf-8")

import requests
payload = {
    "timestamp": timestamp,
    "sign": sign,
    "msg_type": "interactive",
    "card": card["card"]
}
resp = requests.post(FEISHU_WEBHOOK, json=payload)
print(f"飞书发送结果:{resp.status_code} {resp.text}")

============ 主流程 ============

def main():
report_path = "report.xml"
ifnot os.path.exists(report_path):
print("未找到报告文件,退出")
return

failures = parse_junit_xml(report_path)
ifnot failures:
    print("无失败用例,无需归因")
    return

context = collect_context(failures)
results = analyze_with_llm(context)
card = build_feishu_card(results, len(failures))
send_feishu(card)
print("归因完成")

if name == "main":
main()
四、接入CI:GitLab CI示例
在 .gitlab-ci.yml 里增加一个 stage:

stages:
-test
-analyze

run-tests:
stage:test
script:
-pipinstall-rrequirements.txt
-pytest--junitxml=report.xml
artifacts:
when:always
paths:
-report.xml
expire_in:1week

ai-analyze:
stage:analyze
needs:["run-tests"]
script:
-pipinstallopenairequests
-pythonci/analyze_failures.py
variables:
DEEPSEEK_API_KEY:$DEEPSEEK_API_KEY
FEISHU_WEBHOOK:$FEISHU_WEBHOOK
FEISHU_SECRET:$FEISHU_SECRET
only:
-merge_requests
-main
关键点:artifacts 必须把 report.xml 传递到下一个stage。when: always 确保测试失败时也会生成报告。

五、实际效果:从两小时到十分钟
我们拿一个跑了3个月的订单中台项目做对比。

改造前:

CI跑完,测试同学打开报告,50条失败
逐条点开,看日志、看截图、对代码
平均每条排查2-3分钟,总计约2小时
最终确认:5条真Bug,45条环境/用例问题
改造后:

CI跑完,飞书收到AI归因报告
测试同学只看“疑似代码Bug”那5条
人工复核5条,每条1-2分钟,总计约10分钟
其余45条自动分类,不占用注意力
效率提升:12倍。

六、避坑指南
坑一:把AI归因当成最终结论。

AI归因是“预筛”,不是“判决”。我们永远在飞书卡片末尾加一句“请人工复核疑似Bug项”。AI负责把噪音过滤掉,你负责做最终判断。

坑二:日志截断太狠,AI看不到关键信息。

一开始我只给AI传了失败消息的前200个字符,结果它经常把“连接超时”误判为“代码Bug”。后来改成传完整failure text的前2000字符,准确率明显提升。

坑三:Prompt写得太模糊。

如果你只写“分析这些失败”,AI会给你一堆模棱两可的结论。必须明确分类选项(环境/用例/代码Bug/数据/未知),并强制输出JSON。分类选项本身就是对AI的约束。

坑四:Token成本失控。

50条失败,每条2000字符,加上Git diff和Prompt模板,一次调用大概消耗3-5万Token。DeepSeek的价格,一次不到一毛钱。但如果你每天跑几十次CI,成本会累积。建议只在MR和main分支触发,feature分支不跑归因。

坑五:忽略安全。

失败日志里可能包含敏感信息(接口地址、Token、用户数据)。在传给大模型之前,做一个简单的脱敏处理:正则替换掉Bearer Token、手机号、邮箱、IP地址。这一步不能省。

最后
CI跑完测试,不是终点。把“失败归因”也自动化,才是终点。

以前你面对50条失败,要一条条翻。现在你面对一条飞书消息,只看5条。

你不需要成为大模型专家。你只需要在CI里加一个节点,让AI替你做那80%的噪音过滤。

下次CI红了的时候,别打开日志了。让飞书给你发结论。

相关文章
|
8天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
7386 12
|
6天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1545 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
7天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1008 8
|
3天前
|
人工智能 JavaScript 芯片
DeepSeek 官方偷偷上传 Harness 桌面端安装包,我已经用上了。。附最新下载地址
DeepSeek Harness 官方的桌面端安装包被网友扒出来了,2 分钟讲明白如何使用,体验如何,适合作为 AI 编程工具么?附最新 Windows 和 Mac 双端的下载地址
1200 1
|
20天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3581 10
|
15天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1611 1
|
4天前
|
编解码 缓存 PyTorch
16G 显卡能跑 Qwen-Image 2.1 吗?
9月20日,阿里Qwen开源Qwen-Image-2.1:7B DiT图像模型+8B文本编码器+VAE,单模型支持文生图与图像编辑,原生输出2K PNG(含Alpha通道),支持10张参考图。在自建Qwen-Image-Bench达60.28分(开源模型第一),GenAI Showdown文生图排名7/15。16G显存可跑1024×1024(需INT8量化+ComfyUI优化),但2K需24G以上。注意其Qwen Research License限非商业用途。
506 1
|
5天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)

热门文章

最新文章