ブログ一覧

AI エージェントを自走させるのはプロンプトではなくハーネス

更新: Web開発者向け

AI コーディングエージェントを「自走」させたいとき、多くの人はまず指示書(プロンプト)を長く・厳密に書く方向に進みます。ですが、本番の複数サイトをエージェントで日々保守してみて、自律性を支えているのはプロンプトの精度ではなく成果物を機械的・決定的に合否判定する検証系だと感じています。この記事では、その検証系を「ハーネス」と呼び、どう設計すると人間の介入を最小化できるかを、実運用の事実に沿って層ごとに整理します。

前提: Astro / EmDash ベースの CMS 群を Cloudflare Workers 上で本番稼働させているモノレポで、Claude Code のサブエージェントを日々の保守に使っています。エージェントの「編成」(司令塔 + 並列の実働班という役割分担)そのものは姉妹記事「Claude Code のサブエージェント並列で本番サイト群を保守する」に書きました。本記事は成果物を通す検証パイプライン(ハーネス)の設計だけを扱います。

主張: 自走の源はプロンプトではなくハーネス

LLM の出力は確率的です。同じ指示を与えても、毎回まったく同じ成果物が出てくる保証はありません。長い指示書に「こうしてはいけない」を積み上げても、逸脱が起きる確率を下げるだけでゼロにはできません。

一方、lint・typecheck・build・テスト・CI といった検証は、同じ入力に同じ合否を返す決定的な仕組みです。この非対称を使って確率的に動くエージェントの成果物を決定的なハーネスで検査する構造にすると、「エージェントが正しく振る舞うことを祈る」のをやめて「ハーネスを通過したものだけを信用する」ことができます。

この置き換えで人間の関わり方が変わります。エージェントを信用する設計では、人間が成果物を毎回目視でレビューしなければなりません。ハーネスを信用する設計では、人間が関わるのはハーネス自体の設計と、ハーネスが原理的に検証できない一点だけになります。以下、この「ハーネス」を 5 つの層に分けて、それぞれ何を決定的に保証しているのかを並べます。

層 1: 決定的パイプライン(lint / typecheck / build / test / CI)

土台は、どのプロジェクトにもある標準的な検証群です。lint(コード規約)、typecheck(型)、build(ビルドが通るか)、テスト、そして CI。道具に特別なものは無く、重要なのはこの層を通ることを成果物のマージの前提条件にする運用です。

このモノレポでは、CI が緑であることを squash merge の前提条件にしています。エージェントの成果物は PR になり、CI を通らなければマージされません。マージするかどうかを「CI が緑か」という決定的な基準で決めるので、この工程は人間の目視を待たずに機械的に進みます。

この層が保証するのは「規約・型・ビルド・テストの水準を、確率に関係なく満たしている」ことです。エージェントが 10 回に 1 回うっかり型を壊しても、その 1 回は決定的にマージ前で止まります。

層 2: Claude Code の Hooks でパイプラインを実行前後に差し込む

層 1 を「エージェントが自分で忘れずに実行する」ことに頼ると、確率の問題が戻ってきます。「編集したら lint する」と指示書に書いても、エージェントはたまに実行を忘れます。Claude Code の Hooks を使うと、この実行をエージェントに任せずに自動で行えます。

Hooks は、エージェントのツール実行のライフサイクル上の特定の点で、シェルコマンド等を自動実行する仕組みです(公式ドキュメント / 日本語版)。よく使うのは次の 2 つのフックイベントです。

  • PreToolUse: ツール呼び出しの前に発火する。条件に応じてそのツール実行をブロックできる(例: 破壊的なコマンドを止める)。
  • PostToolUse: ツール呼び出しが成功した後に発火する。すでに実行済みなので後追いだが、結果を検査してエージェントにフィードバックを返せる。

たとえば「Edit または Write のたびに lint を走らせる」は、PostToolUse に matcher として Edit|Write を指定し、hooks にコマンドを 1 つ置くだけで実現します。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "/path/to/lint-check.sh" }
        ]
      }
    ]
  }
}

matcher はフックを発火させるツール名を絞り込むフィルタで、Edit|Write のように | 区切りで複数指定できます(* / 空文字 / 省略で全マッチ)。フックのシェルコマンドは終了コードで挙動を制御でき、exit 0 は正常(標準出力の JSON を解釈)、exit 2 はブロッキングエラーとして扱われます。PreToolUse の exit 2 はツール呼び出し自体を止め、PostToolUse の exit 2 は標準エラーの内容をエージェントに見せて次の一手を促します。

プロンプトに「必ず lint して」と書いても lint が走る確率が上がるだけですが、Hooks で編集のたびに lint を自動実行すれば、エージェントが lint を忘れても lint は必ず走ります。この層は、層 1 のパイプラインをエージェントの記憶や善意に頼らずツール実行のライフサイクル上の決まった点で機械的に実行します。

層 3: ドメイン固有の検証プロトコル

汎用の lint や型では守れない不変条件があります。「この変更でサイトの表示が変わっていないこと」「パフォーマンス修正が実際に速くなっていること(遅くなっていないこと)」などは、lint には判定できません。こうしたドメイン固有の合否基準は、タスク指示に検証手順として埋め込みます。指示書には「何をやるか」より先に「良し悪しをどう機械的に測るか」を書きます。

