ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于AI Agent的CI故障自动修复系统设计与实现

基于AI Agent的CI故障自动修复系统设计与实现

1. 为什么我要自己搭一个 CI 故障处理 Agent

CI 挂了这件事,但凡在团队里负责过合并请求的人都有体会。凌晨两点流水线红了,打开日志一看是某个依赖版本冲突,或者一个测试用例因为时区问题偶发失败,又或者 lint 规则在某个新文件上没通过。修起来可能只要三分钟,但从收到通知、切分支、定位问题、改代码、推提交到重新跑流水线,整套动作下来半小时就没了。如果一天来上四五次,基本不用干正事了。

我所在的团队每周大概有 60 到 80 次流水线运行,其中约 15% 会以失败告终。这个失败率不算离谱,但处理这些失败消耗的注意力非常可观。更麻烦的是,很多失败是重复性的:同样的依赖缺失、同样的格式问题、同样的超时。人做重复劳动会烦,烦了就会拖,拖了就会阻塞别人的合并请求,最后变成团队层面的效率问题。

所以我开始琢磨:能不能让一个 AI Agent 来接管这件事?它监听流水线事件,失败时自动拉取日志,分析根因,如果是可自动修复的类型就直接改代码、推提交、重新触发流水线,如果是需要人判断的类型就整理好上下文发到合并请求里。这样人只需要处理真正需要决策的部分。

这个想法落地之后,我把它做成了一个完整的 Agent,跑在我们自己的基础设施上。这篇文章就是把整个构建过程拆开讲清楚:架构怎么设计、日志怎么抓、分析怎么做、修复怎么落地、哪些坑我踩过。如果你也在被 CI 故障消耗精力,这套东西可以直接参考。

2. 整体架构设计与技术选型思路

2.1 Agent 的核心工作流拆解

一个能处理 CI 故障的 Agent,本质上要完成一个闭环:感知、分析、决策、执行、反馈。这五个环节缺一不可,而且顺序不能乱。

感知层负责接收流水线事件。最常见的触发源是代码托管平台的 Webhook,比如流水线运行状态变为 failed 时推送一个事件过来。这个事件里通常包含仓库信息、分支名、提交 SHA、运行 ID 等元数据。感知层要做的就是把这些信息接住,然后交给下一层。

分析层是核心。它需要根据运行 ID 去拉取完整的日志,然后从日志里提取出失败的关键信息。这里有个误区:很多人以为直接把日志丢给大模型就行了。实际上日志动辄几千行,里面大量是正常输出,真正有用的可能就那几十行。所以分析层要先做日志裁剪和关键信息提取,再把精简后的内容交给模型做根因判断。

决策层根据分析结果决定下一步动作。这里要预设一个修复策略表:哪些失败类型可以自动修,哪些只能建议,哪些必须人工介入。比如格式问题、依赖版本问题、明显的拼写错误,这些可以自动修;而涉及业务逻辑的测试失败、架构层面的问题,就只能生成分析报告。

执行层负责落地修复动作。如果是自动修复,它需要拉取代码、应用修改、提交、推送、重新触发流水线。如果是建议模式,它需要把分析结果格式化成评论发到合并请求上。

反馈层是很多人会忽略的。Agent 做完一次修复后,要记录这次修复的类型、结果、耗时,用于后续优化策略。比如某种修复经常失败,就要把它从自动修复列表里移到建议列表。

2.2 为什么选 GitHub Actions 作为切入点

市面上流水线平台很多,我选 GitHub Actions 作为第一个支持对象,原因很实际。

它的 API 设计比较统一,运行日志、运行状态、重新触发这些操作都有对应的接口,不需要为不同平台写适配层。它的 Webhook 事件模型也清晰,workflow_run事件里直接带了结论字段,判断失败很简单。另外它的日志存储结构规整,每个步骤的日志是分开的,裁剪起来比那种把所有输出混在一起的平台容易得多。

当然这不意味着只能支持它。架构上我把平台相关的部分抽象成了一个适配器接口,后面要接别的平台,只需要实现对应的日志拉取和事件解析方法就行。第一版先把一个平台做透,比同时铺开几个平台要靠谱。

2.3 AI Agent 的架构模式选择

关于 Agent 的架构,业界讨论比较多的有几种模式:ReAct 模式、Plan-and-Execute 模式、多 Agent 协作模式。我最终选的是简化版的 Plan-and-Execute。

ReAct 模式是边想边做,每一步都让模型决定下一步动作。这种模式灵活,但用在 CI 修复场景里有个问题:修复流程其实是相对固定的,先拉日志、再分析、再改代码、再推送,不需要模型每一步都重新规划。用 ReAct 反而增加了不确定性和 token 消耗。

