/note/tech

Claude Codeで開発期間を2.5か月から1か月に縮めた「ハーネス」の設計手法

要約:

■ 1. ハーネスとは何か

  • ハーネスの定義:
    • 周囲の環境を設計してAIエージェントの成果を最大化する仕組み
    • 手順を書いたスキルファイル、進行状態を持つJSONファイル、完了条件を判定するシェルスクリプトの3点セット
  • 事前定義の対象:
    • どのフェーズで、誰が、何を読んで、何を出力し、何をもって完了とするかをあらかじめ定義する
    • 毎回「次はこれをやって」と口頭で指示する方式を取らない
  • セッションの独立性:
    • 定義に従って1タスクずつ進むため、指示の内容が毎回の会話や履歴に依存しない
  • 実際の成果:
    • 情シスプロダクトへの新機能追加で、当初の見積もりは実質工数でおおよそ2.5か月
    • ハーネスを先に作ってから開発した結果、大きな手戻りがないままほぼ1か月で実装を一通り終えた

■ 2. ハーネスを作った理由

  • コンテキストの限界:
    • 今回の機能は1つのコンテキストにまったく収まらない規模だった
    • セッションを長く使い続けるほどコンテキストが会話の履歴で埋まっていく
  • 二乗のコスト増加:
    • 会話が長引くほど過去の全ログを毎回AIに送り直すため、API料金とコンテキスト容量が雪だるま式に膨らむ
    • 履歴が占める分だけ、後半のタスクでは仕様やコードの読み込みに回せる残量が減る
    • 1回のセッションで扱う範囲と、そのとき読ませるものの両方を絞らないと1機能を作りきれない
  • デグレへの懸念:
    • 既存プロダクトのリポジトリに相乗りする形(モジュラーモノリス)で追加するため、デグレは自チーム以外にも影響する
    • 仕様の取り違えを終盤でまとめて発見すれば、そこから数週間分の工数が簡単に膨らむ
  • 入社直後という条件:
    • 既存コードの知識も背景も持たない状態だったため、早い段階で機械的に止める仕組みが欲しかった
    • モデルにデグレ検証を任せずハーネスで制御することで懸念を減らせた

■ 3. 登場人物と責務分担

  • 4種類の登場人物:
    • 進行役のオーケストレータ、実装エージェント、レビュアー、機械検査に責務を分けた
    • コンテキストを膨らませない、デグレを見逃さないという点では、あえて持たせなかったもののほうが効いた
  • オーケストレータ:
    • 実体はClaude Codeのメインセッションそのもので、統括専任のエージェントを別に立てない
    • フェーズ遷移、ユーザーとの対話、状態ファイルの更新、検査スクリプトの実行を担当する
    • 自分では実装せず、仕様の本文とレビューの全文をコンテキストに載せない
  • 実装エージェント:
    • 分解したタスク1つにつき1体だけ起動し、TDDで実装してコミットまで行う
    • ユーザーに質問できず、疑問は未解決の質問として戻り値に載せて返す
  • レビュアー:
    • 仕様突合、並行性、認可、機能の完結性の4観点を担当する
    • 全タスクの完了後に1回だけ4体を並列で起動し、タスク単位では起動しない
  • 機械検査:
    • 完了条件を判定するシェルスクリプトで、12項目を機械的に判定する
    • 主要な判定は、そのタスクで書いたテストがgreenであること
    • 機能フラグをOFFにしたとき既存の振る舞いと一致すること
    • 今回の規律が、この機能以外の開発セッションに染み出していないこと
    • エージェントを1体も使わないのでトークンを消費しない
  • 統括専任エージェントの不採用:
    • 統括役を1体挟むと、受け取った内容を要約して次へ渡す分だけノイズが増える
    • 精度が落ちたうえにトークンも余計にかかったため、進行役はメインセッションが兼ねる形に落ち着いた

