APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

AIのルール遵守は確率的、hooksは決定論的 — ハーネスの2つのレイヤー

こんにちは。ACS事業部の越川です。

AIエージェントと一緒に技術ブログを毎日書いています。今月だけで20本の記事を公開する中で、過去のミスを繰り返さないためにCLAUDE.md(AIとの合意文書)にルールを足し続けてきました。

気づけば181行。フィードバックから学び、ルールを昇格させ、丁寧に育ててきた自負があります。

それでも、ルールは破られました。

ルール 理由
太字の中にカギ括弧を入れない はてなブログでレンダリングが崩れる
Qiitaの$記号は\$にエスケープする TeX数式記法と衝突する

どちらもCLAUDE.mdに明記してある。なのに、何度か抜けました。

なぜ、書いてあるルールが守られないのか。この疑問が、本記事の出発点です。

CLAUDE.mdは「お願い」である

Claude Codeの公式ドキュメントにはこう書かれています。

CLAUDE.mdファイルあたり200行以下を目標にします。より長いファイルはより多くのコンテキストを消費し、遵守を減らします

「遵守を減らす」。設定ファイルであれば「無視される」とは書かないはずです。CLAUDE.mdは強制的な設定ではなく、AIがコンテキストとして読み込む文書です。書いたルールが100%守られる保証はありません。AIがそのルールに注意を払えるかどうかは、コンテキストの量やセッションの状態に依存します。

外部の検証データもこれを裏付けています。SFEIR Instituteの調査では、200行のインストラクションで遵守率92%、400行を超えると71%まで低下するという結果が報告されています。

遵守率92%は高い数字です。しかし、8%の「守られない」が、どのルールで発生するかは予測できません。ニッチな書式ルールが抜けるのか、コンプライアンス上の人名露出が抜けるのか。確率的だからこそ、重要なルールほど別の仕組みで担保する必要があります。

ガードレールも確率的である

CLAUDE.mdの補完として、Gemini Gemでガードレールを作りました。ロジック、ファクト、コンプライアンス、セキュリティなど10段階のチェックポイントで記事をスキャンする仕組みです。

これは強力でした。他部門が自発的に採用し、100枚の画像を20分でチェックした事例もあります。

しかし、ガードレールもまた確率的です。AIがAIを評価する構造なので、評価する側のAIも注意力やコンテキストに依存します。実際に、ガードレールAIが「Apple M5チップは未発売」と警告した事例がありました。M5 MacBook Proは公式に販売されている製品です。これはガードレールAI自身の知識カットオフによる誤検知でした。

確率的なものをいくら重ねても、確率的なままです。

CLAUDE.md(確率的)→ Gemini Gem(確率的)。2層にすることで精度は上がりますが、構造は変わりません。

確率的レイヤーの3つの穴

ここで、確率的レイヤーだけでは防げなかったものを整理します。

穴1: コンテキスト圧縮で消える

Claude Codeは長時間セッションでコンテキストウィンドウが逼迫すると、自動的にコンテキストの圧縮(compaction)を行います。このとき、CLAUDE.mdの内容は要約されます。

「太字の中にカギ括弧を入れない」のような細かいルールは、圧縮の過程で真っ先に落ちます。AIにとって、これらは「プロジェクトの本質」よりも優先度が低い情報だからです。

圧縮後のAIは、プロジェクトの目的や大まかな方針は覚えています。しかし、181行の中に埋もれていた細かな禁止事項は、もう見えていません。

穴2: モデルの注意力に依存する

181行のルールを全て同時に意識し続けるのは、人間でもAIでも難しい。

特に、AIが複数のタスクを並行して処理しているとき——記事の構成を考えながら、表現をチェックし、コンプライアンスにも配慮する——個々のルールへの注意力は分散します。

ルールを都度足していく運用では、この問題が顕著でした。足すたびにコンテキストの負荷が上がり、既存のルールへの注意力が薄れる。CLAUDE.mdが育つほど、個々のルールが守られにくくなるパラドックスです。

穴3: 人間のうっかりは防げない

CLAUDE.mdもGemini Gemも、AIの振る舞いを制御するための仕組みです。しかし、ブログ執筆のワークフローには人間側のチェック漏れもあります。

  • エビデンスを記録するreferences.mdを作り忘れた
  • フッターテンプレートを末尾に配置し忘れた
  • ideas.md(ネタ帳)のステータスを更新し忘れた

