LLMとGitHub Actionsを活用したテクニカルドキュメントの自動化

AI tutorial - IT technology blog
AI tutorial - IT technology blog

午前2時のドキュメント危機

火曜日の午前2時、ついに本番環境へのホットフィックスがリリースされました。競合状態(レースコンディション)を解決するために、重要な認証モジュールをリファクタリングし、3つの主要なAPIエンドポイントを変更しました。コードは完璧に動作していました。しかし、午前9時までにフロントエンドチームから14件の未読Slackメッセージが届いていました。苦情の内容はどれも一貫していました。「APIが404を返す」「READMEに古い認証ヘッダーが載ったままだ」「このリリースの変更履歴(チェンジログ)はどこにあるんだ?」

その時、私は気づきました。コードのためのCI/CDパイプラインは最新鋭だったものの、ドキュメント作成は実質的に手動だったのです。Markdownファイルの更新を開発者の記憶に頼っていました。プレッシャーのかかるデプロイ作業において、ドキュメントは常に真っ先に犠牲になります。この「ドキュメントの形骸化(Documentation rot)」は、単に同僚を困らせるだけでなく、統合バグを引き起こし、エンジニアリング組織全体のスピードを低下させます。

根本原因:なぜドキュメントは常に古くなるのか

その件の後、なぜ私たちのプロセスが失敗したのかを分析しました。問題は怠慢ではなく、従来のワークフローに内在する摩擦にありました。

  • 認知負荷: 複雑な機能を完成させた後、開発者が「ロジック・モード」から「テクニカルライティング・モード」に切り替えるのは困難です。
  • 手動の同期: auth.py の関数シグネチャを変更すると、README.mdAPI_DOCS.md 全体を対象に手動で検索・置換を行う必要があります。
  • バリデーションの欠如: Pre-commitフックは構文エラーをチェックできますが、英語の段落がGoやPythonのロジック変更を正確に反映しているかどうかまでは検証できません。

JSDocやDoxygenのようなツールはある程度の助けになりますが、出力される内容は無味乾燥でロボットのようなものになりがちです。変更の背後にある「なぜ(理由)」が抜け落ちてしまいます。さらに、これらのツールは、スピード感のあるチームがスプリント中に維持するのが難しいレベルのコメント規律を要求します。

アプローチの比較:手動 vs 従来型 vs LLM

カスタムソリューションを構築する前に、テクニカルライティングを処理する3つの主要な方法を評価しました。

  1. 手動更新: 正しく行われれば正確ですが、信頼性はゼロで、摩擦が非常に大きいです。
  2. 静的ジェネレーター(Swagger/Doxygen): 構造に関しては信頼できます。残念ながら、ハイレベルな説明は苦手で、結局のところ開発者がコード「内」にドキュメントを書く必要があります。
  3. LLMによる自動化: 大規模言語モデル(LLM)を使用してコードの差分(diff)を分析し、人間が読みやすいドキュメントを生成します。構文だけでなく、変更の意図を汲み取ることができます。

選択は明白でした。GitHubのワークフローにLLMを統合することで、ドキュメント作成の退屈な部分を自動化しつつ、高品質で読みやすいコンテンツを維持できるのです。

解決策:GitHub Actions + LLMパイプライン

最も効果的な戦略は、プルリクエストがmainブランチにマージされるたびにドキュメントの同期を実行することです。私はこれを本番環境に導入してきましたが、結果は一貫して安定しています。パイプラインのロジックは単純です。変更を検出し、コンテキストを抽出し、LLMにプロンプトを送り、ファイルを更新して、変更をコミットします。

ステップ1:GitHub Actionの設定

mainブランチへのプッシュ時にトリガーされるワークフローが必要です。このワークフローには、リポジトリへのアクセス権限と、選択したLLM(OpenAI、Claude、Geminiなど)のAPIキーが必要です。

name: 自動ドキュメント生成

on:
  push:
    branches:
      - main

