1. 为什么我要自己搭一个 CI 故障处理 AgentCI 挂了这件事但凡在团队里负责过合并请求的人都有体会。凌晨两点流水线红了打开日志一看是某个依赖版本冲突或者一个测试用例因为时区问题偶发失败又或者 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(status401) payload request.json() if payload[action] ! completed: return Response(status200) if payload[workflow_run][conclusion] ! failure: return Response(status200) task_key f{payload[workflow_run][id]}:{payload[workflow_run][head_sha]} if is_processed(task_key): return Response(status200) enqueue_task(payload) return Response(status200)这段逻辑里先校验签名再过滤事件类型只处理失败的运行然后做幂等检查最后入队。每一步都是必要的少一步都可能出问题。3.2 日志拉取与智能裁剪日志拉取是第一个技术难点。一次失败的运行日志可能有几千行直接全部塞给模型既浪费 token 又降低分析准确率。所以必须裁剪。我的裁剪策略分三步。第一步是按步骤过滤只保留失败的步骤和它前一个步骤的日志。因为失败往往和前一步的输出有关比如前一步安装了依赖后一步因为依赖问题失败。第二步是去除噪音行比如进度条、重复的下载信息、时间戳前缀这些。第三步是提取错误上下文用正则匹配错误关键词把匹配行前后各若干行保留下来。ERROR_PATTERNS [ rerror, rfailed, rexception, rtraceback, rcannot find, rnot found, rtimeout, ] def extract_error_context(log_lines, window10): 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.85syntax是运行格式化工具自动修复0.9test否生成分析报告发到合并请求-timeout部分调整超时配置0.9config否生成分析报告-infrastructure否通知运维-unknown否生成分析报告-这张表不是拍脑袋定的是根据实际运行数据逐步调整的。一开始我把 test 类型也放进了自动修复结果模型改测试用例的断言来让测试通过这完全是错误的行为。后来就把 test 类型移出了自动修复范围。dependency 类型的自动修复也要小心。模型可能会把依赖升级到一个不兼容的版本。我的做法是限制修复动作只允许补全缺失的依赖不允许升级已有依赖的主版本号。这个约束写在修复执行的代码里不依赖模型自觉。3.5 自动修复的执行与安全边界自动修复的执行流程是这样的从仓库拉取失败提交对应的代码创建一个新分支应用修复提交推送然后重新触发流水线。这里的安全边界非常重要。我设了几条硬规则第一修复只能改特定类型的文件比如依赖清单文件、格式化相关的文件不能改业务逻辑代码。第二每次修复的改动行数不能超过一个上限比如 50 行超过就转人工。第三同一个运行最多自动修复一次修完还失败就不再自动处理避免无限循环。ALLOWED_FILE_PATTERNS [ rrequirements.*\.txt$, rpackage\.json$, rpyproject\.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: ffile 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 中添加了 requests2.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。成功率低的类型就调整策略要么降低置信度阈值要么移出自动修复范围。这个数据驱动的调整方式比拍脑袋定策略靠谱得多。如果你也想搭一套类似的系统我的建议是先从建议模式做起只分析不修复观察一段时间分析准确率。准确率稳定之后再逐步开放自动修复而且一定要从影响范围最小的类型开始。自动化修复这件事稳比快重要。