Skip to content
检测中

Issue Triage 实战:最低风险的最高杠杆 loop

系列第十六篇。前十五篇:概念 · pi L1 落地 · L3 设计 · Memory · Multi-Loop · 网关层 · 反衰减 · Meta-Loop · 韧性与评估 · Sub-agent 编排 · Skills 工程化 · Worktree 并行 · Scheduling 模式 · PR Babysitter · Dependency Sweeper

Issue Triage 是 Loop Engineering 里最经典的 L1 loop。它不碰一行代码、不合并一个 PR,却喂给所有其他 loop——CI Sweeper 从它拿优先级,Daily Triage 从它读今日待办,PR Babysitter 从它判断哪个 PR 先看。极低风险,极高杠杆。这篇讲怎么在 pi 上从零搭一个。


一、Issue Triage 解决什么问题

先看没有它时的世界:

症状根因后果
issue 堆积无人看新 issue 比处理速度快backlog 指数增长,团队失去全局视图
无优先级好 issue 和噪音混在一起工程师不知道先做哪个
重复报告同一 bug 被 3 个人各报一次两个人同时开始修,浪费
标签混乱标签依赖人工心情打统计/过滤形同虚设
第一个 label 要等一周人太忙没空 triage高优 issue 延迟响应,用户流失

Issue Triage loop 做的事:持续扫描 open issues,自动分类/去重/排优先级/打标建议,让 backlog 始终干净、可操作、有优先级。

Cobus Greyling 给它下了一句精准定位:

「Continuous discovery, deduplication, prioritization, and labeling — so the team (and other loops) always have a clean, actionable top-of-queue. Pure report / proposal mode in week one. Extremely low risk, high leverage.

三个关键词:

  • Pure report:L1 只出报告/建议,绝不自动动作
  • Extremely low risk:因为它不碰代码、不碰 PR,出错也只是报告质量差
  • High leverage:它的输出是所有其他 loop 的输入源

二、四个核心动作

Issue Triage 的一切围绕四个动作展开。

2.1 分类(Classify)

每个 issue 进来,判断它是什么:

分类含义典型标签
bug行为不符合预期bug
feature请求新功能enhancement
question用法咨询,非缺陷question
duplicate与已有 issue 重复duplicate
stale长期无活动,可能过时stale
needs-info信息不足,无法判断needs-more-info

分类是后续一切动作的基础。分类错了,优先级全错。

2.2 去重(Deduplicate)

两个不同的人报告了同一个 bug。loop 要识别并标记。

去重的难点在于:标题不同、描述不同,但说的是同一个问题。纯关键词匹配漏检严重,需要结合标题相似度 + body 语义匹配 + 关键错误信息(stack trace、错误码)。

L1 的去重策略:只标记「possible duplicate of #NNN」,不自动关 issue。留给人确认。

2.3 优先级(Prioritize)

给每个 issue 算一个分数,排出优先队列。这是 Issue Triage 最核心的输出。

优先级算法见第六节,核心是信号加权:age(多老了)、reactions(多少人 👍)、linked PR(有人已动手?)、label(bug 优先于 feature)。

2.4 打标建议(Label Propose)

给每个 issue 建议一组标签,如 bug + needs-repro + area:export

L1 铁律:绝不自动打标。 只产出建议,写进 state 文件。L2 可对 allowlisted 的标签(如 area:*needs-repro)自动打,但 bug/security/P0 等敏感标签永远人审。


三、为什么是 L1,为什么高杠杆

为什么是 L1

维度Issue Triage 的选择
碰代码?不碰。纯只读。
碰 PR?不碰。只读 issue。
碰外部系统?只读 GitHub Issues API,不写(L1)
出错后果?最坏:分类建议不准、优先级排偏——你看一眼报告就知道,改一下 skill 就行
tools 白名单read, grep, find, ls, bash(bash 仅用于 gh issue list),无 edit/write

L1 = 零风险试错。这正是 Issue Triage 适合作为第一个 loop 的原因。Cobus 的 daily-triage story 讲得很清楚:

「Report-only is not wasted time — it calibrates triage. Skipping L1 is how loops get a reputation for noise.」 (报告模式不是浪费时间——它在校准 triage。跳过 L1 正是 loop 落得「噪音制造机」名声的原因。)

为什么高杠杆

一张图说清:

                   Issue Triage (L1)
                   产出: 干净的优先队列

          ┌─────────────┼──────────────┐
          ▼             ▼              ▼
     Daily Triage   CI Sweeper    PR Babysitter
     读它出今日待办   读它判断哪些   读它判断哪个
                     issue 关联 CI   PR 先 review
                     失败
          ▼             ▼              ▼
         人决策       loop 执行      loop 执行