これらはAIではなく人間の行動の問題です。CLAUDE.mdにいくら書いても、セッションを終了しようとする人間を止めることはできません。

決定論的レイヤー — Claude Code hooks

ここで発想を変えました。

確率的レイヤーの精度を上げるのではなく、確率に依存しないレイヤーを別に作る

Claude Code hooksは、AIがツールを実行する前後やセッションの節目で、シェルスクリプトを自動実行する仕組みです。正規表現やファイル存在確認で判定するため、モデルの状態に関係なく同じ結果を返します。注意力に依存しません。コンテキスト圧縮の影響も受けません。100回実行すれば100回同じ結果です。

なお、同様のhooks機構はGitHub Copilot CLI、Cursor(v1.7〜)、Windsurf(Cascade Hooks)にも搭載されています。いずれも exit 2 でAIの操作をブロックできる共通パターンです。お使いのツールに読み替えてください。

実装した5つのhook

筆者が実装した5つのhookを紹介します(記事中ではコードを2つ掲載し、残り3つは概要のみ記載します)。

hook イベント 何を防ぐか 判定方法
lint-article.sh PreToolUse(Write/Edit前) 太字+カギ括弧、[:contents]位置、Mermaid誤用(はてな)、Privateリポジトリリンク(Qiita)、役職表記、拡張子+括弧のリンク誤認、技術名の略称 正規表現 + ホワイトリスト
guard-commit.sh PreToolUse(git commit前) Co-Authored-By混入、機密ファイルのステージング grep + git diff
check-qiita-dollar.sh PostToolUse(Write/Edit後) Qiita記事の\$未エスケープ awk + 正規表現
stop-checklist.sh Stop(セッション終了前) references.md 未作成、フッター未配置、ideas.md 未更新 ファイル存在確認 + grep
post-compact-remind.sh SessionStart(圧縮後) コンテキスト圧縮による必須ルールの消失 固定テキスト再注入

5つのhookと settings.json の全ソースコードを公開しています。ぜひ手元で動かして、自分のプロジェクトに合わせてカスタマイズしてみてください。

github.com

hookの設定

.claude/settings.json に以下のように定義します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-article.sh",
            "timeout": 10,
            "statusMessage": "記事リンター実行中..."
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-checklist.sh",
            "timeout": 15,
            "statusMessage": "記事チェックリスト確認中..."
          }
        ]
      }
    ]
  }
}

matcher でどのツールに反応するかを指定し、type: "command" でシェルスクリプトを実行します。hookはstdinでJSON(ツール名、引数、ファイルパス等)を受け取り、exit 2で操作をブロックできます。

コード例1: lint-article.sh(AIの手前で止める)

記事ファイルへの書き込み前に実行され、パターンマッチで検出可能なルール違反をブロックします。

#!/bin/bash
# PreToolUse (Write|Edit) — article.md への書き込み前にルール違反を検出

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# article.md 以外は無視
if [[ "$FILE_PATH" != *"/article.md" ]]; then
  exit 0
fi

# Write の場合は content、Edit の場合は new_string を検査
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty')
if [ -z "$CONTENT" ]; then
  exit 0
fi

# コードブロック外のテキストのみを検査対象にする
PROSE=$(echo "$CONTENT" | awk '/^```/{in_code=!in_code; next} !in_code{print}')

# Qiita記事かはてな記事かを判定
IS_QIITA=false
if echo "$CONTENT" | grep -q '@m_koshikawa'; then
  IS_QIITA=true
fi

ERRORS=""

# === はてなブログ固有のルール ===
if [ "$IS_QIITA" = false ]; then
  # 太字の中にカギ括弧(レンダリング崩壊)
  if echo "$PROSE" | grep -qE '\*\*[^*]*[「」][^*]*\*\*'; then
    ERRORS="${ERRORS}\n- 太字+カギ括弧: はてなブログでレンダリングが崩れます"
  fi
  # Mermaid記法(はてなブログ非対応)
  if echo "$CONTENT" | grep -q '```mermaid'; then
    ERRORS="${ERRORS}\n- Mermaid記法: はてなブログでは使えません"
  fi
fi

# === Qiita固有のルール ===
if [ "$IS_QIITA" = true ]; then
  # Privateリポジトリへのリンク(ホワイトリスト方式)
  GITHUB_URLS=$(echo "$PROSE" | grep -oE 'github\.com/[A-Za-z0-9_.-]+/' \
    | grep -vE 'github\.com/apc-m-koshikawa/')
  if [ -n "$GITHUB_URLS" ]; then
    ERRORS="${ERRORS}\n- 非公開リポジトリの可能性: 個人の公開リポジトリ以外のURLです"
  fi
