1. 为什么我想把 Issue 分诊这件事交给机器维护过开源项目的人大概都有同感真正消耗精力的往往不是写代码而是每天打开仓库时扑面而来的那一堆 Issue。新提交的、复现步骤残缺的、标题只写求助两个字的、把使用问题当 Bug 报的、重复提了三遍的全混在一起。你还没开始看代码光是给这些 Issue 打标签、问补充信息、判断优先级半小时就没了。我维护过几个中小型仓库最夸张的一段时间每天新增二三十条 Issue其中真正需要我立刻处理的不到五条。剩下的要么是提问姿势不对要么是信息不全需要来回追问要么干脆是重复的。这种第一轮脏活有个共同特点判断规则相对固定但极其耗神。它不需要你有多深的架构理解只需要你耐心读完、对照模板、打个标签、回一句麻烦补充下版本号和复现步骤。这类活儿恰恰是当下大模型最擅长的区间。所以我就动手做了一个Issue 分诊助手一条新 Issue 进来自动读标题和正文判断它属于 Bug、功能请求、使用咨询还是文档问题给出建议标签、优先级甚至直接生成一段礼貌的追问回复草稿。维护者只需要扫一眼、点确认而不是从零开始。这篇文章我会把整个思路、技术选型、提示词设计、代码实现、踩过的坑全部摊开讲。适合两类人看一是正在维护开源仓库、被 Issue 淹没的开发者二是想找一个真实场景练手 LLM 应用、又不想做烂大街 Demo 的人。哪怕你之前没接触过任何 AI 接口跟着走也能跑起来一个能用的版本。先说清楚边界这个助手不替代人做最终决策它只做第一轮分诊把整理信息、初步归类、生成回复草稿这三件事自动化。最终打不打标签、关不关 Issue还是人来拍板。这个定位很重要后面很多设计取舍都围绕它展开。2. 整体设计思路与方案选型2.1 先想清楚分诊助手到底要解决哪几个动作在写任何代码之前我把维护者处理一条新 Issue 的完整动作拆了一遍发现其实是四个独立的小决策分类这条 Issue 是什么类型Bug、功能请求、使用问题、文档改进、还是无效内容。完整性判断信息够不够有没有版本号、复现步骤、期望行为、实际行为。优先级评估是不是阻塞性问题影响范围大不大有没有人已经跟帖表示同样遇到。回复生成如果信息不全需要追问什么如果是重复的指向哪条已有 Issue。这四个动作里第 1、2、4 个几乎可以完全交给模型第 3 个模型只能给建议因为优先级往往涉及项目路线判断人更清楚。所以我的助手输出结构就定成类型 缺失信息清单 优先级建议 回复草稿四件套一起给。提示不要一上来就想做全自动闭环让 AI 直接打标签、直接关闭 Issue。第一版一定是AI 给建议人点确认跑顺了再考虑对低风险动作放开自动化。2.2 为什么选规则 大模型混合而不是纯规则或纯模型我一开始试过纯关键词规则标题里有崩溃报错error就判 Bug有希望建议能不能就判功能请求。结果很快翻车。有人写希望能修复这个崩溃规则就懵了有人用中文描述英文报错关键词匹配不到还有人标题写求助正文里其实是个很清晰的功能请求。纯模型呢也不是没问题。模型偶尔会过度自信把一条明显是使用问题的 Issue 判成 Bug因为它看到了报错堆栈。而且每次调用都有成本如果每条 Issue 都全量丢给模型量大了账单不好看。最后我采用的是混合方案先用轻量规则做预处理和兜底再把清洗后的内容交给模型做语义判断。规则负责几件事过滤明显的垃圾内容纯广告、空白、提取结构化字段如果 Issue 模板里有版本号输入框、做初步的关键词打标作为模型的参考信号。模型负责真正的语义理解和回复生成。这样既控制了成本又保留了判断准确率。2.3 触发方式的选择Webhook 还是定时轮询分诊助手要能感知到新 Issue有两种常见做法方案优点缺点适用场景Webhook 实时触发延迟低Issue 一进来就处理需要公网可访问的接收端点配置略麻烦有服务器、追求实时定时轮询部署简单本地就能跑有延迟需要记录已处理状态个人项目、快速验证我第一版选了定时轮询因为我不想为了一个助手专门去搞公网端点。每 5 分钟拉一次最近更新的 Issue 列表用一个本地状态文件记录哪些已经处理过没处理过的才走分诊流程。等验证稳定了再考虑换成 Webhook。这个顺序很重要——先用最低成本验证核心逻辑再优化触发链路别本末倒置。2.4 输出落地到哪里分诊结果最终要回到 Issue 上常见有三种落地方式直接发评论、打标签、或者只在本地生成一份报告给人看。我建议第一版只生成报告不自动写回。原因很简单你还没验证模型判断准不准就让它往仓库里发评论、打标签万一判错了对社区观感很不好。等你看了一两百条分诊结果确认准确率可以接受再逐步放开自动发追问评论这种低风险动作。3. 核心细节解析与实操要点3.1 提示词是整个系统的心脏这个项目里代码其实是最不重要的部分真正决定成败的是提示词。我前后改了十几版总结出几条硬经验。第一角色和任务要写得极其具体。不要写你是一个助手帮我分析 Issue。要写清楚你是一个开源项目的 Issue 分诊助手你的任务是把 Issue 归入以下五类之一并判断信息完整性。类别要枚举出来每类给一句定义。第二输出必须是结构化 JSON。这是让后续程序能处理结果的前提。我会在提示词里明确给出 JSON schema包括每个字段的类型和取值范围。比如category只能是bug、feature、question、docs、invalid之一。第三要求模型给出判断理由。这一点很多人会忽略。让模型在 JSON 里加一个reason字段简述它为什么这么分类。这不只是为了可解释性——实测下来要求模型写理由之后分类准确率会明显提升因为它被迫想一步再下结论。第四给几个 few-shot 示例。尤其是那些容易混淆的边界情况比如报错但其实是配置问题该归到 question 还是 bug。给两三个示例模型的表现会稳定很多。下面是我最终用的提示词骨架做了脱敏和简化你是一个开源项目的 Issue 分诊助手。请阅读下面这条 Issue完成分诊。 分类标准 - bug代码缺陷导致功能异常有明确错误表现 - feature请求新增功能或增强现有功能 - question使用咨询、配置求助、概念疑问 - docs文档错误、缺失、表述不清 - invalid广告、灌水、与项目无关 完整性检查项版本号、复现步骤、期望行为、实际行为、环境信息。 逐项判断是否缺失。 优先级建议high阻塞/崩溃/安全问题、medium功能异常但有绕过方案、low咨询/建议/文档。 请严格输出如下 JSON不要输出任何其他内容 { category: ..., missing_info: [...], priority: ..., reason: ..., reply_draft: ... } Issue 标题{title} Issue 正文{body}注意reply_draft让模型直接生成一段可以直接粘贴的回复语气要礼貌、具体指出缺什么信息、怎么补。这一段是维护者最省事的地方一定要让它生成。3.2 输入内容的清洗不能省直接把 Issue 原文丢给模型会遇到几个问题。一是正文里可能有大段日志、代码块、截图链接token 消耗大且干扰判断。二是有些 Issue 正文里带 提及、引用块模型可能被带偏。三是超长正文会超出上下文限制。我的清洗策略是保留标题、正文前 2000 字符、以及从模板字段里提取的结构化信息其余截断。同时把连续的空白行压缩把超长的代码块替换成[代码块已省略]占位。实测下来这样处理之后判断质量几乎不降但 token 消耗能砍掉一半以上。还有一个细节如果仓库用了 Issue 模板正文里通常会有### 版本号这样的固定小标题。我会用正则把这些字段单独抽出来作为结构化信号一起喂给模型比让它自己从大段文字里找要准得多。3.3 状态管理别重复处理同一条 Issue轮询方案最大的坑就是重复处理。如果每 5 分钟拉一次列表同一条 Issue 会被反复分诊既浪费钱又刷屏。我的做法是维护一个本地 JSON 状态文件记录每条 Issue 的编号和最后处理时间。每次拉取时只处理没记录过或者更新时间晚于上次处理时间的 Issue。这里有个容易忽略的点Issue 的评论也会更新updated_at。如果你只看更新时间别人在 Issue 下回了一句话你的助手就会重新分诊一遍。所以更稳妥的做法是记录 Issue 的正文哈希值只有正文变了才重新处理。这个细节我在踩坑之后才补上前面白白多跑了不少次。3.4 成本控制的实际账很多人担心调用大模型太贵。我算过一笔账一条普通 Issue 清洗后大概 500 到 1500 token加上提示词和输出单次调用大约 2000 token 上下。按主流模型的价格一条 Issue 的分诊成本在几分钱级别。一个中等活跃的仓库一天二三十条 Issue一个月成本也就几十块。相比维护者每天省下的半小时这个投入非常划算。真正要控制的是无效调用重复处理、垃圾 Issue、超长正文。把这三块用规则挡掉成本还能再降一半。4. 实操过程与核心环节实现4.1 环境准备与依赖我用 Python 写的依赖很轻一个 HTTP 请求库、一个大模型 SDK、一个用于解析时间的库。不需要数据库状态文件用 JSON 就够。下面是我实际用的依赖清单pip install requests openai python-dateutil如果你用的是别家模型服务把 SDK 换掉即可逻辑完全一样。核心就是发一段文本拿回一段 JSON。4.2 拉取待处理 Issue这一步的关键是分页和过滤。仓库 Issue 列表接口通常支持按更新时间排序、按状态过滤。我只拉stateopen且按updated倒序的前若干条避免全量拉取。import requests from datetime import datetime, timezone def fetch_recent_issues(repo, token, per_page30): url fhttps://api.github.com/repos/{repo}/issues headers {Authorization: fBearer {token}} params { state: open, sort: updated, direction: desc, per_page: per_page, } resp requests.get(url, headersheaders, paramsparams, timeout15) resp.raise_for_status() items resp.json() # 过滤掉 Pull Requestissues 接口会把 PR 也返回 return [it for it in items if pull_request not in it]注意GitHub 的 issues 接口会把 Pull Request 也混在返回结果里必须用pull_request字段过滤掉否则你的助手会去分诊 PR闹笑话。4.3 正文清洗与结构化字段提取清洗函数我写得比较克制只做必要的处理import re def clean_body(body, max_len2000): if not body: return # 压缩连续空行 body re.sub(r\n{3,}, \n\n, body) # 超长代码块占位 body re.sub(r[\s\S]{500,}?, [代码块已省略], body) # 截断 return body[:max_len] def extract_template_fields(body): 从 Issue 模板的 ### 小标题里抽取字段 fields {} pattern r###\s*(.?)\n([\s\S]*?)(?\n###|\Z) for name, value in re.findall(pattern, body or ): fields[name.strip()] value.strip()[:300] return fieldsextract_template_fields这个函数很实用。很多仓库的 Issue 模板长这样### 版本号下面跟一行内容### 复现步骤下面跟一段。用正则把每个小标题和它的内容配对抽出来就能得到一份干净的结构化信息直接塞进提示词。4.4 调用模型并解析结果调用部分要注意两点一是要求模型返回 JSON二是解析时要容错。模型偶尔会在 JSON 外面包一层 json 代码块或者多写一句解释。我的做法是先尝试直接解析失败就用正则把第一个{...}抠出来再解析。import json from openai import OpenAI client OpenAI(api_key你的密钥) PROMPT_TEMPLATE ... # 前面 3.1 的提示词 def triage_issue(title, body): cleaned clean_body(body) fields extract_template_fields(body) field_text \n.join(f{k}: {v} for k, v in fields.items()) prompt PROMPT_TEMPLATE.format( titletitle, bodycleaned \n\n[模板字段]\n field_text, ) resp client.chat.completions.create( model你的模型名, messages[{role: user, content: prompt}], temperature0.2, ) raw resp.choices[0].message.content.strip() return parse_json_safe(raw) def parse_json_safe(raw): try: return json.loads(raw) except json.JSONDecodeError: match re.search(r\{[\s\S]*\}, raw) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return {error: 解析失败, raw: raw[:200]}temperature我设成 0.2因为分诊需要稳定、可复现的判断不需要创意。设太高会导致同一条 Issue 两次分诊结果不一样很难排查问题。4.5 状态记录与主循环主循环把上面几步串起来核心是跳过已处理的import os, hashlib STATE_FILE triage_state.json def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, encodingutf-8) as f: return json.load(f) return {} def save_state(state): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def body_hash(text): return hashlib.md5((text or ).encode(utf-8)).hexdigest() def main(): state load_state() issues fetch_recent_issues(你的仓库, 你的token) for issue in issues: num str(issue[number]) h body_hash(issue.get(body, )) if state.get(num, {}).get(hash) h: continue # 正文没变跳过 result triage_issue(issue[title], issue.get(body, )) print(f#{num} {issue[title]}) print(json.dumps(result, ensure_asciiFalse, indent2)) state[num] {hash: h, result: result} save_state(state)跑起来之后控制台会一条条打印分诊结果。我一般先让它跑一两天把结果存下来人工核对确认准确率之后再考虑写回仓库。4.6 从生成报告到写回评论的过渡等你对准确率有信心了可以加一个写回函数把reply_draft作为评论发出去。但强烈建议加一道人工确认或者只对missing_info非空的 Issue 自动发追问评论——因为追问信息不全这种动作即使措辞略有偏差风险也很低。def post_comment(repo, token, issue_number, body): url fhttps://api.github.com/repos/{repo}/issues/{issue_number}/comments headers {Authorization: fBearer {token}} resp requests.post(url, headersheaders, json{body: body}, timeout15) resp.raise_for_status()5. 常见问题与排查技巧实录5.1 模型分类总在 bug 和 question 之间摇摆这是最高频的问题。根因是这两类的边界确实模糊用户配置错了导致报错算 bug 还是 question我的解决办法是在提示词里加一条明确规则如果错误由用户环境或配置引起、且项目本身行为符合预期归为 question如果项目在标准环境下行为异常归为 bug。同时给两个 few-shot 示例。加上这条之后摇摆明显减少。5.2 回复草稿语气太生硬或太谄媚模型生成的回复有时像客服机器人有时又过度热情。我在提示词里固定了语气要求简洁、专业、直接指出需要补充的信息不要客套话堆砌。并且给了一个正面示例。如果还是不满意可以在后处理阶段做一次轻量改写或者干脆只让模型输出需要追问的要点列表由维护者自己组织语言。5.3 长 Issue 被截断后判断失准有些 Issue 前面一大段是背景铺垫关键信息在最后。简单截断前 2000 字符会丢掉重点。我的改进是优先保留模板字段和正文最后 500 字符中间部分再截断。因为用户往往把关键信息放在模板字段里或者写在最后。这个调整之后长 Issue 的判断准确率提升明显。5.4 状态文件损坏导致重复处理JSON 状态文件如果写入过程中程序被中断可能损坏。我加了一个简单的容错加载失败时返回空字典并备份损坏文件而不是直接崩溃。另外写入时先写临时文件再重命名保证原子性。5.5 常见问题速查表现象可能原因排查方向同一条 Issue 反复处理状态哈希没更新或正文被编辑检查 body_hash 逻辑分类结果每次不一样temperature 过高降到 0.2 以下JSON 解析失败模型输出带了额外文字用正则兜底提取成本异常高重复调用或超长正文检查去重和截断逻辑把 PR 当 Issue 处理没过滤 pull_request 字段加过滤条件回复草稿跑题提示词角色定义不清强化角色和任务描述实操心得每次改完提示词一定要拿一批历史 Issue 做回归测试对比新旧结果。我建了一个几十条的测试集每次调整都跑一遍避免改好一个 case 又弄坏另一个。6. 我踩过的坑和几条实在建议做这个助手最大的体会是AI 应用的价值不在于模型多强而在于你把它的输出嵌进了什么流程。同样一个分类能力如果只是弹个结果给你看价值有限如果它能自动生成追问草稿、自动打上建议标签、自动把重复 Issue 关联起来那省下的就是实打实的时间。几个具体的建议。第一先做减法再做加法。我一开始想让它同时做分类、优先级、去重、回复、甚至自动关闭结果哪个都不准。后来砍到只做分类和回复草稿反而好用了。第二永远保留人工确认环节至少在验证期。第三把模型的判断理由存下来出问题时这是最好的排查线索。第四别追求 100% 准确分诊助手的目标是把 80% 的常规情况处理掉剩下 20% 边界情况交给人这已经很值了。还有个小技巧如果你维护的仓库有比较固定的 Issue 模板可以在提示词里直接把模板字段名列出来让模型对照检查。这比让它自己猜应该有哪些信息要准得多。模板越规范助手越好用——某种程度上这个助手也在倒逼你把 Issue 模板设计得更清晰。后续如果还想扩展我打算加两个方向一是把历史已关闭的 Issue 做向量化新 Issue 进来先做相似度检索自动识别重复二是根据分诊结果自动生成每周的 Issue 概览报告让维护者一眼看清这周社区都在关心什么。这两个方向都不难核心逻辑已经跑通了剩下的就是接数据。