多 Agent 协作模式是拆成多个专职 Agent,比如一个负责分析、一个负责写代码、一个负责审查。这种模式适合复杂任务,但对于 CI 故障处理来说有点过度设计。一个 Agent 加上清晰的工具定义就够了。

Plan-and-Execute 模式是先让模型生成一个修复计划,然后按计划执行。这个模式的好处是可控:计划生成后可以先做一轮校验,确认修复动作在允许范围内再执行。对于自动化修复这种需要谨慎的场景,可控性比灵活性更重要。

具体实现上,我把整个流程拆成了几个固定的阶段,每个阶段调用模型时给它明确的输入和输出格式要求。这样既保留了模型的判断能力,又避免了流程失控。

2.4 技术栈与依赖清单

后端语言我选了 Python。原因很简单:AI 生态的库最全,日志处理、HTTP 请求、Git 操作这些都有成熟的包。虽然有人用 Rust 写 Agent 追求性能,但 CI 故障处理这个场景对延迟不敏感,一次处理几十秒完全可以接受,没必要为了性能牺牲开发效率。

核心依赖包括:用于调用模型的 SDK、用于 Git 操作的库、用于 HTTP 请求的客户端、用于解析日志的正则和文本处理工具。模型方面我用的是支持长上下文和工具调用的通用模型,具体哪家可以根据自己的情况选,接口层做一层抽象就行。

部署上我用了容器化方案,Agent 作为一个常驻服务运行,接收 Webhook 推送。这样比定时轮询要实时,也比无服务器方案更容易控制状态。

3. 核心模块的细节实现与实操要点

3.1 事件接收与任务队列设计

Webhook 接收看起来简单,但有几个细节不注意会出问题。

第一是签名校验。代码托管平台推送事件时会带一个签名头,必须校验通过才能处理,否则任何人都能伪造事件触发你的 Agent。校验逻辑是用共享密钥对请求体做哈希,然后和签名头比对。这一步不能省。

第二是幂等性。同一个失败事件可能会被推送多次,比如网络重试。如果每次都触发一次修复,就会出现重复提交。我的做法是用运行 ID 加提交 SHA 作为唯一键,处理前先查一下这个键有没有处理过,处理过就直接返回。

第三是异步处理。Webhook 请求要求快速响应,不能在里面做耗时的日志拉取和模型调用。所以接收到事件后,先丢进任务队列,立即返回成功,后台 worker 再从队列里取任务处理。队列我用的是轻量的内存队列加持久化,保证服务重启后任务不丢。

def handle_webhook(request): if not verify_signature(request): return Response(status=401) payload = request.json() if payload["action"] != "completed": return Response(status=200) if payload["workflow_run"]["conclusion"] != "failure": return Response(status=200) task_key = f"{payload['workflow_run']['id']}:{payload['workflow_run']['head_sha']}" if is_processed(task_key): return Response(status=200) enqueue_task(payload) return Response(status=200)

这段逻辑里,先校验签名,再过滤事件类型,只处理失败的运行,然后做幂等检查,最后入队。每一步都是必要的,少一步都可能出问题。

3.2 日志拉取与智能裁剪

日志拉取是第一个技术难点。一次失败的运行,日志可能有几千行,直接全部塞给模型既浪费 token 又降低分析准确率。所以必须裁剪。

我的裁剪策略分三步。第一步是按步骤过滤,只保留失败的步骤和它前一个步骤的日志。因为失败往往和前一步的输出有关,比如前一步安装了依赖,后一步因为依赖问题失败。第二步是去除噪音行,比如进度条、重复的下载信息、时间戳前缀这些。第三步是提取错误上下文,用正则匹配错误关键词,把匹配行前后各若干行保留下来。

ERROR_PATTERNS = [ r"error", r"failed", r"exception", r"traceback", r"cannot find", r"not found", r"timeout", ] def extract_error_context(log_lines, window=10): hits = [] for i, line in enumerate(log_lines): if any(re.search(p, line, re.IGNORECASE) for p in ERROR_PATTERNS): start = max(0, i - window) end = min(len(log_lines), i + window + 1) hits.append((start, end)) merged = merge_overlapping_ranges(hits) result = [] for start, end in merged: result.extend(log_lines[start:end]) return result

这里有个经验:窗口大小不要设太大。我一开始设了 30 行,结果提取出来的内容还是很多,模型分析时容易被无关信息干扰。后来改成 10 行,准确率反而提升了。因为真正的错误信息通常就在错误行附近,太远的上下文帮助不大。