■ 4. 1起動1タスクの原則

  • 中心にある決まり:
    • 実装エージェントの1回の起動でタスクを1つだけ進めて終わる
    • タスクとは準備フェーズで切り出す最小実装単位を指す
  • 同一セッションの再利用を避ける:
    • 同じ実装エージェントに2つのタスクを続けて担当させない
    • 初めは同じセッションで行っていたが、ノイズが入りトークン数が増え実装精度が悪化した
  • 引き継ぎが壊れない理由:
    • 進行状況はJSONに書き出してあり、次のセッションはそれを読み直す
    • 決まったことは仕様ドキュメントとrulesに書き戻してあり、前の会話を覚えている必要がない
    • 引き継ぐのは「いまどこまで進んだか」だけで、「何を決めたか」はすべてファイル側にある

■ 5. 処理の流れ

  • 準備フェーズ:
    • 仕様ドキュメントと依頼内容を突き合わせる認識合わせ、未確定の設計判断、タスク分解の3つを行う
    • ユーザーとの合意が必須で、合意した内容は状態ファイルではなく仕様ドキュメント側に書き戻す
  • 繰り返しフェーズ:
    • 1タスクごとに実装、完了条件の検査、簡単な報告を行ってセッションを終える
    • 区切りのよいところでだけ、関係する仕様全体のテストをフル実行する
    • 画面を作ったときは目視の確認を依頼する
  • 最終段階:
    • 全タスクが終わったところで初めてレビュアー4体を並列で起動し、ユーザーの承認を待つ
  • 失敗時の扱い:
    • 検査がfailなら新しい1体でやり直し、3回で人に戻す

■ 6. TDDでの実装

  • 実装方式の固定:
    • 実装エージェントの進め方はTDDに固定している
    • 「この機能を実装して」と漠然と頼むと迷走するが、間にテストを挟むだけで振る舞いが変わる
  • 曖昧さのないゴール:
    • 「テストをgreenにする」という客観的なゴールを与えると目に見えて精度が上がった
    • 日本語の指示文よりもテストコードのほうが精度の高いプロンプトとして働いている
  • ハルシネーションの即時検出:
    • 存在しないメソッドやライブラリを書いてもテストを走らせた時点でエラーになる
    • 人が読んで気づくのを待つ必要がない
  • 自己判定による修正:
    • 合否の判断基準が手元にあるので、実装が正しいかを自分で実行して確かめられる
    • 人の確認を毎回挟まずに修正まで進められ、往復の回数もトークンも減る
  • 受け入れ条件との関係:
    • 受け入れ条件はタスク分解のときに決める「このタスクが終わったと言える条件」で、1タスクにつき数件を文で書き出す
    • 登録処理なら、設定で無効にしている対象には作成できない、同じ内容の重複は弾く、といった粒度
    • 途中で失敗してロールバックしたときは通知を飛ばさない、という粒度も含む
    • オーケストレータが確定して渡すため、最初に書いたテストを見れば解釈が合っているかその場で分かる
    • 実装から始めさせると、解釈のずれが完成間際まで見えない

■ 7. 仕様と規律の分離

  • 2種類に分ける理由:
    • 矛盾したときにどちらが正かをあらかじめ決めておくため
  • spec:
    • 「何を作るか」を書いた仕様ドキュメントで、唯一の仕様源
  • rules:
    • データモデル、ドメイン、境界の3ファイルに分けた「常に守ること」
    • 仕様ドキュメントと矛盾した場合はこちらが正
  • 必ずセットで渡す:
    • rulesを渡さないと、仕様ドキュメントに書いていないから自由と解釈し、ロックの取り方やスコープの規律を破る
    • rulesだけでは何を作るかが分からない
    • specとrulesを必ずセットで渡すことが、実装のぶれや規律違反を防ぐうえでもっとも効果的
  • CLAUDE.mdに書かない理由:
    • CLAUDE.mdはセッションの最初から最後まで常に読み込まれる
    • 今回の機能と関係のない作業をしているときにはノイズになる
    • モジュラーモノリスのため、他サービス担当者のセッションにも規律が常時流し込まれてしまう
  • パス指定による動的読み込み:
    • ルールファイルに適用範囲のパスを書くと、その配下のファイルを触ったときにだけ内容が読み込まれる
    • 今回の機能を触るセッションでは規律が効き、他サービスを触るセッションでは存在しないのと同じになる
    • 必要なコンテキストだけを、必要なセッションに、必要なときだけ渡せる
  • やらないことの宣言:
    • 今回の機能に入れない実装もrulesに書き込んだ
    • エージェントは指示した以上に作り込もうとすることが何度かあった
    • やらないことを宣言しておくと、毎回指示しなくても実装時に読み取って守る