実例として、パフォーマンス変更のタスクには次のプロトコルを必須条件として書き込んでいます。

  • ウォームアップ後の A/B でクエリ数を計測する(初回リクエストのばらつきを排除し、修正前後を同条件で比べる)。
  • レンダリング結果の HTML をバイト単位で比較し、表示が変わっていないことを証明する(「速くなったが表示が壊れた」を出荷しないため)。
  • 悪化したら出荷しない(これを指示の絶対条件として明記する)。

このプロトコルで実際に出荷を止めたことがあります。本命の修正を実装したエージェントが計測して「クエリ数が 19 から 22 に増えた(悪化した)」と報告し、「悪化したら出荷しない」という基準に従って指示された修正を自ら不採用にしました。汎用ハーネスで守れない不変条件は、このようにタスクごとの検証手順として指示書に書きます。この計測プロトコルの中身(リクエスト内キャッシュの重複排除で見かけのクエリ数が動く落とし穴など)は、姉妹記事「無駄なクエリを削ったらクエリが増えた」に詳しく書きました。

層 4: 観測とフィードバック(自走の入力側)

ここまでの層は、エージェントが出した成果物を検査して止めるハーネスでした。もう一つ、次にやるべきタスクを決定的に作るハーネスがあります。エージェントが自走するには、人間が毎回タスクを与えなくても作業が続く必要があるので、タスクの供給もハーネスが担います。

このモノレポでは、監視(死活チェックと週次の Cloudflare Analytics レポート)を GitHub Actions で定期実行し、その結果を GitHub Issue として自動起票しています。「どのサイトのどのページが 404 を返している」「どのサイトのトラフィックが落ちている」といった観測が、そのままエージェントの作業キュー(明確なタスク)になります。この監視・起票の運用は「Google Analytics を入れずにブログ群を運用する」にまとめています。

この層があると、エージェントは「何をやればいいですか」と人間に聞きにこなくなります。タスクは Issue として決定的に与えられ、成果物は層 1〜3 のハーネスを通ってからマージされるので、タスクを受け取ってからマージされるまでエージェントが自走できます。

層 5: 権限の層

最後の層は、エージェントが書き込める場所を仕組みで制限します。成果物の合否を判定する層ではありませんが、逸脱が起きないことを仕組みで保証する点で、ほかの層と同じ考え方のハーネスです。

具体的には、実働するエージェントに対して本番への書き込み禁止・共通パッケージの改変禁止を最初から課し、作業は git worktree で物理的に分離し、必要なら読み取り専用トークンで権限を絞ります。エージェントが行えるのは自分の作業領域での調査・実装・計測までで、その外の操作が必要になったら人間に判断を求めます。

この制限で越権の操作を止めたことがあります。あるエージェントが、認証まわりで作業が進まなくなったときに「本番 DB を直接 UPDATE すれば迂回できる」という技術的には正しい提案をしてきました。提案自体は妥当でしたが、認証に関わる不可逆操作は権限の外なので実行させず、人間が判断して正規の経路を選びました。エージェントが賢いほど「正しいが越権な近道」を見つけてくるので、エージェントに操作する能力があっても、権限の外の操作は最初から許さないようにしておきます。

ハーネスが検証できないもの(=人間の介入点)

ハーネスが決定的に合否を返せないものは、必ず人間の介入点として残ります。UI の見た目の良し悪し、コンテンツ(文章)の正しさやトーン、そして認証やドメイン切り替えのような不可逆で一度きりの操作は、lint でも HTML のバイト比較でも判定できないので、人間が判断します。ハーネスを設計することは、どこを機械に任せ、どこを人間の介入点として選ぶかを設計することでもあります。介入点を減らすのが目的ではなく、介入点を「ハーネスで守れない一点」に絞り込むのが目的です。

まとめ

  • AI エージェントの自走を支えるのは、長い指示書ではなく、成果物を決定的に合否判定するハーネス。LLM は確率的、ハーネスは決定的で、この非対称を使って「エージェントを信用する」を「ハーネスを通したものだけを信用する」に置き換える。
  • 層 1・決定的パイプライン: lint / typecheck / build / test / CI。CI 緑をマージの前提条件にし、マージするかどうかを機械的な基準で決める。
  • 層 2・Claude Code の Hooks: PostToolUse などで編集のたびに lint を自動実行し、エージェントが lint を忘れても lint が走るようにする。
  • 層 3・ドメイン固有の検証プロトコル: 汎用 lint で守れない不変条件(表示不変・計測悪化の禁止)をタスク指示に埋め込む。実際にエージェントが計測悪化(19→22 クエリ)を理由に自ら出荷を止めた。
  • 層 4・観測とフィードバック: 監視の結果を GitHub Issue に自動起票し、エージェントが次にやるタスクを決定的に供給する。
  • 層 5・権限: 本番書き込み禁止・worktree 分離・読み取り専用トークンで、エージェントが書き込める場所を制限する。
  • 限界: UI の見た目、コンテンツの正しさ、不可逆操作はハーネスで検証できない。ハーネス設計とは、人間の介入点を選ぶ設計でもある。

この記事をシェア