还有一个细节是日志的编码问题。有些日志里包含特殊字符,直接处理会报错。我统一做了编码清洗,把无法解码的字符替换掉,避免处理中断。

3.3 根因分析的提示词工程

把裁剪后的日志交给模型分析,提示词的设计直接决定分析质量。我试过几种写法,最后稳定下来的结构是这样的。

首先给模型一个明确的角色定位:你是一个 CI 故障分析专家,负责判断流水线失败的根本原因。然后给出日志内容,用分隔符包起来避免和指令混淆。接着要求模型按固定格式输出:失败类型、根因描述、置信度、建议动作。最后给出失败类型的枚举值,让模型从预设类型里选,而不是自由发挥。

你是一个 CI 故障分析专家。下面是一次失败的流水线日志片段。 <log> {log_content} </log> 请分析这次失败的根本原因,并按以下格式输出: 失败类型:[从以下选项中选择一个] - dependency:依赖相关问题 - syntax:语法或格式问题 - test:测试用例失败 - timeout:超时 - config:配置问题 - infrastructure:基础设施问题 - unknown:无法判断 根因描述:[一句话说明] 置信度:[0-1 之间的小数] 建议动作:[自动修复 / 建议人工处理 / 需要更多信息]

这个结构的关键在于约束输出格式。如果不约束,模型会写一大段分析,后续程序没法解析。约束之后,解析就很简单了。

置信度这个字段很有用。我设了一个阈值,比如 0.8,只有置信度高于阈值的自动修复类型才会真正执行,低于阈值的转成建议模式。这样能避免模型在不确定的情况下乱改代码。

3.4 修复策略表的设计与维护

修复策略表是整个 Agent 的决策核心。它定义了每种失败类型对应的处理方式。我目前的策略表是这样的:

失败类型自动修复修复方式置信度阈值
dependency是更新依赖版本或补全缺失依赖0.85
syntax是运行格式化工具自动修复0.9
test否生成分析报告发到合并请求-
timeout部分调整超时配置0.9
config否生成分析报告-
infrastructure否通知运维-
unknown否生成分析报告-

这张表不是拍脑袋定的,是根据实际运行数据逐步调整的。一开始我把 test 类型也放进了自动修复,结果模型改测试用例的断言来让测试通过,这完全是错误的行为。后来就把 test 类型移出了自动修复范围。

dependency 类型的自动修复也要小心。模型可能会把依赖升级到一个不兼容的版本。我的做法是限制修复动作:只允许补全缺失的依赖,不允许升级已有依赖的主版本号。这个约束写在修复执行的代码里,不依赖模型自觉。

3.5 自动修复的执行与安全边界

自动修复的执行流程是这样的:从仓库拉取失败提交对应的代码,创建一个新分支,应用修复,提交,推送,然后重新触发流水线。

这里的安全边界非常重要。我设了几条硬规则:第一,修复只能改特定类型的文件,比如依赖清单文件、格式化相关的文件,不能改业务逻辑代码。第二,每次修复的改动行数不能超过一个上限,比如 50 行,超过就转人工。第三,同一个运行最多自动修复一次,修完还失败就不再自动处理,避免无限循环。

ALLOWED_FILE_PATTERNS = [ r"requirements.*\.txt$", r"package\.json$", r"pyproject\.toml$", r".*\.lock$", ] MAX_CHANGED_LINES = 50 MAX_AUTO_FIX_ATTEMPTS = 1 def apply_fix(repo, fix_plan): changed_files = fix_plan["files"] for f in changed_files: if not any(re.match(p, f) for p in ALLOWED_FILE_PATTERNS): return {"status": "rejected", "reason": f"file not allowed: {f}"} if fix_plan["total_changed_lines"] > MAX_CHANGED_LINES: return {"status": "rejected", "reason": "too many changes"} if get_attempt_count(fix_plan["run_id"]) >= MAX_AUTO_FIX_ATTEMPTS: return {"status": "rejected", "reason": "max attempts reached"} return execute_fix(repo, fix_plan)

这几条规则看起来简单,但每一条都是踩坑之后加的。没有文件类型限制,模型改过测试文件;没有行数限制,模型做过大规模重构;没有次数限制,出现过修复失败后反复重试的情况。

3.6 结果反馈与合并请求评论

不管是自动修复还是建议模式,最后都要把结果反馈到合并请求上。自动修复的情况下,评论里说明做了什么修改、为什么这么改、流水线重新运行的结果。建议模式下,评论里给出根因分析、相关日志片段、建议的处理方向。

评论的格式我调了好几版。最初是纯文本,可读性差。后来改成 Markdown,用标题分段,关键信息加粗,日志片段用代码块包起来。这样人在手机上看也能快速抓住重点。