Issue Triage 是 backlog 的「单一事实源」。所有 loop 和人都从它读优先级,不需要各自重复扫描 issue。这正是 Multi-Loop 篇「分离 state 文件 + 各司其职」原则的体现:Issue Triage 维护 issue 库,其他 loop 消费它。


四、与 Daily Triage 的分工

这两个最容易混。一句话区分:

Issue TriageDaily Triage
关注对象issue 仓库(全部 open issues)今天的待办(今日该做什么)
时间尺度滚动维护(持续更新优先队列)每日快照(一天一次)
输入GitHub Issues APISTATE.md(含 Issue Triage 的输出)+ git log + TODO
输出issue-triage-state.md(优先队列 + 标签建议)STATE.md 的 Today 段(今日待办报告)
频率2h 或事件驱动1d
类比图书馆管理员(整理分类编号)每日晨报(今天看哪几本)

协作链路

Issue Triage (2h)
  → 维护 issue-triage-state.md(完整优先队列)
  → Daily Triage (1d) 读它
  → 产出 STATE.md Today 段(今日 Top 5 + 建议行动)
  → 人决策

Issue Triage 是「上游」,Daily Triage 是「下游」。Issue Triage 保证 issue 库始终有优先级;Daily Triage 从中挑出今天最该做的。分开是因为它们频率不同(2h vs 1d)、粒度不同(全量队列 vs 今日精选)、消费者不同(所有 loop vs 人)。


五、STATE 设计

Issue Triage 的输出是 issue-triage-state.md——一个滚动维护的 issue 优先队列。

markdown
# Issue Triage State
Last run: 2026-06-15 10:00 UTC
Open actionable: 14 (was 17 last run)
New since last run: 3
Needs human: 2

## Top 5 (by loop score)
1. #487 (bug, 2d, 12👍, linked PR #501) — "Crash on export with large files" — 建议: bug + needs-repro + area:export — score: 87
2. #482 (bug, 5h, 8👍) — "Login redirect fails on Safari" — 建议: bug + browser:safari + area:auth — score: 79
3. #479 (feature, 3d, 15👍) — "Add dark mode" — 建议: enhancement + area:ui — score: 72
4. #476 (question, 1d, 2👍) — "How to configure custom headers?" — 建议: question → close-after-answer — score: 35
5. #475 (duplicate?, 4h, 1👍) — "Export crash" — possible duplicate of #487 — 建议: duplicate + needs-human — score: 20

## Needs Human
- #475: 可能与 #487 重复(置信度 70%),需人确认
- #480: 触碰 auth 模块,标签建议需人审

## Recently Closed (pruned this run)
- #460 (已合并,pruned)
- #458 (已关闭,pruned)

## Run Log
2026-06-15 10:00 | 3 new | 14 actionable | 2 needs-human | 0 auto-labeled | ~8k tokens
2026-06-15 08:00 | 1 new | 17 actionable | 1 needs-human | 0 auto-labeled | ~7k tokens

设计要点

区段作用更新策略
Header运行元信息 + 计数趋势每次 run 覆盖
Top 5优先队列快照每次 run 重算
Needs Human需人确认的歧义项loop 只增不删(删由人确认后操作)
Recently Closedprune 证据(防 State Rot)保留最近 N 条
Run Log可观测性(token/发现/动作)append-only

