ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于大模型的CI失败用例智能归因:pytest与飞书推送实践

基于大模型的CI失败用例智能归因:pytest与飞书推送实践 1. 为什么我要把大模型塞进 CI 流水线做自动化测试的同行大概都有同感CI 跑完报告里躺着几十条失败用例红彤彤一片但真正需要人介入判断的其实就那么几条。剩下的要么是环境抖动、要么是断言写得太死、要么是上游接口字段改了个名字。可问题是你得一条条点开日志看堆栈才能分辨哪条是真 bug、哪条是噪音。一个中等规模的项目每天 CI 跑个七八轮光看失败日志这件事就能吃掉一个测试同学小半天。我最初的想法很朴素既然大模型读日志、做归纳的能力已经够用那能不能让它替我做第一轮筛选CI 跑完把失败用例的日志喂给模型让它输出这条大概率是什么原因、建议怎么处理然后直接把结论推到飞书群里。这样我早上到工位看到的不是一堆原始日志而是一份已经归好类的简报。这个项目落地之后实际效果比我预期好。失败用例的归因准确率在常见场景下能到八成以上剩下两成模型会标不确定需人工确认反而帮我建立了信任感——它不瞎猜。整条链路是pytest 跑测试 → 收集失败用例的结构化信息 → 调用大模型做归因 → 生成 Markdown 报告 → 飞书机器人推送。关键词里的 pytest、大模型、飞书、失败用例归因基本就是这条链路的四个核心环节。这篇文章我会把每个环节的选型理由、踩过的坑、以及最终跑稳的配置都摊开讲。适合两类人看一是已经在用 pytest 做自动化、想给 CI 加一层智能筛选的测试或运维同学二是想把大模型能力接进现有工程流程、但不知道从哪下手的后端同学。不需要你有大模型训练经验会调 API 就够了。2. 整体链路的拆解与关键选型2.1 四个环节各自解决什么问题先把链路拆清楚不然后面配置容易乱。整条流水线我分成四段每段职责单一方便单独调试和替换。环节职责关键产出可替换性测试执行跑 pytest产出原始结果junit xml / 控制台日志高任何测试框架都行信息收集从结果里抽取失败用例的结构化数据用例名、断言、堆栈、耗时中依赖框架的报告格式大模型归因对每条失败做原因分类和建议归因标签 置信度 建议高换模型只改一处报告推送汇总成 Markdown 发飞书群消息卡片高换钉钉/企微同理这个拆法的好处是归因逻辑和推送逻辑解耦。我一开始把两者写在一起结果想换个推送渠道时发现归因代码里到处是飞书的字段名重构了一遍才理清。所以强烈建议从第一天就分开。2.2 为什么选 pytest 而不是别的框架项目本身用的是 pytest这点没什么好纠结的。但我要说的是 pytest 在这个场景下的一个隐藏优势它的插件生态让收集失败信息这件事变得极其省事。pytest-json-report或者直接解析--junitxml产出的 XML都能拿到结构化的失败数据包括完整的 traceback 文本。相比之下如果用的是自研的测试脚本你得自己往日志里塞标记工作量翻倍。具体来说pytest 的 junit xml 里每条失败用例会有failure节点里面是完整的异常堆栈。这个堆栈就是喂给大模型的核心原料。我实测下来堆栈里最有价值的三样东西是异常类型和消息比如AssertionError: expected 200 but got 500、出错的文件和行号、调用链的最后几层。前面那些框架内部的调用栈其实可以裁掉能省不少 token。2.3 大模型选型的现实考量关键词里出现了免费大模型 api企业大模型私有化部署ollama 部署私有大模型这些说明大家对成本和数据安全都敏感。我的建议分两种情况内部项目、日志不含敏感数据直接用云端 API选一个性价比高的通用模型即可。归因任务对模型能力要求不算高不需要顶配。日志涉及业务数据、有合规要求走本地部署用 ollama 之类跑一个中等参数量的模型。归因这种任务7B 到 14B 级别的模型在中文日志理解上已经够用没必要上大参数。我自己的做法是双通道默认走本地模型遇到本地模型置信度低的用例再走云端做二次确认。这样既控制了成本又保证了疑难用例的准确率。这个设计后面第 4 节会详细讲。注意无论走哪条通道喂给模型的日志都要先做脱敏。手机号、身份证、内部 IP、密钥这类东西在收集阶段就用正则替换掉。别等出了事再补。2.4 飞书推送为什么用机器人而不是 webhook 裸发飞书群机器人有两种常见玩法自定义 webhook 和自建应用。我选的是自定义 webhook 消息卡片。理由很简单webhook 配置成本极低一个 URL 搞定不需要申请应用权限、不需要维护 token 刷新。而消息卡片能让报告排版好看很多支持 Markdown 语法、支持折叠、支持颜色标记。关键词里有飞书机器人发送表格这点我踩过坑。飞书的卡片消息对表格支持有限直接塞 Markdown 表格经常渲染错乱。我的处理是报告正文用 Markdown 列表和加粗只有汇总统计用飞书原生的表格组件。这样既清晰又不会翻车。具体格式后面第 5 节给模板。3. 从 pytest 结果里榨出有用的失败信息3.1 用 junit xml 还是 json report两种我都试过最后选了 junit xml。原因是它零依赖——pytest 自带--junitxml参数不需要额外装插件CI 环境里少一个依赖就少一个坑。json report 虽然字段更丰富但要多装一个pytest-json-report而且不同版本字段名会变。生成命令很简单pytest --junitxmlreport.xml -v跑完之后report.xml里每个testcase节点如果失败会带一个failure子节点。解析用 Python 标准库xml.etree.ElementTree就够不用引入额外依赖。3.2 抽取哪些字段才够模型归因这是整个项目最关键的细节。字段抽多了浪费 token抽少了模型判断不准。我反复调了几轮最终定下来这几个字段{ case_name: test_user_login_invalid_password, classname: tests.test_auth.TestLogin, duration: 1.23, failure_type: AssertionError, failure_message: expected status 401 but got 200, traceback_tail: 最后 15 行堆栈, file_line: tests/test_auth.py:88 }重点说三个failure_type 单独抽出来异常类型是模型判断的第一信号。AssertionError通常是断言问题ConnectionError是环境问题KeyError多半是数据结构变了。把它单独拎出来模型分类准确率明显提升。traceback 只取尾部完整堆栈动辄上百行前面全是框架内部调用。我实测取最后 15 行信息量足够token 消耗降了六成以上。file_line 保留方便模型结合文件名做上下文推断比如test_auth.py里的失败和test_payment.py里的失败归因方向天然不同。3.3 堆栈裁剪的具体规则裁剪不是简单截断那样会把关键信息切掉。我的规则是从异常类型那一行开始往下取到最后一个业务代码帧。实现上先按行切分找到第一个匹配^[A-Za-z]Error|^[A-Za-z]Exception|^AssertionError的行作为起点然后往下取 15 行。如果 15 行内出现了框架路径比如site-packages/pytest就提前截断。import re def trim_traceback(tb_text, max_lines15): lines tb_text.strip().splitlines() start 0 for i, line in enumerate(lines): if re.match(r^[A-Za-z_](Error|Exception), line.strip()): start i break tail lines[start:start max_lines] # 遇到框架内部帧就停 result [] for line in tail: if site-packages in line and result: break result.append(line) return \n.join(result)这段逻辑我调了好几版。最早是直接取最后 15 行结果经常把 pytest 的收尾日志也带进去模型被干扰。加上框架路径截断之后归因准确率肉眼可见地涨了。3.4 一个容易被忽略的坑编码问题CI 环境里跑出来的 XML如果测试代码里有中文断言消息编码处理不当会变成乱码喂给模型就是一堆问号。我踩过一次排查了半天才发现是ElementTree解析时没指定编码。解决办法是读文件时显式用utf-8tree ET.parse(report.xml) # 或者更稳妥 with open(report.xml, r, encodingutf-8) as f: tree ET.parse(f)另外如果 CI 机器的 locale 不是 UTF-8pytest 输出的 XML 声明里编码可能不对。保险起见在 CI 脚本里加一句export PYTHONIOENCODINGutf-8一劳永逸。4. 让大模型做归因提示词设计与置信度控制4.1 归因分类体系怎么定模型要分类你得先告诉它有哪些类。我一开始让模型自由发挥结果它每次给的标签都不一样没法统计。后来固定了一套分类效果立刻稳定归因标签含义典型信号真实缺陷代码逻辑确实有问题断言值与预期不符且堆栈指向业务代码环境问题依赖服务、网络、配置异常ConnectionError、Timeout、DNS 解析失败数据问题测试数据被污染或过期KeyError、数据校验失败、唯一约束冲突断言过严断言写得太死非功能问题时间戳、浮点数、顺序相关的断言失败用例缺陷测试代码本身有 bug变量未定义、fixture 报错不确定信息不足需人工判断模型置信度低于阈值这套分类覆盖了我项目里九成以上的失败场景。关键是**不确定这一档必须留**否则模型会硬猜反而误导人。4.2 提示词的实际写法提示词我改了十几版最终稳定下来的结构是角色设定 分类定义 输出格式约束 输入数据。核心是输出格式必须严格约束成 JSON否则解析起来很痛苦。你是一名资深测试工程师负责对 CI 失败用例做归因分析。 请根据提供的失败信息从以下标签中选择一个 真实缺陷 / 环境问题 / 数据问题 / 断言过严 / 用例缺陷 / 不确定 判断依据 - 异常类型和消息是首要信号 - 堆栈指向业务代码还是框架代码 - 断言内容是否涉及时间、浮点、顺序等易变因素 输出严格的 JSON不要任何额外文字 { label: 标签, confidence: 0.0到1.0之间的小数, reason: 一句话说明判断依据, suggestion: 一句话给出处理建议 } 失败信息 用例名{case_name} 异常类型{failure_type} 异常消息{failure_message} 堆栈尾部 {traceback_tail}几个细节值得说confidence 字段是灵魂。让模型自己报置信度虽然不完全准但相对排序很有用。低于 0.6 的我统一归到不确定。reason 和 suggestion 各限一句话。不限制的话模型会写小作文报告变得又长又难读。明确说不要任何额外文字。有些模型喜欢在 JSON 前后加解释加了这句能压住大部分。4.3 置信度阈值怎么定阈值不是拍脑袋定的。我的做法是先跑一批历史失败用例人工标注真实标签然后看模型在不同置信度区间的准确率。实测下来置信度 0.8 以上的准确率能到九成0.6 到 0.8 之间掉到七成左右0.6 以下基本靠猜。所以我把阈值定在0.7高于 0.7 直接采信低于 0.7 标记为待确认并触发云端二次归因。这个数字不是固定的你的项目失败模式如果比较集中阈值可以调低如果失败原因五花八门就调高。4.4 批量归因与并发控制失败用例多的时候一条条调 API 太慢。我做了批量处理把多条失败信息拼成一个请求让模型返回 JSON 数组。但要注意两点单次批量别超过 10 条。太多模型会漏答或串行实测 10 条以内准确率稳定。并发要限流。CI 环境里同时跑多个 job如果每个 job 都并发调 API容易触发限流。我用一个简单的信号量控制并发数在 3 到 5 之间。import asyncio sem asyncio.Semaphore(4) async def analyze_batch(batch): async with sem: return await call_llm(batch)4.5 本地模型和云端模型的协同前面提到的双通道实现上是这样本地模型先跑一遍置信度高的直接采信置信度低的用例攒起来一次性发给云端模型。这样云端调用量能压到总失败数的两成以内成本可控。本地模型我用 ollama 跑模型选的是中文能力较好的中等参数量版本。这里有个经验本地模型对提示词的格式敏感度更高同样的提示词云端模型能理解本地模型可能输出格式就跑偏了。解决办法是在本地模型的提示词里把 JSON 示例写得更完整甚至给出一个完整的输入输出样例。5. 报告生成与飞书推送的工程细节5.1 报告结构怎么设计才好读报告是给人看的结构比内容还重要。我的报告分三块概览统计、按归因分组的明细、待确认清单。概览让人一眼看到全貌明细让人按需深入待确认清单提醒人工介入。## CI 失败归因报告 **运行时间**2026-01-15 09:30 **失败总数**12 **真实缺陷**3 | **环境问题**5 | **断言过严**2 | **待确认**2 ### 真实缺陷需优先处理 - **test_payment_refund**断言失败退款金额计算错误 - 建议检查 refund 计算逻辑疑似精度问题 ### 环境问题可重试 - **test_user_sync**连接超时 - 建议检查上游服务可用性可重试 ### 待确认需人工判断 - **test_order_export**置信度 0.55信息不足这个结构我用了几个月反馈很好。关键是把真实缺陷放最前面因为那是唯一需要立刻动手的。5.2 飞书消息卡片的格式约束飞书自定义机器人的消息体是 JSON支持text、post、interactive几种类型。报告这种带格式的内容用interactive卡片最合适。但卡片对 Markdown 的支持是子集不是全量。踩过的坑表格渲染不稳Markdown 表格在卡片里经常错位我改成用列表 加粗。标题层级有限卡片里###和####效果差不多别指望多级标题。颜色标记卡片支持red、green、grey等模板色用**加粗**配合颜色模板能突出重点。发送的核心代码import requests def send_to_feishu(webhook_url, markdown_content): payload { msg_type: interactive, card: { header: { title: {tag: plain_text, content: CI 失败归因报告}, template: red }, elements: [ {tag: markdown, content: markdown_content} ] } } resp requests.post(webhook_url, jsonpayload, timeout10) return resp.json()5.3 消息长度超限怎么办飞书卡片有长度限制失败用例多的时候报告会超。我的处理是截断 折叠概览统计永远完整显示明细只显示前 10 条剩下的提示完整报告见附件。更彻底的做法是把完整报告上传到飞书云文档消息里只放链接。关键词里有lark sync 同步飞书云盘飞书云文档内容嵌到自己网站说明云文档集成是常见需求。我目前用的是简化方案——报告存到 CI 的 artifact 里消息里附一个内网链接。如果你们团队重度使用飞书云文档可以调飞书开放接口上传文件体验更好。5.4 推送失败的兜底webhook 偶尔会抽风网络抖动、飞书限流都可能。我的兜底策略是重试三次 降级为纯文本。如果卡片发送失败退化成text类型再发一次至少保证信息送达。def send_with_fallback(webhook_url, content): for i in range(3): try: result send_to_feishu(webhook_url, content) if result.get(code) 0: return True except Exception: time.sleep(2 ** i) # 降级 return send_text(webhook_url, content[:2000])注意重试间隔用指数退避别用固定间隔否则限流时越重试越糟。6. 上线后踩过的坑与调优记录6.1 模型把环境抖动误判成真实缺陷这是上线初期最严重的问题。CI 机器偶尔网络抖动导致ConnectionError模型却判成真实缺陷因为堆栈里确实指向了业务代码的调用处。结果每天早上报告里一堆真实缺陷点进去一看全是重试就过的。根因是提示词里没强调环境信号。我在分类定义里补了一句如果异常类型是 ConnectionError、Timeout、DNS 相关且堆栈中没有明确的业务逻辑错误优先判为环境问题。改完之后误判率降了一大截。6.2 断言过严这一类识别不准时间戳、浮点数、字典顺序这类断言失败模型经常判成真实缺陷。原因是它看到AssertionError就默认是逻辑问题。我的解法是在提示词里给具体例子以下情况判为断言过严 - 断言涉及时间戳、日期格式 - 断言涉及浮点数相等比较 - 断言涉及列表或字典的顺序 - 断言涉及随机生成的 ID给了例子之后这一类准确率明显提升。给模型举反例比讲道理管用这是我在这个项目里最大的体会。6.3 token 消耗超预算上线第一周 token 消耗比预估高了三倍。排查发现两个原因一是堆栈没裁干净二是批量归因时把成功用例也带进去了。修复后消耗降到预估的 1.2 倍。具体优化堆栈裁剪从取最后 20 行改成从异常行开始取 15 行 框架帧截断。只对失败用例调模型成功用例直接跳过。相同异常类型的用例合并归因比如 5 条都是ConnectionError只调一次模型结果复用。最后这条省得最多。很多 CI 失败是同一个原因导致的连锁反应合并处理既省 token 又让报告更清爽。6.4 报告没人看的问题技术上跑通了但团队里没人看报告这是最尴尬的。后来我做了两件事一是把报告推到值班群并 相关人二是在报告里直接给出建议动作比如可重试需人工确认建议提 bug。关键转变是报告不再是信息展示而是行动指引。测试同学看到可重试就直接点重跑看到建议提 bug就去建单。这样报告才真正融入了工作流。6.5 一个关于模型稳定性的经验同一个失败用例模型今天判真实缺陷明天判环境问题这种不稳定很让人头疼。我的应对是温度参数调到最低接近 0并且在提示词里强调基于给定信息做判断不要发挥。温度调低之后同一输入的输出基本稳定了。另外如果你们用的是支持结构化输出的模型接口一定要开启。它能强制模型按 JSON schema 输出省掉大量解析容错代码。7. 几个可以立刻上手的扩展方向7.1 把归因结果回写到测试管理系统现在报告是发到飞书但归因结果其实可以回写到测试管理平台给每个失败用例打标签。这样历史数据积累起来就能分析哪类失败最常出现反过来指导测试用例的优化。关键词里的大模型知识抽取框架思路类似都是把非结构化信息转成结构化资产。7.2 自动重试环境类失败既然模型能识别出环境问题那就可以自动触发重试。我的做法是归因为环境问题的用例自动重新跑一次如果通过就标记为抖动不通过才进报告。这一步能进一步减少噪音。7.3 用历史数据做趋势分析归因结果存下来之后可以按周统计各类失败的占比变化。如果断言过严突然增多说明最近有人加了一批脆弱的断言如果环境问题持续高位说明基础设施该优化了。这种趋势比单次报告有价值得多。7.4 扩展到其他 CI 场景这套链路不只适用于 pytest。任何产出结构化失败信息的 CI 环节都能接——比如构建失败、部署失败、静态检查失败。核心逻辑是一样的抽取关键信息、模型归因、推送报告。我最近在把构建失败的日志也接进来复用同一套归因和推送代码只改了信息抽取那一段。我个人在实际操作中的体会是这套东西的价值不在于模型多聪明而在于它把人肉筛选这一步自动化了。模型判错的那些人工兜底模型判对的那些省下的时间就是净赚。别追求百分之百准确追求的是让人只看该看的那部分。最后分享一个小技巧上线初期先让模型只报告不行动跑一两周对比人工判断建立信任之后再逐步放开自动化动作这样团队接受度会高很多。
返回列表