🤖

GitHubで管理しているカスタムGem用のプロンプトを、Issueの内容に応じて自動で修正させる

に公開

1. 始めに

前回の記事に書いた通り私は自創作キャラのカスタムGemを作って公開しているのですが、実際に会話していると設定と違ったり不自然な会話が生成されることがよくあります。
例えば自動車免許を持っていないのに車を運転していたり(そもそも17歳なので藤◯拓海でもないと無理)、深夜に学校の部室にいたり……。
そうなったら逐一プロンプトを修正して調整すればいい話なのですが、これをAIに任せて楽をしたいなと思いました。

前提

  • GeminiのカスタムGemプロンプトをGitHubのリポジトリで管理している。
  • プロンプトの改善案があれば、メモ代わりにIssueを立てる

やりたいこと

Issueの内容をもとに、プロンプトの改善案(リポジトリ内にあるプロンプトを加筆修正したもの)を自動的に生成し、それを自動でプルリクエストするようにしてほしい。

2. AIに相談だ

ChatGPTに相談したところ、以下の提案とともにコードが返ってきました。

  1. 新しいIssueが作成される
  2. GitHub Actionsがトリガーされる(issuesイベント)
  3. Issue本文をGeminiに送信し、改善案を生成
  4. 対象のプロンプトファイルを自動編集
  5. 新しいブランチを切って変更をコミット
  6. 自動でPull Requestを作成

ところが……生成されたコードを何度試してもエラーが出てしまい、結局Claudeに修正をお願いしてようやく動くコードが返ってきました。ありがとうClaude、餅は餅屋だね。

3. 手順

  1. Gemini APIキーを発行(無料枠で大丈夫そう)
  2. GitHub Actions SecretsにAPIキーを登録
    • GEMINI_API_KEY:Gemini APIキー
  3. リポジトリ内に以下のファイルを設置
scripts/generate_prompt_update.js
import fetch from "node-fetch";
import fs from "fs";

const apiKey = process.env.GEMINI_API_KEY;
const issueBody = process.env.ISSUE_BODY || "";
const promptPath = process.env.PROMPT_PATH || "GEMINI.md";

if (!apiKey) {
  console.error("GEMINI_API_KEYが設定されていません。");
  process.exit(1);
}

if (!issueBody.trim()) {
  console.error("ISSUE_BODYが空です。");
  process.exit(1);
}

async function generateUpdatedPrompt() {
  // 既存のプロンプトファイルを読み込む
  let currentPrompt = "";
  try {
    currentPrompt = fs.readFileSync(promptPath, "utf8");
  } catch (err) {
    console.error(`${promptPath}の読み込みに失敗しました:`, err);
    process.exit(1);
  }

  const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${apiKey}`;

  const requestBody = {
    contents: [
      {
        parts: [
          {
            text: `あなたはキャラクターAI用プロンプトの改善を行うアシスタントです。

以下は現在のプロンプトファイル(GEMINI.md)の内容です:

\`\`\`
${currentPrompt}
\`\`\`

以下はユーザーから提案された改善要求です:

\`\`\`
${issueBody}
\`\`\`

上記の改善要求に基づいて、プロンプトファイルを更新してください。
以下の点に注意してください:

1. 改善要求の内容を適切に反映すること
2. 既存のプロンプトの構造や書式を維持すること
3. 矛盾する内容がないか確認すること
4. キャラクター設定の一貫性を保つこと

更新後のプロンプトファイル全体を出力してください。説明や前置きは不要で、プロンプトの内容のみを出力してください。`,
          },
        ],
      },
    ],
    generationConfig: {
      temperature: 0.7,
      maxOutputTokens: 8192,
    },
  };

  try {
    console.log("Gemini APIにリクエスト送信中...");
    const res = await fetch(endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(requestBody),
    });

    if (!res.ok) {
      const text = await res.text();
      console.error(`APIレスポンス: ${text}`);
      throw new Error(`APIエラー: ${res.status} ${res.statusText}`);
    }

    const data = await res.json();
    console.log("APIレスポンス受信完了");

    // レスポンス構造を確認
    if (!data.candidates || data.candidates.length === 0) {
      console.error("APIレスポンス:", JSON.stringify(data, null, 2));
      throw new Error("candidates が空またはundefinedです");
    }

    const candidate = data.candidates[0];
    if (!candidate.content || !candidate.content.parts || candidate.content.parts.length === 0) {
      console.error("候補データ:", JSON.stringify(candidate, null, 2));
      throw new Error("生成されたコンテンツが見つかりません");
    }

    let updatedPrompt = candidate.content.parts[0].text;

    if (!updatedPrompt || updatedPrompt.trim() === "") {
      throw new Error("生成されたプロンプトが空です");
    }

    // マークダウンのコードブロックで囲まれている場合は除去
    updatedPrompt = updatedPrompt.replace(/^```markdown?\n?/i, "").replace(/\n?```$/, "");

    // ファイルに書き込み
    fs.writeFileSync(promptPath, updatedPrompt.trim() + "\n", "utf8");
    console.log(`✅ プロンプトを${promptPath}に更新しました。`);
  } catch (err) {
    console.error("❌ エラーが発生しました:", err.message);
    if (err.stack) {
      console.error(err.stack);
    }
    process.exit(1);
  }
}