铁律(Cobus anti-pattern #3「Vague triage output」)

「Triage skill returns paragraphs of narrative → Loop cannot parse priorities; humans ignore STATE.md.」

对策:结构化 markdown sections,one-line items,explicit Suggested loop action

每条 issue 必须一行写完:#ID (type, age, signals) — "title" — 建议: labels — score: N。绝不写段落叙述——Daily Triage 和人都要快速扫读,段落 = 被忽略


六、优先级算法

这是 Issue Triage 的核心智力。优先级不是「拍脑袋」,而是信号加权

六个信号

信号含义权重方向为什么
Typebug vs feature vs questionbug > feature > questionbug 影响现有用户,feature 是增量
Ageissue 开了多久越久越高(但不无限)老问题被忽视太久 = 用户流失
Reactions👍 数量越多越高社区诉求强度
Linked PR有人已提关联 PR有 PR 的降优先级(已有人在修)已有人动手,不需催
Security/Auth触碰安全/认证模块一律 P0,人审Cobus human gate 铁律
Author报告者身份内部员工/reporter 权重高反噪声(Cobus: "weight by signals the team actually cares about")

打分公式

typescript
function scoreIssue(issue: Issue): number {
  let score = 0;

  // ① 类型权重
  const typeWeight = { bug: 40, feature: 25, question: 10, "needs-info": 5 };
  score += typeWeight[issue.type] ?? 15;

  // ② 年龄(对数增长,防老 issue 永远霸榜)
  const ageDays = daysSince(issue.createdAt);
  score += Math.min(20, Math.log2(ageDays + 1) * 5);  // 30天封顶 20 分

  // ③ Reactions(每人 +2,封顶)
  score += Math.min(15, issue.reactions * 2);

  // ④ Linked PR(有人在修了 → 降优先级)
  if (issue.linkedPRs.length > 0) score -= 15;

  // ⑤ 安全模块 → 直接 P0,不参与普通排队
  if (touchesSecurity(issue)) return -1;  // 特殊标记,进 Needs Human

  return score;
}

关键设计

  • 年龄用对数:防止老 issue 永远霸榜(30 天和 300 天的差距不该是 10 倍)。
  • Reactions 封顶:防止一个网红 issue 靠 500 个 👍 压垮真正的 bug。
  • Linked PR 降权:已有人动手的 issue 不需要 triage 催——它该转给 PR Babysitter。
  • 安全模块短路:触碰 auth/payments/security 的 issue 不参与打分排队,直接进 Needs Human(Cobus human gate 铁律)。

信号防噪声

Cobus 特别警告:

「Over-prioritizing noisy reporter — Weight by signals the team actually cares about (reactions, linked PRs, internal +1s). Human overrides recorded in state.」

意思是:不是所有信号都可信。一个用户连续报 10 个低质量 issue,不应把他的 issue 全推到高优。对策:记录人的 override——人把某项降过级,下次 loop 记住,不再推高。


七、去重策略

去重是 Issue Triage 最难的动作,也是最容易出错的。

三层去重

方法成本精度用途
L1 标题相似度Jaccard / Levenshtein 编辑距离极低快速粗筛
L2 关键信息匹配error code / stack trace / 文件名提取精确匹配已知模式
L3 语义匹配body embedding + cosine 相似度捕获「说法不同但意思一样」

L1 保守原则

「Conservative matching + always surface 'possible duplicate of #NNN' for human confirmation in L1.」

L1 的去重宁可漏检不可误杀。误杀(把两个不同的 issue 标成重复)= 一个真 bug 被埋没。漏检(漏掉一个重复)= 浪费但不致命。

实现:三层匹配后,只标「possible duplicate」并写进 Needs Human,不自动关 issue。置信度 < 70% 不标(保守)。

typescript
function detectDuplicates(newIssue: Issue, existing: Issue[]): DuplicateMatch[] {
  const matches: DuplicateMatch[] = [];

  for (const ex of existing) {
    // L1: 标题相似度
    const titleSim = jaccardSimilarity(tokenize(newIssue.title), tokenize(ex.title));
    // L2: 关键信息
    const sharedErrors = sharedErrorPatterns(newIssue.body, ex.body);
    // L3: 语义(可选,需 embedding)
    // const semSim = cosineSimilarity(embed(newIssue.body), embed(ex.body));

    const confidence = titleSim * 0.4 + (sharedErrors ? 0.5 : 0);  // + semSim * 0.3
    if (confidence > 0.7) {
      matches.push({ of: ex.number, confidence });
    }
  }
  return matches;   // 全部进 Needs Human,不自动关
}

八、必备 Skill

issue-triage SKILL.md

markdown
---
name: issue-triage
description: 扫描 open issues,分类/去重/排优先级/打标建议。L1 只读,绝不自动打标或关 issue。
---

# Issue Triage

## 任务
每次运行,扫描所有 open issues,维护一个干净的优先队列。

## 步骤
1.`issue-triage-state.md`,了解上次状态和已有队列
2. `gh issue list --state open --json number,title,body,labels,reactions,createdAt` 获取全部 open issues
3. 识别上次以来新增/更新的 issue
4. 对每个 issue:
   a. 分类(bug/feature/question/duplicate/stale)
   b. 检测重复(标题相似度 + 关键错误信息 + 保守阈值 70%)
   c. 打分(type + age + reactions + linkedPR,见 scoreIssue)
   d. 建议标签
5. 产出更新后的 Top 5 + Needs Human + 计数
6. prune 已关闭的 issue(State Rot 防护)

## 铁律
- **绝不自动打标、关 issue、评论 issue**(L1)
- 重复检测只标 "possible duplicate",写 Needs Human
- 触碰 security/auth/payments 的 issue → 直接进 Needs Human,不参与排队
- 每条 issue 一行写完,不写段落(vague output = 人不看)
- 置信度 < 70% 的判断不输出(保守)

skill 设计要点

要点为什么
「绝不自动打标/关/评论」写在最显眼处软约束给模型;硬约束靠 tools 白名单兜底
明确的 gh issue list 命令减少模型猜 API 用法(intent debt)
「每条一行」防 Cobus anti-pattern #3(vague output)
「置信度 < 70% 不输出」保守原则——宁可漏检不可误杀

九、pi 实现

9.1 事件 + 定时混合调度

Issue Triage 最适合混合调度(Scheduling 篇模式 ④):

调度源触发用途
GitHub webhookissues event)新 issue opened即时响应,秒级 triage
cron 兜底(2h)定时巡检防 webhook 漏接;扫已更新但未重 triage 的 issue
yaml
# .github/workflows/issue-triage.yml
name: Issue Triage Loop
on:
  # ① 事件驱动:新 issue
  issues:
    types: [opened, reopened, edited]
  # ② cron 兜底:每 2 小时
  schedule:
    - cron: "0 */2 * * *"
  workflow_dispatch: {}

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm install @earendil-works/pi-coding-agent
      - name: Run Issue Triage
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # L1 严格只读:read/grep/find/ls/bash,无 edit/write
          pi -p "执行 /skill:issue-triage。读 issue-triage-state.md,扫描 open issues,更新优先队列。绝不打标或关 issue。" \
            -t read,grep,find,ls,bash

