ブログ一覧

AI エージェントを「自走」させるのはプロンプトではなくハーネス — 決定的な検証パイプラインの設計

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 を走らせる」は、PostToolUsematcher として 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 で編集のたびに自動実行するのは決定的です。層 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 を自動実行し、「エージェントが忘れる」余地を仕組みで消す。指示ではなく構造で守る。
  • 層 3・ドメイン固有の検証プロトコル: 汎用 lint で守れない不変条件(表示不変・計測悪化の禁止)をタスク指示に埋め込む。実際にエージェントが計測悪化(19→22 クエリ)を理由に自ら出荷を止めた。
  • 層 4・観測とフィードバック: 監視 → GitHub Issue 自動起票が、決定的なタスク供給になる。自走の入力側もハーネスが作る。
  • 層 5・権限: 本番書き込み禁止・worktree 分離・読み取り専用トークンで、逸脱の入口を構造的に塞ぐ。
  • 限界: UI の見た目、コンテンツの正しさ、不可逆操作はハーネスで検証できない。ハーネス設計とは、人間の介入点を選ぶ設計でもある。

参考リンク