■ 8. 状態の外部化

  • 2つのJSONファイル:
    • オーケストレータは毎ターン、この2本を読み直してから動く
  • progress.json:
    • 現在のフェーズ、全体としての進捗、終わったぶんの1行サマリ、未確定事項、最終レビュー結果を持つ
    • 毎ターン読む
  • stages.json:
    • タスク一覧として状態、受け入れ条件、参照する仕様、次のタスクへの注意点を持つ
    • 進行中のぶんだけを読み、終わったぶんのファイルは開かない
  • 分割した経緯:
    • もともとは1ファイルにまとめていたが、10タスクの時点で118KBまで育った
    • 状態を読むだけでコンテキストを大きく使うようになったため分割した
  • 確定した内容はJSONに書かない:
    • 確定した仕様は仕様ドキュメントへ、守るべき規律はrulesへ書き戻す
    • 実装上の不変条件はコードのコメントへ書き戻す
    • JSONに残すのは未確定の前提、未回答の質問、次のタスクへの注意点1行だけ
    • 書き戻す先がない決定は記録しない
  • 役割分担の効果:
    • 次回のセッションはJSONで状況を知り、仕様ドキュメントとrulesで内容を知る
    • この分担により、セッションをいくら細かく切っても引き継ぎが壊れない
  • チームで共有しない理由:
    • 2つのJSONは.gitignoreに追加している
    • どのタスクを進めているか、どう分解したかという作業中の状況は他の人が知る必要がない
    • 共有すべきものはコミット対象である仕様ドキュメントとrulesに書き戻してある
    • 状態ファイルにはブランチ名やコミットの基点といったローカル固有の値が入り、渡してもそのままは使えない
    • 別の人が続きをやる場合は状態ファイルを捨ててゼロから起動し直し、実装済みの範囲は準備フェーズでgitの履歴から拾い直す

■ 9. 完了条件のシェルスクリプト化

  • 判定の主体:
    • 毎タスクの完了判定は12項目の検査を行うシェルスクリプト1本に任せる
    • 実装エージェントの自己申告は信用せず、オーケストレータが同じスクリプトを自分で1回叩いて判定する
  • 対象テストのgreen判定:
    • テスト件数が0件の場合はfailにする
    • RSpecは対象0件でも終了コード0を返すため、通すと「テストを書かずにgreen」が成立してしまう
  • 機能フラグOFFでの一致:
    • フラグをONにする設定を追加していないかも同時に見る
    • 追加されるとOFFでの検証が空振りする
  • 変更パスの制限:
    • 変更したファイルがすべて許可パスの中にあることを見る
    • 変更してよいパスを列挙した定義ファイルを1つだけ置き、それを唯一の実体として判定する
  • 依存違反の抑制:
    • パッケージ間の依存違反が増えていないことを見る
    • 違反の除外リストに追記して回避していないかも見る
  • TDDの痕跡:
    • テスト駆動開発の痕跡がコミットに残っていることを見る
  • 人が目視する2項目:
    • 既存の共有テーブルへの変更と画面側のフラグ漏れは「要確認」として人が解決する
    • 自動でpassにしないのは、この2つがデグレの主な経路だから
  • シェルスクリプト化の効果:
    • エージェントを1体も使わないのでトークンを消費しない
    • モデルの判断が入らないので自己申告ができない
    • AIの言葉ではなく、実際の実行結果という不変の事実に基づいて判定できる

■ 10. 手戻りがほぼ出なかった理由

  • 効いたのは準備フェーズ:
    • 1か月で実装を終えられた最大の要因は大きな手戻りが出なかったこと
    • 実装を始める前の認識合わせが効いていた
  • 乖離の解消手順:
    • 依頼内容と仕様ドキュメントを突き合わせ、乖離を1件ずつユーザーと合意する
    • 合意した結果を仕様ドキュメントとrulesに書き戻す
    • 乖離が1件でも未解決ならタスク分解には進まない
  • 得られた結果:
    • 実装が進んでから「そもそも仕様の理解が違っていた」という類の手戻りが起きなかった
    • rulesへの追記でチーム開発上のズレもなくなり、他の人が実装しても同じ仕様とrulesで共開発できる
  • レビュー指摘の傾向:
    • 最後のレビューで出た指摘はタスクをまたぐ欠陥に集中した
    • 個々のタスクの範囲で見つかる問題は毎タスクの機械検査が先に止めるため、人とレビュアーの目は「つなぎ目」に集中できる