generateUpdatedPrompt();
.github/workflows/auto_pr.yml
name: Auto Prompt Update

on:
  issues:
    types: [labeled]

jobs:
  generate-pr:
    runs-on: ubuntu-latest
    # 条件: "prompt-update"ラベルが付いている、かつラベルを付けた人が管理者
    if: |
      github.event.label.name == 'prompt-update' &&
      github.event.issue.state == 'open' &&
      (github.actor == github.repository_owner || 
       contains(fromJson('["chisamikan"]'), github.actor))

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm install node-fetch@3

      # Secrets デバッグ用ステップ(値は表示せず存在確認のみ)
      - name: Debug GEMINI_API_KEY
        run: |
          if [ -z "$GEMINI_API_KEY" ]; then
            echo "GEMINI_API_KEY is empty"
            exit 1
          else
            echo "GEMINI_API_KEY is set"
          fi
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}

      - name: Generate updated prompt
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          PROMPT_PATH: "GEMINI.md"
        run: node scripts/generate_prompt_update.js

      - name: Create Pull Request
        id: create-pr
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "Auto update prompt based on Issue #${{ github.event.issue.number }}"
          title: "Prompt Update: Issue #${{ github.event.issue.number }}"
          body: |
            このプルリクエストは Issue #${{ github.event.issue.number }} に基づいて自動生成されました。

            ## 変更内容
            Issueの内容に基づいてGEMINI.mdを更新しました。

            ## 確認事項
            - [ ] プロンプトの内容が意図通りに更新されているか
            - [ ] キャラクター設定に矛盾がないか
            - [ ] 既存の設定が失われていないか

            関連Issue: #${{ github.event.issue.number }}
          branch: "update-prompt-${{ github.event.issue.number }}"
          delete-branch: true
          committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
          author: "${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>"

      - name: Comment on Issue
        if: steps.create-pr.outputs.pull-request-number
        uses: actions/github-script@v7
        with:
          script: |
            const prNumber = '${{ steps.create-pr.outputs.pull-request-number }}';
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `✅ プロンプト更新のPRを自動生成しました: #${prNumber}\n\n内容を確認してマージしてください。`
            })

GEMINI.mdは元のプロンプトのファイル名です。ファイルパスやファイル名は適宜変更してください。
※chisamikanになっているところは、ご自身のGitHubのユーザーIDを入力してください。

  1. GitHubのリポジトリでIssue管理用のラベルを作成(例:prompt-update
  2. Issueを立てて、プロンプトの変更内容を本文内に書く。
  3. 管理者(Owner)がIssueにprompt-updateラベルを貼ると、GitHub Actionsが実行される。
    ※管理者以外の第三者がラベルを貼っても実行されない。
  4. 修正されたプロンプトと共にプルリクが立つので、内容を確認してマージ。
  5. GeminiのカスタムGem管理画面にプロンプトをコピペ。

4. 最後に

実はバイブコーディングが初めてだったのですが、なんとかできました……。
非エンジニアの自分でもここまでできるのは凄いですね。
これで当初の目的が達成されるのかは謎ですが、しばらくこれで回してみます。

Discussion