目次
Issueを起票したあと、「優先度」と「領域」のラベルを付け忘れる。そんなことが何度か続いて、分類されないIssueが溜まっていました。
個人開発のダッシュボードアプリでは、TodoをGitHub Issueで管理しています。変更の種類はIssueテンプレートが自動で付けてくれますが、対応の優先度や、バックエンド・フロントエンドといった作業領域は手で選んでいました。そこで、Issueが作られたらGitHub ActionsからJevに内容を渡し、分類結果に応じてラベルを付けることにしました。
Jevでできる分類#
JevはTypeSafeが提供する、判断に特化した種類のモデルです。質問に対して文章を生成するのではなく、あらかじめ決めた形式で答えます。今回使う形式はChoiceとNoulです。
Choiceは、決まった選択肢から一つを選ぶ分類に使います。選択肢ごとの確率も返るので、「どの答えを選んだか」に加えて、判定の確信度も確認できます。優先度をcritical、high、medium、lowから選ぶ用途に合います。
Noulは、質問への答えが「はい」である確率を0から1で返します。領域ごとに「このIssueを解決するために、ここで変更が必要か」と尋ねれば、複数の領域に該当するIssueも扱えます。フロントエンドとバックエンドの両方に作業が必要なら、それぞれの質問にyesが返る形です。
もう一つのScoreは、段階ごとの確率と期待値を返す形式です。今回は優先度の検証にも使いました。各形式の詳しい仕様は、TypeSafeのドキュメントで確認できます。
今回の検証環境と運用ルール#
動作確認には、次の環境を使いました。
- モデルはjev-1.13
- Python SDKはtypesafe-sdk 0.7.1
- Pythonは3.13
- GitHub Actionsはubuntu-latest
ラベル付けでは、二つのルールを決めています。すでに優先度や領域のラベルがあれば、その観点は変更しません。判定に自信がない場合もラベルを付けません。具体的には、優先度はChoiceのconfidenceが0.5未満なら見送り、領域はyesの確率が0.5未満なら見送る設定です。
これなら、モデルの判定が曖昧なIssueを無理に分類せずに済みます。ラベルが付かなかったものは、必要に応じて人が確認できます。
質問と説明は英語で用意する#
TypeSafeのドキュメントでは、Jevは英語を主な言語として学習しており、英語で最も高い精度が出ると説明されています。日本語を含む中国語・日本語・韓国語の文章も扱えますが、英語と同じ精度とは限りません。
そこで、質問文と選択肢の説明は英語にしました。Issueのタイトルと本文は日本語のまま渡しています。領域の説明も、リポジトリで使っている日本語の定義を英訳して用意しました。
この設定でどの程度分類できるかは、実際のIssueで確認が必要です。今回は過去に自分でラベルを付けたIssueを正解データとして、質問文や判定方法を変えながら比較しました。
領域は「言及」ではなく「変更が必要か」を聞く#
領域ごとに「このIssueはフロントエンドに関係するか」と質問すると、対象範囲が広くなりすぎました。たとえば、画面にエラーが表示されても、原因がバックエンドならフロントエンドのコード変更は不要かもしれません。それでも「関係するか」という質問では、フロントエンドにもyesが付くことがあります。
そこで、質問を「このIssueを解決するために変更が必要な主な領域の一つか」に変えました。さらに、領域が本文に書かれているだけ、間接的に影響するだけ、問題が起きている場所というだけでは該当しない、とnoの条件に加えています。
Noul(
instructions=f"Is {description} one of the main areas that must be changed to resolve this issue?",
criteria={
"true": "Substantial work in this area is required. A person doing this work would edit files or configuration belonging to this area.",
"false": "This area is only mentioned, only indirectly affected, or merely the context in which the problem appears. No substantial work happens here.",
},
)質問の言い方を変えると、判定の基準も変わります。どこまでを該当とするのか、文章で具体的に指定してから既存Issueで確かめるのがよさそうです。
優先度はChoiceで判定する#
優先度はChoiceとScoreの両方を試しました。過去のIssueを使った検証結果は次のとおりです。
- Choice、confidenceの足切りなし:付与率100%、正解と一致67%、±1段階以内95%
- Choice、confidence 0.5以上:付与率81%、正解と一致73%、±1段階以内95%
- Choice、confidence 0.7以上:付与率59%、正解と一致76%、±1段階以内98%
- Score、confidence 0.5以上:付与率92%、正解と一致56%、±1段階以内94%
今回のデータでは、ScoreよりChoiceの一致率が高くなりました。Scoreでは段階の間の値が返ることがあり、たとえば2.5をどちらの段階にするか決める必要があります。検証では四捨五入しました。一方、Choiceなら最も確率の高い選択肢を採用できます。
confidenceを0.5以上に絞ったChoiceは、付与率81%、正解との一致73%でした。0.7以上にすると一致率は上がりますが、ラベルが付くIssueは59%に減ります。今回は付与率も考慮し、0.5以上を採用しました。
質問は「How urgently should this issue be addressed?」です。各選択肢には、ラベルの定義を英訳した説明を設定します。たとえばcriticalは、本番障害、深刻な脆弱性、データ破損など、すぐに対応が必要なもの。lowは、時間に余裕があるときに進める小規模な改善や整理、といった基準です。
GitHub Actionsから実行する#
ワークフローはIssueのopenedイベントで起動し、手動実行もできるようにします。手動実行ではIssue番号を入力します。必要な権限はIssueの読み書きに絞り、TypeSafeのAPIキーはリポジトリのSecretsにTYPESAFE_API_KEYとして登録します。
name: Issue Triage
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 分類する issue 番号
required: true
permissions:
contents: read
issues: write
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
cache: pip
cache-dependency-path: scripts/requirements.txt
- run: pip install -r scripts/requirements.txt
- env:
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
GH_TOKEN: ${{ github.token }}
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
run: python scripts/issue_triage.py "$ISSUE_NUMBER"checkoutとsetup-pythonはタグではなくコミットSHAで固定しています。ワークフローで使うアクションの参照先を固定する方法の一つです。依存パッケージはrequirements.txtに typesafe-sdk~=0.7.1 と記載します。
スクリプトにはIssue番号だけを渡します。タイトル、本文、既存ラベルは gh issue view で取得します。既存ラベルを先に確認し、優先度がすでにあればChoiceの質問を省略。領域ラベルがあればNoulの質問を省略します。足りない観点だけをJevに尋ね、設定したしきい値を超えた結果だけラベルに追加する流れです。
issue = json.loads(
subprocess.run(
["gh", "issue", "view", issue_number, "--json", "title,body,labels"],
check=True,
capture_output=True,
text=True,
).stdout
)
existing_labels = [label["name"] for label in issue["labels"]]
if not any(label.startswith("priority:") for label in existing_labels):
questions["priority"] = Choice(
instructions="How urgently should this issue be addressed?",
criteria=PRIORITIES,
)
if not any(label.startswith("area:") for label in existing_labels):
for name, description in AREAS.items():
questions[f"area_{name}"] = Noul(
instructions=f"Is {description} one of the main areas that must be changed to resolve this issue?",
criteria={
"true": "Substantial work in this area is required.",
"false": "This area is only mentioned, indirectly affected, or the context where the problem appears.",
},
)この例では質問の組み立てまでを抜粋しています。API呼び出しや確率の取り出し方、ラベル付与の処理はSDKの仕様に合わせてスクリプトへ追加してください。ラベル名やAPIの戻り値も、実際のリポジトリに合わせて確認が必要です。
まず過去のIssueでしきい値を決める#
今回の設定は、個人開発アプリの既存Issueで試した結果です。Issueの内容やラベル定義が違えば、同じ一致率になるとは限りません。
最初に過去のIssueをいくつか正解データとして用意し、質問文とconfidenceのしきい値を変えて結果を比べると、運用に合わせた設定を選びやすくなります。導入するなら、まずは過去のIssueを使って分類を確認し、自分のリポジトリに合う基準を決めてみてください。
目次