## CI 故障分析报告 **失败类型**:dependency **置信度**:0.92 **处理方式**:已自动修复 ### 根因 依赖清单中缺少 `requests` 包,导致导入失败。 ### 修复内容 在 `requirements.txt` 中添加了 `requests==2.31.0`。 ### 相关日志

ModuleNotFoundError: No module named 'requests'

### 后续 流水线已重新触发,请关注运行结果。

这个格式的好处是信息层次清晰,人扫一眼就知道发生了什么、Agent 做了什么、接下来要关注什么。

4. 完整实操流程与关键环节实现

4.1 环境准备与项目初始化

先把项目骨架搭起来。我用的目录结构是这样的:

ci-agent/ app/ webhook/ # 事件接收 queue/ # 任务队列 analyzer/ # 日志分析与根因判断 fixer/ # 修复执行 reporter/ # 结果反馈 adapters/ # 平台适配器 config/ policies.yaml # 修复策略表 tests/ Dockerfile requirements.txt

配置和代码分离很重要。修复策略表、置信度阈值、允许的文件类型这些都用配置文件管理,改策略不用改代码,也不用重新部署。

环境变量里需要配几个关键项:代码托管平台的访问令牌、Webhook 签名密钥、模型服务的接口地址和密钥、Git 提交用的用户名和邮箱。这些敏感信息不要写进代码,用环境变量注入。

4.2 Webhook 服务的搭建与调试

Webhook 服务用轻量 Web 框架起一个 HTTP 端点就行。本地调试的时候,代码托管平台没法直接推送到你的本地机器,所以需要一个公网可达的地址。我调试时用了一个临时的隧道工具把本地端口暴露出去,配置到平台的 Webhook 设置里。

调试阶段建议先把处理逻辑简化:收到事件后只打印日志,不做实际处理。确认事件能正常收到、签名校验能通过、字段解析没问题之后,再逐步加上后续逻辑。这样出问题的时候容易定位是哪一层的问题。

有个细节要注意:Webhook 的响应时间。平台通常要求几秒内返回,超时会重试。所以接收端点里不要做任何耗时操作,校验、解析、入队,然后立即返回。

4.3 日志分析模块的联调与验证

日志分析模块可以独立测试,不需要每次都触发真实流水线。我的做法是先把几次真实失败的日志保存下来,作为测试用例。然后写一个脚本,把这些日志喂给分析模块,看输出的失败类型和根因判断是否准确。

验证的时候重点关注两类错误:一是把可自动修复的类型误判成不可修复,导致本可以自动处理的问题转成了人工;二是把不可自动修复的类型误判成可修复,导致模型乱改代码。前者影响效率,后者影响安全,后者更严重。

我建了一个小型的评测集,大概 30 条真实失败日志,覆盖各种失败类型。每次调整提示词或者换模型,都跑一遍这个评测集,看准确率有没有下降。这个习惯帮我避免了好几次因为提示词微调导致的准确率回退。

4.4 修复执行的端到端测试

修复执行涉及 Git 操作,测试的时候要小心,别把测试提交推到主分支。我的做法是建一个专门的测试仓库,所有端到端测试都在这个仓库里跑。

测试流程是这样的:在测试仓库里故意制造一个可自动修复的失败,比如删掉依赖清单里的某个包,然后触发流水线。观察 Agent 是否能正确接收事件、分析出依赖缺失、自动补全依赖、推送提交、重新触发流水线,并且第二次运行成功。

这个测试跑通之后,基本的主流程就没问题了。然后再测试各种边界情况:不可修复的类型是否正确转成建议模式、超过修改行数限制是否正确拒绝、重复事件是否正确幂等。

4.5 部署与运行监控

部署我用的是容器方案,Agent 作为一个常驻服务跑在内部服务器上。容器里包含 Webhook 服务和后台 worker,通过进程管理工具保证两个进程都活着。

监控方面,我记录了几个关键指标:接收事件数、成功分析数、自动修复数、修复成功数、修复失败数、平均处理耗时。这些指标定期输出到日志,方便观察 Agent 的运行状况。

告警方面,如果连续多次修复失败,或者模型调用连续报错,就发通知给我。这样能及时发现 Agent 本身的问题,而不是等它默默失效。

5. 常见问题与排查技巧实录

5.1 模型分析不准的几种情况和应对

模型分析不准是最常见的问题,表现有好几种。

第一种是把正常输出误判成错误。日志里有些行包含 "error" 这个词,但其实是正常的错误处理逻辑输出。这种情况通过调整错误模式匹配规则来改善,比如排除掉包含 "error handling"、"no error" 这类短语的行。

