Uncategorized

【月600円】誤字脱字をAIで自動校正・ぽちぽち修正できる仕組み作った

先日、社長が外部から「サイトに誤字脱字がある」と指摘を受けました。確認してみると確かにいくつか残っていて、「再発防止の対策を入れてほしい」との指示が。

対策はいくつか考えましたが、その一つとして「コードに誤字脱字が混入しにくくなる仕組み」の導入を進めることにしました。MRの差分だけGemini APIに投げて自動検出するCIジョブです。まだ本番導入前ですが、仕組みが動くところまでできたので整理しておきます。

全体の流れはこんな感じです。

  1. MR 作成/更新
  2. GitLab CI パイプライン起動
  3. git diff で追加・変更行のみ抽出
  4. 対象ファイル (.html, .php 等) をフィルタ
  5. Gemini 2.5 Flash に差分テキストだけ送信
  6. JSON で修正提案を受け取る
  7. GitLab API で MR にインラインコメント投稿

設計方針

外部ライブラリへの依存はゼロにして、Pythonの標準ライブラリだけで完結させています。CI環境で pip install を走らせたくないし、依存が増えるとメンテコストも上がります。urllib.request でHTTPを叩くのは多少泥臭いですが、まあリクエスト数本程度なので十分です。

もうひとつ大事にしたのは「差分だけ送る」設計です。リポジトリ全体を送るのはトークンの無駄ですし、セキュリティ的にも良くない。git diff から追加行だけ抜き出して、ファイル名・行番号付きでGeminiに渡します。

result = subprocess.run(
    ["git", "diff", f"{base_sha}..{head_sha}",
     "--unified=0", "--diff-filter=ACMR"],
    capture_output=True, text=True, check=False,
)

パースした差分は === ファイルパス === + L行番号: 内容 という形に整形して送っています。

def build_diff_text(entries: list[dict]) -> str:
    if not entries:
        return ""

    grouped: dict[str, list] = {}
    for e in entries:
        grouped.setdefault(e["file"], []).append(e)

    lines = []
    for filepath, file_entries in grouped.items():
        lines.append(f"=== {filepath} ===")
        for e in file_entries:
            lines.append(f"L{e['line']}: {e['content']}")
        lines.append("")
    return "\n".join(lines)

処理の流れ

CIの設定はこれだけです。python:3.12-slimgit を入れてスクリプトを叩くだけ。

ai-typo-review:
  stage: review
  image: python:3.12-slim
  variables:
    GIT_DEPTH: 0
    REVIEW_TARGET_EXTENSIONS: ".html,.php,.blade.php,.vue,.jsx,.tsx,.md,.txt"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $TYPOS_REVIEW_WHEN == "manual"
      when: manual
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  before_script:
    - apt-get update
    - apt-get install -y --no-install-recommends git
    - rm -rf /var/lib/apt/lists/*
  script:
    - python scripts/review_typos.py
  allow_failure: true

GeminiのレスポンスはJSON配列で返してもらい、ファイルパス・行番号・元テキスト・修正案・理由をセットで受け取ります。出力フォーマットはプロンプト内で厳密に定義していて、「問題がなければ空配列 [] を返せ」と明示しています。これで無理やり指摘をひねり出す挙動がかなり減りました。

最後にGitLab APIでインラインコメントとして投稿します。该当行にそのままコメントが付くのに加えて、suggestionブロックも生成しているので、ワンクリックで修正を適用することもできます。

修正したい誤字をぽちぽちするだけ。

最後にコミットメッセージを書いて終わり

工夫したところ

コストと実用性を両立させるために、いくつか工夫を入れています。

差分テキストの送信量を最小限に

まず対象ファイルを拡張子でフィルタリングして、テンプレート系(.html, .php, .vue など)だけに絞っています。Pythonコードに「漢字の使い方がおかしい」とか言われても困りますからね。

さらに送信量が大きいMR向けに MAX_DIFF_CHARS(デフォルト12,000文字)で自動カットしています。

if len(diff_text) > MAX_DIFF_CHARS:
    diff_text = diff_text[:MAX_DIFF_CHARS] + "\n... (truncated)"

インクリメンタル差分と重複抑止

MRを更新するたびにパイプラインが走るので、何も考えないと同じ指摘が何回も投稿されます。これは邪魔。

対策として、各コメントにSHA1ベースの内部キーをHTMLコメントとして埋め込み、投稿前に既存キーと突き合わせています。

def make_suggestion_key(suggestion: dict) -> str:
    key_payload = {
        "file": suggestion["file"],
        "line": suggestion["line"],
        "original": suggestion["original"],
        "suggested": suggestion["suggested"],
    }
    serialized = json.dumps(key_payload, ensure_ascii=False, sort_keys=True)
    digest = hashlib.sha1(serialized.encode("utf-8")).hexdigest()[:16]
    return f"{suggestion['file']}:{suggestion['line']}:{digest}"

加えて、前回レビュー時のコミットSHAをMRノートに記録しておくことで、次回は増分だけをGeminiに送るインクリメンタルモードも実装しました。二重チェックを避けつつAPI消費も最小限にできます。

プロンプトのfalse positive対策

実はこれを作る前に、同じ誤字検出の仕組みをWordPress用プラグインとして先に作っていました。そこで育てたプロンプトをGitLab CI向けに移植・調整したのが今回のベースになっています(WPプラグインの話はまた別の機会に)。

移植してきただけでは当然そのままでは使えなくて、特に日本語まわりの調整が必要でした。ひらがな・カタカナの揺れについて、「こと」「もの」「できる」「ください」など文脈でひらがなが自然な語を無理に漢字に直す提案はしないように、と書いたのが効きました。CSSクラス名を「英語として間違っている」と指摘してくる問題もルールの明示で抑え込めています。

コスト感

現状はAI Studioの無料枠で運用しています。モデルはGemini 2.5 Flashで、差分だけ送る設計のおかげで1回のMRレビューで使うトークンは数千程度です。

仮に無料枠を超えて課金が発生したとしても、ざっくり月600円程度の計算。誤字がそのまま本番に出るリスクと天秤にかければ、十分ペイする金額だと思います。

それでもコストを抑えたい場合は TYPOS_REVIEW_WHEN=manual を設定すれば手動トリガーのみになります。

# CI/CD Variables で設定
TYPOS_REVIEW_WHEN: "manual"

今後の展望と振り返り

実装を通して感じたのは、AIを使った自動化は「何を送って何を受け取るか」の設計が8割だということです。プロンプト設計とJSONパースの防御的実装にほとんどの時間を使いました。

標準ライブラリだけで書いたのは正解で、新しいリポジトリへの導入がスクリプト1ファイル + CI設定のコピーだけで完結します。

今後はまずチームへの正式導入を進めつつ、運用しながらプロンプトを継続的に調整していく予定です。さらに、ゆくゆくはスケジュール実行やSlack通知など、より自律的に動くボット化も検討しています。そしてこの仕組みの原型になったWPプラグインについても、いずれ記事にまとめられればと思っています。

おすすめ記事

Recommend