9.2 Runner(SDK 版,含 verifier)

更可控的写法用 SDK,加一个轻量 verifier:

typescript
// issue-triage/index.ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

const REPO = process.env.REPO_CWD!;

async function runAgent(role: string, tools: string[], prompt: string) {
  let out = "";
  const { session } = await createAgentSession({
    cwd: REPO,
    tools,
    sessionManager: SessionManager.inMemory(REPO),
  });
  session.subscribe((e) => {
    if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta")
      out += e.assistantMessageEvent.delta;
  });
  await session.prompt(prompt);
  session.dispose();
  return out;
}

// ① 先拉取 issue 数据(用 gh CLI,不靠模型调 API)
//    这一步在 pi 外做,保证数据准确
import { execSync } from "child_process";
const issuesJson = execSync(
  `gh issue list --state open --json number,title,body,labels,reactions,createdAt --limit 100`,
  { cwd: REPO, encoding: "utf8" }
).trim();

// ② Implementer: 分类/去重/打分
const result = await runAgent(
  "triage",
  ["read", "grep", "find", "ls", "bash"],   // L1 严格只读
  `执行 /skill:issue-triage。
   读 issue-triage-state.md 获取上次状态。
   下面是当前所有 open issues(JSON):
   ${issuesJson}
   产出: 更新后的 issue-triage-state.md(Top 5 + Needs Human + 计数 + Run Log)。
   铁律: 绝不打标/关 issue/评论。重复只标 possible duplicate。`,
);

// ③ Verifier: 轻量检查(L1 的 verifier 可以很轻)
const verdict = await runAgent(
  "verifier",
  ["read"],
  `审这份 issue triage 输出,只检查三点:
  (a) 有没有自动打标或关 issue(应一律是建议)?
  (b) Needs Human 里有没有触碰 security/auth 但漏标的?
  (c) Top 5 有没有把 question 排在 bug 前面?
  输出 PASS 或 FAIL + 理由。
  --- 输出 ---
  ${result}`,
);

const ok = verdict.trim().toUpperCase().startsWith("PASS");

// ④ 回写 state + 通知
if (ok) {
  // 写回 issue-triage-state.md(在 runner 里写,不让 agent 写)
  // pi agent 的输出是 state 内容,runner 验证后落盘
  console.log("Triage PASS, state updated");
} else {
  console.log("Triage FAIL:", verdict);
  // FAIL 时不落盘,保留旧 state,告警
}

console.log(`done: ${ok ? "PASS" : "FAIL"}`);

9.3 关键设计点

设计原因
gh issue list 在 pi 外做数据获取靠确定性 CLI,不靠模型「猜 API」。模型只做推理(分类/打分),不做数据获取
tools 白名单无 edit/writeL1 硬边界。模型物理上无法打标/关 issue
state 由 runner 落盘,不由 agent 写agent 产出 state 内容 → runner 验证(verifier PASS)→ runner 写文件。agent 无 write 工具,无法绕过验证
verifier 轻量L1 的 verifier 不需要跑测试(没代码改),只检查「有没有越权」和「分类有没有明显错误」
混合调度webhook 即时 triage 新 issue + cron 兜底防漏。Scheduling 篇模式 ④

