Backlog の完了チケットの内容を、AI エージェント kiro から自然言語で検索してみる。

記事タイトルとURLをコピーする

はじめに

プロジェクトが長く続くと、
「このエラー、前にも見た気がするけどどう解決したっけ?」
「あの仕様、なんでこうなったんだっけ?」
という疑問が必ず発生します。

答えは過去のチケット(Backlogなど)にあるはずですが、検索しても大量の
「お世話になります」
「山本が担当させていただきます」
「確認します」
「お待ちください」
「了解です」
「ありがとうございます」
に埋もれて、肝心の結論に辿り着くのは大変です。

そこで今回は、完了した課題を収集し、kiro が理解しやすい形式で長期記憶にする仕組みを作ってみました。
Backlog の MCP サーバーも使えないか検討しましたが、600 件近くの完了課題があり、テキストにすると 10 万字を悠に超えたため、検索しきれませんでした。
そこで、課題 1 件ごとにテキストファイルに格納し、データベースとして使うことにしました。
そちらの方が毎回 API を実行するよりも経済的かつ速いというのもあります。

全体像:kiro 活用への道

やっていることはシンプルです。

  1. Pythonスクリプトで Backlog から「完了済み課題」を取得し、課題ごとに本文とコメントをテキストファイル化する
  2. テキストファイルを kiro が読みやすいようにする。kiro を使って「6W3H」形式などの要約データに変換する
  3. kiro がそれらのファイルを参照して質問に答えることができる状態にする。

これにより、「自然言語で過去の経緯を質問できる」環境が整います。きっと。

1. Pythonスクリプトで Backlog から「完了済み課題」を取得する (sync_backlog_issues.py)

Pythonスクリプトで Backlog から「完了済み課題」を取得します。
kiro の知識として使うには、常に最新の状態にしておきたいですが、毎回全件取得するのは時間的にも、API 使用の面でも無駄です。 ローカルにファイルがない課題だけを取りに行くように差分更新する Pythonスクリプトを作成しました。

他は・・

  • SPACE_DOMAIN: Backlog のドメインを入れてください。
  • PROJECT_KEY: Backlog のプロジェクトキーを入れてください。
import requests
import time
import os
import re

# ==========================================
# 設定エリア
# ==========================================
API_KEY = "xxxxx"  # BacklogのAPIキー
SPACE_DOMAIN = "xxxx.backlog.jp" # 例: nulab.backlog.jp
PROJECT_KEY = "ABC_PROJECT"
OUTPUT_DIR = "backlog_issues" # 保存先フォルダ名

# ステータスID (4: 完了)
TARGET_STATUS_ID = 4 
# ==========================================

BASE_URL = f"https://{SPACE_DOMAIN}/api/v2"

def get_existing_keys(directory):
    """
    保存先ディレクトリにあるファイル名から、既に取得済みの課題キーを抽出する
    """
    keys = set()
    if not os.path.exists(directory):
        return keys
    
    for filename in os.listdir(directory):
        if filename.endswith(".txt"):
            # 拡張子を除去して課題キーを取得 (例: KEY-123.txt -> KEY-123)
            key = os.path.splitext(filename)[0]
            keys.add(key)
    return keys

def get_issues(project_id, status_id):
    """
    課題一覧(メタデータ)を取得する
    """
    issues = []
    offset = 0
    count = 100
    
    print("Backlogから課題リスト(目録)を取得中...")
    
    while True:
        params = {
            "apiKey": API_KEY,
            "projectId[]": project_id,
            "statusId[]": status_id,
            "count": count,
            "offset": offset,
            "sort": "created",
        }
        
        try:
            response = requests.get(f"{BASE_URL}/issues", params=params)
            response.raise_for_status()
            data = response.json()
            
            if not data:
                break
                
            issues.extend(data)
            # print(f"... {len(issues)} 件のリストを取得") # ログが多いのでコメントアウト
            
            if len(data) < count:
                break
                
            offset += count
            time.sleep(1) # 一覧取得のペーシング
            
        except requests.exceptions.RequestException as e:
            print(f"一覧取得中にエラーが発生しました: {e}")
            break
            
    return issues

def get_comments(issue_id_or_key):
    """
    特定の課題のコメントを取得し、テキストとして結合して返す
    """
    params = {
        "apiKey": API_KEY,
        "count": 100,
    }
    
    try:
        response = requests.get(f"{BASE_URL}/issues/{issue_id_or_key}/comments", params=params)
        response.raise_for_status()
        comments_data = response.json()
        
        formatted_comments = []
        for c in comments_data:
            if c.get("content"):
                author = c.get("createdUser", {}).get("name", "Unknown")
                created = c.get("created", "")[:10]
                body = c.get("content", "")
                formatted_comments.append(f"[{created} {author}]\n{body}")
        
        return "\n\n---\n\n".join(formatted_comments)

    except requests.exceptions.RequestException:
        return "(コメント取得失敗)"

def save_to_file(filename, content):
    """
    ファイルに保存する
    """
    safe_filename = re.sub(r'[\\/*?:"<>|]', "_", filename)
    filepath = os.path.join(OUTPUT_DIR, safe_filename)
    
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(content)

def main():
    # 1. フォルダ作成と既存ファイルの確認
    if not os.path.exists(OUTPUT_DIR):
        os.makedirs(OUTPUT_DIR)
    
    existing_keys = get_existing_keys(OUTPUT_DIR)
    print(f"ローカルフォルダを確認: {len(existing_keys)} 件の課題が既に保存済みです。")

    # 2. プロジェクト情報の取得
    print(f"プロジェクト {PROJECT_KEY} の情報を確認中...")
    try:
        p_res = requests.get(f"{BASE_URL}/projects/{PROJECT_KEY}", params={"apiKey": API_KEY})
        p_res.raise_for_status()
        project_data = p_res.json()
        project_id = project_data["id"]
    except Exception as e:
        print(f"プロジェクト取得エラー: {e}")
        return

    # 3. 課題全取得(リストのみ)
    all_issues = get_issues(project_id, TARGET_STATUS_ID)
    print(f"Backlog上の完了課題総数: {len(all_issues)} 件")
    
    # 4. 未取得の課題をフィルタリング
    target_issues = [i for i in all_issues if i["issueKey"] not in existing_keys]
    
    if not target_issues:
        print("新しい課題はありません。全て取得済みです。")
        return

    print(f"未取得の {len(target_issues)} 件のダウンロードを開始します...")
    
    # 5. 詳細取得&ファイル保存(新規分のみ)
    total = len(target_issues)
    
    for i, issue in enumerate(target_issues):
        issue_key = issue["issueKey"]
        summary = issue["summary"]
        description = issue["description"] if issue["description"] else "(詳細なし)"
        
        print(f"[{i+1}/{total}] 新規取得: {issue_key} {summary[:20]}...", end="\r")
        
        comments_text = get_comments(issue_key)
        
        full_text = f"""【課題キー】
{issue_key}

【件名】
{summary}

【課題詳細】
{description}

【コメント履歴】
{comments_text}
"""
        filename = f"{issue_key}.txt"
        save_to_file(filename, full_text)
        
        # API負荷軽減
        time.sleep(1.0)

    print(f"\n\n処理完了! 新しく {len(target_issues)} 件のファイルを保存しました。")

if __name__ == "__main__":
    main()

実行すると backlog_issues のフォルダに課題ごとにテキストファイルができます。

更新がない場合

2. kiro のためのデータ整形

Python スクリプトで単に「件名」と「詳細」を保存するだけでは不十分です。
Backlog には挨拶や試行錯誤の過程(ノイズ)が多いからです。
そこで、最終的に保存するファイルは以下のような構成を目指します。Kiro にテキストファイルを読ませて生成させます。

ファイル例:PROJ-65.txt

【課題キー】
ABC_PROJECT-123

【6W3H分析】
■ What(何を)
ECS タスクが起動に失敗した際の通知の検討

■ Why(なぜ)
ECS タスクが起動に失敗した際にメトリクスが出ないことから

■ How(どのように)- 結論
EventBridge で通知する

---
■ オリジナル全文
(以下、生のログデータ)

この「6W3H分析」が冒頭にあることで、AIエージェントは「結論、どうしたのか」を瞬時に理解し、ユーザーに的確な回答を返せるようになります。

kiro に頼んでみた画面です。

kiro が Python で機械的にやろうとしたので、kiro の自然言語処理能力を頼りたいと伝えました。

私は途中で寝ました。

起きた時、kiro はエラーで止まっていました。「やってる?」と聞いてみたら、「続けています。」と返答があり、再開しました。

そして 12 時間後くらいに 640 ファイル程度の処理を終えました。
これは5 年におよぶ AWS 関連の QA のデータベースです。

3. kiro がそれらのファイルを参照して質問に答えることができる状態にする。

これを、kiro のカスタムエージェントに読み込ませます。 ~/.kiro/knowledge/ フォルダにコピーします

ちなみに、~/.kiro/knowledge/~/.kiro/steering/ の違いは以下のようです。

また、開いているワークスペースに .kiro/knowledge/ というフォルダを作成すると、そのワークスペースでのみ使用できます。

4. 使ってみる

ECS が起動に失敗した調査をしている課題を教えて

このように、「誰が」「どのチケットで」「どう解決したか」が即座に出てきます。

まとめ

過去の完了チケットは情報の宝庫とも言えます。 簡単なスクリプトでデータを吸い出し、AIが読みやすい形に整えてあげるだけで、検索性が上がります。 例えば、長く続いた案件の引き継ぎ資料にもなりえます。

山本 哲也 (記事一覧)

カスタマーサクセス部のインフラエンジニア。

山を走るのが趣味です。