fi

# === 共通ルール ===
# 役職表記の露出
if echo "$PROSE" | grep -qE '(部長|室長|マネージャー|リーダー|課長|上司|部下)'; then
  ERRORS="${ERRORS}\n- 役職表記: 公開記事では「メンバー」と表記してください"
fi
# 拡張子+全角括弧でリンク誤認
if echo "$PROSE" | grep -qE '\.\w+('; then
  ERRORS="${ERRORS}\n- ファイル名+括弧: バッククォートで囲んでください"
fi

if [ -n "$ERRORS" ]; then
  printf "記事リンターが問題を検出しました:%b" "$ERRORS" >&2
  exit 2  # ← これで書き込みがブロックされる
fi

exit 0

exit 2 がポイントです。このコードが返した瞬間、AIのWrite/Edit操作はブロックされます。AIは理由を読み、修正してから再試行します。モデルが「うっかり」することは物理的にできなくなります。

コード例2: stop-checklist.sh(人間のうっかりを止める)

セッション終了前に実行され、記事執筆のやり残しを検出します。

#!/bin/bash
# Stop — 記事執筆セッションの終了前チェック

INPUT=$(cat)

# 無限ループ防止
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

CWD=$(echo "$INPUT" | jq -r '.cwd')

# 直近30分以内に変更のある article.md を探す
RECENT_ARTICLE=$(find "$CWD" -name "article.md" -mmin -30 -type f 2>/dev/null | head -1)
if [ -z "$RECENT_ARTICLE" ]; then
  exit 0  # 記事執筆セッションではない
fi

ARTICLE_DIR=$(dirname "$RECENT_ARTICLE")
FOLDER_NAME=$(basename "$ARTICLE_DIR")

# 公開済み記事はスキップ
if grep -q "$FOLDER_NAME" "$CWD/published.md" 2>/dev/null; then
  exit 0
fi

# Qiita記事かはてな記事かを判定
IS_QIITA=false
if grep -q '@m_koshikawa' "$RECENT_ARTICLE" 2>/dev/null; then
  IS_QIITA=true
fi

CHECKLIST=""

# 1. references.md が存在するか
if [ ! -f "$ARTICLE_DIR/references.md" ]; then
  CHECKLIST="${CHECKLIST}\n- [ ] references.md が未作成です"
fi

# 2. フッターが含まれているか(はてなブログのみ)
if [ "$IS_QIITA" = false ]; then
  if ! grep -q 'お知らせ' "$RECENT_ARTICLE" 2>/dev/null; then
    CHECKLIST="${CHECKLIST}\n- [ ] フッターが未配置です"
  fi
fi

if [ -n "$CHECKLIST" ]; then
  REASON=$(printf "やり残しチェック:%b" "$CHECKLIST" | sed 's/"/\\"/g' | tr '\n' ' ')
  echo "{\"decision\": \"block\", \"reason\": \"${REASON}\"}"
fi

exit 0

"decision": "block" を返すと、AIはセッションを終了できません。やり残しがあることをユーザーに伝え、対応を促します。

このhookは、導入した直後に実際に発火しました。Qiita記事に対して「フッターが未配置です」と警告したのです。Qiita記事にはフッターは不要なので、これは誤検知でした。すぐにQiita/はてなの判別ロジックを追加して修正しました。

さらに、この記事自体を書いている最中にもhookが発火しました。記事中のコード例に「太字+カギ括弧」のパターンが含まれていたため、lint-article.shがブロックしたのです。コードブロック内はチェック対象外にすべきだったので、即座にawkでコードブロックを除外するロジックを追加しました。

hooksが、hooksの改善を促している。 使ってみて穴が見え、直すことでhooks自体が育つ。これはCLAUDE.mdのlessons-learned → ルール昇格と同じフィードバックループです。

確率的 × 決定論的 = ハーネス

ここで重要なのは、決定論的レイヤーだけでも不十分だということです。

正規表現では「この記事は読者にとって価値があるか」は判定できません。「一次情報が含まれているか」「論理が飛躍していないか」は、文脈を理解できるAIの仕事です。

逆に、「太字の中にカギ括弧がないか」は正規表現の方が確実です。AIに任せる意味がありません。