jobs:
  update-docs:
    runs-on: ubuntu-latest
    steps:
      - name: コードをチェックアウト
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Pythonのセットアップ
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: 依存関係のインストール
        run: |
          pip install openai gitpython

      - name: ドキュメント生成スクリプトの実行
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: python scripts/generate_docs.py

      - name: 変更をコミットしてプッシュ
        run: |
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@users.noreply.github.com'
          git add README.md API_DOCS.md CHANGELOG.md
          git commit -m "docs: LLMパイプラインによる自動更新 [skip ci]" || echo "コミットする変更はありません"
          git push

ステップ2:コアロジック・スクリプト

このPythonスクリプトがエンジンとして機能します。最新のコミットでの変更を特定し、関連するファイルを更新するようLLMに指示します。GitPython を使用することで、現在のコミットとその前のコミットの正確な差分を抽出できます。

import os
from openai import OpenAI
from git import Repo

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_code_diff():
    # コードの差分を取得する
    repo = Repo(".")
    diff = repo.git.diff('HEAD~1', 'HEAD')
    return diff

def update_file(filename, prompt_context):
    with open(filename, "r") as f:
        current_content = f.read()

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": f"あなたはテクニカルライターです。コードの変更に基づいて {filename} を更新してください。既存のトーンとフォーマットを維持してください。"},
            {"role": "user", "content": f"現在の {filename}:\n{current_content}\n\nコードの変更点:\n{prompt_context}"}
        ]
    )
    return response.choices[0].message.content

def main():
    diff = get_code_diff()
    if not diff:
        return

    # READMEと変更履歴を更新する
    for doc_file in ["README.md", "CHANGELOG.md"]:
        updated_content = update_file(doc_file, diff)
        with open(doc_file, "w") as f:
            f.write(updated_content)

if __name__ == "__main__":
    main()

ステップ3:信頼性の高いプロンプトエンジニアリング

ドキュメントの品質は、プロンプトの内容に完全に依存します。単にLLMに「ドキュメントを更新して」と頼むだけでは、ハルシネーション(もっともらしい嘘)が発生したり、重要なセクションが削除されたりする可能性があります。具体性が不可欠です。CHANGELOG.md については、「Keep a Changelog」標準(Added、Changed、Deprecated、Removed、Fixed)を遵守させるプロンプトを使用しています。

既存のファイル内容をLLMに提供することは、譲れないステップです。このコンテキストがあることで、モデルは一貫したトーンとフォーマットを維持できます。これがないと、ドキュメントはいずれ、異なる執筆スタイルが継ぎ接ぎされたバラバラなものになってしまうでしょう。

エッジケースへの対応と信頼性

初期の導入では、よく2つの障害にぶつかります。1つ目は、膨大な差分がGPT-4oなどのモデルの128kトークン制限を超えてしまうことです。これを解決するために、APIにデータを送信する前に、 .css.svg 、ロックファイルなどの無関係なファイルを除外するようにスクリプトを修正しました。

2つ目は、「無限ループ」を避けることです。GitHub Actionが変更をリポジトリにコミットするため、理論上は自分自身を無限にトリガーし続ける可能性があります。コミットメッセージに [skip ci] を追加することは、この再帰を防ぐための業界標準の方法です。

結果:コードと共に進化するドキュメント

この自動化パイプラインに移行して以来、午前9時のSlackパニックは解消されました。開発者がPRをマージすると、ドキュメントは約90秒以内に更新されます。精度は驚くほど高いです。これは、LLMが開発者の「書こうとした意図」ではなく、「実際のソースコード」を分析するためです。

ドキュメント作成は、スプリントの終わりに恐れるような雑用ではなくなりました。開発プロセスの自然な副産物となったのです。ドキュメントの乖離に疲れているなら、重労働をLLMに任せることは、あらゆるエンジニアリングチームにとって実用的で効果の高い解決策となります。

Share: