每天早上到工位的第一件事,不再是打开邮箱,而是点开 CI 流水线页面,像做侦探一样逐个翻失败用例。看一眼日志,翻一下这次提交的代码变更,猜一个原因,在群里 @ 对应的开发,然后祈祷他不是在开会。这套流程我干了整整一年,终于在某天早上爆发了——那天有 17 个失败用例,我花了快三个小时才归完因,而其中有 12 个其实是同一个根因。
所以我做了个决定:把大模型接进 CI。测试用例失败之后,自动收集失败堆栈、相关日志、这次提交涉及的代码变更,交给大模型做根因分析,再把分析结果和修复建议整理成消息卡片,直接推到飞书群里。整个过程在流水线里新增一个阶段完成,测试挂了以后大约 90 秒内,群里就会收到一份带“疑似根因”和“修复建议”的报告。
这篇文章就把整个落地的过程完整拆一遍,重点讲清楚三件事:失败用例的上下文怎么采集和裁剪、大模型的 Prompt 怎么设计才能让归因靠谱、飞书的机器人消息怎么接得又稳又不烦人。适合被 CI 失败用例反复折磨的测试开发、SRE、以及想在 DevOps 流程里引入 AI 能力但不知道从哪下手的同学。
1. 为什么我决定把大模型塞进 CI 流水线
1.1 每天早上的手工归因,真的做不动了
先交代一下背景。我们的 CI 是自建的 GitLab Runner,每天合并请求触发流水线,跑完单元测试、接口测试、前端构建之后,测试报告会归档成一个 JUnit XML 文件。单测和集成测试加起来大概 3000 多个用例,高峰期每天要执行七八次全量流水线,失败用例少则两三个,多则十几个。这些失败用例聚在一起,就是每天早上最折磨人的活。
手工归因为什么累?因为大多数失败用例的日志都是“看起来很像”的。比如一个接口超时,可能是服务没起来,可能是数据库连接池满了,也可能就是某段代码引入了死循环。你得先看堆栈,再翻这次 MR 改了哪些文件,猜测是不是改动引起的,然后打开对应服务的日志确认。每个用例平均要花 5 到 10 分钟,十几个失败用例就是一两个小时。更气人的是,同一批失败的用例往往来自同一个根因,比如测试环境部署出了问题,但你手动看的时候根本不知道这一点,只能一个个重复劳动。
我当时最强烈的需求是:能不能在流水线跑完的同时,就有一份报告告诉我“这些用例为什么失败,相互之间有没有关联,可能是谁改的代码导致的”。哪怕准确率不是 100%,也能把 80% 的重复劳动省掉。
1.2 我为什么不选“规则脚本 + 关键字匹配”的老路
一开始我确实想过用正则和规则脚本做。比如抓到NullPointerException就归因为空指针,抓到Connection refused就归因为服务不可用,抓到TimeoutException就归因为超时。这类方案实现起来很快,但有两个过不去的坎。
第一个坎是规则库的维护成本。你永远预料不到下一次失败是以什么姿势出现的。上周是 Redis 连接池耗尽,这周是某个 JSON 字段为 null 导致反序列化失败,下周可能又是某个第三方 SDK 的版本兼容问题。每出现一种新模式就得改一次规则,而且规则写多了以后相互之间还会冲突,同一个堆栈能匹配上三个规则,你根本不知道信谁。
第二个坎是跨模块根因很难通过关键字发现。A 服务改了接口字段,B 服务的消费方没有适配,导致 B 服务的用例在 CI 里挂掉——日志里可能根本没有 A 服务的任何痕迹,纯靠关键字匹配完全归因不出来。而大模型的语义理解能力恰好擅长解决这个问题:它可以综合“失败堆栈 + 本次提交的变更内容 + 相关历史日志”来做跨模块推断,输出一段带逻辑的归因解释。
这是我选择大模型路线的最核心原因:我要的不是一个应急 keyword 匹配器,而是一个能读懂“改动之间的关联关系”的分析助手。
1.3 方案目标与边界:自动归因不等于自动修复
想清楚目标才能不被带偏。我给这个项目定的目标是:在 CI 测试失败后的 2 分钟内,给出每个失败用例的疑似根因、影响范围、修复建议,并把报告推送到飞书群。同时必须控制成本,不能每次流水线都调用大模型,只有失败用例才走分析链路。
但我也给自己划了一条边界:自动归因不等于自动修复。大模型的输出只是给开发一个起点,而不是替代开发和测试的判断。所以卡片消息里我会把大模型的分析和原始日志、用例链接放在一起,让人能快速下钻验证。这个边界很重要,否则你会陷入“模型说的是错的但没人发现”的新风险里——后面我会详细说我们是怎么用数据评估这个风险的。
2. 全链路架构拆解:流水线里多出来的“分析环节”
2.1 完整链路一句话描述
整个方案在 CI 里新增了一个post-test-analysis阶段,挂在测试任务之后。链路可以概括成六个步骤:
- 测试任务结束后,归档 JUnit XML 报告和测试日志。
- 分析任务解析 JUnit XML,筛选出失败用例。
- 对每个失败用例,采集失败堆栈、相关代码变更、对应服务的错误日志片段。
- 把这些上下文拼接成 Prompt,调用大模型。
- 大模型返回结构化的 JSON 结果,包含根因分类、详细分析、修复建议。
- 脚本把结果打包成飞书消息卡片,发到指定的群。
如果一句话说清楚:测试挂了,流水线自动触发一个分析程序,程序把“失败现场”打包喂给大模型,再把模型的分析写成报告发出来。
下面我把几个关键节点的细节展开讲。
2.2 数据采集:从 JUnit XML 到 git diff
数据采集是整个方案里最容易被低估的环节。Prompt 写得再好,喂进去的数据不对,输出质量也上不去。我按四类数据来做采集:
| 数据来源 | 说明 | 采集方式 |
|---|---|---|
| JUnit XML | 失败用例的类名、方法名、失败信息、堆栈 | 解析testsuites/testcase/failure节点 |
| 控制台日志 | 失败服务在测试前后的错误日志 | 读取日志文件,按时间范围抽取 |
| git diff | 本次 MR 或 commit 涉及的文件变更 | 通过 GitLab API 或git diff命令 |
| 最近提交信息 | 关联的 commit message,辅助定位 | git log |
这里有个经验:不要直接把整个堆栈倒进去,大部分堆栈里 80% 是没用的框架调用。我会先做一层裁剪,只保留从项目自己的包名开始的栈帧,过滤掉java.lang.reflect、sun.*、org.springframework.test这些框架层。裁剪后的堆栈长度通常能压缩到原来的三分之一,信息密度反而更高。
2.3 调用大模型的成本与控制策略
大模型的调用成本是团队会直接质疑的点,我必须把这件事量化出来。我们的用量其实非常克制:只有失败用例才触发调用,正常通过的用例完全不涉及。以每天 10 次全量流水线、每次平均 2 个失败用例来算,一天的调用量大约 20 次左右。
模型选型上,我一开始试了旗舰款,效果确实最好,但单价太高。后来换成了通用款,把 Prompt 调好之后,归因质量差别并不大。我们的实际配置是:输入上下文控制在 4000 个 token 左右,输出限制在 800 个 token,单次成本约 0.02 元。按这个量级算下来,一个月的大模型调用成本不到 15 元。这个成本对比人工归因两三小时的时间成本来说,基本可以忽略。
如果你的团队没有 API 预算,也可以部署私有化模型。我们后来在另外一条业务线就用了私有化部署的版本,效果接近,只是响应速度稍微慢一点。这块完全取决于你们的合规要求和预算。
3. 归因质量的关键:上下文怎么采集,Prompt 怎么设计
3.1 只给堆栈远远不够,上下文要拼得“像人一样看”
大模型归因质量的高低,不在于模型有多强,而在于你有没有把“人平时会看的信息”完整地给它。我之前第一版 Prompt 只把失败堆栈丢进去,模型输出的归因基本就是复述堆栈,什么“空指针异常导致”,没有任何增量信息,属于典型的“正确废话”。
后来我重新设计思路:把自己平时手工归因时会看的信息完完整整地列出来,包括四块。第一块是失败用例的完整上下文:用例名、所属模块、失败阶段。第二块是裁剪后的堆栈和关键日志片段,尽量保留错误出现前后的 30 行日志。第三块是本次提交涉及的代码变更,这里尤其要注意,不要只给文件清单,要把具体改了哪一行、原始代码是什么样的都带上。因为很多失败就是改了一行引起的。第四块是历史数据:这个用例过去 7 天的通过情况,以及最近一次修改过相关代码的 commit。
在实际操作中,我发现第三块的贡献最大。很多用例失败是因为改了公共方法、改了数据库表结构、改了接口字段,而这些在堆栈里根本看不出来,只有结合代码变更才能定位。比如有一次一个用例报错是“字段 user_type 不匹配”,堆栈看了半天看不出问题,但结合同一批提交里改了实体类、把用户类型从 String 改成了 Integer,一眼就明白了。
3.2 Prompt 模板与输出约束
Prompt 我建议直接走“角色设定 + 数据 + 输出约束”三段式结构。角色设定很重要,它会显著改变模型的输出风格。在模型内部,我是这么写的:
你是一名资深的测试工程师和代码审查专家。你的任务是分析 CI 流水线中的失败用例,定位可能导致失败的根因。你需要结合失败堆栈、日志片段、代码变更上下文,推断失败原因,并给出可执行的修复建议。 以下是本次失败用例的相关信息: <失败用例> 名称:{testCaseName} 所属模块:{moduleName} 失败类型:{failureType} </失败用例> <失败堆栈> {stackTrace} </失败堆栈> <相关日志> {logSnippet} </相关日志> <本次代码变更> {diffContent} </本次代码变更> 请严格遵守以下输出格式,只输出 JSON,不要输出任何多余文字: { "root_cause_category": "AMBIGUOUS_ERROR", "root_cause_summary": "一句话根因摘要", "detailed_analysis": "详细的分析过程,说明你看到了哪些证据、哪些证据支持你的推断", "confidence": 0.85, "suggested_fix": "具体的修复建议,尽量指出涉及的文件和修改方式", "related_files": ["文件路径1", "文件路径2"] }输出约束这里我给了一个枚举值root_cause_category,这个设计很关键。我们预定义了几类根因:CODE_CHANGE、ENV_ISSUE、TEST_CODE_BUG、DATA_ISSUE、DEPENDENCY_CHANGE、AMBIGUOUS_ERROR。有了枚举值,后面做数据统计、错误分类、以及跟开发拉通时的沟通成本都会大幅降低。confidence字段用来给每个失败案例打一个置信度,低于 0.5 的结果我们会在报告里标记为“仅供参考”,提醒开发不要过度采信。
3.3 输出校验与失败兜底
大模型返回的结果不能直接用,至少要做两层校验。第一是 JSON 格式校验。模型偶尔会输出多余的 Markdown 包裹或者解释文字,我们必须用代码严格解析json.loads,失败就进入重试或降级逻辑。第二是字段合法性校验,比如confidence必须在 0 到 1 之间,root_cause_category必须是枚举值里的一个,related_files里的文件路径必须存在。这一步能过滤掉相当一部分模型的胡言乱语。
我在这块吃过一个亏:有一次模型返回的 JSON 里多了一个尾随逗号,直接导致json.loads抛异常,整个分析任务挂掉,群消息没发出来。后来我改用两步解析策略:先用正则提取最外层花括号的 JSON 快,再做json.loads,实在解析失败就发一条“解析失败,请看原始报告”的降级消息,同时附带原始日志链接。这样至少保证人不会漏掉失败信息。
4. 飞书端的工程细节:从 Webhook 到消息卡片
4.1 自定义机器人 Webhook 接入
飞书这边的接入方式挺简单,我们在飞书群里添加一个自定义机器人,拿到 Webhook 地址。要注意的是,机器人有两种安全设置可选:关键字校验和签名校验。我强烈建议你用签名校验,因为只用关键字校验的话,任何往这个群里发消息的程序只要带上关键字就能发,安全边界太松散。
签名校验的逻辑需要在代码里实现:把当前时间戳、密钥、时间戳拼接成字符串做 HMAC-SHA256 签名,用 base64 编码后拼到请求参数里。下面是我用 Python 实现的签名代码:
import base64 import hashlib import hmac import time def gen_sign(secret, timestamp): string_to_sign = f"{timestamp}\n{secret}" hmac_code = hmac.new( string_to_sign.encode("utf-8"), digestmod=hashlib.sha256 ).digest() return base64.b64encode(hmac_code).decode("utf-8") timestamp = str(int(time.time())) sign = gen_sign(secret, timestamp) # 请求 URL 需要带上 timestamp 和 sign url = f"{webhook_url}?timestamp={timestamp}&sign={sign}"这里有一个容易踩的坑:签名里的时间戳必须用请求发出的当前时间,而且飞书那边会检测时间戳偏差,超过一小时会拒绝请求。如果服务器时间不准,或者把时间戳缓存了,就会收到签名过期的错误。我第一次是从模板消息里手动带时间戳去测,发现怎么都调不通,排查了半天才意识到是自己本地时间比对逻辑写错了。
4.2 卡片消息的模板与按钮交互
飞书机器人支持文本消息和交互卡片两种形态。我强烈建议直接用卡片。原因很简单:卡片可以结构化展示根因摘要、置信度、修复建议和链接按钮,读起来不费劲。我们实际的卡片结构是这样组织的:
- 标题:
测试失败分析报告 - {项目名} - {构建号} - 正文正文:总失败数、分布统计(按根因类别)、一句话总结。
- 二级标签:每个失败用例单独列出来,包含用例名、错误类型、根因类别卡、置信度条、一句话摘要。
- 底部按钮:
查看完整报告、跳转 CI 流水线。
卡片消息的结构可以写成 JSON 模板,Python 里用字典拼出来,最后调飞书的interactive类型消息接口。要注意卡片里的字段是有长度限制的,长文本不要一股脑全怼进去,特别是堆栈这种超长内容,放到详情页(比如 CI 日志页或我们内部的一个报告页面),卡片里只放摘要和链接。
4.3 群报告怎么做到“一屏看完”分
实际使用中,开发们最关心的不是单个用例的归因细节,而是“这轮失败总体上是什么原因”“我负责的模块有没有问题”。所以报告一定要有聚合视图。
我的做法是在卡片顶部加一段“聚合结论”,由大模型对整轮失败先做一次汇总分析,比如“本次 8 个失败用例中,5 个与 user-service 的接口字段变更有关,2 个是测试环境数据库连接池耗尽,1 个待确认。”这个聚合结论对开发来说价值最大,它能立刻帮人建立整体认知。
为了做到这一点,我在流水线里的分析脚本会先单独调用一次“汇总调用”:把所有失败用例的归因结果聚合到一起,再让大模型生成综合结论。这次调用因为输入比较浓缩,成本非常低,但收益很大。实践下来,开发拿到卡片后判断“是否紧急处理”的时间从平均 5 分钟降到了 30 秒。
另外,关于“飞书机器人发送表格”这个需求,我也试过。如果想让报告里的失败用例以表格形式展示,可以用飞书的附件表格或卡片表格。我最后采用的是卡片自带的“表格组件”,把用例名、模块、根因类别、置信度、修复建议这几列塞进去。这个组件在手机端飞书上滚动有点卡,但在 PC 端体验很好。如果你要展示的列太多,考虑做一个跳转链接,指向云文档或内部页面,别硬塞卡片。
5. 实测效果、踩坑记录与还能怎么扩展
5.1 跑了一个季度的真实数据
这个方案上线到现在跑了差不多一个季度,我把关键数据列一下。平均每天分析 15 个失败用例,大模型归因的整体准确率在 68% 左右。这里说的“准确率”是指:模型输出根因类别与人工最终确认的一致率。听起来不算惊艳,但要注意,原来我们没有归因,现在至少 68% 的结果可直接参考,另外还有一部分是“部分正确”(方向对了但细节不对)。如果只看“方向正确”的话,大概在 84%。
最明显的效率提升在于跨模块根因的发现。以前手工归因跨模块问题经常要找到对应开发问一圈才能确定,现在模型往往会在一开始就指出“user-service 的接口变更可能影响了 mobile-api 的消费方用例”,减少了大量沟通成本。
数据统计也验证了一个判断:ENV_ISSUE类占比最高,大约 38%,主要就是测试环境不稳定、数据库连接数打满、依赖服务未就绪这几类。这类根因在过去纯手工归因时最容易被忽略,因为大家都习惯把锅甩给“测试代码写得不好”,而模型会基于日志里的证据给出相对客观的归属。
5.2 三个印象最深的坑
第一个坑是超时。我们调用大模型的 API 平均响应时间在 15 秒左右,但偶尔会飙到 60 秒以上。刚开始没有设置超时,分析阶段直接卡了 8 分钟,严重影响流水线总时长。后来我做了双层保护:代码层设 25 秒超时,超时则跳过该用例并把“分析超时”写进报告;流水线层设 3 分钟总超时,过了就强制失败但保证消息一定发出。这样最坏情况也不会让 CI 无限阻塞。
第二个坑是上下文溢出。有一次某个模块的 MR 改了 200 多个文件,git diff生成的内容有几万行,直接把模型输入上下文撑爆了。解决办法是给 diff 加限制:只保留测试用例直接相关的文件、以及被测试用例依赖的公共模块文件,每个文件的 diff 最多取前 80 行。超出部分我会在 Prompt 里写“XX 文件变更过大,已截断,重点看其余文件”。
第三个坑是误报。模型有几次把ENV_ISSUE错误地归因为CODE_CHANGE,导致开发组在完全没有问题的代码里反复排查。后来我在 Prompt 里加了一条约束:“如果日志中存在超时、连接重试、资源不足等环境类证据,优先归类为 ENV_ISSUE;除非你能明确指出具体代码变更与本失败存在直接因果链。”这一改之后,ENV 类的误报率降了不少。这说明大模型归因的边界需要我们靠经验去对齐,Prompt 迭代是持续的事。
5.3 下一步可以做的方向
现在这个系统解决的是“失败之后告诉你怎么回事”,但还没做到“失败之前拦住它”。我接下来的规划有两个方向。
第一个是把分析结果沉淀成知识库。每次确认真实的根因之后,可以把这轮失败用例、归因结果、最终确认结论存起来,形成历史样本库。后续大模型再归因时,可以先用相似度在样本库里检索历史案例,找到最接近的 1-2 条作为 Few-shot 示例喂给模型。这个方向如果跑通,准确率应该还能再上一个台阶。
第二个方向是把归因结果和 MR 改动自动关联。比如当模型判断是代码变更引起的失败时,可以直接在飞书卡片里生成一个“建议退回此 MR”的按钮,人工确认后一键触发操作。这个涉及权限和流程审批,不会做成全自动,但可以把“定位到责任人”和“建议操作”这两段流程打通。
这个系统整体已经变成我们团队早上的“值班助手”了。工程师来上班第一件事不是翻 CI,而是看一眼群里的自动报告,10 秒就知道昨天的失败是什么情况。按我个人的体验,这类实践最值得投入的点在于:它没有改变工程师原有的工作习惯,只是把最费时的“信息收集和初步判断”这部分自动化了,而把最终判断权留给了人。这才是 AI 接进开发流程里我觉得最舒服的姿势。