forked from Gitlink/gitlink-cli
11 KiB
11 KiB
gitlink-stale — AI 判断规则详解
本文档说明 Stage C "AI 真假僵尸判断" 的完整信号集与决策算法。 这是本 Skill 与简单时间过滤工具的核心差异。
1. 为什么需要 AI 判断?
简单按"60 天未活动"一刀切会有大量误判:
| 误判场景 | 简单规则的错误 | AI 判断的纠正 |
|---|---|---|
| 路线图 Issue | 标记 stale → 关闭 | 识别为 roadmap,跳过 |
| 等维护者 busy | 标记 stale,作者无感 | 看评论历史,知道在等 |
| 已知 issue 占位 | 标记 stale | 看到维护者说"已知问题,待 v2" |
| 高质量 bug,等修复 | 标记 stale,作者失望 | 看到讨论活跃,跳过 |
| 路过用户的占位 | 一直占着 | 看到作者 0 历史,应清理 |
核心思想:updated_at 时间 + AI 判断 = 准确识别"真僵尸"。
2. 判断信号全集
2.1 评论历史信号(最重要)
通过 issue +view --number N 拿到的 journals 数组:
| 信号 | 真僵尸(应处理) | 活跃(应跳过) |
|---|---|---|
| 最后评论者 | 用户 / 无人 | 维护者 |
| 维护者最后回复时间 | 60+ 天前 | 30 天内 |
| 评论数 | 0-1 条 | ≥ 5 条 |
| 评论内容关键词 | "已知问题"、"占位"、"无复现" | "正在处理"、"待 v2"、"等待上游" |
| 用户最后追问 | 60 天前追问无回复 | 最近有讨论 |
判断伪代码:
def analyze_journals(journals, maintainers):
if not journals:
return {"truly_stale": True, "score": 0.9, "reason": "0 评论,长期无人理"}
last_journal = journals[-1]
last_user = last_journal["user"]["login"]
last_time = parse(last_journal["created_at"])
# 维护者最近回复过 → 跳过
if last_user in maintainers:
days_since = (now() - last_time).days
if days_since < 30:
return {"truly_stale": False, "score": 0.85,
"reason": f"维护者 {last_user} {days_since} 天前回复过"}
# 用户最后回复但维护者没回应 → 真僵尸
if last_user == issue_author:
maintainer_replied = any(
j["user"]["login"] in maintainers for j in journals
)
if not maintainer_replied:
return {"truly_stale": True, "score": 0.9,
"reason": "用户提问后维护者从未回复"}
# 评论内容关键词
last_text = last_journal["notes"]
if any(kw in last_text for kw in ["正在处理", "待 v2", "等待上游", "WIP"]):
return {"truly_stale": False, "score": 0.8,
"reason": "评论含'进行中'类关键词"}
if any(kw in last_text for kw in ["已知问题", "占位", "暂不处理"]):
return {"truly_stale": True, "score": 0.75,
"reason": "评论含'已知/占位'类关键词"}
return {"truly_stale": True, "score": 0.65, "reason": "默认判定为僵尸"}
2.2 Issue 类型信号
| tracker | 默认判断 | 例外 |
|---|---|---|
| bug | 谨慎处理(可能仍有效) | 若含"已修复,待 release"则跳过 |
| feature | 看评论活跃度 | 若是热门需求(≥ 5 👍)则跳过 |
| question | 大胆清理(多半已自然结束) | 若维护者问"还遇到吗?"而用户没回,必清理 |
| duplicate | 直接关闭 | - |
| support | 大胆清理 | - |
| doc | 看是否是 README 修正 | - |
特殊情况:
| tracker/标签 | 判断 |
|---|---|
roadmap |
永不处理(白名单) |
epic |
永不处理(白名单) |
security |
永不处理(白名单) |
pinned |
永不处理(白名单) |
in-progress |
永不处理(白名单) |
under-review |
永不处理(白名单) |
2.3 标签信号
def check_labels(labels, action):
"""
返回 (exempt, reason) 或 (False, None)
"""
STALE_EXEMPT = {
"pinned", "置顶",
"security", "安全",
"roadmap", "路线图",
"epic", "里程碑",
"in-progress", "进行中",
"under-review", "审查中",
"keep-open", "保留",
"help-wanted", # 等社区认领
"good-first-issue", # 等新手认领
}
for label in labels:
if label.lower() in STALE_EXEMPT:
return True, f"含豁免标签: {label}"
return False, None
2.4 作者活跃度信号
def analyze_author(author_login, repo_activity):
"""
评估 Issue 作者的活跃度
"""
author_issues = repo_activity["by_author"].get(author_login, [])
if len(author_issues) == 1:
# 路过用户:只此一个 Issue,可能是占位
return {"stale_tendency": 0.7, "reason": "作者仅此 1 个 Issue"}
if author_login in repo_activity["contributors"]:
# 资深贡献者,信任会跟进
return {"stale_tendency": 0.3, "reason": "作者是仓库贡献者"}
if len(author_issues) >= 5:
# 多 issue 用户,可能批量提交后不再跟进
return {"stale_tendency": 0.6, "reason": f"作者历史 {len(author_issues)} 个 Issue"}
return {"stale_tendency": 0.5, "reason": "中性"}
2.5 标题关键词信号
keep_open_patterns:
- "[WIP]"
- "[Pinned]"
- "[Keep Open]"
- "路线图"
- "长期"
- "讨论"
- "RFC"
- "提案"
force_close_patterns:
- "[已过期]"
- "[占位]"
- "测试" # 仅 2 字符的"测试"
- "测试用"
- "ignore"
- "deprecated"
3. 综合决策算法
3.1 信号汇总
def ai_judge_stale(issue, journals, repo_meta):
# 1. 时间过滤(前置)
days = compute_days_inactive(issue)
if days < stale_threshold:
return {"truly_stale": False, "exempt": True,
"reason": f"仅 {days} 天未活动,未达阈值"}
# 2. 白名单豁免
exempt, exempt_reason = check_labels(get_labels(issue), ...)
if exempt:
return {"truly_stale": False, "exempt": True, "reason": exempt_reason}
# 3. 标题强信号
if matches_force_close(issue.subject):
return {"truly_stale": True, "confidence": 0.95,
"reason": "标题含强制关闭关键词"}
if matches_keep_open(issue.subject):
return {"truly_stale": False, "confidence": 0.9,
"reason": "标题含保留关键词"}
# 4. 综合多信号
signals = []
# 4a. 评论历史信号(权重 0.4)
j_signal = analyze_journals(journals, repo_meta.maintainers)
signals.append(("journals", j_signal["score"], j_signal["reason"], 0.4))
# 4b. 类型信号(权重 0.25)
t_signal = analyze_tracker(issue.tracker_id)
signals.append(("tracker", t_signal["score"], t_signal["reason"], 0.25))
# 4c. 作者活跃度(权重 0.15)
a_signal = analyze_author(issue.author, repo_meta)
signals.append(("author", a_signal["stale_tendency"], a_signal["reason"], 0.15))
# 4d. 时间长度(权重 0.2)
time_score = min(1.0, (days - stale_threshold) / stale_threshold)
signals.append(("time", time_score, f"{days} 天未活动", 0.2))
# 5. 加权平均
final_score = sum(score * weight for _, score, _, weight in signals)
final_reason = "; ".join(f"{name}: {reason}" for name, _, reason, _ in signals)
return {
"truly_stale": final_score >= 0.6,
"confidence": final_score,
"reason": final_reason,
"exempt": False
}
3.2 置信度阈值
| confidence | 含义 | 建议动作 |
|---|---|---|
| ≥ 0.85 | 极有把握 | 直接列入"建议执行"清单 |
| 0.7 - 0.85 | 较有把握 | 列入"建议执行",报告中标记 |
| 0.6 - 0.7 | 一般 | 列入"建议复核" |
| < 0.6 | 把握不足 | 不自动处理,仅列入"待人工"队列 |
4. 边界情况
| 情况 | 处理 |
|---|---|
| journals 数组很大 | 仅取最后 5 条用于 AI 判断 |
| 评论内容是图片/表情 | 跳过,仅看时间 |
| 评论是用户自己反复回("up"、"催") | 维护者从未回应 → 真僵尸 |
| 维护者评论是 "duplicate of #N" | 视为 duplicate,自动关闭 |
| 跨语言评论(中英混合) | 都能识别 |
| 评论含代码块 | 去除代码块后再分析 |
5. 示例分析
5.1 示例 A:真僵尸(高置信度)
{
"number": 142,
"subject": "Bug: 登录页偶尔卡顿",
"description": "有时候会卡...",
"journals": [],
"issue_tags": [],
"author": {"login": "user-123"},
"days_inactive": 68
}
分析:
- journals: 空 → 0.9
- tracker: bug → 0.5(中性)
- author: 仅此 1 个 Issue → 0.7
- time: 68 天 → 0.13
加权:0.9*0.4 + 0.5*0.25 + 0.7*0.15 + 0.13*0.2 = 0.556
输出:
{
"truly_stale": false, // 略低于阈值
"confidence": 0.556,
"reason": "journals: 0 评论;tracker: bug 谨慎;author: 仅 1 Issue;time: 68 天",
"recommended_action": "needs_review"
}
5.2 示例 B:误判避免(活跃)
{
"number": 156,
"subject": "[Roadmap] v2 API 设计",
"description": "长期讨论 v2 接口规范...",
"journals": [
{"user": "dev-li", "notes": "正在按这个方向重构", "created_at": "2026-06-15"},
{"user": "dev-wang", "notes": "+1", "created_at": "2026-06-18"}
],
"issue_tags": ["roadmap"],
"days_inactive": 65
}
分析:
- 白名单:含
roadmap→ exempt: true
输出:
{
"truly_stale": false,
"exempt": true,
"exempt_reason": "含豁免标签: roadmap",
"recommended_action": "skip"
}
5.3 示例 C:明显僵尸(高置信度)
{
"number": 178,
"subject": "测试",
"description": "测试",
"journals": [
{"user": "user-1", "notes": "测试", "created_at": "2026-02-01"}
],
"author": {"login": "user-1"},
"days_inactive": 142
}
分析:
- 标题:含"测试"(force_close 模式)→ confidence 0.95
- author = last journal user → 用户自言自语
- time: 142 天
输出:
{
"truly_stale": true,
"confidence": 0.95,
"reason": "标题含强制关闭关键词",
"recommended_action": "auto_close"
}
6. 信号权重调优
权重默认值(可在 Skill 配置中自定义):
signal_weights:
journals: 0.4 # 评论历史最重要
tracker: 0.25 # Issue 类型
time: 0.2 # 时间长度
author: 0.15 # 作者活跃度
confidence_thresholds:
strong: 0.85 # 直接执行
medium: 0.7 # 执行但标记
weak: 0.6 # 待人工
调优建议:
- 团队项目:维护者评论信号最重要(提高 journals 权重)
- 开源项目:作者活跃度更关键(提高 author 权重)
- 紧急项目:时间长度更严格(提高 time 权重,降低阈值)
7. 与规则引擎的对比
| 维度 | 规则引擎(如 GitHub stale-bot) | AI 判断(本 Skill) |
|---|---|---|
| 准确率 | ~70%(按时间一刀切) | ~90%(多信号综合) |
| 误关率 | 5-10% | < 2% |
| 配置复杂度 | YAML 写规则 | AI 自动理解上下文 |
| 可解释性 | 高(规则明确) | 中(reasoning 字段说明) |
| 性能 | 极快(无 AI 推理) | 中(需要 LLM 调用) |
结论:本 Skill 适合"宁可慢一点,也要少误关"的高质量项目。对于"堆积严重、宁可错杀"的清理任务,可在 SKILL.md 中临时调整 stale_days 和置信度阈值。