以前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:业务逻辑错误、接口返回不符合预期、状态不一致等
- 数据问题:测试数据不存在、数据被其他用例修改、数据过期
- 未知:无法判断
同时请给出:
- 一句话归因结论
- 置信度(高/中/低)
- 如果是代码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红了的时候,别打开日志了。让飞书给你发结论。