■ 11. レビューを最後に1回だけ回す

  • 2層構成の理由1: トークンの節約:
    • 今回の規模でタスクごとに4体を起動していたら、レビューのコストはタスクの数だけ積み上がっていた
  • 2層構成の理由2: タスクをまたぐ欠陥の検出:
    • タスク単位の機械検査では自分の担当差分しか見えず、タスクをまたぐ欠陥を原理的に検出できない
    • デッドロックはロックを取る処理が出そろって初めて判定できる
    • 先に片付けたタスクだけを見ても、同じリソースを触る後続タスクが未実装なので順番の食い違いようがない
    • 仕様突合のレビュアーは後続タスクの担当分を「未実装の不具合」として指摘し、大量のノイズを返す
  • 4体の観点:
    • 仕様突合は仕様書どおりに作られているかを見る
    • 並行性はロックや整合性の面で同時に操作されても壊れないかを見る
    • 認可は誰がどの操作をできるかという権限チェックを見る
    • 機能の完結性は導線や事後処理の面で入口だけ作って終わっていないかを見る
  • 重複の排除:
    • 最終段階の1回だけ4体を並列で起動し、重複を排除した上で指摘をまとめる

■ 12. 反証役のバイアス対策

  • バイアスの正体:
    • 「この指摘を反証してみて」と頼むと、反証する方向に寄った結論が返ってきやすい
    • 頼まれた仕事をやり遂げようとするため、根拠が薄いときでも「問題ない」と書いてしまう
    • 放っておくと本物の不具合が偽陽性扱いで静かに消える
  • 縛り1: 対象を絞る:
    • 報告したレビュアーが1体だけ、かつ修正コストが高い、を両方満たす指摘だけを対象にする
    • 複数のレビュアーが独立に見つけたこと自体が証拠なので反証は回さない
  • 縛り2: 証拠の限定:
    • 根拠として認めるのはコードの該当行かテストの出力だけ
    • 「おそらく問題ない」「一般的にはこう」は根拠として扱わない
  • 縛り3: 判定不能なら残す:
    • 結論が出なかった指摘は消さずに強度だけ下げて残す
    • 多数決も使わない
  • 実測の結果:
    • 複数のレビュアーが独立に報告した指摘に反証を回したケースは、結局すべて指摘のほうが正しかった
    • 反証役は1呼び出しあたりの単価がもっとも高いエージェントであり、条件を絞ったことでコストも下がった

■ 13. モデルの使い分け

  • 選定の主体:
    • オーケストレータが実装エージェントを起動するとき、そのタスクにどのモデルを使うかまで決めて渡す
  • 判断の目安:
    • 設計の余地がどれだけ残っているかで決める
  • Opusを充てる場合:
    • データモデルの設計や状態遷移の組み立てのように、仕様を読んで組み立てを決める必要があるタスク
  • Sonnetで十分な場合:
    • 決まった型に沿ってAPIや画面を足すだけのタスク
    • テストの追加が中心のタスク
    • 仕様と規律を絞って渡しているため、迷う余地が小さいタスクほどSonnetでも結果が安定する

■ 14. 結論と限界

  • 最も重要な学び:
    • AIエージェントに大きな機能を任せるときは、モデルの良し悪しよりも周囲の環境を設計して成果を最大化することが大事
  • 要点の再掲:
    • オーケストレータは仕様の本文とレビューの全文をコンテキストに載せない
    • 規律はパス指定つきのrulesに置き、必要なセッションにだけ動的に読み込ませる
    • 1タスクごとにサブエージェントを立て直し、セッションを分けてコンテキストを持ち越さない
    • 完了条件をシェルスクリプトに落とし、エージェントを使わずにデグレを止める
  • 銀の弾丸ではない:
    • プロジェクトによってはハーネスが必要ないこともある
    • インフラの設計のように、今回のハーネスの設計がどのプロジェクトでも適用できるとは限らない
    • 適材適所にうまくAIを使っていくことが大事