簡単なまとめ
前提
Perl+10年以上運用のレガシーシステム
巨大なコードベース+テスト基盤もなし
結論
- AI基盤の作成がAI開発での品質担保に効く
- 基盤=TDD・Clean Architecture・動的言語の制御 + 指示書整備
- エンジニアの設計力がAI開発の効率を最大化する
なぜこのAI基盤が必要?
私の場合、開発体制に対してかなり大きな規模の開発が必要になったこと、
AI活用を進めようという方針があったことなどからAIをチームで運用し、工数を大きく削減する必要がありました
AI活用によるバイブコーディングは工数を破壊的に削減できますが、
運用負債を残しやすいデメリットがあり、このままでは仕事に採用できません
そこで、AIについてGitHubやAnthropicのウェビナーなどで勉強しました
結果として、私が特に共感しなおかつ共通していたのはテストコードによるガードレールが重要という話でした
-
Claudeのベストプラクティス
https://code.claude.com/docs/ja/best-practices#give-claude-a-way-to-verify-its-work -
GitHub Copilotのチュートリアル
https://docs.github.com/ja/copilot/tutorials/review-ai-generated-code#1-start-with-functional-checks
上記にも記載がある通り、静的解析やテストコード(決定論的な解析・検証)をしっかり整備するのがAI活用の大前提です
特にPerlには型がないため、静的解析で防げる範囲も限定的です
決定論的な検証をテストに寄せる必要が、より強くありました
なお、既存システムにはテスト基盤自体がありませんでした
Mockの仕組み、カバレッジ計測、CIの整備を含め、すべて一から用意する必要がありました
決定論的な解析・検証の利点を最大化することで、AI活用も最大化できます
それが私の考えたAI開発基盤の考えの根底です
AI開発基盤の具体
TDD
まず大枠のTDD( Test-Driven Development )について
この開発基盤ではテスト(決定論的検証)が最重要なので、必ずテストが書かれる開発手法であるため選択しています
図1 TDDの基本開発サイクル
AIの指示書(Agents.md)では上記を徹底するように指示しています
また、テストの網羅性はカバレッジで定量化し、AIが自己改善(リファクタ)できるようにします
作業ごとのPRについてもGitHub ActionsによるCIでリグレッションを仕組みとして検知可能にします
CIにはコーディングルールをテスト化したものや全Unitテストなどを実行します
(動的言語の制御で説明するテストもここでも実行される)
テスト中心になるため、mdファイルでテスト契約表(Domain,Repository)またはフローチャート(Usecase,Driver)を作成します
テスト契約表は煩雑なDomainなどで境界値や正常系異常系の観点網羅、フローチャートはオーケストレーションする層のユースケースの網羅を可視化します
これにより、事前にテストの確度を上げつつ、人間がハンドルしやすく開発を進められるようになります
図2 設計文書を起点としたAI自走ループとCIゲート
Clean Architecture
次はアーキテクチャです
関心分離によってテストのしやすさを構造的に担保したいです
心臓部となるロジックをドメインとして依存を最低限にし、厳密なテストを高速でかけるようなアーキテクチャを考えた結果、Clean Architectureに辿り着きました
また、今回私が導入するシステムでは長年レイヤードが採用されていたため、そこからシームレスにこの基盤へ乗り換えしやすいように、レイヤードの構造(Page/Logic/Model)にClean Architectureを取り入れる結果となっています
具体的には既存レイヤー自体はほぼそのままに、DIP(依存性逆転)による関心分離を導入して、Repository層を追加し、そこを今回構築する基盤と外部の唯一の交流点にしました
最後に、依存の向きをすべてDomainへ向かうよう統一しました
緑=決定論的・高速(AIが自走可能) / 赤=非決定論的・低速(人手が必要)
図3 レイヤードベースのClean Architecture構成
このアーキテクチャも独自のもので重要なため、Agents.mdで順守をしてもらいます
特に最初は指示書の比重は重く、サンプルコードもある方がよいです
ある程度概形が仕上がった段階で指示書と合わせて、AIはほぼ誤解なく処理を書いてくれるようになります
動的言語の制御
最後に今回の中核となる動的言語の制御です
具体的には基底クラスの作成、Mock作成のスクリプト化、ダックタイピングのテスト、useチェッカーなどです
AIにとっても品質面で不都合が出やすい動的言語の特性(非決定論的なAI×動的言語)をこれで決定論的に検出します
これらを今回のように基底クラスやテストにせず、コンテキストにすることもできますが、非決定論的で品質低下やレビュー負荷につながるため、コンテキストでなくとことん決定論的に仕組み化して対応します
各制御機構は下表のようにそれぞれの層に効きます
表1 層と制御機構の対応
◎=純粋性の静的保証まで含む ○=適用 −=対象外
| 層 | 基底クラス | Mock生成 | ダックタイピング | useチェッカー |
|---|---|---|---|---|
| Page(Driver) | − | ○利用側 | ○ Page→Logic | ○ |
| Logic(Usecase) | − | ○ 生成対象 | ○ Logic→Repository | ○ |
| Domain | ○ | − 実物を使用 | − | ◎ 境界違反も検出 |
| Repository | − | ○ 生成対象 | − | ○ |
| 外部(Model・レガシー) | − | − | − | − |
最下段の外部(Model・レガシー)がすべて「−」であるとおり、これらの制御が効くのは新規に作った内側だけです
基底クラスについて
Perlだと下記がざらにありえます
# ハッシュに好きなキーを詰めるだけ
# pet => { id => undef, name => undef, hunger => undef } これを想定する
my $pet = { id => 1, name => 'ポチ', hunger => 5 };
# 基底クラスで下記などをエラーにする
# 書き込みで想定外のkeyへ書き込み
$pet->{henyar} = 1; # 書き込める。参照もできてしまう
# 呼び出し側でtypo
print $pet->{nama}; # undef。何も起きない
これを$petというオブジェクトにして、基底クラスによって存在しないフィールドへのデータの書き込みおよび参照をエラーにします
# 基底クラスを継承したオブジェクトにすると
# ATTRIBUTESを id,name,hungerとする
my $pet = Domain::Pet->new({ id => 1, name => 'ポチ', hunger => 5 });
$pet->set_henyar(1); # => Can't locate object method
print $pet->nama; # => Can't locate object method
# 定義してないattributeを指定すると
my $pet_ng = Domain::Pet->new({ pet_id => 1 }); # => unknown attribute
Mock作成のスクリプト化について
unitテスト用にMock(外部オブジェクトの代わりに模擬の外部オブジェクト)を作成します
Mockを決定論的に作成するという意図です
定数系+Domainはそのままテストに使うため(指示書のルール)、対象はRepository層とLogic層になります
手書きモックは実物が変わっても追随しないので、テストが緑のまま乖離します
実クラスの定義から機械的に生成すれば、乖離が起きた瞬間に生成物が変わり、テストが落ちます
また、この差分を検知するテストを作成することで、この生成漏れも検知できます
ダックタイピングのテストについて
動的言語は呼び出すインターフェイスがない時、実行時エラーで発覚しますが、このテストでテスト実行時およびCIにて検知可能になります
特に今回はDIP利用+動的言語ということで、Logic(Usecase)から呼び出し時にインターフェイスが合っているかのチェックがないと、
テストがALLグリーンでも結合テストをしないとエラーになるという事故がありえてしまいます
これを未然に防げます
これはMock作成スクリプト+Unitテストで担保されています
Mockは実クラスの定義から生成されるため、呼び出し側のUnitテストがそのままインターフェイス一致の検証になります
Logic→Repository、Page→Logic の各呼び出しについて、Unitテストの時点でダックタイピングの安全性が保証される形です
useチェッカー
事前にuse漏れおよび不要なuseをチェックして、問題あればエラーにします
これで実行時にuseによるエラーとAIの修正漏れによる不要なuseを決定論的に制御します
また、Domainのレイヤー境界の違反を、機械的に検出することができます
これによって、Domain層の純粋性を静的に守ることができます
useに現れないDI境界のほうは、ダックタイピングのテストで担保しています
型がないためにおこる実行時エラーを決定論的に事前検知できるようにして、AIが自己修正できるようにするということが要です
導入してどうなったか
- AIに任せられる範囲が広がりました(新規部ほぼすべて)
- これによる工数削減(手動時の工数出しに対して、実働工数が約50%ほど削減に成功しました)
- 目標値が工数算出時の約50%削減だったため、達成
- これによる工数削減(手動時の工数出しに対して、実働工数が約50%ほど削減に成功しました)
- レビューの観点が「コードが正しいか」から「テスト観点が妥当か」に移りました
- 実行時にしか分からなかったエラーが、CIで止まるようになりました
- 運用実装におけるメンバー差分を抑えられました
- AIに聞けばわかるのでキャッチアップのリードタイムも削減されました
限界とトレードオフ
正直に書くと、この基盤は万能ではありません
儀式は重い
設計文書、テスト、Mock生成と、機能あたりの手数は確実に増えます
ただし、AIがテストを書くため、儀式の重さは実用的な範囲に収まっています
今回は既存のレガシーシステムに対して、大きな機能追加をする場合で記事を書いています
規模が小さくても新規機能ならこのアーキテクチャを適用する価値はあると思います
基盤として複数の機能で再利用し、初期コストを回収する前提の投資になります
テストは書かれた分しか守らない
カバレッジ100%でも、テスト観点が間違っていれば緑のまま間違えます
つまり人間の仕事は、コードを書くことからテスト観点を設計することへ移ったと言えます
=> ここについてはレイヤーごとのテストの徹底分担およびテストケースのフローチャート化による可視化、別AIによるレビューでも対応していきます
外側は依然として人手
図3の赤い領域(既存レガシー・DB)は、Playwrightと手動テストに頼っています
この基盤が決定論的に守れるのは、あくまで新規に作った内側だけです
持ち帰れること
言語やフレームワークを問わず使えると思っているのは、この3点です
- 型がないなら、決定論的に落ちる仕組みを作る
- AIへのガードレールは、コンテキスト(助言)ではなく実行される仕組み(判定)に置く
- 決定論的に検証できる範囲を広げることが、そのままAIに任せられる範囲を広げる
結局のところ、新しいものは何もありませんでした
TDDもClean Architectureも、何十年も前からある定石です
しかし定石を守ろうとしたのではなく、AI活用の最大化を図ってここに再着地したことが今回の発見です
AIに任せられる範囲は、あなたが作った構造の強度で決まります
だからこそ、AI時代においてエンジニアの設計力の価値は上がっていると考えています