はじめに
GMOコネクトの永田です。
「過去3年間で15,000件以上のレビューコメントが蓄積されているけど、誰も活用できていない」
「新人に『過去のレビュー観点を参考にして』と言っても、どこから見ればいいか分からない」
「レビュー基準が属人化していて、レビュアーによって指摘内容がバラバラ」
こんな課題、ありませんか?
ソフトウェア開発プロジェクトでは、過去のレビュー活動から多くの知見が蓄積されていきます。しかし、それを実際に活用しようとすると、以下のような壁に阻まれます:
- 膨大なデータ量: 数万件のチケットから有益な情報を抽出するには、人手で分析する工数が確保できない
- 機密情報の壁: プロジェクト固有の情報が含まれており、外部のクラウドLLMサービスにデータを送信できない
- 知見の属人化: ベテランレビュアーの観点が暗黙知のまま、新メンバーに伝わらない
今回、これらの課題をローカルLLM(Ollama)などを活用することで、どこまで実現できるか試してみました。
結果、約16,000件のRedmineチケットから、1,222件のレビューコメントを自動抽出し、36個のテーマに分類、最終的に35個の実用的なチェックリスト(496観点)を自動生成させてみました
今回のアーキテクチャの場合、すべてオンプレミス環境で完結するため、機密情報を外部に送信することなく安全に、過去の膨大な知見を形式知化できます。
まとめ
約16,000件のRedmineチケットから、ローカルLLM(Ollama)で35個のレビューチェックリスト(496観点)を自動生成
LLMコンテキスト制限(128kトークン)などへの工夫:
- チャンク分割処理: 100件単位で分割→個別処理→マージで100%カバレッジ
- テーマ単位生成: 1,222件を36テーマに事前分類し、テーマごとにチェックリスト生成
- Primary/Secondary分類: 主テーマ+副テーマで横断的関心事も網羅(1,222→1,825分類)
- Few-shot学習: 128kコンテキストを活かし多様な具体例で分類精度向上
- バッチ処理: 10件ずつ処理(124バッチ)+事前フィルタで効率化
成果:
- 総トークン消費: 約13M tokens(ローカル実行で機密情報を外部送信せず)
- チェックリスト: 35個(coding 13個、design 13個、testing 9個)
この手法により、機密性の高い大規模データからでも、実用的なナレッジベースを構築できます。
実現したアーキテクチャ
本ツールは4つのフェーズで構成され、段階的にレビューコメントを分析・構造化していきます。
処理フロー
補足: Phase 1のLLM呼び出し回数について、約16,000件のチケットすべてをLLMに送信しているわけではありません。事前に特定レビュアーのコメントが含まれるチケットのみをフィルタリング(Pythonスクリプトで処理)した結果、1,222件のチケットに絞り込まれており、これらに対してLLMを呼び出しています。
LLM活用箇所(青色):
- Phase 1: チケット本文からレビューコメント抽出・カテゴリ分類(※約16,000件を事前フィルタリング後、1,222件に対してLLM呼び出し)
- Phase 2: 全レビューコメントからの主要テーマ抽出
- Phase 3: 各レビューコメントのテーマへの自動分類
- Phase 4: テーマ別チェックリストの生成
使用モデル・環境
- モデル: Ollama gpt-oss:120b
- コンテキスト長: 128kトークンに拡張
- 温度パラメータ: 0.3(再現性重視)
-
実行環境: ローカルサーバー(機密データを外部に送信しない)
- 一般のご家庭?のPC(DGX Spark)
各処理のポイント
Phase 1: レビューコメントの抽出・分類
処理概要
Redmineチケットの本文とコメントから、設計・実装・テストに関するレビュー観点を抽出し、カテゴリ別に分類します。
Input例:
id: 123456
subject: "本番ビルド時にエラーになる"
journals:
- user: {id: 1, name: "レビュアーA"}
notes: "JavaScriptリソースはES5準拠で記述してください。"
Output例:
ticket_id: 123456
category: coding
confidence: 0.95
review_comments: |
JavaScriptリソースはES5準拠で記述すること
(理由: productionビルド時のUglifyJSがES5のみサポート)
プロンプト設計のポイント
1. Few-shot学習
128kトークンのコンテキスト長を活かし、複数の多様な例を提示することで分類精度を向上させました。
以下は実際のプロンプトから抜粋して一部修正したものです。
## 例1: コーディングレビュー - ES5準拠
コメント: "JavaScriptのリソースはES5準拠で記述してください。"
分類結果:
- ticket_id: 234567
category: coding
confidence: 0.95
review_comments: "JavaScriptリソースはES5準拠で記述すること(理由: productionビルド時の
トランスパイルエラーを回避)。スプレッド構文は使用禁止"
(略)
Few-shot学習の例は、実際のチケットからサンプリングを行い、GitHub Copilot Agent(Claude Sonnet 4.5)を活用しながら期待する出力形式を定義しました。複数の具体例を提示することで、LLMが分類パターンを学習しやすくなります。
実際のプロジェクトに合わせて、Few-shot例をカスタマイズすることをお勧めします。
プロンプトの指示部分:
以降のプロンプト全て、Github Copilot Agent(Claude Sonnet 4.5)と相談しながら作成しました。
プロジェクトの実データを使いながら、Agentと繰り返し試行して理想的なアウトプットとなるよう、promptをカスタマイズすることをお勧めします。
以下は実際のコードから抜粋したものです。
あなたはソフトウェア開発のレビュー分析の専門家です。
以下のRedmineチケットを分析し、レビュー観点(設計・実装・試験)が含まれているかを判定してください。
{FEW_SHOT_EXAMPLES}
## 分析対象チケット
チケットID: {ticket_id}
件名: {subject}
作成者: {author.get('name', 'Unknown')} (ID: {author.get('id', 'N/A')})
作成日時: {created_on}
説明:
{description}
コメント:
{journals_combined}
## 指示
1. このチケットに設計レビュー、実装レビュー、試験レビューのいずれかが含まれているか分析してください
2. 複数のレビュー観点が含まれている場合は、それぞれ別の要素として返してください
- 異なるカテゴリー(design/coding/testing)は必ず別要素として分割
- 同じカテゴリー内でも、独立した異なるレビュー観点(例: コーディング規約違反とロジックバグ修正)がある場合は、別要素として分割
- 重要: 1つの要素のcategoryフィールドには必ず1つのカテゴリーのみを指定(配列形式禁止)
3. 特に以下の観点は見逃さずに抽出すること:
- コーディング規約違反(ES5準拠、命名規則、フォーマット等)
- セキュリティ上の問題(認可・認証の欠陥、データ改ざんリスク等)
- パフォーマンス・リソース問題(メモリ枯渇、タイムアウト等)
- アーキテクチャ設計の根本的な問題
4. レビューコメントが含まれない場合(単なる進捗報告など)は、category: none として返してください
5. 以下のYAML形式で結果を返してください(YAML以外の説明は不要です):
```yaml
- ticket_id: {ticket_id}
category: design # 必ず1つだけ指定: design, coding, testing, none のいずれか(配列形式は禁止)
confidence: 0.XX # 0.0-1.0の信頼度
review_comments: "レビュー観点(チェックポイント)を簡潔に記述。重要: 「何を」だけでなく「なぜ(理由・背景)」も含めること。説明的な文末(「指摘しています」「含まれる」等)は不要。単なる作業漏れや進捗報告は除外"
```
【重要】複数のカテゴリがある場合の例(設計とコーディングの両方がある場合):
```yaml
- ticket_id: {ticket_id}
category: design
confidence: 0.XX
review_comments: "設計上のチェック観点と理由を簡潔に記述"
- ticket_id: {ticket_id}
category: coding
confidence: 0.XX
review_comments: "コーディング上のチェック観点と理由を簡潔に記述"
```
category フィールドに配列(例: [design, coding])を使用しないでください。必ず別の要素として分けてください。
2. 信頼度スコアによるフィルタリング
LLM自身に分類の確信度を0.0-1.0で評価させ、0.7未満のコメントは保存時に除外することで、ノイズを削減しています。これにより、曖昧な分類結果を排除できます。
3. temperature パラメータの調整
temperature: 0.3 に設定し、再現性を重視。同じチケットを複数回処理しても、ほぼ同じカテゴリに分類されるようにしています。(案件の実データに合わせて調整してください)
Phase 2: レビューテーマの抽出(チャンク分割方式)
処理概要
カテゴリ内の全レビューコメントから、繰り返し出現する主要なレビュー観点(テーマ)を抽出します。
最終的なチェックリスト生成時に、LLMのコンテキスト制限に収まるよう、レビューコメントをクラス分けするためです。
ただし、テーマ抽出時もLLMのコンテキスト制限(128kトークン)を超過する可能性があるため、チャンク分割方式 を採用しました。
例: codingカテゴリ(374件のレビューコメント)の場合:
Input例(Chunk 1):
- "JavaScriptリソースはES5準拠で記述してください"
- "ORM使用時はMultipleObjectsReturned例外に注意"
- "認証トークンの有効期限チェックが不足"
- ...(100件)
Output例(各チャンクからの抽出):
themes:
- name: "フロントエンド言語仕様・型安全"
description: "JavaScript ES5/ES6準拠、ビルドツール互換性"
estimated_count: 7
- name: "ORM・データベースクエリの正確性"
description: "Django ORM使用方法、MultipleObjectsReturned例外"
estimated_count: 5
プロンプト設計のポイント
1. チャンク抽出プロンプト(実際のコード)
各チャンクに対して以下のプロンプトを使用してテーマを抽出します。
以下は、プロジェクトにおける{category_names.get(category, category)}レビューコメントの一部です
(チャンク {chunk_num}/{total_chunks}、{len(comments_chunk)}件)。
これらのレビューコメントから、主要なレビュー観点・テーマを5〜10個抽出してください。
# レビューコメント
{comments_text}
# 指示
1. 上記のレビューコメントを分析し、繰り返し現れる主要なレビュー観点・テーマを特定してください
2. テーマは具体的で、チェックリスト作成に適したものにしてください
3. テーマ名は日本語で、簡潔(3〜10文字程度)にしてください
4. 各テーマには簡単な説明(1行)を付けてください
5. **重要**: 少数のコメントでも以下のような観点は**必ず抽出してください**:
- 特定言語仕様の遵守(例: JavaScript ES5準拠、Python型ヒント)
- ビルドツール固有の制約(例: UglifyJS、webpack、トランスパイル)
- プロジェクト固有のコーディング規約
- ブラウザ互換性要件
6. 汎用的なテーマ(例: コーディング規約)だけでなく、技術的に具体的なテーマも抽出する
# 出力形式
以下のYAML形式で出力してください:
```yaml
themes:
- name: "テーマ名1"
description: "テーマの説明(どのようなレビュー観点か)"
estimated_count: 推定該当件数
- name: "テーマ名2"
description: "テーマの説明"
estimated_count: 推定該当件数
```
YAMLのみを出力し、他の説明は不要です。
重要ポイント: 指示5で「少数のコメントでも重要観点は必ず抽出」と明示することで、技術的に具体的なテーマが抜け落ちるのを防止しています。
2. マージプロンプト(実際のコード)
次に、チャンクごとに抽出されたテーマリストを統合します:
以下は、プロジェクトの{category_names.get(category, category)}レビューコメント(全{total_comments}件)を複数のチャンクに分割してテーマ抽出した結果です。
これらのテーマリストを統合し、重複を排除して、最終的な統一テーマリストを作成してください。
# 各チャンクから抽出されたテーマ
{themes_text}
# 指示
1. 類似したテーマを統合する(例: "Flake8違反" + "PEP8準拠" → "コーディング規約")
2. 重複を排除する
3. 粒度を適切に調整する(大きすぎず、小さすぎず)
4. **重要**: 少数のコメントでも以下のような観点は**必ず独立テーマとして残す**:
- 特定言語仕様の遵守(例: JavaScript ES5準拠、Python 3.x対応)
- ビルドツール固有の制約(例: UglifyJS互換性、トランスパイル設定)
- プロジェクト固有の重要規約
5. 最終的なテーマ数は7〜15個程度を目安にする(重要観点を保持するため上限を拡大)
6. estimated_count は各チャンクの合計を計算して設定する
7. **汎用テーマに統合せず、技術的に明確な観点は分離する**(例: 「コーディング規約」に「JavaScript記法」を埋もれさせない)
# 出力形式
以下のYAML形式で出力してください:
```yaml
category: {category}
total_comments: {total_comments}
themes:
- name: "統合されたテーマ名1"
description: "統合されたテーマの説明"
estimated_count: 推定該当件数の合計
- name: "統合されたテーマ名2"
description: "統合されたテーマの説明"
estimated_count: 推定該当件数の合計
```
YAMLのみを出力し、他の説明は不要です。
重要ポイント: 指示4と7で「技術的に具体的な観点を汎用テーマに埋もれさせない」と明示。
Phase 3: コメント自動分類(Primary/Secondary)
処理概要
Phase 2で抽出した36個のテーマに対し、全1,222件のレビューコメントを自動分類します。
Input:
- 36個のテーマリスト
- 1,222件のレビューコメント
Output例:
theme_index: 7
theme_name: "フロントエンド言語仕様・型安全"
total_comments: 22
comments:
- ticket_id: 345678
review_comments: "JavaScriptリソースはES5準拠で記述..."
is_primary: true
classification_reason: "ES5準拠違反が主要な指摘のため"
- ticket_id: 456789
review_comments: "古いNode.js環境でES6構文が動作しない"
is_primary: false # Secondary
classification_reason: "ビルド環境が主テーマだが、言語仕様にも関連"
Primary/Secondary分類の意義
1つのコメントが複数のテーマに関連する場合、主テーマ(Primary)と副テーマ(Secondary)に分けることで、横断的な関心事も漏れなくキャッチします。
- Primary: そのテーマに最も強く関連(必須、1つのみ)
- Secondary: 横断的に関連(任意、0-1個)
メリット:
- 横断的な関心事(言語仕様など)を見逃さない
- 最終的に1,825分類(Primary 1,222 + Secondary 603)を得られた
プロンプト設計のポイント
バッチ分類プロンプト(実際のコード)
10件ずつのコメントを一括で分類します:
あなたは技術レビューの専門家です。以下のレビューコメントを、提供されたテーマに分類してください。
## テーマリスト
{theme_list}
## 分類対象のコメント
{comment_list}
## 指示
各コメントに対して、最も関連性の高いテーマを1-2個選んでください。
- 主テーマは必須(1つ)
- 副テーマは該当する場合のみ(0-1個)
## 出力形式(JSON)
以下の形式で出力してください:
```json
{{
"classifications": [
{{
"comment_index": 1,
"primary_theme": 1,
"secondary_theme": null,
"reason": "認証フローに関する指摘のため"
}},
{{
"comment_index": 2,
"primary_theme": 3,
"secondary_theme": 5,
"reason": "ストレージ処理とエラーハンドリングの両方に関連"
}}
]
}}
```
必ず有効なJSONのみを出力し、他のテキストは含めないでください。
ポイント:
- JSON形式で構造化された出力を要求(今回試した範囲では、YAML解析より安定したため)
- バッチサイズ10件で効率化(全124バッチで処理完了)
- primary/secondary の2段階分類で横断的関心事もキャッチ
Phase 4: チェックリスト生成
処理概要
LLMのコンテキスト長からあふれないようにテーマ別に分類したコメントから、実用的なレビューチェックリストを生成します。
Input例:
theme_name: "フロントエンド言語仕様・型安全"
comments:
- ticket_id: 567890
review_comments: "JavaScriptリソースはES5準拠で記述..."
is_primary: true
- ticket_id: 678901
review_comments: "テンプレートリテラルはES2015なので使用不可"
is_primary: true
...(略)
Output例
| # | 項目名 | 背景 | 確認方法 | 例 |
|---|---|---|---|---|
| 1 | TLS/HTTPS の徹底 | 混在コンテンツや平文通信は盗聴・改ざんリスクを招く。 | - すべての外部エンドポイントが https:// で始まるか確認- 開発・テスト環境でも TLS が有効か確認 |
Ticket #123456 – http://…:9393 が残っていたため混在コンテンツ警告 |
プロンプト設計のポイント
1. チェックリスト生成プロンプト(実際のコード)
以下は、プロジェクトにおける{category_names.get(category, category)}レビューコメント集です。
これらのコメントは「{theme_name}」テーマに分類されています。
# テーマ: {theme_name}
{theme_desc}
# レビューコメント一覧(主分類 {len(primary_comments)}件 + 副分類 {len(secondary_comments)}件)
{comments_text}
# 指示
上記のレビューコメントから、「{theme_name}」に関する実用的なレビューチェックリストを**表形式**で作成してください。
要件:
1. レビューコメントを分析し、共通する問題パターンや観点を特定
2. 具体的で実践的なチェック項目として整理
3. 各項目は明確で、レビュアーが実際にチェックできる形式にする
4. 重要度の高い順に並べる(必須項目 → 推奨項目)
5. 同じような内容は統合する
6. 実際のチケット番号を例として含める
7. **必ず表形式(Markdownテーブル)で出力すること**
# 出力形式
以下のMarkdown形式で出力してください:
```markdown
# {category_names.get(category, category)}レビューチェックリスト: {theme_name}
## テーマ概要
{theme_desc}
---
## 必須項目
| # | 項目名 | 背景 | 確認方法 | 例 |
|---|--------|------|----------|----|
| 1 | **項目名1** | なぜこのチェックが必要か、どんな問題を防ぐか | - 具体的なチェック手順1<br>- 具体的なチェック手順2 | Ticket #XXXXX – 実際に発生した問題の例 |
| 2 | **項目名2** | なぜこのチェックが必要か、どんな問題を防ぐか | - 具体的なチェック手順1<br>- 具体的なチェック手順2 | Ticket #XXXXX – 実際に発生した問題の例 |
---
## 推奨項目
| # | 項目名 | 背景 | 確認方法 | 例 |
|---|--------|------|----------|----|
| A | **項目名3** | なぜこのチェックが推奨されるか | - 具体的なチェック手順1<br>- 具体的なチェック手順2 | Ticket #XXXXX – 実際に発生した問題の例 |
| B | **項目名4** | なぜこのチェックが推奨されるか | - 具体的なチェック手順1 | Ticket #XXXXX – 実際に発生した問題の例 |
---
## 関連チケット一覧
- **#チケット番号** – 件名 - 簡潔な説明
- **#チケット番号** – 件名 - 簡潔な説明
---
## 参考情報
- 関連する技術文書、ガイドライン、コーディング規約などがあれば記載
```
**重要**:
- 「確認方法」列が複数行になる場合は `<br>` タグで改行すること
- 必ず表形式(| 列1 | 列2 | ... |)で出力すること
- 必須項目の # は数字(1, 2, 3...)、推奨項目の # はアルファベット(A, B, C...)を使用すること
Markdownのみを出力し、他の説明は不要です。
実施してみた結果
あるプロジェクトの実データを使って、実測してみた結果をまとめます。
他の案件でも同様な取り組みをする場合に、トークン消費量の予測などの参考になれば、と思います。
処理統計
フェーズ別実行結果
| フェーズ | 入力 | 出力 | LLM呼び出し回数 |
|---|---|---|---|
| Phase 1: レビューコメント抽出 | 約16,000件のチケット | 1,222レビューコメント | 1,222回(※事前フィルタ後) |
| Phase 2: テーマ抽出 | 1,222レビューコメント | 36テーマ | 13回(チャンク+マージ) |
| Phase 3: レビューコメント分類 | 1,222レビューコメント × 36テーマ | 1,825分類 | 124バッチ |
| Phase 4: チェックリスト生成 | 36テーマ別分類 | 35チェックリスト | 35回 |
LLMトークン消費量
総トークン消費: 約13M tokens
| フェーズ | 呼び出し回数 | Input Tokens | Output Tokens | 合計 | 割合 | 1回あたりInput | 1回あたりOutput |
|---|---|---|---|---|---|---|---|
| Phase 1 | 1,222回 | 10.5M | 800K | 11.3M | 87% | ~8.6K | ~655 |
| Phase 2 | 13回 | 195K | 1.8K | 197K | 1.5% | ~15K | ~138 |
| Phase 3 | 124回 | 396K | 19K | 415K | 3.2% | ~3.2K | ~153 |
| Phase 4 | 35回 | 985K | 105K | 1,090K | 8.3% | ~28K | ~3K |
考察:
- Phase 1が全体の87%を占める(チケット本文が長大、かつ呼び出し回数が最多)
- 今回はテキストのみだが、画像を含めたり、ソースdiffがあってかなり長いとかだと、トークン消費量は大きく変わる
- Phase 2-4は効率的(チャンク分割、バッチ処理が有効)
- Phase 4は1回あたりのトークン数が最大(28K input)だが、呼び出し回数が少ない(35回)
- チケット件数が増えると、LLMのコンテキストサイズを超える可能性があるので、その場合には別途チャンク処理の検討が必要になる
- 処理の重要度に応じて、軽量モデル・高性能モデルの選択をしても良いかもしれない
- クラウドLLMのAPIを利用する場合、「Prompt Caching」(同一プロンプトの再利用)でよりコスト最適化ができるが、今回は未実施
- 今回のprompt例では未考慮のため、必要に応じて最適化が必要
生成されたチェックリスト例
整理してみると「まあよくある観点だな」とは思いますが、今までプロジェクトでこのような観点がまとまっていなく、結果として色々とレビューコメントとして蓄積されてきたのだと思います。
coding カテゴリ
抽出されたテーマ(13個):
- セキュリティ・認証・認可
- エラーハンドリング・例外処理
- 入力検証・バリデーション
- ORM・データベースクエリ
- Docker・ビルド環境
- コーディング規約・スタイル
- フロントエンド言語仕様・型安全
- 依存ライブラリ・SDKバージョン管理
- UI・国際化・アクセシビリティ
- パフォーマンス・トランザクション制御
- API設計・HTTPステータス
- 設定管理・環境変数
- ファイル処理・ストリーム
design カテゴリ
抽出されたテーマ(13個):
- 認証・認可・アクセス制御
- ストレージ対応
- データベース整合性・トランザクション
- API設計・エラーハンドリング
- リソーススケーラビリティ・HA
- 設定・フラグ管理
- ファイル処理・ハッシュ・圧縮
- クォータ・使用量集計
- 移行・マイグレーション設計
- ログ・監査・運用性
- キャッシュ・セッション管理
- 通知・メール・外部連携
- UI/UX・ユーザビリティ
testing カテゴリ
抽出されたテーマ(10個、チェックリスト9個):
- テストカバレッジ・網羅性
- テスト自動化・CI統合
- パフォーマンス・負荷テスト
- セキュリティ・認証テスト
- エラーハンドリング・異常系
- 環境・デプロイ検証
- ブラウザ・UI互換性
- (コメント0件のため除外)
- 統合テスト・E2Eテスト
- テストデータ・モック管理
(再掲)まとめ
約16,000件のRedmineチケットから、ローカルLLM(Ollama)で35個のレビューチェックリスト(496観点)を自動生成
LLMコンテキスト制限(128kトークン)などへの工夫:
- チャンク分割処理: 100件単位で分割→個別処理→マージで100%カバレッジ
- テーマ単位生成: 1,222件を36テーマに事前分類し、テーマごとにチェックリスト生成
- Primary/Secondary分類: 主テーマ+副テーマで横断的関心事も網羅(1,222→1,825分類)
- Few-shot学習: 128kコンテキストを活かし多様な具体例で分類精度向上
- バッチ処理: 10件ずつ処理(124バッチ)+事前フィルタで効率化
成果:
- 総トークン消費: 約13M tokens(ローカル実行で機密情報を外部送信せず)
- チェックリスト: 35個(coding 13個、design 13個、testing 9個)
適用できる場面:
本記事で紹介した手法は、Redmine以外にも以下のようなシーンで応用できると思います。
- GitHub Issues/Pull Requestsからのレビューパターン抽出
- 技術ドキュメント・設計書からのベストプラクティス抽出
- 障害報告チケットからの再発防止チェックリスト生成
今後の発展:
さらに大規模なプロジェクト(10万件超)では、Phase 4のチェックリスト生成でもチャンク分割が必要になる可能性があります。また、継続的にチケットが追加される環境では、増分更新の仕組みも検討の余地があります。
ローカルLLMの進化により、機密性の高いエンタープライズデータでも安心してAI活用できる時代になりました。本記事が、皆さんのプロジェクトにおけるナレッジ活用の参考になれば幸いです😊
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。