9.4 L2 升级路径

Issue Triage 的 L2 很克制——只允许自动打 allowlisted 标签

可自动打(L2)仍需人审
area:*(area:export, area:auth...)bug
needs-reprosecurity
questionP0/P1
stale(超 N 天无活动)关 issue(close)
typescript
const ALLOWLIST_AUTO_LABEL = /^area:/;
const proposed = parseLabels(result);
for (const label of proposed) {
  if (ALLOWLIST_AUTO_LABEL.test(label)) {
    await execSync(`gh issue edit ${issue.number} --add-label "${label}"`);
  } else {
    // 非 allowlist → 写进 Needs Human
    needsHuman.push({ issue: issue.number, suggested: label });
  }
}

L2 的升级标准:L1 连续 2 周运行,triage 准确率 > 85%(人抽检判定),且 Needs Human 的误报率 < 10%。没到这个标准,L1 的报告模式继续校准 triage。


十、失败模式与护栏

失败严重度症状对策
过度自动打标S2L1 越权自动打标tools 白名单无 edit/write(硬边界)+ skill 铁律(软约束)
误去重S2两个不同 issue 被标重复,真 bug 被埋没保守阈值 70% + 只标 possible duplicate + 人确认
优先级被噪声干扰S1→S2网红 feature 靠 reactions 压过真 bugreactions 封顶 + 人 override 记录
vague outputS1triage 输出段落叙述,人看也不看强制 one-line items + structured sections
State RotS1→S2state 引用已关闭的 issue每 run prune + 校验 ID 存活
context overloadS1一次 run 新增 >X 个 issue,模型处理不过来Cobus human gate: ">X new issues → escalate"
标签膨胀S1建议越来越多无意义标签label allowlist + 去重
notification fatigueS1→S2每个新 issue 都 ping 人只对 Needs Human 通知,其余写 state

Cobus 的三层防噪声

  1. Weight by signals the team actually cares about(不是所有信号都可信)
  2. Human overrides recorded in state(人改过的优先级,loop 记住不再推高)
  3. Only notify human for the "needs human" slice(其余写 state,不 ping)

十一、成功指标

指标目标怎么测
issue → 首个 label 的时间< 2h(之前可能 > 1 周)GitHub issue timeline
24h 内有明确优先级的 issue 占比> 90%扫 state 的覆盖率
重复 caught 率> 80% 的重复在 4h 内标出人工事后审计
人 override 率< 15%(太高说明 triage 不准)state 记录的 override
工程师反馈"我总知道 top 5 是什么"定性,从 state review

Cobus 给了一个漂亮的定性指标:「Engineer-reported 'I always know what the top 5 things are' score」。如果工程师打开 state 文件就知道今天该关注什么,Issue Triage 就成功了。


十二、回顾

  1. Issue Triage 是最低风险的最高杠杆 loop。L1 只读,出错只是报告不准;但它的输出喂给所有其他 loop。
  2. 四个核心动作:分类、去重、优先级、打标建议。L1 全部只建议不执行。
  3. 与 Daily Triage 分工:Issue Triage 维护 issue 库(2h,上游),Daily Triage 读它出今日待办(1d,下游)。
  4. 优先级 = 信号加权:type + age(对数) + reactions(封顶) - linkedPR,安全模块短路到 Needs Human。
  5. 去重三层 + 保守原则:标题相似度 + 关键信息 + 语义匹配;置信度 < 70% 不标;只标 possible duplicate,人确认。
  6. STATE 一行一项:每条 issue 一行写完,绝不段落叙述(Cobus anti-pattern #3)。
  7. pi 实现gh issue list 在 pi 外取数据(确定性),模型只做推理;tools 严格只读;混合调度(webhook + cron 兜底)。
  8. L2 升级:只自动打 allowlisted 标签(area:*/needs-repro/question/stale),bug/security/P0 永远人审。
  9. 防噪声三层:信号权重可信度 + 人 override 记录 + 只对 Needs Human 通知。
  10. 成功标准:工程师打开 state 就知道 top 5——Issue Triage 的终极指标。

一句话收尾:Issue Triage 不碰一行业务代码,却让整个 loop 系统知道「先做什么」。它是 backlog 的图书馆管理员——从不自己写书,但让每本书都在正确的时间被正确的人找到。


参考资料

Released under the ISC License.