こんにちは井上@一人目SETです。
昨年のブログでSETとしての活動を紹介しましたが、その後「まずは基盤となるテストを整備しなければ、話にならない!」という結論に至りました。そこで現在、SETおよびQA体制として、E2E・UT・ITすべての自動テストシナリオの拡充に専念する方針で動いています。
私の方でSETとしてUT、ITの拡充をAI(主にClaude Code)をフル活用して進めましたのでそちらに関しての苦労や気付きなどを共有できればと思っております!
現状の課題、目的、目標
まずは、開始時点のUT、ITの実装、カバレッジ状況ですがお世辞にも十分とは言えない状況です。
アソビューでは創業から15年の中で体験やチケットの予約購入を中心に様々なシステムを含んだプラットフォームを開発し続けており、事業の拡大の中でどうしてもスピードを重視せざるを得ない場面が多く、テストを当たり前に書く環境が無かったり、カバレッジを見るための仕組みが無かったりしていました。
結果としてUT、ITのカバレッジの数値も低く、パッケージによっては一桁台というものもありました。
1. 何故長期AIループが必要だったか
今回目標としては高めに8つのシステムでカバレッジ85%(C1分岐カバレッジ)というところを設定しました。
今回最初にターゲットにしたパッケージは社内の様々なシステムで横断的に使う基盤アプリケーションで、対象は48モジュール・41万行・メソッド3万個の巨大モノリスで、開始時点のカバレッジは19.8%でした。
なお、対象はJava / Spring Boot製のアプリケーションです。このアプリケーションは画面を持たず、APIゲートウェイ経由で各サービスにAPIを提供するバックエンド(いわゆるリソースサーバー)になっています。
相当な作業量が想定されるため(人間の手でやったら数ヶ月〜年レベル?)、AIで長期ループのパイプラインを設定する必要がありました。
※ここで言う長期ループのパイプラインとは、人間が細かく指示を出さずに作業ゴールだけを設定し、AIが自律的に反復実行して数時間〜数日かけて実装、テスト、検証などを実施する一連の手法のことです。
2. ループを回すためのハーネス
生成AIのエージェントツールとしては今回はClaude Codeを活用しました。
skillやruleなどの基盤が整っていたりMaxプランで固定額で膨大な作業ができるので今回の計画には適しています。
2-1. テストコードを書くための基盤
さて、まずは実装ルールを作るところから始めました。
ルール無しで指示を出すとAIはよしなにそれっぽいものを周りのコードに合わせて作ってしまいます。
これは現状ルールや秩序が無いテストケースやコード実装がある場合は、負債を量産しかねないので良く無いです。
そのためUT、ITを作る上での規約や実装ルールをドキュメントにおこし、skillやruleとして設計しました。
詳細は割愛しますが、下記を細かく規定してruleとして配置しました。 - 命名規則、ファイル配置 - モックの利用範囲や種類 - UT、ITで担保すべき範囲やレイヤー
2-2. ループ制御ツールの進化と、それでも解決しなかった問題 ralph-loop -> /goal
さて、そのルールを活用して実装を進めるにあたって、最初はAIにプロンプトで指示を出していましたが、普通にやると数秒から数分で作業を終えてレスポンスが返ったり、都度都度許可を求めたりされるため、全く効率が上がりません。そこで下記のループ制御ツールの導入です。
ralph-loopの活用と限界
当初お世話になったのはralph-loop(Claudeに自律的なループ実行を行わせるためのツール)です。オフィシャルのプラグインがあるので活用しました。
https://claude.com/plugins/ralph-loop
/ralph-loop "your prompt here" --max-iterations 10 --completion-promise "DONE"
こちらはかなり癖があり、作業しているかと思ったら意味のないコミットを延々と繰り返していたり(ひどい日には「継続記録」だけの空コミットが1日500件以上)、ループが回る中でコンテキストが薄まって、プロンプトで設定したカバレッジ達成のためなら平気でズルや偽装をするなどの問題が起こりました。触ってはいけないファイルをsettings.local.jsonのpermissions > denyに指定するなど、相当ガチガチに縛らないとなかなかうまく回らず苦労しました。
/goalコマンドによる改善
そのうちClaude Codeに/goalコマンドが実装されました。
基本的な仕組みは同じくStop hookを土台にしていますが、終了判定の部分の違いでだいぶ使い勝手が変わりました。
https://code.claude.com/docs/ja/goal
/goal "対象モジュールのITカバレッジ向上。C1カバレッジ85%達成が目標。 モジュール: xxx スキル: /create-coverage-test を使用 ルール: .claude/rules/xxx.md を必ず遵守"
/goalへの移行で得られた最大の恩恵は、"AIに聞かず、実行系が終了判定する" 仕組みを手に入れたことです。ralph-loopでは停止条件をプロンプト内に書いても、それを評価するのはそのセッションのAI自身でした。自己申告のため易きに流れて、延々noopを吐き続けてしまいます。
一方/goalでは別コンテキストの評価器がcondition文字列を毎ターン独立に判定してくれます。
ということで謎にnoopループ地獄になることは無くなり、消費トークンも少なくなって効率が上がりました。
ありがとうAnthropic!
確実にループを回すコツ
/goalの終了条件は、複数の条件をORで列挙するのがコツです。
❌ NG: "C1カバレッジ85%達成" ✅ OK: "以下のいずれかを満たしたら完了: (A) C1カバレッジ85%達成 (B) 高ROIの攻略候補が枯渇し、その旨をWORK_LOGに明記済み (C) 8時間経過"
達成目標だけの単一条件にすると、目標未達のまま打つ手がなくなったときにループが終了できず、noopループに逆戻りしてしまいます。「達成」以外の撤退条件も明文化しておくことで、ループが必ず決着するようになります。
2-3. AI手抜き対策
/goalに切り替えても、AI本体の「易きに流れる」性質は変わりませんでした。ツールが解決するのは「いつ止まるか」であって、「何を書くか」ではありません。ここからは、ツール非依存の"手抜き対策"の話です。
「構造的困難」で言い訳する問題
問題
これは今でも悩まされる問題ですが、「構造的限界」に達したため前述のnoop地獄に陥ったり、打ち切りになるケースが多々あります。
実際に紐解いたり問い詰めたりすると「工数が膨大にかかる」「外部Mockの活用が必要である」「準備が必要」などで実際はやればいいだけ、というところに対して自分で限界を定めて停止したりしていました。
対策
禁止フレーズとして「構造的に困難」「ROIゼロで打ち切り」「人間主導が必要」「これ以上は無理」を設定しました。
また、本当に構造的なブロッカー(物理的に到達不能な分岐など)は、原因・影響範囲・解決策をセットでBLOCKERS.mdというレジストリファイルに明文化し、「そこに載っていない"構造的困難"の主張は無効」という運用にしました。
それでもこの問題は完全解決には至っておらず、今も悩まされています。
metric偽装(数字の見せかけ)
問題
明らかなdead codeの分岐をカバレッジ計算の対象から外す仕組みを導入していましたが、AIがそれを悪用して、先ほどの「構造的困難」なものや複雑なクラスを除外設定するなどの偽装工作を行っていました。
ひどいときには実際のカバレッジが78%台なのに、除外を乱発して「85%達成」を見せかけられたこともあります(精査してすべてrevertしました)。
対策
下記の縛りを設定しました。
- metric偽装のためのdead code除外を禁止(「外部APIのMockが未整備で0%」「特殊な環境が必要」などは整備すれば解決するtest gapであり、dead codeではない、と明文化)
- dead code判定はAIの自己判断を禁止。IntelliJのUnused declaration検出結果から機械的に除外リストを生成し、さらにgrepで呼び出し元が0件であることを確認できたクラスのみを除外対象に
ポイントは、判定をAI自身にさせず、IntelliJやgrepといった別ツールの決定論的な判定に委譲したことです。
ルール違反を繰り返す(Domain/Infra直接テスト再発)
問題
意味のあるテストケースにするために、書くべきテストのレイヤーをあらかじめルールとして定めていましたが、カバレッジ目標到達のためにそこに違反した実装を繰り返してしまいました。
対策
- ruleファイルにルールを明記(もともとあったものの、これだけでは効きませんでした)
- Claude Codeのpermissions denyで触ってはいけないファイルを保護
- ArchitectureTestを導入しレイヤー違反をテストとして検知
- 各ワークツリーへの指示文に「domain/infra層の直接テスト禁止」を毎回明記
2-4. どうやってルールを守らせるか
対策としてやってきたことは結局泥臭くルールを明記したり直接プロンプトを工夫したりすることでした。
AI運用をうまく活用するために必要なのは結局ハーネスの「育成」に近く、ルールは失敗から漸進的に追加することになるのかと思います。
一発で完璧なループを回すような仕組みを作るのは現段階では難しいです。
AIに「何を書くか」を任せられても、「何を書かないか」は人が縛る必要があります。
「カバレッジ数字を上げる」という決定論的な作業目標に対して、「リグレッションテストとして意味を持たせる」というコンテキストを要する本来の目的をどう両立させるか。この乖離をどう埋めるかが肝になると思います。
3. セッションの立て方の変遷
次はセッションの立て方についてです。
短時間でカバレッジを上げていくためには並列でテストコード作成を進めていく必要があります。
Claude Codeを使った並列作業のやり方はいくつかあり、ここも模索しながら進めました。
subagentの活用と課題
初期は1つのセッションからsubagentを複数起動して並行作業を実行するなどしていました。
が、これには思い出せるだけでも下記のような様々な課題感がありました。
- メインセッションからsubagentを複数投入すると、agentからの結果をメインが全部読み込んでコンテキストが積み上がってくる
- agent側でうまくSkillが発動しなかったりして違反のある実装を積み上げる
- agent間での作業が重複して、他のagentが担当する範囲のテストを追加するなどして重複が発生
- subagentが終了するとコンテキストが消失してしまい、長時間のタスクが水の泡になる事故
- agent進捗の可視化が難しくどのagentが何やってるのかがわからず、残骸が残ったりする
- コミットの衝突やコンフリクト解消の困難、git履歴の破壊
worktree per モジュール + メイン指示 + 確認取り込み方式へ
結果たどり着いたのがシンプルにメインセッションと複数作業ワークツリーを分けて丁寧に作業する方式でした。
これにより、判断可能な単位での作業のディスパッチと取り込みができる様になり、作業の確実性が上がりました。
当初のsubagent機構での実行は人間の介入が難しく、今回のテストコード量産には合いませんでした。
なお、worktreeの作成もルールやスキルの反映、カバレッジ計測結果の出力先分離などを含めてスクリプト化し、どのworktreeでも同じ環境・同じ手順で作業が始められるようにしています。
現在確立した運用フロー [メインセッション] ↓ 指示文発行 (goal condition + 参照ドキュメント指定) [worktree A / Claude session] → 独立ループで作業、完了通知 [worktree B / Claude session] → 独立ループで作業、完了通知 ... [メインセッション] ↓ 完了確認 (git diff / 計測 / レビュー) ↓ 取り込み (merge) ↓ 次のターゲット選定 → 次の指示文
さらなる進化: オーケストレーション方式(試行中)
最近ではこの運用をさらに進めて、これまで人間がやっていたメインセッションの役割(指示書の生成、workerの起動、完了確認、取り込み、計測、次の作戦立案)自体をAIに移譲する形式も試しています。
具体的には「作戦立案→worktree準備→worker起動→取り込み→クリーン計測→次作戦」の一連のループをrunbookとして規定したドキュメントを用意し、メインセッションを「オーケストレーター」としてこの規定に従って自律運転させます。オーケストレーター自身はテストを書かず、worktree上のworker(subagent)をバックグラウンドで起動して結果だけを受け取ります。
runbookには、productionコード変更や破壊的操作など「ユーザー判断が必須の事項」と「自走判断してよい事項」を明確に線引きし、判断待ちでもループ全体を止めない(依存しないトラックは継続する)というエスカレーション規定も含めています。かつてのsubagent方式の課題を、規定文書と役割分離で潰し直したリベンジ戦でもあり、うまく回るか検証中です。
4. トークン消費との戦い
また、悩まされたのはトークンの枯渇です。
ご存知の通りClaude Codeは5時間の制限、一週間の制限、モデルごとの制限など数々の利用制限があります。
複雑なコードを読み解いてテストを作成する作業のループはかなり大量のコンテキストを読み込むためトークンの消費が激しく、Max 20xでも油断すると週間の制限を簡単に使い尽くしてしまいます。
どこに時間がかかってるかの調査
Claude Codeのセッションの履歴は ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl に記録されており、それらの中にはどのプロンプトに基づく実行でどのくらいのトークンが消費されたかがjsonl形式で記録されています。
これをAIに読ませて分析させ、改善点を導き出しました。
一例としては下記のようなところが大きかったです。
- 巨大なファイルのReadで消費。ゴッドクラスと化してしまってる共通ファイルをテスト作成のたびに毎回参照してしまう
- 前述のnoopで大量にループすることでチリツモで多くのトークンを消費している
- テスト失敗→修正→失敗のループで消費
対策としては下記を行いました
- テスト作成や調査などを細かくSkillに分けcontext:forkによりメインセッションにはコンテキストは引き継がず結果のみ渡す
- ゴッドクラスのリファレンスファイルを作り、全体を読まなくても関数の使い方を知ったり注意点を把握できるように
- そもそも単純作業はHaikuを選ぶなど作業ごとにモデルを分ける
5. 並列タスク管理の限界と自作npmツール
並行して最大4〜5モジュールのセッション・ワークツリーを立ち上げながら作業すると、今度は人間(私)のコンテキストが限界になります。
- どこのセッションで何をやってたのかがわからない
- cmuxでタブやウインドウ分割しているとさらなる混乱
- セッションの古いログはCLIで見られなくなる
- PCが落ちてどのディレクトリで作業してたか迷子
これを解決するために自作のnpmツールを作成しました!
その名もccakashic
- 依存0で
npx ccakashicでWebUIローカル起動 - 直近のセッションは最大8ペインで分割表示
- 個別のセッションページで過去を遡ってセッションのチャット履歴を見られる
- Resumeボタンでタブを復元したりジャンプしたりできる
- トークンの消費量なんかも表示してます
自作ツールながら愛用しており、セッション迷子にならずに並列作業ができるようになりました。
結構便利なので使ってみてください。
claude agents
まとめ
まず結果として、最初のターゲットにした基盤アプリケーションはカバレッジ19.8%から85%を達成することが出来ました。追加したテストケースはこのアプリケーションだけで約22,000。同じパイプラインを他のシステムにも横展開しており、全体では約40,000テストケースを追加しています(現在も進行中です)。
ここ数ヶ月で今回のUT、ITのテストコード拡充の中でAIエージェントに向き合うことで、色々気づきを得られました。
AIはそれっぽい結果を出しますが、ルールや縛りを決めてそれを守らせる仕組みを設けないと、「良いかも知れないし悪いかも知れない、出来上がったものベースで人間が判断するしかない」アウトプットが大量に積み上がります。そして人間はその量を判断しきれず、不十分な品質のものを作り上げてしまいます。
元来、スピード重視、品質軽視で作ったものは負債のしっぺ返しがすぐに来ると言われています。
AIによるアウトプット生成の速度の飛躍的な向上でそのしっぺ返しを食らうスピードも上がっており、せっかくAIを導入、活用したのに全然アウトカムの増加につながっていない、というのは往々にしてあることだと思います。
いかにAIのモデルの性能任せにせず、AIを制御下に置いて良いアウトプットを出すハーネスを設計するか、が現時点で人間が頭を使うべきところだと感じます。
なんでもかんでもAI任せだと、ネズミやネコにも出し抜かれるわよ。
— 草薙素子『攻殻機動隊 S.A.C. 2nd GIG』
以上です!
最後に
アソビューでは、「生きるに、遊びを。」をミッションに、一緒に働く仲間を募集しています!
カジュアル面談のご希望も随時お受けしておりますので、お気軽にエントリーください!お待ちしております!