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

举报
霍格沃兹测试开发学社 发表于 2026/09/30 13:17:17 2026/09/30
【摘要】 以前CI红了,一群人围在屏幕前翻日志猜原因。现在CI红了,飞书里直接弹出一条消息:“本次失败50条,其中45条为环境问题,5条疑似代码Bug,详情见下。”大家好,我是某互联网公司的测试架构师。上个月,团队里一个做自动化的同事跟我吐槽:“哥,我每天花在‘看失败报告’上的时间,比写测试脚本还多。”我问他怎么回事。他说每天CI跑完,少则二三十条失败,多则上百条。点开一看,有的是环境超时,有的是元素...
以前CI红了,一群人围在屏幕前翻日志猜原因。现在CI红了,飞书里直接弹出一条消息:“本次失败50条,其中45条为环境问题,5条疑似代码Bug,详情见下。”

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

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

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

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

他问:“怎么看?”

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

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

本次回归失败50条。

  • 环境问题:42条(连接超时/容器启动失败)
  • 用例问题:3条(定位器失效/数据污染)
  • 疑似Bug:5条(断言失败,涉及订单状态不一致) 详情:点击查看完整报告

他跟我说:“我现在每天花在失败排查上的时间,从两小时变成了十分钟。”

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

一、先搞清楚:这个方案的核心逻辑是什么?

传统CI流水线的测试阶段是这样的:

代码提交 → 触发CI → 跑测试 → 生成报告 → 人工看报告 → 人工归因 → 决定下一步

人工看报告、人工归因,是整个链路里最耗时的环节。

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

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

AI归因节点做三件事:

  1. 收集失败用例的日志、截图路径、相关代码变更
  2. 调用大模型对每条失败进行分类(环境/用例/代码Bug/数据/未知)
  3. 汇总成结构化报告,通过飞书机器人推送

人工只需要看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红了的时候,别打开日志了。让飞书给你发结论。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。