ルールの性質によって、適切なレイヤーが異なります。

ルールの性質 適切なレイヤー
パターンマッチで判定可能 決定論的(hooks) 太字+括弧、\$エスケープ、Co-Authored-By
文脈理解が必要 確率的(CLAUDE.md 読者視点、一次情報の判断、トーン
主観的品質判断 確率的(Gemini Gem) 記事全体の価値、構成の良し悪し
人間の行動チェック 決定論的(Stop hook) ファイル存在確認、ステータス更新

AnthropicのPrithvi Rajasekaranは、GANs(敵対的生成ネットワーク)に着想を得た「Generator + Evaluator」構造を提唱しています。生成と評価を分離し、Evaluatorがジェネレーターの出力を検証するアーキテクチャです。

筆者の実装では、Evaluatorが2層になっています。確率的Evaluator(Gemini Gem)が文脈依存の品質を評価し、決定論的Evaluator(hooks)がパターンマッチ可能なルール違反をブロックする。両方あって初めて、ハーネスが完成します。

Hashimotoの思想、2つの実装

Mitchell Hashimotoは「My AI Adoption Journey」で、こう述べています。

エージェントがミスしたら、二度と繰り返さない仕組みを作る

筆者がこれまでやってきたことは、まさにこれでした。ミスが起きたらlessons-learned.mdに記録し、CLAUDE.mdに昇格させる。AIが「学習」するサイクルです。

hooksの導入で、同じ思想にもう1つの実装が加わりました。

  • CLAUDE.md = 学習(ミスをルールとして言語化し、AIのコンテキストに注入する)
  • hooks = 防止(ルール違反を物理的にブロックし、そもそも通さない)

学習と防止は補完関係にあります。CLAUDE.mdで「なぜこのルールがあるのか」を理解させ、hooksで「仮に忘れても通さない」ようにする。前者がなければAIは理由を理解しないまま制限され、後者がなければ理解していても忘れることがある。

おわりに — ブログの外にも同じ構造がある

5つのシェルスクリプトで始められます。大げさなフレームワークは要りません。

本記事はブログ執筆の品質管理を題材にしましたが、ここまで読んでくださった方は気づいているかもしれません。この「確率的 × 決定論的」の振り分けは、AIと協働するあらゆるドキュメント作成に当てはまります。

ドメイン 決定論的に防げること 確率的に判断すべきこと
ブログ執筆(本記事) 太字+括弧、\$エスケープ、フッター有無 一次情報の価値、読者への配慮、トーン
コーディング lint、型チェック、セキュリティスキャン 設計判断、命名の妥当性、アーキテクチャ
仕様書・設計書 テンプレート必須項目の充足、用語統一 ビジネス要件との整合性、実現可能性
ビジネス文書 宛名・日付・書式、社外秘マーク トーン、読み手への配慮、説得力

エンジニアなら気づくはずです。CIとコードレビューの関係がそのまま当てはまります。CIで弾けるものをレビューで指摘するのは時間の無駄であり、レビューでしか判断できないものをCIに任せるのは不可能です。AIとの協働でも同じ原則が成り立ちます。

パターンマッチで判定できるルールは機械に任せ、文脈理解が必要なルールはAI(と人間)に任せる。 この振り分けを意識するだけで、品質管理の解像度が変わります。

このhooksも、明日には改善点が見つかるでしょう。stop-checklist.shが導入初日に誤検知したように、この記事の執筆中にlint-article.shがコードブロック内を誤検知したように、使ってみて初めて穴が見えます。穴が見えたら直す。直したルールがまたhooksかCLAUDE.mdに蓄積される。このサイクルが、ハーネスを育てるということだと考えています。

参考


ACS 事業部のご紹介

私の所属する ACS 事業部では、開発者ポータル Backstage、Azure AI Service などを活用し、Platform Engineering + AI の推進・内製化を支援しています。

www.ap-com.co.jp www.ap-com.co.jp www.ap-com.co.jp

また、GitHub パートナーとしてお客様に GitHub ソリューションの導入支援を行っています。 GitHub Copilot などのトレーニングなども行っておりますので、ご興味を持っていただけましたらぜひお声がけいただけますと幸いです。

一緒に働いていただける仲間も募集中です! ご興味持っていただけましたらぜひお声がけください。

※求人名の冒頭に【ACSD】と入っている求人が当事業部の求人です

www.ap-com.co.jp www.ap-com.co.jp

本記事の投稿者: 越川将人