第二种是根因判断偏移。比如真正的问题是依赖版本冲突,但模型判断成了网络问题。这种情况通常是日志裁剪没做好,把无关的错误信息也带进去了。缩小裁剪窗口、提高错误模式匹配的精确度能改善。

第三种是置信度虚高。模型对错误的判断也给出很高的置信度。这个问题比较难完全解决,我的应对是加一层规则校验:如果模型判断的失败类型和建议的修复动作不匹配,就强制降低置信度或者转人工。

5.2 自动修复引入新问题的防范

自动修复最怕的是修好一个问题引入两个新问题。我遇到过几次,总结了几条防范措施。

修复后的代码必须经过流水线验证,不能直接合并。Agent 推的是新分支,触发的是新分支的流水线,只有流水线通过并且人工确认后才合并。这样即使修复有问题,也不会污染主分支。

修复动作要可回滚。每次自动修复都记录清楚改了什么,如果发现问题,能快速回滚。我在评论里会附上修复的提交 SHA,方便追溯。

限制修复的影响范围。前面提到的文件类型限制和行数限制就是干这个的。宁可少修,不可乱修。

5.3 处理超时与并发冲突

Agent 处理一次故障涉及多次外部调用:拉日志、调模型、Git 操作、触发流水线。任何一步都可能超时。我的做法是给每一步设独立的超时时间,超时后重试有限次数,重试还失败就放弃并记录。

并发冲突主要出现在同一个仓库短时间内多次失败的情况。如果两个任务同时处理,可能会同时拉代码、同时推提交,产生冲突。我的做法是按仓库加锁,同一个仓库的任务串行处理。这样牺牲了一点吞吐,但避免了冲突。

5.4 常见问题速查表

问题现象可能原因排查方向解决方法
收不到 Webhook签名校验失败或地址不可达检查签名密钥和网络重新配置密钥,确认地址可达
重复处理同一事件幂等检查失效检查唯一键生成逻辑修正唯一键,加持久化记录
日志拉取为空权限不足或运行 ID 错误检查令牌权限补充日志读取权限
分析结果为空提示词格式问题检查模型返回内容修正提示词,加输出校验
修复被拒绝触发安全边界查看拒绝原因调整策略或转人工
修复后仍失败根因判断错误对比修复前后日志降低该类修复的置信度阈值
模型调用报错配额或网络问题检查服务状态加重试和降级逻辑

这张表是我实际运维中积累的,大部分问题都能在里面找到对应。遇到新问题就补充进去,慢慢就成了一套排查手册。

5.5 几个我踩过的坑

第一个坑是日志里的敏感信息。有些日志会打印环境变量或者配置内容,里面可能有密钥。Agent 拉取日志后如果直接存下来或者发给模型,就有泄露风险。我的做法是在日志处理前先做敏感信息脱敏,把看起来像密钥的字符串替换掉。

第二个坑是模型返回格式不稳定。即使提示词里明确要求了输出格式,模型偶尔还是会返回不符合格式的内容。所以解析的时候必须做容错,解析失败就当作分析失败处理,转人工,不能让它带着错误的数据往下走。

第三个坑是修复分支的命名冲突。如果两次修复用了同样的分支名,第二次推送会失败。我的做法是分支名里带上运行 ID 和时间戳,保证唯一。

第四个坑是流水线重新触发的权限。有些平台的令牌权限分得很细,触发流水线需要单独的权限。一开始没注意,修复做完了但触发不了流水线,白忙一场。配令牌的时候要把需要的权限都勾上。

6. 后续可以扩展的方向

这套 Agent 跑了一段时间之后,我陆续加了一些扩展。

一个是修复知识的积累。每次成功的修复都记录下来,形成一个修复案例库。下次遇到类似的失败,先查案例库,命中就直接用之前的修复方案,不用再调模型。这样既快又稳。

另一个是多平台适配。把平台相关的逻辑抽象成适配器之后,接第二个平台的工作量比想象中小。核心的分析和修复逻辑完全复用,只需要写一个新的适配器处理事件解析和日志拉取。

还有一个是修复效果的统计。记录每种失败类型的自动修复成功率,定期 review。成功率低的类型就调整策略,要么降低置信度阈值,要么移出自动修复范围。这个数据驱动的调整方式比拍脑袋定策略靠谱得多。

如果你也想搭一套类似的系统,我的建议是先从建议模式做起,只分析不修复,观察一段时间分析准确率。准确率稳定之后再逐步开放自动修复,而且一定要从影响范围最小的类型开始。自动化修复这件事,稳比快重要。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进