资讯详情 开源Issue自动化分诊:规则引擎与AI结合的设计与实践
📅 2026/10/15 5:46:51
维护开源仓库的人应该都有同感每天打开 Issues 列表看到的不是“需求”而是一堆等着做第一轮筛选的原始信息——标题写了半句话、模板没按格式填、bug 报告里没有环境信息、两边明明是同一个问题却各发了一帖。我做过一个小工具用规则引擎加少量 AI 判断把这轮“脏活”接过来让机器先给每个新 Issue 做一次分诊查信息完整性、找重复、推标签、估算该谁来管。这篇文章就把这个分诊助手的设计思路、核心代码和踩坑过程完整还原一遍适合被 Issue 日常淹没的开源维护者也适合想给仓库加自动化的开发者参考。我选择的路子和很多人想象的不太一样没有一上来就接大模型而是先把所有能用确定性规则做的事全部规则化AI 只兜底处理规则覆盖不到的部分。原因很直接Issue 分诊这件事90% 的判断其实不需要“智能”只需要把维护者脑内的检查清单翻译成机器能执行的逻辑。剩下的 10%再用轻量模型去理解语义和意图。这样做的好处是每个判断都可解释、可调参、出了问题能追责——你总不希望机器人因为一次抽风把别人的高优 bug 标成“已解决”。1. 分诊助手到底解决什么问题1.1 开源维护者的“第一轮脏活”是什么先定义清楚什么是“第一轮脏活”。它不是修 bug也不是写功能而是新 Issue 进来之后那一连串重复劳动看看标题写得清不清楚、正文有没有按模板填写、复现步骤有没有给全、环境信息缺不缺、这个问题是不是已经有人报过了、大概属于哪个模块、应该分配给谁处理。这些事情技术难度不高但极其消耗注意力。我自己带过一个模拟项目 X巅峰时期一天涌入十几个 Issue其中相当一部分是重复提问和残缺报告。单独看每一条都能在两分钟内处理完可一旦你要在写代码、review、开会之间穿插做这件事会明显感到节奏被打断。更麻烦的是不同维护者的分诊标准不一致有人觉得没贴日志就该打回去有人觉得差不多能看懂就放进来。时间一长仓库里就会堆积一批“半残”的 Issue后人翻历史记录时根本不知道当初为什么开这个任务。分诊助手要做的就是把维护者的个人经验沉淀成仓库级的统一规则。新 Issue 进来的瞬间机器人按照同一套标准做检查把缺失项、风险项、归属项一次性标注出来。维护者不再是面对一个裸标题而是面对一份已经整理好的“病例单”只需要做最终裁定。1.2 为什么值得自动化这一轮有人说分诊一次不过两分钟值得写个工具吗我的回答是单次两分钟确实不多但 Issue 的咨询特性决定了这项工作永远不会结束。只要项目还有人用就不断有新用户、新问题、新报告。一次十万行的 PR 你都愿意花三小时去 review为什么不愿意花一个下午写一个能永久分担重复劳动的脚本更关键的是速度优势。自动分诊能在 Issue 提交后十几秒内完成用户刚提交完就会在页面上看到机器人给出的反馈——“你缺了复现步骤请补充”“这个问题和 #1234 重复请先去看那边的进度”。这种即时反馈对提问者体验的改善是巨大的。人的响应速度再快也要等几个小时而一个用户如果在提交后十分钟内得到回应他大概率愿意花时间继续完善报告如果等了两天才有人理他他可能已经放弃这个项目了。自动化分诊的另一个隐性价值是数据沉淀。每一条分诊记录实际上都在帮助建立仓库的 Issue 知识库哪些类型的报告最容易信息缺失哪个模块的问题最集中哪些关键词组合经常出现。维护者从这些数据里能看到项目健康度的变化而这些信息之前都散落在个人脑子和不成系统的标签里。1.3 助手的边界只做第一轮不做最终裁决在设计这个助手时我给自己定了一条原则机器只做第一轮不做最终裁决。分诊助手可以指出“这个 Issue 缺少版本号”但它不会直接关掉这个 Issue它可以判断“这大概率是性能问题”但它不会直接分配给某个核心维护者。原因是自动化的价值在于解放人力而不是取代人力。强行让机器做最终决策就会产出一堆让社区反感的“AI 官僚”明明是一个紧急的生产事故报告却被机器人以“缺少模板字段”为由关掉了明明是一个设计很好的新功能建议却被机器人打上“重复 issue”的标签给合并了。这种误判带来的社区信任损失远大于省下来的那几分钟人工时间。所以我的架构是机器负责检查和标注维护者负责决策和收官。所有自动化动作都必须可追溯——哪个规则触发的判断基于什么关键词匹配到什么模板字段全部记录在案。维护者如果发现某条规则过于激进可以直接在配置里调低优先级或者干脆停用整个过程不需要改代码。2. 整体设计规则引擎优先AI 兜底2.1 为什么不用纯提示词一次性生成结论刚开始构思时我一度想直接用 LLM 把整个 Issue 丢进去让它输出“是否完整、是否重复、标签建议、负责人建议”一整套结论。实际试了之后发现这条路有个致命问题不可控。同一个 Issue 换个措辞模型的结论就可能变更麻烦的是模型会一本正经地给出错误理由比如把“我的环境是 Windows 10”误判成“缺少环境信息”因为提示词里没有明确告诉它环境字段的关键词。另一个问题是成本。每个 Issue 都走一次模型调用日均上百条就是不小的开销而且大部分 Issue 根本不需要大模型的理解能力。一个“如何在 Mac 上安装”的提问关键词规则表里写一句“macOS、brew、darwin”就能识别没必要让模型来语义理解。所以我最终的方案是分层处理。第一层是纯规则引擎字段完整性、标签关键词、路径归属、标题相似度全部走本地代码。第二层才是模型判断规则层判定不出来、或者置信度不足的情况再交给 LLM 去分析主题类别和意图。这样既保证了 90% 场景的确定性与零成本又保留了对复杂语义的处理能力。2.2 整体架构与事件链路整个系统的链路不复杂Issue 事件产生 → 工作流被触发 → 拉取 Issue 完整信息 → 执行规则引擎 → 落库/打标签/写评论 → 结束。我把核心逻辑放在一个独立的 Node.js 脚本里通过 GitHub Actions 作为事件触发器。这样做的好处是可以白嫖平台的免运维能力不用自己部署常驻服务。选 GitHub Actions 而不是自建 Webhook 服务理由有四个。第一零托管成本仓库自带的工作流基础设施不需要额外服务器第二令牌安全性环境变量里用平台自动注入的 GITHUB_TOKEN不需要自己管理密钥第三日志可观测每次分诊的完整输出都在 Actions 日志里出问题可以直接翻记录第四事件完整性Actions 对 Issue 的各类动作有原生的支持接入成本几乎为零。脚本本身是无状态的只接收一个 Issue 编号作为入参通过 REST API 去拉取标题、正文、标签、评论等所有信息。无状态设计让我可以在本地跑同样的脚本去批量处理存量 Issue只需要传入不同的编号逻辑完全复用。2.3 仓库接入成本与权限模型接入成本其实低到很多人想不到。你只需要在工作流目录里放一个 YAML 文件和一个脚本首次运行的初始化阶段会自动补齐缺失的标签我在脚本里做了标签不存在则自动创建的逻辑。整个过程不需要维护者在仓库设置里做任何手工配置。权限模型方面工作流里的 GITHUB_TOKEN 默认是只读的如果想让它能打标签和发评论必须在 workflow 文件里显式声明permissions: issues: write。这个坑我一开始就踩了明明脚本逻辑没问题但评论发不出去排查半天才发现是默认权限不足。对多维护者的项目我建议把分诊标签统一加上triage/前缀方便按前缀筛选和清理。我给模拟项目 X 设计的标签体系是triage/incomplete、triage/duplicate、triage/feature、triage/bug、triage/docs、triage/performance。维护者一眼就能从标签颜色和前缀分辨出哪些是流程标记哪些是内容分类。3. 分诊规则拆解四个核心能力3.1 信息完整性检查把模板缺口找出来完整性检查的原理不复杂把项目 Issue 模板里的关键段落作为结构化规则检查新提交的正文是否包含这些段落的特征。我用了一个字段映射表每个字段定义语义关键词正则命中即认为该字段存在。比如“复现步骤”对应的关键词是复现|重现|Steps to reproduce|reproduce“版本信息”对应版本|Version|v1\.\d只要正文里有一个匹配就判定该字段已填。这里有个细节值得展开为什么用关键词而不是检查模板的 Markdown 标题因为很多用户提交时会删掉模板的小标题只留一段自由文本。如果只检查## 复现步骤这种标题误判率会非常高。用关键词做模糊匹配虽然不完美但能覆盖大部分真实用户的填写习惯。遇到缺失项时我不建议直接关闭 Issue而是打上triage/incomplete标签同时发一条评论把缺失字段列出清单。评论模板我用的是感谢提交这个 Issue。为了让维护者更快定位问题请补充以下缺少的信息 - 期望行为 / 实际行为 - 复现步骤 - 运行环境操作系统、版本号 补充完成后我会重新分诊。这条评论的措辞是经过几次迭代的。最早版本写得比较生硬类似“请按模板重新编辑”效果很差不少用户直接弃坑不回来了。后来改成上面对话式的措辞补充率明显提升。分诊工具不只是机器逻辑也是对外沟通的界面。3.2 重复 Issue 识别基于标题与内容相似度重复检测是分诊里最有技术含量的一环。策略上我用了三层递进精确标题匹配、关键词搜索候选集、文本相似度计算。前两层是为了缩小范围让相似度计算只在少量候选里执行避免对全仓库所有 Issue 做一遍计算。搜索候选集用 REST API 的搜索接口按标题关键词检索当前仓库下所有处于开启状态的 Issue取前五条结果。然后对每一条候选计算它与新 Issue 的文本相似度。相似度算法用的是 Jaccard bigram先把标题和正文清洗成小写、去停用词再拆成相邻字符的双字母组集合两个集合的交集大小除以并集大小就是相似度。举个例子新 Issue 标题是“登录页在手机端样式乱”候选标题是“移动端登录页面样式错乱”。清洗后分别是“登录页手机端样式乱”和“移动端登录页面样式错乱”bigram 集合会有相当一部分重合相似度大概在 0.6 左右。我实测下来阈值定在 0.65 比较合适低于 0.5 的基本是不同问题高于 0.7 的几乎可以肯定重复0.5 到 0.7 之间的需要人工复核。对判定为重复的 Issue我不会直接关闭而是打上triage/duplicate标签并评论一句“这个 Issue 看起来和 #1234 相关那边已经有进展请先去那边补充信息如果情况不同请回复说明”。关闭这个动作始终留给维护者因为有时表面重复的问题实际有不同的触发条件机器很难完全区分。3.3 自动标签推荐关键词规则表标签推荐是最适合用规则表解决的任务。我把标签规则集中放在一个 JSON 文件里维护者可以直接编辑而不需要碰代码。每一组规则包含标签名、关键词列表、优先级三部分。命中逻辑是正文和标题中任意一个关键词出现即触发取优先级最高的一组作为推荐标签。我用的规则表示例{ rules: [ { label: triage/bug, keywords: [报错, 异常, 崩溃, bug, crash, fail, 错乱], priority: 10 }, { label: triage/feature, keywords: [建议, 希望增加, 需求, feature, roadmap], priority: 5 }, { label: triage/docs, keywords: [文档, 文档缺失, typo, 拼写, README], priority: 5 }, { label: triage/performance, keywords: [卡顿, 慢, 性能, performance, 延迟, 内存溢出], priority: 8 } ] }为什么要把优先级单独拎出来因为“性能”和“bug”的边界很模糊。一个“内存溢出导致崩溃”的 Issue同时命中性能关键词和 bug 关键词如果不设置优先级机器人会纠结于该打哪个标签。我设置了 bug 优先级高于性能这样这类问题统一归为 bug性能细节留给维护者在处理时补充。这个规则表需要持续维护。我每周末会扫一遍本周的分诊日志看有哪些 Issue 没有被任何规则命中或者命中的标签明显不合理然后补关键词进去。规则表的生命力在于持续迭代不是一次写完就完事。3.4 负责人推荐路径与模块映射负责人推荐我实现了两种模式按仓库规模灵活切换。小仓库用一个简单的模块映射表{ 认证: [jwt, oauth, login, session, token], 数据库: [sql, mysql, postgres, query, 索引], 前端: [react, vue, css, 界面, 样式] }命中哪一组关键词就在评论里提示“这个 Issue 可能和前端模块相关建议由前端方向维护者跟进”。这个模式够用且完全透明维护者可以随时调整关键词不会出现莫名其妙的分配结果。大仓库则优先走 CODEOWNERS 机制。如果仓库已经维护了 CODEOWNERS 文件我会尝试解析文件路径规则映射到 Issue 正文里提到的文件目录。比如正文提到src/auth/login.ts而 CODEOWNERS 里写了src/auth/ auth-team我就能推断这是认证模块的活。难点在于很多用户不会主动贴文件路径这时候还要结合关键词映射表做兜底。我不建议机器人直接把 Issue 分配给具体的人在多人协作的仓库里擅自分配容易引发被分配者的反感。最好的方式是“指向不指人”说清楚问题可能属于哪个模块让维护者自己决定要不要接手。这又一次体现了“只做第一轮、不做最终裁决”的原则。4. 实操落地从零到一4.1 项目骨架与依赖整个项目就两个核心文件工作流定义和分诊脚本。依赖只有octokit/restGitHub REST API 的官方 JavaScript SDK和 Node.js 运行环境。目录结构issue-triage/ ├── .github/ │ └── workflows/ │ └── issue-triage.yml ├── rules/ │ ├── labels.json │ └── owners.json └── triage.jstriage.js是唯一需要关心的代码文件大概 200 行左右。剩下的都是 JSON 配置维护者改规则时连代码库都不需要懂。4.2 工作流配置一个重要的坑工作流是分诊助手的事件入口我先给出完整配置name: Issue Triage on: issues: types: [opened] permissions: issues: write jobs: triage: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 - name: Setup Node uses: actions/setup-nodev4 with: node-version: 20 - name: Install dependencies run: npm install octokit/rest - name: Run triage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }} run: node triage.js这段配置里最容易出问题的就是第三段permissions。没有它脚本能执行但打不了标签、发不了评论我一开始调试时就是死在这个坑里日志里 API 调用报 403换了 token 也一样最后才发现是 workflow 默认权限不够。另外注意触发事件只用了issues: [opened]。这是有意为之我不会监听评论事件和标签变更事件避免机器人的评论再次触发自身逻辑形成死循环。这个设计后面在避坑章节会展开讲。4.3 核心代码实现分诊主流程主脚本的骨架是这样的。先从环境变量和配置里初始化对象然后拉取 Issue 详情const { Octokit } require(octokit/rest); const fs require(fs); const token process.env.GITHUB_TOKEN; const [owner, repo] process.env.REPO.split(/); const issueNumber Number(process.env.ISSUE_NUMBER); const octokit new Octokit({ auth: token }); async function main() { const { data: issue } await octokit.rest.issues.get({ owner, repo, issue_number: issueNumber }); const report { number: issue.number, title: issue.title, hasCodeBlock: /[\s\S]*/.test(issue.body || ), bodyLength: (issue.body || ).length, labels: issue.labels.map(item item.name) }; console.log(分诊报告:, JSON.stringify(report, null, 2)); } main().catch(error { console.error(分诊出错:, error.message); process.exit(1); });这段代码本身不复杂但把报告打出来是很有必要的。每次分诊执行Actions 日志里都会留下一份结构化的 JSON 报告方便事后复盘。有段时间我靠这个日志发现标签规则的关键词命中率偏低于是针对性的补了一批常见的口语化表达。信息完整性检查和重复检测是核心函数。完整性检查我做了模板字段的映射表重复检测用搜索接口加相似度计算。这两个函数的设计在前面章节已经详细拆解过代码上注意的一点是搜索接口要用encodeURIComponent对查询内容编码否则标题里带引号、空格等特殊字符时 API 会报错。4.4 冷启动给存量 Issue 补一轮分诊很多人忽略了存量 Issue 的处理。新工具接上之后如果只对新 Issue 生效仓库里的历史遗留问题依然是一团乱麻维护者还是无法从整体上获得清晰的视图。我在接入模拟项目 X 时做了一次冷启动原理很简单写一个批次脚本从 Issue 列表接口依次拉取所有开启状态的 Issue 编号然后逐个调用分诊函数。冷启动和在线分诊的唯一区别是入参不同在线分诊从 Actions 环境变量里拿 Issue 编号冷启动从本地循环读取编号。核心逻辑完全复用不需要写两套代码。我当时跑了一轮把仓库里积压的 200 多个旧 Issue 全部补上了标签和分诊记录列表瞬间从“一片空白”变成“分类清晰”。冷启动有一个执行节奏的讲究。我建议不要一次性全量并发跑因为搜索 API 有速率限制全仓库扫描很容易触发限流。当时的做法是每处理一个 Issue 间隔 500 毫秒一晚上跑完全部存量第二天维护者打开列表时整个视图焕然一新。也别在仓库流量高峰期跑线上新 Issue 的分诊优先级肯定要高于存量清洗。5. 常见问题与避坑清单5.1 五个高频问题排查表我前后跑了几个月的分诊助手把实际遇到的高频问题整理成了下面这张表。如果你也打算做类似工具这些坑大概率会依次踩到。现象原因处理方法机器人不评论不打标签日志 403workflow 缺少 issues 写权限在 workflow 里添加permissions: issues: write搜索接口报 422 参数错误查询串未做 URL 编码对标题和查询词使用encodeURIComponent打标签时报 404标签不存在API 默认不允许直接附加未知标签在 code 里调用createLabel先建标签或忽略 404机器人评论触发了新的工作流监听事件过宽机器人行为回流触发自身只在opened事件触发不监听评论事件关键词命中过多导致标签泛滥规则表太粗如“错误”这个关键词命中率过高引入括号表达式和排除词比如“错误”但要排除“不是错误”这类语境排查时最有效的手段不是看脚本日志而是直接查看 Actions 工作流的完整运行日志里面会打印 API 请求的状态码和错误消息。大部分问题都是权限、接口参数、事件回流这三类按这张表对号入座基本能解决。5.2 防止事件死循环一个必须处理的设计问题事件死循环是自动化工具最常见的隐形杀手。你的机器人给 Issue 发了条评论这条评论本身会触发issue_comment事件如果你的工作流监听这个事件就会再跑一次分诊再发一条评论再触发一轮……往返几次之后仓库就被机器人的评论刷屏了。解决这个问题有三种常见路径。第一种是只在issues.opened事件触发完全不监听评论事件这是最省心的方案但缺点是用户补完信息后没办法自动重新分诊需要维护者手动跑一次。第二种是在脚本里检查触发事件的 actor 是不是github-actions[bot]是就跳过本次处理。第三种是在脚本开头检查评论者身份只有非机器人用户的新评论才触发二次分诊。我最终选了第一种方案配合一个手动触发机制维护者想对某个 Issue 重新分诊时通过仓库内的一个自定义命令比如在评论里输入/triage触发工作流。这个方案既能避免死循环又保留了二次分诊的能力实现起来也不复杂。5.3 评论噪音与社区体验自动化工具容易变成“噪音制造机”这是我在迭代过程中体会很深的一点。早期的版本每次分诊都会在 Issue 下发一条长评论把标签、负责人建议、完整性报告全部列出来。结果是用户看到的不是帮助而是一堆机器话术评论区被机器人和用户来回刷屏淹没。后来我调整了评论策略正常分类不评论异常才发声。如果 Issue 信息完整、没有重复、标签识别清晰机器人就只静默打标签不留下任何评论只有两种情况会发声——信息缺失需要用户补充或者疑似重复需要用户确认。这样的交互模式更接近真实维护者的习惯评论区的可读性得到了很大改善。另一个降噪技巧是把长报告放到 Action 日志里评论里只保留摘要。用户需要看完整细节时点开工作流日志就能看到结构化的分诊报告不需要时评论区的体验和原来完全一致。这个设计对新手用户特别友好不会因为一堆机器术语劝退他们继续反馈问题。给这个系统调优的这段时间我最深的感受是开源维护者面对的最大成本不是工作量本身而是上下文切换带来的注意力损耗。分诊助手不能替你写代码但它能把“每天刷几十个 Issue 做第一轮判断”这种琐碎且重复的事情稳稳接住。我个人体会比较深的一点是关键词规则表要持续“喂养”它决定了这个工具的智商边界另一个小技巧是别急着接大模型先把规则层做到极致你会发现很多问题根本不需要“智能”就能解决。如果你的仓库也积压了一堆没人处理的 Issue花一个下午做一个这样的分诊助手可能比想象中回报更快。