AIにコードを書かせると、驚くほど速く形になります。一方で、「本当に設計書どおり?」「テストは意味のあることを確かめている?」という不安も残ります。
この記事では、設計書(Markdown)からSpring Bootのバッチ・APIのコードと単体テストをAIに作らせ、機械のチェックと人のレビューで品質を担保する仕組みを紹介します。実物は ai-coding リポジトリで公開しています。サンプルの業務(会員・注文)とIDは、すべて架空です。
前提:AIの生成物は「β版」
この仕組みでは、最初に立場をはっきりさせています。
AI(GitHub Copilot のエージェント)は、設計書から β 版のソースと単体テストを作る道具です。最終的な品質の担保は人が行います。
AIを使っても、成果物の責任の所在は変わりません。そのうえで、品質担保の原則を5つ決めています。
- 実行の指示と成果物の格納は人が行う。 どの設計書で、どの工程を回すかは人が決めます。
- AIの生成物(β版)は全量レビューする。 そのまま採用しません。
- AIレビューは補助。 人のレビューを置き換えません。
- 課題はチケットで追跡する。 レビューで見つけた修正事項の対応漏れを防ぎます。
- 設計書にAIが必要な情報を書く。 パラメータファイル名、メッセージID、テーブル名などです。
5つ目は、シリーズの最初の記事で書いた「input不足」の話そのものです。AIが推測で埋めがちな情報を、最初から設計書に書いておきます。
全体像:ルール・検証・実行を分ける
リポジトリは、役割ごとに置き場所を分けています。
| 役割 | 置き場 | 内容 |
|---|---|---|
| ルール(唯一の正) | guidelines/ |
規約、構成、品質ゲート、レビュー観点、AI活用の原則 |
| 検証 |
template/、tools/spec-trace、scripts/
|
機械で判定する品質ゲート |
| 実行 |
.github/agents/、.github/prompts/
|
役割別のエージェントと、呼び出し用のプロンプト |
ポイントは、ルールの正を guidelines/ の1か所にしていることです。AIも人も、ここだけを根拠にします。エージェントへの指示にも「ここに書いていないルールを作らない。迷ったら推測せず確認事項として人に返す」と書いています。
5つのフェーズと、人のレビュー
開発は5つのフェーズで進めます。フェーズの区切りごとに、人のレビュー(RP1〜RP5)を挟みます。
| フェーズ | Copilot Chatで | やること | 人のレビュー |
|---|---|---|---|
| 1 計画 | /ai-plan |
生成するファイルの一覧と、要件の対応表を作る | RP1 計画 |
| 2 コード生成 | /ai-code |
ソースを作り、機械のチェックを通す | RP2 コード |
| 3 テスト生成 | /ai-test |
単体テストとAPIのシナリオテストを作る | RP3 テスト |
| 4 突合 | /ai-review |
設計書とコードの判定表を作る | RP4 最終確認 |
| 5 CI + PR | /ai-pr |
全チェックを再実行し、PR本文を下書きする | RP5 PR |
今どのフェーズにいるか分からなくなったら、/ai-status で確認できます。
エージェントを役割で分ける
.github/agents/ には、役割ごとのエージェントを置いています。
- planner:計画を立てる
- coder:コードを書く(テストは書かない)
- tester:テストを書く
- reviewer:設計書とコードを突き合わせる
- orchestrator:今のフェーズを判断し、次の担当と人のレビューへ引き継ぐ(自分では書かない)
コードを書く担当とテストを書く担当を分けているのがポイントです。同じAIが両方を書くと、テストが実装に引きずられやすくなります。
AIの「できました」を信じない仕組み
AIは「実装しました」「テストが通りました」と報告します。ところが実際には、関数が無い、テストが何も確かめていない、ということが起きます。
そこで、状態を機械で裏付けるようにしています。中心になるのが、要件・実装・テストの対応表です。
spec_id: "FEAT-JOB0001"
source: "samples/design-docs/batch/JOB0001_order-import.md"
source_sha256: "..." # 人が承認後に spec-trace pin で記録
requirements:
- id: "REQ-JOB0001-003"
source: "JOB0001_order-import.md#STEP2"
type: "business-rule"
description: "登録済みの注文番号は登録しない"
status: "proven"
implements:
- file: "template/src/main/java/com/example/app/order/OrderImportService.java"
symbol: "OrderImportService#importLines"
proofs:
- file: "template/src/test/java/com/example/app/order/OrderImportServiceTest.java"
symbol: "OrderImportServiceTest#rerunIsIdempotent"
要件ごとに、状態が3段階あります。
| 状態 | 誰が付けるか | 条件(ツールが確かめる) |
|---|---|---|
| pending | planner | なし |
| implemented | coder |
implements のファイルと関数が実在する |
| proven | tester |
proofs のテストが実在し、アサーションがある |
エージェントが状態を上げてもかまいません。ただし条件を満たしていなければ、CIの spec-trace check が失敗します。「作った」と書くだけでは通らない、というわけです。
設計書が変わったら気づける
対応表には、人が承認したときの設計書のハッシュ(source_sha256)を記録します。設計書があとから書き換わると、ハッシュが合わなくなり、CIで検知されます。差分を見て、影響する要件を pending に戻してやり直します。
品質ゲート:機械で判定できることは機械に任せる
マージの前には、機械で判定するチェック(決定論ゲート)を全部通すことを必須にしています。
| ID | 内容 | ID | 内容 |
|---|---|---|---|
| D1 | コンパイル(Java 17) | D7 | テストの実在とアサーション |
| D2 | Checkstyle | D8 | 設計書の変更の検知 |
| D3 | 単体テスト | D9 | Mapper XMLの ${} 禁止 |
| D4 | 分岐網羅(Service・Job) | D10 | APIシナリオテストの実在・有効性 |
| D5 | SpotBugs | D11 | ルールとチェックの対応 |
| D6 | 実装の実在 | N1〜N6 | AIレビュー(設計書との突合。補助) |
N1〜N6のAIレビューは、あくまで補助の位置づけです。白黒はっきりすることはD1〜D11の機械判定に任せ、AIと人は中身の判断に集中します。
同じ判定は手元でも実行できます。
./scripts/verify-gates.sh --final # Windows: .\scripts\verify-gates.ps1 -Final
同梱のサンプルは、5フェーズを終えた状態(全要件が proven)になっています。template/ のコードをわざと少し壊して実行すると、どのゲートで止まるかを確かめられます。
ゲートの「こじ開け」を禁止する
AIにチェックを通すよう頼むと、チェックの方を骨抜きにしてしまうことがあります。そこで、次の変更をはっきり禁止しています。
- テストの期待値を、設計書ではなく実装に合わせて書き換える
- アサーションを消す・弱める(
assertTrue(true)など)、実行するだけのテストにする - Checkstyle・SpotBugsの除外や
@SuppressWarningsを理由なく足す - テストやシナリオを
@Disabled/@ignoreで無効化する - 対応表から要件を消す、状態を根拠なく上げる
- カバレッジの閾値を下げる
この禁止事項は、エージェントへの指示と、CIのチェックの両方に入れています。指示だけだとAIが忘れることがあるので、機械でも防ぐ二重の構えです。
テストの書き方のルールは、テストのファイルを編集するときだけ読み込まれる指示ファイル(.github/instructions/test.instructions.md)に書いています(抜粋)。
---
applyTo: "template/src/test/**"
---
- 期待値は設計書から決める(実装の動きに合わせない)。1 テスト 1 観点、アサーション必須
- 単体テストは DB・外部サービスを使わない(Mapper はインメモリ実装・ラムダで差し替え)
- アサーションを消す・弱める、`@Disabled` は禁止
自動修正ループの止め方
エージェントは、チェックが通らないと原因を直して再実行します。ただし、次のどれかに当たったらすぐに止めて人に戻すように決めています。
- 同じフェーズで3回直しても通らない
- 同じエラーが2回続いた(直せていない)
- A → B → A のように、同じ修正を行き来している
- 直すたびにエラーが増えている
3つ目は、このシリーズの最初の記事で書いた「AならB、BならA」のループです。行き来が起きるのは、判断に必要な前提が足りていないサインでした。そこでAIに粘らせるのではなく、人に戻して前提を埋めます。
人に戻すときは、「各回で直した内容」「エラーの推移」「収束しないと判断した理由」を添えさせます。報告には、実行したコマンドと結果も必ず付けさせます。「実行していないことを『確認済み』と書かない」もルールに入れています。
試してみる手順
-
開発環境(Java 17とRancher Desktop)と、Python 3.12を用意する。環境は dev-environment で用意できます
-
リポジトリをcloneして、ゲートを実行する
git clone https://github.com/shoyo-maya-bot/ai-coding.git && cd ai-coding pip install -e tools/spec-trace ./scripts/verify-gates.sh --final -
全部のゲートが通ることを確認する
-
template/の中のテストから、アサーションを1つ消して、もう一度実行する -
どのゲートで止まるかを見る
壊して止まるところを一度見ておくと、「機械が何を守ってくれているか」が実感できます。
よくある失敗
AIの報告をそのまま信じる
「テストが通りました」と言われても、実行されていないことがあります。報告には実行したコマンドと結果を添えさせ、状態はCIで裏付けましょう。
ルールがあちこちに散らばる
プロンプト、指示ファイル、Wikiにそれぞれルールを書くと、内容が食い違っていきます。ルールの正は1か所に決め、他の場所からはそこを参照させましょう。
チェックを通すことが目的になる
エラーを消すために、チェックの方を緩めてしまうのはよくある失敗です。禁止事項として明文化し、CIでも検知できるようにしておきます。
AIに何回でも直させる
同じエラーを何度も行き来するのは、前提が足りないサインです。回数の上限を決めて、早めに人に戻しましょう。
まとめ
AIにコードを書かせるときに大事なのは、AIを賢くすることよりも、AIの出力を確かめる仕組みを作ることです。
- AIの生成物はβ版と割り切り、人が全量レビューする
- 「実装した」「テストした」は、対応表とCIで裏付ける
- チェックのこじ開けを禁止し、ループしたら人に戻す
この仕組みは、ハーネスの記事で書いた考え方を、実際の開発工程に当てはめたものです。モデルを変える前に、まずは確かめる仕組みから整えてみてください。
この記事は Zenn にも投稿しています。